*/ public function calculate(array $input): array { $annualFee = $this->nonNegativeInt($input, 'annual_fee_allocation_cents'); $bookCharge = $this->nonNegativeInt($input, 'issued_book_charge_cents'); $totalWeeks = $this->positiveInt($input, 'total_instructional_weeks'); $validPayments = $this->optionalNonNegativeInt($input, 'valid_payment_cents'); $completedPayouts = $this->optionalNonNegativeInt($input, 'completed_payout_cents'); $openReservations = $this->optionalNonNegativeInt($input, 'open_reservation_cents'); $otherCharges = $this->optionalNonNegativeInt($input, 'other_charge_cents'); $includesBooks = (bool) ($input['annual_fee_includes_books'] ?? true); if (! $includesBooks) { throw new InvalidArgumentException('The active withdrawal policy requires annual tuition to include books.'); } if ($bookCharge > $annualFee) { throw new InvalidArgumentException('Issued-book charges exceed the student annual tuition allocation.'); } $schoolStart = $this->date($input, 'school_year_start_date'); $enrollmentDate = $this->date($input, 'enrollment_date'); $withdrawalDate = $this->date($input, 'withdrawal_request_date'); $chargeStart = $enrollmentDate > $schoolStart ? $enrollmentDate : $schoolStart; $totalChargeableDays = $totalWeeks * 7; $studiedDays = 0; if ($withdrawalDate >= $chargeStart) { $studiedDays = ((int) $chargeStart->diff($withdrawalDate)->format('%a')) + 1; } $studiedDays = min($totalChargeableDays, max(0, $studiedDays)); $studiedWeeks = $studiedDays === 0 ? 0 : min($totalWeeks, intdiv($studiedDays + 6, 7)); $annualInstruction = $annualFee - $bookCharge; $earnedTuition = $this->roundRatio($annualInstruction * $studiedWeeks, $totalWeeks); $retainedCharge = $bookCharge + $earnedTuition + $otherCharges; $netPayments = max(0, $validPayments - $completedPayouts); $refundableCredit = max(0, $netPayments - $retainedCharge); $balanceDue = max(0, $retainedCharge - $netPayments); $newRefundRequest = max(0, $refundableCredit - $openReservations); return [ 'annual_fee_includes_books' => true, 'school_year_start_date' => $schoolStart->format('Y-m-d'), 'enrollment_date' => $enrollmentDate->format('Y-m-d'), 'withdrawal_request_date' => $withdrawalDate->format('Y-m-d'), 'charge_start_date' => $chargeStart->format('Y-m-d'), 'total_instructional_weeks' => $totalWeeks, 'total_chargeable_days' => $totalChargeableDays, 'studied_calendar_days' => $studiedDays, 'studied_weeks' => $studiedWeeks, 'annual_fee_allocation_cents' => $annualFee, 'issued_book_charge_cents' => $bookCharge, 'annual_instruction_cents' => $annualInstruction, 'earned_tuition_cents' => $earnedTuition, 'other_charge_cents' => $otherCharges, 'retained_charge_cents' => $retainedCharge, 'valid_payment_cents' => $validPayments, 'completed_payout_cents' => $completedPayouts, 'net_payment_cents' => $netPayments, 'open_reservation_cents' => $openReservations, 'refundable_credit_cents' => $refundableCredit, 'new_refund_request_cents' => $newRefundRequest, 'balance_due_cents' => $balanceDue, ]; } private function date(array $input, string $key): DateTimeImmutable { $raw = trim((string) ($input[$key] ?? '')); $date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw); $errors = DateTimeImmutable::getLastErrors(); if ($date === false || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) { throw new InvalidArgumentException($key . ' must be a valid Y-m-d date.'); } return $date; } private function positiveInt(array $input, string $key): int { $value = filter_var($input[$key] ?? null, FILTER_VALIDATE_INT); if ($value === false || $value <= 0) { throw new InvalidArgumentException($key . ' must be a positive integer.'); } return $value; } private function nonNegativeInt(array $input, string $key): int { $value = filter_var($input[$key] ?? null, FILTER_VALIDATE_INT); if ($value === false || $value < 0) { throw new InvalidArgumentException($key . ' must be a non-negative integer.'); } return $value; } private function optionalNonNegativeInt(array $input, string $key): int { if (! array_key_exists($key, $input) || $input[$key] === null || $input[$key] === '') { return 0; } return $this->nonNegativeInt($input, $key); } private function roundRatio(int $numerator, int $denominator): int { if ($denominator <= 0) { throw new InvalidArgumentException('The calculation denominator must be positive.'); } return intdiv($numerator + intdiv($denominator, 2), $denominator); } }