db = $db ?? db_connect(); $this->invoiceModel = $invoiceModel ?? new InvoiceModel(); $this->invoiceLineModel = $invoiceLineModel ?? new InvoiceLineModel(); $this->invoiceLedgerService = $invoiceLedgerService ?? new InvoiceLedgerService(); } public function issueInvoice(IssueInvoiceCommand $command): InvoiceLedgerResult { $invoiceData = $command->invoiceData; $invoiceData['status'] = FinancialStatus::INVOICE_DRAFT; $invoiceData['total_amount'] = $invoiceData['total_amount'] ?? number_format($command->tuitionAmount + $command->eventAmount, 2, '.', ''); $invoiceData['balance'] = $invoiceData['balance'] ?? $invoiceData['total_amount']; $this->db->transBegin(); try { $invoiceId = $this->invoiceModel->insert($invoiceData); $this->requireWrite($invoiceId, 'INVOICE_ISSUE_INSERT_FAILED', $this->invoiceModel); $this->db->query('SELECT id FROM invoices WHERE id = ? FOR UPDATE', [(int)$invoiceId]); $inserted = $this->invoiceLedgerService->issueInitialInvoiceLines( (int)$invoiceId, $command->tuitionAmount, $command->eventAmount, $command->metadata ); if ($inserted <= 0) { throw new FinancialPersistenceException('INVOICE_ISSUE_NO_LINES'); } $lineTotals = $this->db->table('invoice_lines') ->select('COUNT(*) AS line_count, COALESCE(SUM(line_amount_cents),0) AS total_cents') ->where('invoice_id', (int)$invoiceId) ->where('voided_at IS NULL', null, false) ->get() ->getRowArray(); if ((int)($lineTotals['line_count'] ?? 0) !== $inserted) { throw new FinancialPersistenceException('INVOICE_ISSUE_LINE_COUNT_MISMATCH'); } if ((int)($lineTotals['total_cents'] ?? 0) === 0) { throw new FinancialPersistenceException('INVOICE_ISSUE_ZERO_LINE_TOTAL'); } $this->requireWrite($this->invoiceModel->update((int)$invoiceId, [ 'status' => FinancialStatus::INVOICE_ISSUED, 'updated_at' => utc_now(), ]), 'INVOICE_ISSUE_STATUS_UPDATE_FAILED', $this->invoiceModel); $ledger = $this->invoiceLedgerService->recalculateInvoice((int)$invoiceId); $this->requireTransactionStatus(); $this->db->transCommit(); return new InvoiceLedgerResult((int)$invoiceId, 0, $ledger); } catch (\Throwable $e) { $this->db->transRollback(); throw $e; } } private function requireWrite($result, string $code, $model = null): void { if ($result === false || $result === null || $result === 0) { $details = is_object($model) && method_exists($model, 'errors') ? (array)$model->errors() : []; throw new FinancialPersistenceException($code, $details); } } private function requireTransactionStatus(string $code = 'TRANSACTION_FAILED'): void { if (method_exists($this->db, 'transStatus') && $this->db->transStatus() === false) { throw new FinancialPersistenceException($code); } } }