db->tableExists('invoice_lines')) { $this->forge->addField([ 'id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true, ], 'invoice_id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false, ], 'line_type' => [ 'type' => 'VARCHAR', 'constraint' => 50, 'null' => false, ], 'source_type' => [ 'type' => 'VARCHAR', 'constraint' => 50, 'null' => true, ], 'source_id' => [ 'type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, ], 'active_source_key' => [ 'type' => 'VARCHAR', 'constraint' => 120, 'null' => true, ], 'description' => [ 'type' => 'VARCHAR', 'constraint' => 255, 'null' => false, ], 'quantity' => [ 'type' => 'DECIMAL', 'constraint' => '10,2', 'null' => false, 'default' => '1.00', ], 'unit_amount_cents' => [ 'type' => 'INT', 'constraint' => 11, 'null' => false, 'default' => 0, ], 'line_amount_cents' => [ 'type' => 'INT', 'constraint' => 11, 'null' => false, 'default' => 0, ], 'discount_eligible' => [ 'type' => 'TINYINT', 'constraint' => 1, 'null' => false, 'default' => 1, ], 'calculation_version' => [ 'type' => 'VARCHAR', 'constraint' => 50, 'null' => true, ], 'metadata_json' => [ 'type' => 'TEXT', 'null' => true, ], 'created_at' => [ 'type' => 'DATETIME', 'null' => false, ], 'updated_at' => [ 'type' => 'DATETIME', 'null' => false, ], 'voided_at' => [ 'type' => 'DATETIME', 'null' => true, ], ]); $this->forge->addKey('id', true); $this->forge->addKey('invoice_id'); $this->forge->addKey(['source_type', 'source_id']); $this->forge->addUniqueKey('active_source_key', 'uniq_invoice_lines_active_source_key'); $this->forge->createTable('invoice_lines', true); } $this->backfillLegacyInvoiceLines(); } public function down() { if ($this->db->tableExists('invoice_lines')) { $this->forge->dropTable('invoice_lines', true); } } private function backfillLegacyInvoiceLines(): void { if (!$this->db->tableExists('invoices') || !$this->db->tableExists('invoice_lines')) { return; } $existingRows = $this->db->table('invoice_lines') ->select('invoice_id') ->groupBy('invoice_id') ->get() ->getResultArray(); $existing = array_fill_keys(array_map(static fn ($row) => (int) ($row['invoice_id'] ?? 0), $existingRows), true); $invoices = $this->db->table('invoices') ->select('id, total_amount, invoice_number, created_at, updated_at') ->orderBy('id', 'ASC') ->get() ->getResultArray(); $now = date('Y-m-d H:i:s'); foreach ($invoices as $invoice) { $invoiceId = (int) ($invoice['id'] ?? 0); if ($invoiceId <= 0 || isset($existing[$invoiceId])) { continue; } $amountCents = (int) round(((float) ($invoice['total_amount'] ?? 0)) * 100); $createdAt = $invoice['created_at'] ?? $now; $updatedAt = $invoice['updated_at'] ?? $createdAt; $this->db->table('invoice_lines')->insert([ 'invoice_id' => $invoiceId, 'line_type' => 'legacy_invoice_total', 'source_type' => 'legacy_invoice', 'source_id' => $invoiceId, 'active_source_key' => null, 'description' => 'Legacy invoice total preserved from invoice ' . (string) ($invoice['invoice_number'] ?? $invoiceId), 'quantity' => '1.00', 'unit_amount_cents' => $amountCents, 'line_amount_cents' => $amountCents, 'discount_eligible' => 0, 'calculation_version' => 'legacy_import', 'metadata_json' => json_encode([ 'source' => 'invoices.total_amount', 'legacy_discount_eligible_base_cents' => 0, 'reconciliation_required' => true, ], JSON_UNESCAPED_SLASHES), 'created_at' => $createdAt ?: $now, 'updated_at' => $updatedAt ?: $now, 'voided_at' => null, ]); } } }