# Laravel-Compatible School Financial System Plan With Carryforward Balances, Collections, Events, Receipts, Installments, Notifications, and Stakeholder Reporting

## 1. Purpose

This document updates the Laravel-compatible financial-system plan into a fuller school finance operating plan.

The earlier Laravel compatibility plan correctly prioritized keeping the old financial system working while adding read-only stakeholder reporting. That remains the right baseline. The expanded plan now adds:

```text
non-paying parent follow-up
event charges and event billing
receipt notification system
installment plans
tuition and fee forecasting
stakeholder financial reporting
parent-facing balances
previous-year balance carryforward
admin collection workflows
school-wide financial controls
audit trails
future ledger hardening
```

The purpose is to cover the financial lifecycle of a school without breaking the existing system. This is not a heroic rewrite. Heroic rewrites are how people discover backups are decorative.

## 2. Non-Negotiable Compatibility Rule

The existing financial system must keep working while improvements are added.

Rules:

```text
Do not delete existing finance routes in the first pass.
Do not remove existing PayPal records, routes, controllers, or reports until a separate decommission is approved.
Do not rewrite payment, invoice, refund, discount, expense, reimbursement, or event charge flows destructively.
Do not mutate historical invoice math during reporting.
Do not silently reinterpret old statuses.
Add new services and reporting endpoints beside the old flow first.
Use feature flags for new behavior that may affect money.
Prefer read-only visibility before write-path replacement.
```

The older financial plan recommended aggressive PayPal removal. That may still happen later, but not inside the compatibility phase. Keeping finance working wins first. Cleanup can wait its turn like an adult.

## 3. Laravel Mapping

| Plan concept | Laravel-compatible location |
|---|---|
| Routes | `routes/api.php`, `routes/web.php` |
| Controllers | `app/Http/Controllers/Api/Finance/*` |
| Services | `app/Services/Finance/*` |
| Form validation | `app/Http/Requests/Finance/*` |
| Authorization | middleware, policies, gates, permissions |
| Models | `app/Models/*` |
| Migrations | `database/migrations/*` |
| Jobs | `app/Jobs/*` |
| Notifications | `app/Notifications/*` |
| Mailables, if used | `app/Mail/*` |
| Console commands | `app/Console/Commands/*` |
| Exports | streamed CSV responses or export classes |
| Private financial files | `storage/app/private/*` |
| Config flags | `config/finance.php` and/or `configuration` table |

## 4. Target School Finance Capabilities

The complete school finance system should support:

```text
student tuition billing
family-based tuition discounts
event charges
additional fees
manual payments
installment plans
previous-year balance carryforward
partial payments
refunds
discounts and waivers
expenses
reimbursements
receipts
parent notifications
non-paying parent follow-up
collection notes
promise-to-pay tracking
financial file uploads
audit logs
stakeholder reports
school-year financial comparison
tuition forecast
collection forecast
aging reports
CSV exports
future ledger recalculation
```

The system must distinguish between:

| Concept | Meaning |
|---|---|
| Invoice | formal amount owed |
| Charge | a billable item on an invoice |
| Payment | money received |
| Refund | money returned or owed back |
| Discount | approved reduction of amount owed |
| Installment plan | schedule for expected payments |
| Carryforward balance | prior-year unpaid amount moved into the new-year financial view |
| Receipt | proof/payment acknowledgement |
| Follow-up | collection workflow for unpaid balances |
| Report | read-only financial analysis |
| Ledger | source of calculated financial truth |

These cannot all be one `status` column having an identity crisis.

## 5. Implementation Strategy

Use phased implementation.

## Phase 1: Additive Reporting and Visibility

Keep all existing write flows untouched.

Add:

```text
StakeholderFinancialAnalysisService
ParentPaymentFollowUpService
EventChargeReportingService
InstallmentReportingService
PriorYearBalanceCarryforwardService
ReceiptNotificationReadinessService
CSV exports
administrator report endpoints
```

This phase is mostly read-only.

## Phase 2: Follow-Up Workflow

Add operational collection tools:

```text
follow-up statuses
collection notes
promise-to-pay dates
last contacted tracking
next follow-up dates
manual reminder generation
bulk follow-up export
```

This phase writes follow-up metadata only. It must not change invoice balances.

## Phase 2B: Previous-Year Balance Carryforward

Add prior-year balance handling before full ledger replacement:

```text
prior-year open balance report
parent-level carryforward preview
student-level balance detail
new-year carryforward charge creation
carryforward adjustment/waiver workflow
carryforward audit trail
parent statement display
collection queue inclusion
stakeholder report inclusion
```

This phase must not silently mutate old invoices. Prior-year invoices remain historically visible. The new year receives a separate carryforward record or charge that references the original unpaid invoices. That way the system can collect old debt without rewriting history like a tiny accounting dictatorship.

## Phase 3: Notifications and Receipts

Add:

```text
payment receipt notifications
installment due reminders
overdue balance reminders
event charge notices
manual resend receipt action
notification logs
notification preferences
```

Start with manual/admin-triggered notifications. Then add scheduled reminders after balances are trusted.

## Phase 4: Event Charges and Installment Hardening

Add or standardize:

