invoiceModel = new InvoiceModel(); $this->projectionService = $projectionService ?? new FinancialReportProjectionService(); } public function invoiceReport(?string $schoolYear = null): array { $query = $this->invoiceModel->orderBy('id', 'ASC'); if ($schoolYear !== null && $schoolYear !== '') { $query->where('school_year', $schoolYear); } $rows = []; foreach ($query->findAll() as $invoice) { $projection = $this->projectionService->invoiceProjection((int) $invoice['id']); $storedTotalCents = $this->toCents((float) ($invoice['total_amount'] ?? 0)); $storedBalanceCents = $this->toCents((float) ($invoice['balance'] ?? 0)); $flags = []; if ($storedTotalCents !== $projection['invoice_gross_charges_cents']) { $flags[] = 'stored_total_mismatch'; } if ($storedBalanceCents !== $projection['amount_due_cents']) { $flags[] = 'stored_balance_mismatch'; } if ($projection['completed_cash_refunds_cents'] < 0) { $flags[] = 'wrong_refund_sign'; } if ($projection['applied_discounts_cents'] > $projection['invoice_gross_charges_cents']) { $flags[] = 'discount_above_eligible_base'; } $rows[] = [ 'invoice_id' => (int) $invoice['id'], 'invoice_number' => $invoice['invoice_number'] ?? null, 'stored_total_cents' => $storedTotalCents, 'canonical_frozen_charge_total_cents' => $projection['invoice_gross_charges_cents'], 'valid_payment_total_cents' => $projection['valid_payments_cents'], 'completed_refund_payout_total_cents' => $projection['completed_cash_refunds_cents'], 'applied_discount_total_cents' => $projection['applied_discounts_cents'], 'expected_balance_due_cents' => $projection['amount_due_cents'], 'expected_customer_credit_cents' => $projection['customer_credit_cents'], 'flags' => $flags, ]; } return $rows; } private function toCents(float $amount): int { return (int) round($amount * 100); } }