add refund logic and fix books inventory logic
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user