```text
event charge lifecycle
event registration billing
invoice line items
installment schedules
installment payment allocation
overdue installment reporting
```

Do not overwrite legacy charge logic until mapping is proven.

## Phase 5: Centralized Ledger

Introduce:

```text
InvoiceLedgerService
finance.ledger_mode = legacy|centralized
dry-run recalculation command
write-path transaction locks
normalized financial statuses
audit logs
```

This is the dangerous phase. It needs tests and dry-runs, because “we changed how balances are calculated” is not a sentence accounting enjoys.

## 6. Previous-Year Balance Carryforward

## 6.1 Purpose

The financial system must carry unpaid balances from a completed school year into the new school year so staff can collect old balances while still preserving historical invoices.

Required rule:

```text
Previous-year unpaid balances must remain visible on the original year invoices.
The new school year may show and collect those balances through a separate carryforward record or charge.
The system must not silently edit, close, or recalculate old invoices just to make the new year look clean.
```

This is not optional. If prior-year balances disappear during new-year enrollment, the school has not simplified finance. It has misplaced money with a login screen.

## 6.2 Carryforward Source

The carryforward source should be the prior school year open balance at the parent/family level.

Use:

```text
all unpaid or partially paid invoices from the previous school year
minus valid payments already recorded
minus approved discounts/waivers
plus valid charges still owed
minus paid refunds only when they actually reduce collectible balance under the business rule
```

The first implementation may read the existing legacy invoice balance field if the old system is still the source of truth. In the later centralized ledger phase, the carryforward preview should use `InvoiceLedgerService` in dry-run mode.

Supported carryforward scopes:

```text
single parent/family
all parents in a school year
single student detail for display
full school-year rollover batch
```

## 6.3 Carryforward Record Design

Create a separate carryforward table or use a dedicated invoice line item type. Recommended additive table:

```text
finance_balance_carryforwards
```

Suggested fields:

```text
id
parent_id
student_id nullable
from_school_year
to_school_year
source_invoice_ids_json
source_balance_amount
carryforward_amount
waived_amount default 0.00
adjusted_amount default 0.00
remaining_amount
status
reason nullable
created_by nullable
approved_by nullable
created_at
approved_at nullable
posted_invoice_id nullable
posted_invoice_line_item_id nullable
metadata_json nullable
```

Recommended statuses:

```text
draft
pending_review
approved
posted_to_new_year
partially_paid
paid
waived
cancelled
superseded
```

The carryforward record should reference the source invoices. Do not copy vague totals with no traceability. Future humans will ask where the number came from, and “the system said so” is not an audit trail.

## 6.4 Carryforward Charge Type

When a carryforward is posted to the new year, create one explicit new-year charge or invoice line item:

```text
charge_type = previous_year_balance
source_type = balance_carryforward
source_id = finance_balance_carryforwards.id
description = Previous year balance from {from_school_year}
amount = carryforward_amount - waived_amount + adjusted_amount
```

This allows the new-year parent statement to show:

```text
New-year tuition
Event charges
Additional charges
Previous-year balance
Payments
Remaining balance
```

The old invoice stays untouched. The new charge makes the old balance collectible in the new year. See how easy life gets when history and current billing stop fighting over the same row.

## 6.5 Parent Enrollment Rule

During returning-student enrollment, prior-year balance should be visible to the parent and staff.

Recommended display:

```text
Previous-year balance: $X.XX
Current-year expected tuition: $Y.YY
Total expected amount due: $Z.ZZ
```

Business policy options:

```text
allow enrollment with prior-year balance but flag for follow-up
allow enrollment only after minimum payment
require admin override if balance exceeds threshold
require payment plan before placement/final enrollment
```

Recommended default for compatibility:

```text
Do not block enrollment in the first implementation.
Show the balance.
Add the parent to the collection queue.
Allow admin to require payment or installment plan later by configuration.
```

Suggested configuration keys:

```text
finance.carryforward.enabled = true
finance.carryforward.auto_post_to_new_year = false
finance.carryforward.block_enrollment_threshold = null
finance.carryforward.require_admin_review_threshold = 0.00
finance.carryforward.allow_installment_plan = true
```

## 6.6 Carryforward Preview Before Posting

Carryforward must use a preview/finalize pattern.

Preview should show:

```text
parent/family
students linked to balance
source invoice IDs
source school year
target school year
source balance
proposed carryforward amount
payments recorded after preview date
existing carryforward records
warnings
```

Warnings:

```text
source invoice already carried forward
negative or zero balance
parent has active installment plan
invoice balance changed since preview
parent has duplicate family account
student withdrawn/inactive
missing parent link
```

Finalize only after admin approval.

## 6.7 Payments Against Carryforward

Payments in the new year can be allocated to carryforward balance using one of these policies:

```text
oldest debt first
current tuition first
manual allocation by staff
parent-selected allocation if allowed
```

Recommended default:

```text
oldest debt first unless an active installment plan defines another allocation order
```

Payment receipts must show allocation clearly:

```text
Payment received: $300.00
Applied to previous-year balance: $150.00
Applied to current-year tuition: $150.00
Remaining previous-year balance: $0.00
Remaining current-year balance: $500.00
```

