fix: make purchases partial-receipt migration idempotent

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 22:39:34 +00:00
parent ac7d5b4733
commit 40478dd2aa
@@ -9,32 +9,41 @@ return new class extends Migration
{ {
public function up(): void public function up(): void
{ {
Schema::table('purchases', function (Blueprint $t) { // Idempotent: MariaDB has no transactional DDL, so a half-applied prior
$t->foreignId('warehouse_id')->nullable()->after('supplier_id') // run can leave columns/tables behind without recording the migration.
->constrained('warehouses')->nullOnDelete(); // 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) { if (! Schema::hasColumn('purchase_items', 'qty_received')) {
$t->decimal('qty_received', 10, 2)->default(0)->after('qty'); 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. // Backfill: items previously marked `received=true` were fully received.
DB::statement('UPDATE purchase_items SET qty_received = qty WHERE received = 1'); DB::statement('UPDATE purchase_items SET qty_received = qty WHERE received = 1');
Schema::create('supplier_part_prices', function (Blueprint $t) { if (! Schema::hasTable('supplier_part_prices')) {
$t->id(); Schema::create('supplier_part_prices', function (Blueprint $t) {
$t->foreignId('company_id')->constrained()->cascadeOnDelete(); $t->id();
$t->foreignId('supplier_id')->constrained()->cascadeOnDelete(); $t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->foreignId('part_id')->constrained()->cascadeOnDelete(); $t->foreignId('supplier_id')->constrained()->cascadeOnDelete();
$t->foreignId('purchase_id')->nullable()->constrained()->nullOnDelete(); $t->foreignId('part_id')->constrained()->cascadeOnDelete();
$t->decimal('price', 12, 2); $t->foreignId('purchase_id')->nullable()->constrained()->nullOnDelete();
$t->string('currency', 6)->default('MDL'); $t->decimal('price', 12, 2);
$t->dateTime('observed_at'); $t->string('currency', 6)->default('MDL');
$t->timestamps(); $t->dateTime('observed_at');
$t->timestamps();
$t->index(['company_id', 'supplier_id', 'part_id', 'observed_at']); $t->index(['company_id', 'supplier_id', 'part_id', 'observed_at']);
$t->index(['company_id', 'part_id', 'observed_at']); $t->index(['company_id', 'part_id', 'observed_at']);
}); });
}
} }
public function down(): void public function down(): void