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, ], 'school_year' => [ 'type' => 'VARCHAR', 'constraint' => 9, '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->ensureSchoolYearColumn(); $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, school_year, 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, 'school_year' => (string)($invoice['school_year'] ?? ''), '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, ]); } } private function ensureSchoolYearColumn(): void { if (!$this->db->tableExists('invoice_lines')) { return; } if (!$this->db->fieldExists('school_year', 'invoice_lines')) { $this->forge->addColumn('invoice_lines', [ 'school_year' => [ 'type' => 'VARCHAR', 'constraint' => 9, 'null' => true, 'after' => 'invoice_id', ], ]); } if ($this->db->tableExists('invoices')) { $this->db->query( "UPDATE invoice_lines il INNER JOIN invoices i ON i.id = il.invoice_id SET il.school_year = i.school_year WHERE il.school_year IS NULL OR TRIM(il.school_year) = ''" ); } $this->db->query("ALTER TABLE invoice_lines MODIFY school_year VARCHAR(9) NOT NULL"); } }