From 40478dd2aaa82b6a8f677a6552387e9c8b1619aa Mon Sep 17 00:00:00 2001 From: Vasyka Date: Thu, 28 May 2026 22:39:34 +0000 Subject: [PATCH] fix: make purchases partial-receipt migration idempotent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On MariaDB (no transactional DDL) a half-applied prior run left purchases.warehouse_id committed without recording the migration. On retry it failed with "Duplicate column name 'warehouse_id'", which aborted the migrate run and blocked every later migration (notifications, push, online store, pricing, labor templates, tire, subcontractor, bodyshop) — so those tables were never created (e.g. bodyshop_jobs missing → 500 on tenant pages). Guard each step with Schema::hasColumn / hasTable so the migration completes on re-run and unblocks the rest of the batch. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...0_extend_purchases_for_partial_receipt.php | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/database/migrations/2026_05_27_160000_extend_purchases_for_partial_receipt.php b/database/migrations/2026_05_27_160000_extend_purchases_for_partial_receipt.php index 47d62ea..00807e8 100644 --- a/database/migrations/2026_05_27_160000_extend_purchases_for_partial_receipt.php +++ b/database/migrations/2026_05_27_160000_extend_purchases_for_partial_receipt.php @@ -9,32 +9,41 @@ return new class extends Migration { public function up(): void { - Schema::table('purchases', function (Blueprint $t) { - $t->foreignId('warehouse_id')->nullable()->after('supplier_id') - ->constrained('warehouses')->nullOnDelete(); - }); + // Idempotent: MariaDB has no transactional DDL, so a half-applied prior + // run can leave columns/tables behind without recording the migration. + // Guard each step so a re-run completes instead of erroring on duplicates. + if (! Schema::hasColumn('purchases', 'warehouse_id')) { + Schema::table('purchases', function (Blueprint $t) { + $t->foreignId('warehouse_id')->nullable()->after('supplier_id') + ->constrained('warehouses')->nullOnDelete(); + }); + } - Schema::table('purchase_items', function (Blueprint $t) { - $t->decimal('qty_received', 10, 2)->default(0)->after('qty'); - }); + if (! Schema::hasColumn('purchase_items', 'qty_received')) { + Schema::table('purchase_items', function (Blueprint $t) { + $t->decimal('qty_received', 10, 2)->default(0)->after('qty'); + }); + } // Backfill: items previously marked `received=true` were fully received. DB::statement('UPDATE purchase_items SET qty_received = qty WHERE received = 1'); - Schema::create('supplier_part_prices', function (Blueprint $t) { - $t->id(); - $t->foreignId('company_id')->constrained()->cascadeOnDelete(); - $t->foreignId('supplier_id')->constrained()->cascadeOnDelete(); - $t->foreignId('part_id')->constrained()->cascadeOnDelete(); - $t->foreignId('purchase_id')->nullable()->constrained()->nullOnDelete(); - $t->decimal('price', 12, 2); - $t->string('currency', 6)->default('MDL'); - $t->dateTime('observed_at'); - $t->timestamps(); + if (! Schema::hasTable('supplier_part_prices')) { + Schema::create('supplier_part_prices', function (Blueprint $t) { + $t->id(); + $t->foreignId('company_id')->constrained()->cascadeOnDelete(); + $t->foreignId('supplier_id')->constrained()->cascadeOnDelete(); + $t->foreignId('part_id')->constrained()->cascadeOnDelete(); + $t->foreignId('purchase_id')->nullable()->constrained()->nullOnDelete(); + $t->decimal('price', 12, 2); + $t->string('currency', 6)->default('MDL'); + $t->dateTime('observed_at'); + $t->timestamps(); - $t->index(['company_id', 'supplier_id', 'part_id', 'observed_at']); - $t->index(['company_id', 'part_id', 'observed_at']); - }); + $t->index(['company_id', 'supplier_id', 'part_id', 'observed_at']); + $t->index(['company_id', 'part_id', 'observed_at']); + }); + } } public function down(): void