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); } }