252 lines
9.5 KiB
PHP
252 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\WithdrawalRefundCalculator;
|
|
use InvalidArgumentException;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class WithdrawalRefundCalculatorTest extends TestCase
|
|
{
|
|
private WithdrawalRefundCalculator $calculator;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->calculator = new WithdrawalRefundCalculator();
|
|
}
|
|
|
|
public function testOneWeekRetainsIssuedBooksAndOneInstructionalWeek(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 38000,
|
|
]));
|
|
|
|
self::assertSame(210, $result['total_chargeable_days']);
|
|
self::assertSame(7, $result['studied_calendar_days']);
|
|
self::assertSame(1, $result['studied_weeks']);
|
|
self::assertSame(5000, $result['issued_book_charge_cents']);
|
|
self::assertSame(33000, $result['annual_instruction_cents']);
|
|
self::assertSame(1100, $result['earned_tuition_cents']);
|
|
self::assertSame(6100, $result['retained_charge_cents']);
|
|
self::assertSame(31900, $result['new_refund_request_cents']);
|
|
self::assertSame(0, $result['balance_due_cents']);
|
|
}
|
|
|
|
public function testPartialPaymentRefundsOnlyTheAmountAboveRetainedCharge(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 10000,
|
|
]));
|
|
|
|
self::assertSame(3900, $result['new_refund_request_cents']);
|
|
self::assertSame(0, $result['balance_due_cents']);
|
|
}
|
|
|
|
public function testPartialPaymentBelowRetainedChargeProducesBalanceAndNoRefund(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 5000,
|
|
]));
|
|
|
|
self::assertSame(0, $result['new_refund_request_cents']);
|
|
self::assertSame(1100, $result['balance_due_cents']);
|
|
}
|
|
|
|
public function testStartedSecondSevenDayPeriodChargesTwoWeeks(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'withdrawal_request_date' => '2026-08-08',
|
|
]));
|
|
|
|
self::assertSame(8, $result['studied_calendar_days']);
|
|
self::assertSame(2, $result['studied_weeks']);
|
|
self::assertSame(2200, $result['earned_tuition_cents']);
|
|
self::assertSame(7200, $result['retained_charge_cents']);
|
|
}
|
|
|
|
public function testCompletedPayoutAndReservationCannotBeRefundedAgain(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 38000,
|
|
'completed_payout_cents' => 10000,
|
|
'open_reservation_cents' => 5000,
|
|
]));
|
|
|
|
self::assertSame(21900, $result['refundable_credit_cents']);
|
|
self::assertSame(16900, $result['new_refund_request_cents']);
|
|
}
|
|
|
|
public function testIssuedBooksCannotExceedInclusiveAnnualAllocation(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->calculator->calculate($this->baseInput([
|
|
'issued_book_charge_cents' => 38001,
|
|
]));
|
|
}
|
|
|
|
public function testBooksExcludedModeIsRejected(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->calculator->calculate($this->baseInput([
|
|
'annual_fee_includes_books' => false,
|
|
]));
|
|
}
|
|
|
|
public function testDayOneChargesOneStartedWeek(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'withdrawal_request_date' => '2026-08-01',
|
|
]));
|
|
self::assertSame(1, $result['studied_calendar_days']);
|
|
self::assertSame(1, $result['studied_weeks']);
|
|
}
|
|
|
|
public function testWithdrawalBeforeChargeStartChargesNoTuitionButRetainsBooks(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'enrollment_date' => '2026-08-10',
|
|
'withdrawal_request_date' => '2026-08-09',
|
|
]));
|
|
self::assertSame(0, $result['studied_calendar_days']);
|
|
self::assertSame(0, $result['studied_weeks']);
|
|
self::assertSame(5000, $result['retained_charge_cents']);
|
|
}
|
|
|
|
public function testLateEnrollmentBecomesChargeStart(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'enrollment_date' => '2026-08-20',
|
|
'withdrawal_request_date' => '2026-08-26',
|
|
]));
|
|
self::assertSame('2026-08-20', $result['charge_start_date']);
|
|
self::assertSame(7, $result['studied_calendar_days']);
|
|
self::assertSame(1, $result['studied_weeks']);
|
|
}
|
|
|
|
public function testEnrollmentBeforeSchoolStartsUsesSchoolStart(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'enrollment_date' => '2026-07-01',
|
|
'withdrawal_request_date' => '2026-08-07',
|
|
]));
|
|
self::assertSame('2026-08-01', $result['charge_start_date']);
|
|
self::assertSame(1, $result['studied_weeks']);
|
|
}
|
|
|
|
public function testDurationCapsAtTotalInstructionalWeeks(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'withdrawal_request_date' => '2027-12-31',
|
|
'valid_payment_cents' => 38000,
|
|
]));
|
|
self::assertSame(210, $result['studied_calendar_days']);
|
|
self::assertSame(30, $result['studied_weeks']);
|
|
self::assertSame(38000, $result['retained_charge_cents']);
|
|
self::assertSame(0, $result['new_refund_request_cents']);
|
|
}
|
|
|
|
public function testOtherChargesRemainPayable(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 10000,
|
|
'other_charge_cents' => 2000,
|
|
]));
|
|
self::assertSame(8100, $result['retained_charge_cents']);
|
|
self::assertSame(1900, $result['new_refund_request_cents']);
|
|
}
|
|
|
|
public function testProrationRoundsOnceInCents(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'annual_fee_allocation_cents' => 10000,
|
|
'issued_book_charge_cents' => 0,
|
|
'total_instructional_weeks' => 3,
|
|
'withdrawal_request_date' => '2026-08-07',
|
|
]));
|
|
self::assertSame(3333, $result['earned_tuition_cents']);
|
|
}
|
|
|
|
public function testZeroTotalInstructionalWeeksIsRejected(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->calculator->calculate($this->baseInput(['total_instructional_weeks' => 0]));
|
|
}
|
|
|
|
public function testMalformedDateIsRejected(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->calculator->calculate($this->baseInput(['withdrawal_request_date' => '2026-02-31']));
|
|
}
|
|
|
|
public function testNoSchoolCalendarDataCannotAffectPureCalculation(): void
|
|
{
|
|
$first = $this->calculator->calculate($this->baseInput());
|
|
$second = $this->calculator->calculate($this->baseInput(['unrelated_no_school_dates' => ['2026-08-03']]));
|
|
self::assertSame($first, $second);
|
|
}
|
|
|
|
public function testNoPaymentProducesNoRefundAndFullRetainedBalance(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput());
|
|
self::assertSame(0, $result['new_refund_request_cents']);
|
|
self::assertSame(6100, $result['balance_due_cents']);
|
|
}
|
|
|
|
public function testPaymentEqualToRetainedChargeProducesNeitherRefundNorBalance(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput(['valid_payment_cents' => 6100]));
|
|
self::assertSame(0, $result['new_refund_request_cents']);
|
|
self::assertSame(0, $result['balance_due_cents']);
|
|
}
|
|
|
|
public function testZeroIssuedBooksProratesTheEntireAnnualAllocation(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'issued_book_charge_cents' => 0,
|
|
'valid_payment_cents' => 38000,
|
|
]));
|
|
self::assertSame(0, $result['issued_book_charge_cents']);
|
|
self::assertSame(1267, $result['earned_tuition_cents']);
|
|
self::assertSame(36733, $result['new_refund_request_cents']);
|
|
}
|
|
|
|
public function testFinalWeekAlwaysClosesAtTheExactAnnualAllocation(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'withdrawal_request_date' => '2027-02-26',
|
|
'valid_payment_cents' => 38000,
|
|
]));
|
|
self::assertSame(30, $result['studied_weeks']);
|
|
self::assertSame(33000, $result['earned_tuition_cents']);
|
|
self::assertSame(38000, $result['retained_charge_cents']);
|
|
}
|
|
|
|
public function testOpenReservationCannotMakeARefundNegative(): void
|
|
{
|
|
$result = $this->calculator->calculate($this->baseInput([
|
|
'valid_payment_cents' => 10000,
|
|
'open_reservation_cents' => 999999,
|
|
]));
|
|
self::assertSame(3900, $result['refundable_credit_cents']);
|
|
self::assertSame(0, $result['new_refund_request_cents']);
|
|
}
|
|
|
|
/** @param array<string,mixed> $overrides */
|
|
private function baseInput(array $overrides = []): array
|
|
{
|
|
return array_replace([
|
|
'annual_fee_allocation_cents' => 38000,
|
|
'issued_book_charge_cents' => 5000,
|
|
'total_instructional_weeks' => 30,
|
|
'school_year_start_date' => '2026-08-01',
|
|
'enrollment_date' => '2026-08-01',
|
|
'withdrawal_request_date' => '2026-08-07',
|
|
'valid_payment_cents' => 0,
|
|
'completed_payout_cents' => 0,
|
|
'open_reservation_cents' => 0,
|
|
'other_charge_cents' => 0,
|
|
'annual_fee_includes_books' => true,
|
|
], $overrides);
|
|
}
|
|
}
|