Fix semester context, attendance rosters, and billing workflows
- 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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user