Do not hide payment allocation. Hidden allocation creates parent disputes, and parent disputes create meetings, which are how afternoons go to die.

## 6.8 Follow-Up and Reporting Impact

Carryforward balances must appear in:

```text
Parent Payment Follow-Up Queue
Non-Paying Parent Aging Report
Stakeholder Financial Summary
Collection Forecast
Parent Statement
Parent Balance Page
Installment Plan Setup
Receipt Allocation Detail
School-Year Comparison
```

Reports must separate:

```text
current-year charges
previous-year carried balances
total receivables
collected against previous-year balances
collected against current-year charges
waived previous-year balances
```

Stakeholder reporting should show prior-year carryforward explicitly so leadership can distinguish current operating revenue from old receivables collection.

## 6.9 Laravel Services and Endpoints

Suggested service:

```text
app/Services/Finance/PriorYearBalanceCarryforwardService.php
```

Responsibilities:

```text
find prior-year open balances
build carryforward preview
detect duplicate carryforward records
create draft carryforward records
finalize approved carryforwards
post carryforward charge to target year
calculate remaining carryforward balance
produce carryforward CSV export
```

Suggested requests:

```text
app/Http/Requests/Finance/CarryforwardPreviewRequest.php
app/Http/Requests/Finance/CarryforwardFinalizeRequest.php
app/Http/Requests/Finance/CarryforwardAdjustmentRequest.php
```

Suggested routes:

```php
Route::prefix('finance/carryforwards')->middleware(['auth:api'])->group(function () {
    Route::get('preview', [BalanceCarryforwardController::class, 'preview']);
    Route::post('draft', [BalanceCarryforwardController::class, 'storeDraft']);
    Route::post('{carryforward}/approve', [BalanceCarryforwardController::class, 'approve']);
    Route::post('{carryforward}/post', [BalanceCarryforwardController::class, 'postToNewYear']);
    Route::post('{carryforward}/waive', [BalanceCarryforwardController::class, 'waive']);
    Route::post('{carryforward}/adjust', [BalanceCarryforwardController::class, 'adjust']);
    Route::get('report', [BalanceCarryforwardController::class, 'report']);
    Route::get('report/csv', [BalanceCarryforwardController::class, 'reportCsv']);
});
```

## 6.10 Carryforward Acceptance Rules

Required tests:

```text
prior-year unpaid invoice appears in carryforward preview
paid prior-year invoice is excluded
partially paid invoice carries only remaining balance
duplicate carryforward is blocked
carryforward posting creates a new-year charge without changing old invoice
parent statement separates prior-year balance from current-year tuition
payment allocation can reduce carryforward balance
receipt shows amount applied to carryforward
follow-up queue includes parents with carryforward balance
stakeholder report separates old receivables from current-year revenue
waived carryforward remains auditable
unauthorized users cannot preview or post carryforwards
```

## 7. Non-Paying Parent Follow-Up System

## 7.1 Purpose

The finance system must help staff identify and follow up with parents who have unpaid balances.

This is not the same as stakeholder reporting. Stakeholder reporting answers, “How much money is outstanding?” Follow-up answers, “Who needs to be contacted, what happened last time, and what happens next?”

## 7.2 Parent Follow-Up Report

Create:

```text
app/Services/Finance/ParentPaymentFollowUpService.php
app/Http/Requests/Finance/ParentPaymentFollowUpRequest.php
```

Suggested endpoints:

```php
Route::prefix('finance')->group(function () {
    Route::get('parent-payment-followups', [FinancialController::class, 'parentPaymentFollowups']);
    Route::get('parent-payment-followups/csv', [FinancialController::class, 'parentPaymentFollowupsCsv']);
});
```

Administrator aliases:

```php
Route::prefix('administrator/reports')->group(function () {
    Route::get('parent-payment-followups', [FinancialController::class, 'parentPaymentFollowups']);
    Route::get('parent-payment-followups/csv', [FinancialController::class, 'parentPaymentFollowupsCsv']);
});
```

## 7.3 Filters

The follow-up report should support:

```text
school_year
date_from
date_to
minimum_balance
aging_bucket
parent_id
student_id
class_id
grade_level_id
follow_up_status
last_contacted_before
next_follow_up_due_before
promise_to_pay_due_before
include_paid_invoices = false by default
include_inactive_students = false by default
```

## 7.4 Aging Buckets

Use standard aging buckets:

```text
current
1-30 days overdue
31-60 days overdue
61-90 days overdue
90+ days overdue
```

If invoice due dates are missing, bucket by invoice creation date and show a warning. Missing due dates are not a personality trait. They are bad data.

## 7.5 Follow-Up Row Fields

Each row should include:

```text
parent_id
parent_name
parent_email
parent_phone
student_names
school_year
invoice_count
open_invoice_count
total_invoiced
total_paid
total_discounted
total_refunded
open_balance
oldest_unpaid_invoice_date
days_overdue
aging_bucket
last_payment_date
last_payment_amount
last_contacted_at
last_contacted_by
follow_up_status
next_follow_up_date
promise_to_pay_date
promise_to_pay_amount
escalation_level
notes_count
risk_flags
```

