Fix semester context, attendance rosters, and billing workflows
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 31s
Tests / PHPUnit (push) Failing after 55s

- load global semester helpers consistently and use date-based semester defaults
- fix grading and daily attendance duplicate student/section rows
- keep attendance violations scoped to the current semester by default
- update invoice, refund, discount, payment, and financial aid flows
- add configuration cleanup migrations for duplicate calendar/semester keys
- refresh parent registration/report-card and print request handling
- update related models, services, views, cron notes, and test coverage
This commit is contained in:
root
2026-08-16 17:41:11 -04:00
parent 36c7e3fc6d
commit 0ac3a8375e
99 changed files with 1598 additions and 814 deletions
+54
View File
@@ -27,6 +27,12 @@ class InvoiceIssuanceService
public function issueInvoice(IssueInvoiceCommand $command): InvoiceLedgerResult
{
$invoiceData = $command->invoiceData;
if (trim((string)($invoiceData['invoice_number'] ?? '')) === '') {
$invoiceData['invoice_number'] = $this->generateInvoiceNumber(
(string)($invoiceData['school_year'] ?? ''),
(int)($invoiceData['parent_id'] ?? 0)
);
}
$invoiceData['status'] = FinancialStatus::INVOICE_DRAFT;
$invoiceData['total_amount'] = $invoiceData['total_amount'] ?? number_format($command->tuitionAmount + $command->eventAmount, 2, '.', '');
$invoiceData['balance'] = $invoiceData['balance'] ?? $invoiceData['total_amount'];
@@ -78,6 +84,54 @@ class InvoiceIssuanceService
}
}
public function generateInvoiceNumber(string $schoolYear, ?int $parentId = null): string
{
$prefix = $this->invoiceNumberPrefix($schoolYear, $parentId);
do {
$invoiceNumber = $prefix . '-' . uniqid();
$exists = $this->db->table('invoices')
->where('invoice_number', $invoiceNumber)
->countAllResults() > 0;
} while ($exists);
return $invoiceNumber;
}
private function invoiceNumberPrefix(string $schoolYear, ?int $parentId = null): string
{
$year = date('y');
if (preg_match('/^(\d{4})-\d{4}$/', $schoolYear, $matches) === 1) {
$year = substr($matches[1], -2);
}
$parentSuffix = $this->parentSchoolIdSuffix((int)($parentId ?? 0));
if ($parentSuffix !== '') {
return 'INV-' . $year . $parentSuffix;
}
return 'INV-' . $year . str_pad((string) max(0, (int)($parentId ?? 0)), 5, '0', STR_PAD_LEFT);
}
private function parentSchoolIdSuffix(int $parentId): string
{
if ($parentId <= 0) {
return '';
}
$row = $this->db->table('users')
->select('school_id')
->where('id', $parentId)
->get()
->getRowArray();
$schoolId = trim((string)($row['school_id'] ?? ''));
if (preg_match('/^\d{2}(\d{5})$/', $schoolId, $matches) === 1) {
return $matches[1];
}
return '';
}
private function requireWrite($result, string $code, $model = null): void
{
if ($result === false || $result === null || $result === 0) {
+6 -4
View File
@@ -103,8 +103,10 @@ class InvoiceLedgerService
$totalAmountCents = $tuitionCents + $eventCents + $additionalCents;
}
$rawBalanceCents = $totalAmountCents - $discountCents - $paidCents + $refundPaidCents;
$netChargeCents = $totalAmountCents - $discountCents;
$rawBalanceCents = $netChargeCents - $paidCents - $refundPaidCents;
$balanceCents = max(0, $rawBalanceCents);
$customerCreditCents = max(0, $paidCents - $refundPaidCents - $netChargeCents);
if ($balanceCents === 0) {
$status = FinancialStatus::INVOICE_PAID;
@@ -120,14 +122,14 @@ class InvoiceLedgerService
'discount_eligible_base_cents' => $discountBaseCents,
'requested_discount_cents' => $discountRawCents,
'applied_discount_cents' => $discountCents,
'net_charge_cents' => $totalAmountCents - $discountCents,
'net_charge_cents' => $netChargeCents,
'totalAmountCents' => $totalAmountCents,
'discountCents' => $discountCents,
'paidCents' => $paidCents,
'completedRefundCents' => $refundPaidCents,
'rawBalanceCents' => $rawBalanceCents,
'balanceDueCents' => $balanceCents,
'customerCreditCents' => max(0, -$rawBalanceCents),
'customerCreditCents' => $customerCreditCents,
'tuition_total' => $this->fromCents($tuitionCents),
'event_total' => $this->fromCents($eventCents),
'additional_total' => $this->fromCents($additionalCents),
@@ -136,7 +138,7 @@ class InvoiceLedgerService
'paid_amount' => $this->fromCents($paidCents),
'refund_paid_total' => $this->fromCents($refundPaidCents),
'total_amount' => $this->fromCents($totalAmountCents),
'customer_credit' => $this->fromCents(max(0, -$rawBalanceCents)),
'customer_credit' => $this->fromCents($customerCreditCents),
'balance' => $this->fromCents($balanceCents),
'status' => $status,
'has_discount' => $discountCents > 0 ? 1 : 0,
@@ -164,6 +164,7 @@ class RefundEligibilityService
{
return match ($sourceType) {
'invoice_overpayment' => $this->invoiceCreditCents($parentId, $invoiceId ?: $sourceId),
'tuition_withdrawal' => $this->invoicePaidCents($parentId, $invoiceId ?: $sourceId),
'payment_duplicate', 'payment_correction' => $this->paymentCreditCents($parentId, $invoiceId, $sourceId),
'credit_memo', 'administrative_credit' => 0,
default => 0,
@@ -175,6 +176,22 @@ class RefundEligibilityService
return $sourceType !== 'invoice_overpayment';
}
protected function invoicePaidCents(int $parentId, int $invoiceId): int
{
if ($invoiceId <= 0) {
return 0;
}
$invoice = $this->invoiceModel->find($invoiceId);
if (!$invoice || (int)($invoice['parent_id'] ?? 0) !== $parentId) {
return 0;
}
$ledger = $this->invoiceLedgerService->calculateInvoice($invoiceId);
return max(0, (int)($ledger['paidCents'] ?? 0));
}
protected function invoiceCreditCents(int $parentId, int $invoiceId): int
{
if ($invoiceId <= 0) {