@@ -68,11 +68,28 @@ class FinancialAttachmentServiceTest extends CIUnitTestCase
|
||||
|
||||
$file->expects($this->once())
|
||||
->method('move')
|
||||
->with($this->stringContains('writable/uploads/payments'), 'proof.pdf');
|
||||
->with($this->stringContains('writable/uploads/_tmp/payments'), $this->stringContains('proof.pdf'))
|
||||
->willReturnCallback(static function (string $dir, string $name): bool {
|
||||
file_put_contents($dir . DIRECTORY_SEPARATOR . $name, 'pdf');
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->assertSame('proof.pdf', $this->service->saveUploadedFile($file, 'payments'));
|
||||
}
|
||||
|
||||
public function testDiscardStagedFileDeletesTemporaryUpload(): void
|
||||
{
|
||||
$tmpDir = $this->service->ensureSubdir('_tmp/checks');
|
||||
$tmpPath = $tmpDir . DIRECTORY_SEPARATOR . 'pending-test.pdf';
|
||||
file_put_contents($tmpPath, 'pdf');
|
||||
|
||||
$this->service->discardStagedFile([
|
||||
'temporary_path' => $tmpPath,
|
||||
]);
|
||||
|
||||
$this->assertFileDoesNotExist($tmpPath);
|
||||
}
|
||||
|
||||
public function testResolvePathUsesBasenameAndReturnsNullForMissingFiles(): void
|
||||
{
|
||||
$dir = $this->service->ensureSubdir('receipts');
|
||||
|
||||
@@ -10,11 +10,32 @@ 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)
|
||||
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
|
||||
@@ -24,22 +45,22 @@ class InvoiceLedgerServiceHarness extends InvoiceLedgerService
|
||||
|
||||
protected function calculateTuitionTotal(array $invoice): float
|
||||
{
|
||||
return 0.0;
|
||||
return $this->tuitionTotal;
|
||||
}
|
||||
|
||||
protected function calculateEventTotal(array $invoice): float
|
||||
{
|
||||
return 0.0;
|
||||
return $this->eventTotal;
|
||||
}
|
||||
|
||||
protected function calculateAdditionalCharges(int $invoiceId): float
|
||||
{
|
||||
return 0.0;
|
||||
return $this->additionalTotal;
|
||||
}
|
||||
|
||||
protected function calculateDiscounts(int $invoiceId): float
|
||||
{
|
||||
return 0.0;
|
||||
return $this->discountTotal;
|
||||
}
|
||||
|
||||
protected function calculateValidPayments(int $invoiceId): float
|
||||
@@ -49,12 +70,275 @@ class InvoiceLedgerServiceHarness extends InvoiceLedgerService
|
||||
|
||||
protected function calculatePaidRefunds(int $invoiceId): float
|
||||
{
|
||||
return 0.0;
|
||||
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([
|
||||
@@ -88,4 +372,60 @@ class InvoiceLedgerServiceTest extends CIUnitTestCase
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Libraries;
|
||||
|
||||
use App\Libraries\ParentLedgerService;
|
||||
use App\Libraries\RefundEligibilityResult;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class ParentLedgerServiceHarness extends ParentLedgerService
|
||||
{
|
||||
public function __construct(
|
||||
private array $invoices,
|
||||
private array $ledgers,
|
||||
private array $eligibilities
|
||||
) {
|
||||
}
|
||||
|
||||
protected function loadInvoices(int $parentId, string $schoolYear, ?string $semester): array
|
||||
{
|
||||
return array_values(array_filter($this->invoices, static function (array $invoice) use ($parentId, $schoolYear, $semester): bool {
|
||||
if ((int)($invoice['parent_id'] ?? 0) !== $parentId || (string)($invoice['school_year'] ?? '') !== $schoolYear) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $semester === null || $semester === '' || (string)($invoice['semester'] ?? '') === $semester;
|
||||
}));
|
||||
}
|
||||
|
||||
protected function ledgerForInvoice(int $invoiceId): array
|
||||
{
|
||||
return $this->ledgers[$invoiceId] ?? [];
|
||||
}
|
||||
|
||||
protected function eligibilityForSource(
|
||||
int $parentId,
|
||||
?int $invoiceId,
|
||||
string $sourceType,
|
||||
int $sourceId
|
||||
): RefundEligibilityResult {
|
||||
return $this->eligibilities[$sourceId] ?? new RefundEligibilityResult(0, 0, 0, 0, []);
|
||||
}
|
||||
}
|
||||
|
||||
class ParentLedgerServiceTest extends CIUnitTestCase
|
||||
{
|
||||
public function testParentProjectionAggregatesLedgerAndEligibilityFields(): void
|
||||
{
|
||||
$service = new ParentLedgerServiceHarness(
|
||||
[
|
||||
['id' => 10, 'parent_id' => 5, 'school_year' => '2026-2027', 'semester' => 'Fall', 'invoice_number' => 'INV-10'],
|
||||
['id' => 11, 'parent_id' => 5, 'school_year' => '2026-2027', 'semester' => 'Fall', 'invoice_number' => 'INV-11'],
|
||||
['id' => 12, 'parent_id' => 5, 'school_year' => '2026-2027', 'semester' => 'Spring', 'invoice_number' => 'INV-12'],
|
||||
],
|
||||
[
|
||||
10 => [
|
||||
'totalAmountCents' => 10000,
|
||||
'discountCents' => 1000,
|
||||
'paidCents' => 11000,
|
||||
'completedRefundCents' => 500,
|
||||
'balanceDueCents' => 0,
|
||||
'customerCreditCents' => 1500,
|
||||
'status' => 'paid',
|
||||
],
|
||||
11 => [
|
||||
'totalAmountCents' => 8000,
|
||||
'discountCents' => 0,
|
||||
'paidCents' => 2000,
|
||||
'completedRefundCents' => 0,
|
||||
'balanceDueCents' => 6000,
|
||||
'customerCreditCents' => 0,
|
||||
'status' => 'partially_paid',
|
||||
],
|
||||
],
|
||||
[
|
||||
10 => new RefundEligibilityResult(1500, 0, 300, 1200, ['HAS_APPROVED_RESERVATIONS']),
|
||||
11 => new RefundEligibilityResult(0, 0, 0, 0, ['NO_AVAILABLE_CREDIT']),
|
||||
]
|
||||
);
|
||||
|
||||
$projection = $service->getParentProjection(5, '2026-2027', 'Fall');
|
||||
|
||||
$this->assertSame(18000, $projection['grossChargesCents']);
|
||||
$this->assertSame(1000, $projection['discountCents']);
|
||||
$this->assertSame(17000, $projection['netInvoiceChargesCents']);
|
||||
$this->assertSame(13000, $projection['validPaymentCents']);
|
||||
$this->assertSame(500, $projection['completedRefundCents']);
|
||||
$this->assertSame(6000, $projection['balanceDueCents']);
|
||||
$this->assertSame(1500, $projection['customerCreditCents']);
|
||||
$this->assertSame(300, $projection['approvedRefundReservationCents']);
|
||||
$this->assertSame(1200, $projection['availableRefundableCreditCents']);
|
||||
$this->assertSame('170.00', $projection['net_invoice_charges']);
|
||||
$this->assertSame('12.00', $projection['available_refundable_credit']);
|
||||
$this->assertCount(2, $projection['invoices']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\App\Libraries;
|
||||
|
||||
use App\Libraries\RefundEligibilityService;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class RefundEligibilityServiceHarness extends RefundEligibilityService
|
||||
{
|
||||
public function __construct(
|
||||
private int $sourceCreditCents,
|
||||
private int $completedPayoutCents,
|
||||
private int $reservedAmountCents
|
||||
) {
|
||||
}
|
||||
|
||||
protected function calculateSourceCreditCents(int $parentId, ?int $invoiceId, string $sourceType, int $sourceId): int
|
||||
{
|
||||
return $this->sourceCreditCents;
|
||||
}
|
||||
|
||||
protected function calculateCompletedPayoutCents(string $sourceType, int $sourceId, ?int $excludeRefundId): int
|
||||
{
|
||||
return $this->completedPayoutCents;
|
||||
}
|
||||
|
||||
protected function calculateReservedAmountCents(string $sourceType, int $sourceId, ?int $excludeRefundId): int
|
||||
{
|
||||
return $this->reservedAmountCents;
|
||||
}
|
||||
}
|
||||
|
||||
class RefundEligibilityServiceTest extends CIUnitTestCase
|
||||
{
|
||||
public function testInvoiceOverpaymentAvailableCreditDoesNotDoubleSubtractCompletedPayouts(): void
|
||||
{
|
||||
$service = new RefundEligibilityServiceHarness(10000, 2500, 1500);
|
||||
|
||||
$result = $service->calculateAvailableCredit(10, 20, 'invoice_overpayment', 20);
|
||||
|
||||
$this->assertSame(10000, $result->sourceCreditCents);
|
||||
$this->assertSame(0, $result->completedPayoutCents);
|
||||
$this->assertSame(1500, $result->reservedAmountCents);
|
||||
$this->assertSame(8500, $result->availableAmountCents);
|
||||
$this->assertNotContains('HAS_COMPLETED_PAYOUTS', $result->reasonCodes);
|
||||
$this->assertContains('HAS_APPROVED_RESERVATIONS', $result->reasonCodes);
|
||||
}
|
||||
|
||||
public function testPaymentSourceAvailableCreditSubtractsCompletedPayoutsAndReservations(): void
|
||||
{
|
||||
$service = new RefundEligibilityServiceHarness(10000, 2500, 1500);
|
||||
|
||||
$result = $service->calculateAvailableCredit(10, 20, 'payment_duplicate', 77);
|
||||
|
||||
$this->assertSame(10000, $result->sourceCreditCents);
|
||||
$this->assertSame(2500, $result->completedPayoutCents);
|
||||
$this->assertSame(1500, $result->reservedAmountCents);
|
||||
$this->assertSame(6000, $result->availableAmountCents);
|
||||
$this->assertContains('HAS_COMPLETED_PAYOUTS', $result->reasonCodes);
|
||||
$this->assertContains('HAS_APPROVED_RESERVATIONS', $result->reasonCodes);
|
||||
}
|
||||
|
||||
public function testValidateRejectsAmountAboveAvailableCredit(): void
|
||||
{
|
||||
$service = new RefundEligibilityServiceHarness(10000, 7000, 1000);
|
||||
$result = $service->calculateAvailableCredit(10, 20, 'payment_duplicate', 20);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Refund amount exceeds available source credit.');
|
||||
|
||||
$service->validateRequestedAmount($result, 2500);
|
||||
}
|
||||
|
||||
public function testValidateRejectsNonPositiveAmount(): void
|
||||
{
|
||||
$service = new RefundEligibilityServiceHarness(10000, 0, 0);
|
||||
$result = $service->calculateAvailableCredit(10, 20, 'invoice_overpayment', 20);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Refund amount must be greater than zero.');
|
||||
|
||||
$service->validateRequestedAmount($result, 0);
|
||||
}
|
||||
|
||||
public function testZeroAvailableCreditCarriesReasonCodes(): void
|
||||
{
|
||||
$service = new RefundEligibilityServiceHarness(0, 0, 0);
|
||||
|
||||
$result = $service->calculateAvailableCredit(10, 20, 'invoice_overpayment', 20);
|
||||
|
||||
$this->assertSame(0, $result->availableAmountCents);
|
||||
$this->assertContains('NO_SOURCE_CREDIT', $result->reasonCodes);
|
||||
$this->assertContains('NO_AVAILABLE_CREDIT', $result->reasonCodes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user