## 7.6 Follow-Up Statuses

Create normalized statuses:

```text
not_contacted
contacted
left_message
email_sent
sms_sent
promise_to_pay
partial_payment_received
disputed_balance
needs_admin_review
financial_aid_review
escalated
resolved
do_not_contact
```

## 7.7 Follow-Up Notes

Add a table:

```text
finance_follow_up_notes
```

Recommended fields:

```text
id
parent_id
invoice_id nullable
school_year nullable
status
note
contact_method nullable
promise_to_pay_date nullable
promise_to_pay_amount nullable
next_follow_up_date nullable
escalation_level default 0
created_by
created_at
updated_at
```

Laravel migration:

```php
Schema::create('finance_follow_up_notes', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('parent_id')->index();
    $table->unsignedBigInteger('invoice_id')->nullable()->index();
    $table->string('school_year')->nullable()->index();
    $table->string('status')->default('not_contacted')->index();
    $table->string('contact_method')->nullable();
    $table->date('promise_to_pay_date')->nullable();
    $table->decimal('promise_to_pay_amount', 12, 2)->nullable();
    $table->date('next_follow_up_date')->nullable()->index();
    $table->unsignedTinyInteger('escalation_level')->default(0);
    $table->text('note')->nullable();
    $table->unsignedBigInteger('created_by')->nullable()->index();
    $table->timestamps();
});
```

## 7.8 Follow-Up Write Endpoints

Suggested endpoints:

```php
Route::post('parent-payment-followups/{parent}/note', [FinancialController::class, 'storeParentFollowUpNote']);
Route::post('parent-payment-followups/{parent}/mark-contacted', [FinancialController::class, 'markParentContacted']);
Route::post('parent-payment-followups/{parent}/promise-to-pay', [FinancialController::class, 'storePromiseToPay']);
Route::post('parent-payment-followups/{parent}/resolve', [FinancialController::class, 'resolveParentFollowUp']);
```

These should update follow-up metadata only. They must not alter invoices or payments.

## 7.9 Collection Queue

Create a queue view ordered by:

```text
highest overdue balance
then oldest unpaid invoice
then 90+ aging bucket
then promise-to-pay missed date
then next follow-up due date
```

Staff should not manually hunt through invoices. The system should tell them who needs attention. That is the entire point of having a system instead of a drawer full of vibes.

## 8. Event Charges

## 8.1 Purpose

The school finance system must support event billing without confusing it with tuition.

Examples:

```text
field trips
fundraisers
graduation fees
exam fees
competition fees
special activities
supplies
lunch programs
after-school events
camp registrations
```

## 8.2 Event Charge Model

If a compatible table already exists, reuse it. Otherwise add:

```text
event_charges
```

Recommended fields:

```text
id
event_id nullable
student_id nullable
parent_id nullable
invoice_id nullable
school_year nullable
title
description nullable
amount
quantity default 1
total_amount
status
due_date nullable
created_by
approved_by nullable
created_at
updated_at
```

Statuses:

```text
draft
approved
invoiced
paid
partially_paid
waived
voided
cancelled
refunded
```

## 8.3 Event Charge Service

Create:

```text
app/Services/Finance/EventChargeService.php
app/Services/Finance/EventChargeReportingService.php
app/Http/Requests/Finance/EventChargeRequest.php
```

Responsibilities:

```text
create event charge
approve event charge
attach event charge to invoice
void event charge
report event revenue
report unpaid event charges
exclude voided/cancelled charges
```

## 8.4 Event Charge Endpoints

Suggested endpoints:

```php
Route::get('event-charges', [EventChargeController::class, 'index']);
Route::post('event-charges', [EventChargeController::class, 'store']);
Route::get('event-charges/{eventCharge}', [EventChargeController::class, 'show']);
Route::patch('event-charges/{eventCharge}', [EventChargeController::class, 'update']);
Route::post('event-charges/{eventCharge}/approve', [EventChargeController::class, 'approve']);
Route::post('event-charges/{eventCharge}/void', [EventChargeController::class, 'void']);
Route::post('event-charges/{eventCharge}/attach-to-invoice', [EventChargeController::class, 'attachToInvoice']);
```

## 8.5 Event Charge Rules

```text
An event charge must belong to a parent, student, invoice, or event context.
Draft event charges must not affect invoice balances.
Approved but uninvoiced event charges appear in pending charge reports.
Invoiced event charges affect invoice balances only through invoice line items or ledger calculation.
Voided/cancelled event charges are excluded from active totals.
Refunded event charges are reflected through refund records, not negative mystery charges.
Event charge reports must distinguish invoiced versus not yet invoiced.
```

## 8.6 Event Charge Parent Notification

When an event charge is approved or invoiced, the system should be able to notify the parent.

Notification types:

```text
event_charge_created
event_charge_due_soon
event_charge_overdue
event_charge_paid
event_charge_voided
```

Do not automatically notify on draft charges. Drafts are staff thoughts. Parents do not need to receive staff thoughts.

## 9. Installment Plans

## 9.1 Purpose

Installments let families pay tuition or invoice balances over time.

The system must distinguish:

```text
invoice amount owed
installment schedule
payments received
installment due status
```

