Files
alrahma_sunday_school/tests/app/Libraries/InvoiceLedgerServiceTest.php
T
root a30c1398a1
Tests / PHPUnit (push) Failing after 1m21s
fix financials
2026-07-18 22:57:40 -04:00

432 lines
15 KiB
PHP

<?php
namespace Tests\App\Libraries;
use App\Libraries\FinancialStatus;
use App\Libraries\InvoiceLedgerService;
use CodeIgniter\Test\CIUnitTestCase;
class InvoiceLedgerServiceHarness extends InvoiceLedgerService
{
private array $invoice;
private float $paidTotal;
private float $refundPaidTotal;
private float $tuitionTotal;
private float $eventTotal;
private float $additionalTotal;
private float $discountTotal;
private ?array $frozenTotals;
public function __construct(
array $invoice,
float $paidTotal = 0.0,
float $refundPaidTotal = 0.0,
float $tuitionTotal = 0.0,
float $eventTotal = 0.0,
float $additionalTotal = 0.0,
float $discountTotal = 0.0,
?array $frozenTotals = null
)
{
$this->invoice = $invoice;
$this->paidTotal = $paidTotal;
$this->refundPaidTotal = $refundPaidTotal;
$this->tuitionTotal = $tuitionTotal;
$this->eventTotal = $eventTotal;
$this->additionalTotal = $additionalTotal;
$this->discountTotal = $discountTotal;
$this->frozenTotals = $frozenTotals;
}
protected function loadInvoice(int $invoiceId): ?array
{
return $invoiceId === (int) ($this->invoice['id'] ?? 0) ? $this->invoice : null;
}
protected function calculateTuitionTotal(array $invoice): float
{
return $this->tuitionTotal;
}
protected function calculateEventTotal(array $invoice): float
{
return $this->eventTotal;
}
protected function calculateAdditionalCharges(int $invoiceId): float
{
return $this->additionalTotal;
}
protected function calculateDiscounts(int $invoiceId): float
{
return $this->discountTotal;
}
protected function calculateValidPayments(int $invoiceId): float
{
return $this->paidTotal;
}
protected function calculatePaidRefunds(int $invoiceId): float
{
return $this->refundPaidTotal;
}
protected function calculateFrozenLineTotals(int $invoiceId): ?array
{
return $this->frozenTotals;
}
}
class InvoiceLedgerServiceTest extends CIUnitTestCase
{
public function testInvoiceWithNoPaymentHasFullBalanceDue(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 20,
'invoice_number' => 'INV-2026-00020',
'total_amount' => '0.00',
'semester' => 'Fall',
], 0.0, 0.0, 100.0);
$calculation = $service->calculateInvoice(20);
$this->assertSame('100.00', $calculation['total_amount']);
$this->assertSame(10000, $calculation['totalAmountCents']);
$this->assertSame(10000, $calculation['balanceDueCents']);
$this->assertSame(0, $calculation['customerCreditCents']);
$this->assertSame(FinancialStatus::INVOICE_UNPAID, $calculation['status']);
}
public function testPartiallyPaidInvoiceHasRemainingBalance(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 21,
'invoice_number' => 'INV-2026-00021',
'total_amount' => '0.00',
'semester' => 'Fall',
], 40.0, 0.0, 100.0);
$calculation = $service->calculateInvoice(21);
$this->assertSame('60.00', $calculation['balance']);
$this->assertSame(6000, $calculation['rawBalanceCents']);
$this->assertSame(FinancialStatus::INVOICE_PARTIALLY_PAID, $calculation['status']);
}
public function testFullyPaidInvoiceHasNoBalanceOrCredit(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 22,
'invoice_number' => 'INV-2026-00022',
'total_amount' => '0.00',
'semester' => 'Fall',
], 100.0, 0.0, 100.0);
$calculation = $service->calculateInvoice(22);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame('0.00', $calculation['customer_credit']);
$this->assertSame(FinancialStatus::INVOICE_PAID, $calculation['status']);
}
public function testOverpaidInvoiceExposesCustomerCreditOnly(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 23,
'invoice_number' => 'INV-2026-00023',
'total_amount' => '0.00',
'semester' => 'Fall',
], 125.0, 0.0, 100.0);
$calculation = $service->calculateInvoice(23);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame('25.00', $calculation['customer_credit']);
$this->assertSame(-2500, $calculation['rawBalanceCents']);
$this->assertSame(2500, $calculation['customerCreditCents']);
}
public function testPartiallyRefundedInvoiceUsesRefundAsCashOut(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 24,
'invoice_number' => 'INV-2026-00024',
'total_amount' => '0.00',
'semester' => 'Fall',
], 125.0, 10.0, 100.0);
$calculation = $service->calculateInvoice(24);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame('15.00', $calculation['customer_credit']);
$this->assertSame(-1500, $calculation['rawBalanceCents']);
}
public function testFullyRefundedOverpaymentClearsCustomerCredit(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 25,
'invoice_number' => 'INV-2026-00025',
'total_amount' => '0.00',
'semester' => 'Fall',
], 125.0, 25.0, 100.0);
$calculation = $service->calculateInvoice(25);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame('0.00', $calculation['customer_credit']);
$this->assertSame(0, $calculation['rawBalanceCents']);
}
public function testPaymentAfterRefundCanRestorePaidStatus(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 26,
'invoice_number' => 'INV-2026-00026',
'total_amount' => '0.00',
'semester' => 'Fall',
], 130.0, 30.0, 100.0);
$calculation = $service->calculateInvoice(26);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame('0.00', $calculation['customer_credit']);
$this->assertSame(FinancialStatus::INVOICE_PAID, $calculation['status']);
}
public function testGeneratedTwiceWithSameSourceDataReturnsIdenticalLedger(): void
{
$invoice = [
'id' => 27,
'invoice_number' => 'INV-2026-00027',
'total_amount' => '0.00',
'semester' => 'Fall',
];
$first = (new InvoiceLedgerServiceHarness($invoice, 55.0, 5.0, 100.0, 20.0, 10.0, 15.0))->calculateInvoice(27);
$second = (new InvoiceLedgerServiceHarness($invoice, 55.0, 5.0, 100.0, 20.0, 10.0, 15.0))->calculateInvoice(27);
$this->assertSame($first, $second);
}
public function testFrozenInvoiceLinesOverrideChangedLiveSources(): void
{
$invoice = [
'id' => 28,
'invoice_number' => 'INV-2026-00028',
'total_amount' => '0.00',
'semester' => 'Fall',
];
$service = new InvoiceLedgerServiceHarness(
$invoice,
paidTotal: 0.0,
refundPaidTotal: 0.0,
tuitionTotal: 999.0,
eventTotal: 888.0,
additionalTotal: 777.0,
discountTotal: 0.0,
frozenTotals: [
'tuition_cents' => 10000,
'event_cents' => 2000,
'additional_cents' => 500,
'total_cents' => 12500,
]
);
$calculation = $service->calculateInvoice(28);
$this->assertSame('100.00', $calculation['tuition_total']);
$this->assertSame('20.00', $calculation['event_total']);
$this->assertSame('5.00', $calculation['additional_total']);
$this->assertSame('125.00', $calculation['total_amount']);
$this->assertSame('125.00', $calculation['balance']);
}
public function testFrozenInvoiceRecalculationIsStableAfterPayment(): void
{
$invoice = [
'id' => 29,
'invoice_number' => 'INV-2026-00029',
'total_amount' => '0.00',
'semester' => 'Fall',
];
$frozenTotals = [
'tuition_cents' => 10000,
'event_cents' => 0,
'additional_cents' => 0,
'total_cents' => 10000,
];
$before = (new InvoiceLedgerServiceHarness(
$invoice,
paidTotal: 0.0,
tuitionTotal: 400.0,
frozenTotals: $frozenTotals
))->calculateInvoice(29);
$after = (new InvoiceLedgerServiceHarness(
$invoice,
paidTotal: 25.0,
tuitionTotal: 600.0,
frozenTotals: $frozenTotals
))->calculateInvoice(29);
$this->assertSame('100.00', $before['total_amount']);
$this->assertSame('100.00', $after['total_amount']);
$this->assertSame('75.00', $after['balance']);
}
public function testFrozenInvoiceExposesEligibleDiscountBaseFromLines(): void
{
$invoice = [
'id' => 30,
'invoice_number' => 'INV-2026-00030',
'total_amount' => '0.00',
'semester' => 'Fall',
];
$service = new InvoiceLedgerServiceHarness(
$invoice,
discountTotal: 50.0,
frozenTotals: [
'tuition_cents' => 10000,
'event_cents' => 3000,
'additional_cents' => 0,
'total_cents' => 13000,
'discount_eligible_base_cents' => 10000,
]
);
$calculation = $service->calculateInvoice(30);
$this->assertSame(13000, $calculation['gross_charge_cents']);
$this->assertSame(10000, $calculation['discount_eligible_base_cents']);
$this->assertSame(5000, $calculation['requested_discount_cents']);
$this->assertSame(5000, $calculation['applied_discount_cents']);
$this->assertSame(8000, $calculation['net_charge_cents']);
}
public function testEventOnlyFrozenInvoiceReceivesNoDiscount(): void
{
$invoice = [
'id' => 31,
'invoice_number' => 'INV-2026-00031',
'total_amount' => '0.00',
'semester' => 'Fall',
];
$service = new InvoiceLedgerServiceHarness(
$invoice,
discountTotal: 25.0,
frozenTotals: [
'tuition_cents' => 0,
'event_cents' => 7500,
'additional_cents' => 0,
'total_cents' => 7500,
'discount_eligible_base_cents' => 0,
]
);
$calculation = $service->calculateInvoice(31);
$this->assertSame(0, $calculation['discount_eligible_base_cents']);
$this->assertSame(2500, $calculation['requested_discount_cents']);
$this->assertSame(0, $calculation['applied_discount_cents']);
$this->assertSame(7500, $calculation['net_charge_cents']);
$this->assertSame('75.00', $calculation['balance']);
}
public function testCarryForwardInvoiceUsesStoredOpeningBalance(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 10,
'invoice_number' => 'CF-20252026-20262027-P8-I4',
'total_amount' => '90.00',
'semester' => 'Opening Balance',
'description' => 'Balance carried over from previous school year 2025-2026.',
]);
$calculation = $service->calculateInvoice(10);
$this->assertSame('90.00', $calculation['total_amount']);
$this->assertSame('90.00', $calculation['balance']);
$this->assertSame(FinancialStatus::INVOICE_UNPAID, $calculation['status']);
}
public function testCarryForwardInvoiceIsPaidAfterFullPayment(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 10,
'invoice_number' => 'CF-20252026-20262027-P8-I4',
'total_amount' => '90.00',
'semester' => 'Opening Balance',
'description' => 'Balance carried over from previous school year 2025-2026.',
], 90.0);
$calculation = $service->calculateInvoice(10);
$this->assertSame('90.00', $calculation['total_amount']);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame(FinancialStatus::INVOICE_PAID, $calculation['status']);
}
public function testCashRefundReducesCustomerCreditWithoutIncreasingOverpayment(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 11,
'invoice_number' => 'CF-20252026-20262027-P8-I5',
'total_amount' => '100.00',
'semester' => 'Opening Balance',
'description' => 'Balance carried over from previous school year 2025-2026.',
], 125.0, 20.0);
$calculation = $service->calculateInvoice(11);
$this->assertSame('100.00', $calculation['total_amount']);
$this->assertSame('20.00', $calculation['refund_paid_total']);
$this->assertSame('5.00', $calculation['customer_credit']);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame(FinancialStatus::INVOICE_PAID, $calculation['status']);
}
public function testCashRefundCanRestoreBalanceAfterOverpaymentIsReturned(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 12,
'invoice_number' => 'CF-20252026-20262027-P8-I6',
'total_amount' => '100.00',
'semester' => 'Opening Balance',
'description' => 'Balance carried over from previous school year 2025-2026.',
], 125.0, 30.0);
$calculation = $service->calculateInvoice(12);
$this->assertSame('0.00', $calculation['customer_credit']);
$this->assertSame('5.00', $calculation['balance']);
$this->assertSame(FinancialStatus::INVOICE_PARTIALLY_PAID, $calculation['status']);
}
public function testIssuedInvoiceUsesPaidRefundsAsCashOutInsteadOfAdditionalCredit(): void
{
$service = new InvoiceLedgerServiceHarness([
'id' => 13,
'invoice_number' => 'INV-2026-00013',
'total_amount' => '0.00',
'semester' => 'Fall',
'description' => 'Current year tuition invoice.',
], 120.0, 15.0, 100.0, 0.0, 0.0, 10.0);
$calculation = $service->calculateInvoice(13);
$this->assertSame('100.00', $calculation['total_amount']);
$this->assertSame('10.00', $calculation['discount_total']);
$this->assertSame('15.00', $calculation['refund_paid_total']);
$this->assertSame('15.00', $calculation['customer_credit']);
$this->assertSame('0.00', $calculation['balance']);
$this->assertSame(FinancialStatus::INVOICE_PAID, $calculation['status']);
}
}