A payment is not an installment. A payment may satisfy one or more installments.

## 9.2 Installment Tables

Add:

```text
invoice_installment_plans
invoice_installments
```

Recommended `invoice_installment_plans` fields:

```text
id
invoice_id
parent_id
school_year
plan_name
total_amount
number_of_installments
status
created_by
approved_by nullable
created_at
updated_at
```

Statuses:

```text
draft
active
completed
cancelled
defaulted
restructured
```

Recommended `invoice_installments` fields:

```text
id
installment_plan_id
invoice_id
sequence
due_date
amount_due
amount_paid
balance
status
paid_at nullable
created_at
updated_at
```

Statuses:

```text
pending
due
partially_paid
paid
overdue
waived
cancelled
```

## 9.3 Payment Allocation

Add:

```text
payment_installment_allocations
```

Fields:

```text
id
payment_id
invoice_id
installment_id
amount
created_at
updated_at
```

Rules:

```text
Payments can be manually allocated to installments.
If no allocation is provided, allocate oldest due installment first.
Do not allocate voided/failed payments.
Do not allow allocation above installment balance unless admin override is recorded.
Installment balance should be recalculated from allocations, not hand-edited casually like a whiteboard.
```

## 9.4 Installment Service

Create:

```text
app/Services/Finance/InstallmentPlanService.php
app/Services/Finance/InstallmentReportingService.php
app/Http/Requests/Finance/InstallmentPlanRequest.php
```

Responsibilities:

```text
create installment plan
generate schedule
activate plan
allocate payment
recalculate installment balances
mark overdue installments
report due/overdue installments
cancel or restructure plan
```

## 9.5 Installment Endpoints

Suggested endpoints:

```php
Route::get('installment-plans', [InstallmentPlanController::class, 'index']);
Route::post('invoices/{invoice}/installment-plans', [InstallmentPlanController::class, 'store']);
Route::get('installment-plans/{plan}', [InstallmentPlanController::class, 'show']);
Route::post('installment-plans/{plan}/activate', [InstallmentPlanController::class, 'activate']);
Route::post('installment-plans/{plan}/cancel', [InstallmentPlanController::class, 'cancel']);
Route::post('installment-plans/{plan}/restructure', [InstallmentPlanController::class, 'restructure']);
Route::post('payments/{payment}/allocate-installments', [InstallmentPlanController::class, 'allocatePayment']);
Route::get('installments/due', [InstallmentPlanController::class, 'due']);
Route::get('installments/overdue', [InstallmentPlanController::class, 'overdue']);
```

## 9.6 Installment Notifications

Notification types:

```text
installment_plan_created
installment_due_soon
installment_due_today
installment_overdue
installment_payment_received
installment_plan_completed
installment_plan_cancelled
```

Start with admin-triggered notifications. Scheduled reminders can come later after staff confirms due dates and balances are reliable.

## 10. Receipt and Financial Notification System

## 10.1 Purpose

Parents should receive clear financial communication:

```text
payment receipt
refund receipt
invoice created
event charge added
installment plan created
installment due reminder
overdue balance reminder
discount applied
balance updated
statement available
```

The system must log notifications so staff can prove what was sent.

## 10.2 Laravel Notifications

Use Laravel notifications:

```text
app/Notifications/Finance/PaymentReceiptNotification.php
app/Notifications/Finance/RefundReceiptNotification.php
app/Notifications/Finance/InvoiceCreatedNotification.php
app/Notifications/Finance/EventChargeNotification.php
app/Notifications/Finance/InstallmentReminderNotification.php
app/Notifications/Finance/OverdueBalanceReminderNotification.php
app/Notifications/Finance/StatementAvailableNotification.php
```

Channels:

```text
mail
database
sms later if provider exists
```

Do not assume SMS exists. Human beings love asking for SMS after nobody budgeted for SMS.

## 10.3 Notification Log

Add:

```text
finance_notification_logs
```

Fields:

```text
id
parent_id nullable
student_id nullable
invoice_id nullable
payment_id nullable
refund_id nullable
event_charge_id nullable
installment_id nullable
notification_type
channel
recipient
subject nullable
status
sent_at nullable
failed_at nullable
error_message nullable
created_by nullable
created_at
updated_at
```

Statuses:

```text
queued
sent
failed
skipped
cancelled
```

## 10.4 Notification Preferences

Add optional parent preferences:

```text
finance_notification_preferences
```

Fields:

```text
id
parent_id
email_receipts_enabled default true
email_reminders_enabled default true
sms_receipts_enabled default false
sms_reminders_enabled default false
statement_notifications_enabled default true
created_at
updated_at
```

Required rule:

```text
Receipts should remain available even if reminders are disabled.
```

Disabling reminders should not erase receipts. That would be like refusing a thermometer because you dislike weather.

## 10.5 Receipt Rules

A receipt should be generated or sent when:

```text
payment is recorded
refund is paid
manual receipt resend is requested
payment is edited and receipt regeneration is approved
payment is voided and void notice is enabled
```

Receipt must include:

```text
receipt number
payment date
parent name
student names if available
invoice number
school year
payment method
amount paid
remaining balance
staff member or system source
school contact information
```

## 10.6 Receipt Numbering

Add receipt numbering if not already present:

```text
finance_receipt_sequences
payment_carryforward_allocations optional if payments are split across old/new balances
```

Fields:

```text
id
school_year
prefix
next_number
created_at
updated_at
```

Receipt format:

```text
R-{school_year}-{sequence}
```

Example:

```text
R-2025-2026-000123
```

Never derive receipt numbers from payment IDs alone if parents see them. Internal database IDs are not receipt policy. They are plumbing.

## 10.7 Notification Endpoints

Suggested endpoints:

```php
Route::post('payments/{payment}/send-receipt', [FinanceNotificationController::class, 'sendPaymentReceipt']);
Route::post('refunds/{refund}/send-receipt', [FinanceNotificationController::class, 'sendRefundReceipt']);
Route::post('invoices/{invoice}/send-statement', [FinanceNotificationController::class, 'sendInvoiceStatement']);
Route::post('parents/{parent}/send-overdue-reminder', [FinanceNotificationController::class, 'sendOverdueReminder']);
Route::post('installments/{installment}/send-reminder', [FinanceNotificationController::class, 'sendInstallmentReminder']);
Route::get('notification-logs', [FinanceNotificationController::class, 'logs']);
```

## 11. School Financial Matter Coverage

A complete school finance system should group functions into domains.

## 11.1 Billing

```text
tuition invoices
registration fees
event charges
activity fees
supplies fees
late fees if approved by policy
additional charges
discounts
waivers
scholarships or financial aid
```

## 11.2 Collections

```text
manual payments
partial payments
installments
payment allocation
receipts
parent statements
overdue reminders
collection notes
promise-to-pay tracking
escalation workflow
```

## 11.3 Adjustments

```text
discounts
waivers
refunds
voided payments
reversed charges
write-offs
balance corrections
audit-approved adjustments
```

## 11.4 Spending

```text
expenses
reimbursements
vendor payments
staff reimbursements
receipt attachments
approval statuses
expense category reporting
```

## 11.5 Reporting

```text
stakeholder dashboard
cash collected
gross and net revenue
open receivables
aging report
event revenue
tuition forecast
collection forecast
expense report
reimbursement report
payment method mix
school-year comparison
class/grade financial summary
parent balance report
audit exports
```

## 11.6 Controls

```text
authorization
parent ownership checks
financial file access control
transaction logging
notification logging
audit trails
dry-run recalculation
data export controls
role-based permissions
```

## 12. Stakeholder Financial Analysis and Reporting

Stakeholders need management-level visibility, not operational noise.

Report metrics:

```text
gross revenue
net revenue
cash collected
refunds
discounts
expenses
reimbursements
net cash
open receivables
collection rate
expense ratio
operating margin
invoice count
paying family count
non-paying family count
payment method mix
event charge revenue
tuition revenue
additional charge revenue
installment receivables
monthly charge/collection/expense trend
receivables aging
largest open invoices
highest-risk parents
school-year comparison
CSV export
```

The stakeholder report must not mutate invoices or trigger recalculation. Reports that change accounting are booby traps with dashboards.

## 13. Tuition Forecast and Collection Prediction

Keep the old tuition calculator as default.

Add services:

```text
app/Services/Finance/Tuition/OldTuitionCalculatorService.php
app/Services/Finance/Tuition/NewTuitionCalculatorService.php
app/Services/Finance/Tuition/TuitionForecastService.php
```

Config:

```text
tuition_calculator_version = old|new
```

New tuition rule:

```text
first student: full amount
second student: full amount minus $50
third student: full amount minus $50
fourth and later students: full amount minus $100 each
```

Forecast must show:

```text
old projected tuition
new projected tuition
difference
already collected
remaining collectible
family breakdown
student breakdown
warnings
CSV export
```

Forecast must be read-only.

## 14. Authorization and Security

Every finance route must be protected.

Minimum:

```text
auth:api
admin.access or equivalent admin/staff middleware
```

Preferred permissions:

```text
view_financial_reports
view_parent_balances
manage_payments
manage_refunds
manage_discounts
manage_event_charges
manage_installments
send_finance_notifications
manage_expenses
manage_reimbursements
export_financial_reports
```

Parent-facing routes must enforce ownership:

```text
A parent can only view their own invoices, payments, receipts, installment plans, statements, and event charges.
```

File access must use IDs, not raw filenames.

## 15. Centralized Ledger Future Phase

Add later:

```text
app/Services/Finance/InvoiceLedgerService.php
```

Feature flag:

```text
finance.ledger_mode = legacy|centralized
```

When centralized mode is enabled, financial writes eventually follow:

```php
DB::transaction(function () use ($invoiceId) {
    Invoice::query()->whereKey($invoiceId)->lockForUpdate()->firstOrFail();

    // create/update payment, refund, discount, event charge, installment allocation, or adjustment

    $this->invoiceLedgerService->recalculate($invoiceId);
});
```

Ledger must calculate:

```text
tuition charges
event charges
additional charges
discounts
waivers
payments
refunds
installment allocations
write-offs
balance
status
```

Do not activate centralized mode until tests and dry-run reports exist. This is accounting, not a place to practice optimism.

## 16. Database Additions Summary

Potential new tables:

```text
finance_follow_up_notes
finance_balance_carryforwards
event_charges
invoice_installment_plans
invoice_installments
payment_installment_allocations
finance_notification_logs
finance_notification_preferences
finance_receipt_sequences
payment_carryforward_allocations optional if payments are split across old/new balances
```

Optional later tables:

```text
invoice_line_items
finance_adjustments
finance_audit_logs
tuition_forecasts
parent_statements
```

If equivalent tables already exist, map to them instead of creating duplicates. Duplicate finance tables are how systems become haunted.

## 17. API Endpoint Summary

Suggested route groups:

```php
Route::prefix('finance')->middleware(['auth:api'])->group(function () {
    Route::get('stakeholder-analysis', [FinancialController::class, 'stakeholderAnalysis']);
    Route::get('stakeholder-analysis/csv', [FinancialController::class, 'stakeholderAnalysisCsv']);

    Route::get('parent-payment-followups', [FinancialController::class, 'parentPaymentFollowups']);
    Route::get('parent-payment-followups/csv', [FinancialController::class, 'parentPaymentFollowupsCsv']);
    Route::post('parent-payment-followups/{parent}/note', [FinancialController::class, 'storeParentFollowUpNote']);
    Route::post('parent-payment-followups/{parent}/promise-to-pay', [FinancialController::class, 'storePromiseToPay']);
    Route::post('parent-payment-followups/{parent}/resolve', [FinancialController::class, 'resolveParentFollowUp']);

    Route::get('carryforwards/preview', [BalanceCarryforwardController::class, 'preview']);
    Route::post('carryforwards/draft', [BalanceCarryforwardController::class, 'storeDraft']);
    Route::post('carryforwards/{carryforward}/approve', [BalanceCarryforwardController::class, 'approve']);
    Route::post('carryforwards/{carryforward}/post', [BalanceCarryforwardController::class, 'postToNewYear']);
    Route::post('carryforwards/{carryforward}/waive', [BalanceCarryforwardController::class, 'waive']);
    Route::post('carryforwards/{carryforward}/adjust', [BalanceCarryforwardController::class, 'adjust']);
    Route::get('carryforwards/report', [BalanceCarryforwardController::class, 'report']);
    Route::get('carryforwards/report/csv', [BalanceCarryforwardController::class, 'reportCsv']);

    Route::apiResource('event-charges', EventChargeController::class);
    Route::post('event-charges/{eventCharge}/approve', [EventChargeController::class, 'approve']);
    Route::post('event-charges/{eventCharge}/void', [EventChargeController::class, 'void']);
    Route::post('event-charges/{eventCharge}/attach-to-invoice', [EventChargeController::class, 'attachToInvoice']);

    Route::apiResource('installment-plans', InstallmentPlanController::class);
    Route::post('invoices/{invoice}/installment-plans', [InstallmentPlanController::class, 'store']);
    Route::post('installment-plans/{plan}/activate', [InstallmentPlanController::class, 'activate']);
    Route::post('installment-plans/{plan}/cancel', [InstallmentPlanController::class, 'cancel']);
    Route::post('installment-plans/{plan}/restructure', [InstallmentPlanController::class, 'restructure']);
    Route::post('payments/{payment}/allocate-installments', [InstallmentPlanController::class, 'allocatePayment']);
    Route::get('installments/due', [InstallmentPlanController::class, 'due']);
    Route::get('installments/overdue', [InstallmentPlanController::class, 'overdue']);

    Route::post('payments/{payment}/send-receipt', [FinanceNotificationController::class, 'sendPaymentReceipt']);
    Route::post('refunds/{refund}/send-receipt', [FinanceNotificationController::class, 'sendRefundReceipt']);
    Route::post('invoices/{invoice}/send-statement', [FinanceNotificationController::class, 'sendInvoiceStatement']);
    Route::post('parents/{parent}/send-overdue-reminder', [FinanceNotificationController::class, 'sendOverdueReminder']);
    Route::post('installments/{installment}/send-reminder', [FinanceNotificationController::class, 'sendInstallmentReminder']);
    Route::get('notification-logs', [FinanceNotificationController::class, 'logs']);
});
```

Route names and controllers should be adjusted to match existing project conventions.

## 18. Reports Required

Admin/staff reports:

```text
Stakeholder Financial Summary
Parent Payment Follow-Up Queue
Non-Paying Parent Aging Report
Promise-to-Pay Report
Missed Promise-to-Pay Report
Prior-Year Balance Carryforward Preview
Posted Carryforward Balance Report
Carryforward Payments and Waivers Report
Overdue Installment Report
Upcoming Installments Report
Event Charge Revenue Report
Uninvoiced Event Charges Report
Receipt Notification Log
Failed Notification Report
Payment Method Mix Report
Discount and Waiver Report
Refund Report
Expense Report
Reimbursement Report
Tuition Forecast
Collection Forecast
School-Year Comparison
Audit Export
```

Parent-facing views:

```text
My Balance
Previous-Year Balance
My Invoices
My Payments
My Receipts
My Installment Plans
My Event Charges
My Statements
```

## 19. Notification Schedule

Start with manual notifications.

Later scheduled jobs:

```text
daily: mark overdue installments
daily: generate overdue parent follow-up queue
daily: send installment due soon reminders
weekly: send overdue balance reminders
weekly: send previous-year balance reminders when enabled
monthly: generate parent statements
monthly: stakeholder financial digest
```

Laravel jobs/commands:

```text
app/Console/Commands/Finance/MarkOverdueInstallmentsCommand.php
app/Console/Commands/Finance/SendInstallmentRemindersCommand.php
app/Console/Commands/Finance/SendOverdueBalanceRemindersCommand.php
app/Console/Commands/Finance/GenerateMonthlyStatementsCommand.php
```

Use dry-run mode for all scheduled financial notifications:

```text
--dry-run
--commit
```

If a notification job cannot dry-run, it is not ready to email parents. The inbox is not a test environment, despite how many systems behave.

## 20. Audit and Logging

Every financial write should eventually produce an audit event.

Audit targets:

```text
invoice created
invoice updated
payment recorded
payment edited
payment voided
refund created
refund paid
discount applied
discount removed
event charge created
event charge approved
event charge voided
installment plan created
installment plan activated
installment plan restructured
follow-up note added
promise-to-pay recorded
carryforward created
carryforward approved
carryforward posted to new year
carryforward waived or adjusted
receipt sent
notification failed
expense approved
reimbursement paid
```

Audit fields:

```text
actor_id
action
entity_type
entity_id
before_json nullable
after_json nullable
ip_address nullable
user_agent nullable
created_at
```

## 21. Testing Plan

Required tests:

```text
stakeholder report does not mutate invoices
non-paying parent report only includes parents with open balances
aging buckets calculate correctly
follow-up note stores without changing invoice balance
promise-to-pay appears in collection queue
prior-year balance appears in carryforward preview
posting carryforward creates a new-year charge without changing old invoice
parent statement separates current-year and prior-year balances
event charge draft does not affect invoice balance
approved event charge appears in pending charge report
invoiced event charge appears in invoice totals when ledger mode supports it
installment plan creates correct schedule
payment allocation applies oldest installment first
overdue installments are detected correctly
receipt notification logs sent status
failed notification logs failed status
parent cannot view another parent's receipt or invoice
unauthorized user cannot export financial reports
CSV totals match JSON totals
old tuition forecast remains unchanged
new tuition rule applies family discounts correctly
```

## 22. Acceptance Criteria

This expanded Laravel financial plan is complete when:

```text
Existing finance endpoints still work.
Existing PayPal endpoints remain untouched unless a separate decommission is approved.
Stakeholder reporting works.
Non-paying parent follow-up report works.
Follow-up notes and promise-to-pay tracking work.
Previous-year balances can be previewed, carried forward, reported, and collected without changing old invoices.
Event charges can be reported and later attached to invoices safely.
Installment plans can be created, tracked, and reported.
Payment receipts can be sent and logged.
Overdue reminders can be manually triggered and logged.
Notification preferences are respected.
Financial exports exist for admin use.
Parent-facing balances remain access-controlled.
No report mutates invoice balances.
New write workflows are transaction-safe.
Ledger replacement remains behind a feature flag.
Tests cover reporting, collections, event charges, installments, and notifications.
```

## 23. Final Target Flow

Financial action flow:

```text
Authenticated staff action
        ↓
Permission check
        ↓
Form request validation
        ↓
Policy/ownership check
        ↓
DB transaction
        ↓
Lock affected invoice when balance-impacting
        ↓
Create/update payment, refund, charge, installment, discount, or note
        ↓
Recalculate through ledger when centralized mode is enabled
        ↓
Write audit log
        ↓
Send/log notification if requested
        ↓
Return response
```

Follow-up flow:

```text
System calculates open balances
        ↓
Aging buckets and risk flags are assigned
        ↓
Staff reviews collection queue
        ↓
Staff records contact note or promise-to-pay
        ↓
System updates next follow-up date
        ↓
Optional reminder is sent and logged
        ↓
Resolved only when balance is paid, waived, or admin-reviewed
```

Carryforward flow:

```text
System previews previous-year open balances
        ↓
Admin reviews source invoices and warnings
        ↓
Draft carryforward records are created
        ↓
Admin approves carryforward
        ↓
Carryforward is posted as a separate new-year charge
        ↓
Payments are allocated and receipts show old/new balance split
        ↓
Reports show prior-year receivables separately from current-year charges
```

Event charge flow:

```text
Staff creates draft event charge
        ↓
Admin approves charge
        ↓
Parent is notified if configured
        ↓
Charge is attached to invoice or reported as pending
        ↓
Payment/refund follows normal finance rules
```

Installment flow:

```text
Staff creates installment plan
        ↓
Plan schedule is generated
        ↓
Parent receives plan notice
        ↓
Payments are allocated to installments
        ↓
System tracks due, overdue, paid, and completed states
        ↓
Receipts and reminders are logged
```

The brutal summary: school finance needs one disciplined lifecycle for money owed, money collected, money adjusted, money spent, and money chased. Anything less becomes a spreadsheet with login screens.
