Files
alrahma_sunday_school/docs/financial_fix1.md
T
root 0ae9993d82
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 49s
Tests / PHPUnit (push) Failing after 1m20s
fix enrollment and email send to parent
2026-08-27 12:39:08 -04:00

1800 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Exact Financial Remediation Plan
## Scope
This plan covers all currently identified defects in:
* Invoices
* Payments and manual payments
* Refund requests, approvals, payouts, and recalculation
* Discounts and vouchers
* Additional charges
* Expenses
* Reimbursements and reimbursement batches
* Purchase orders and inventory receiving
* Financial reports
* Routes, APIs, authorization, and persistence
* Historical data integrity
* Automated testing
The implementation must follow the order below. Later work depends on earlier accounting rules being correct.
---
# Work Package 1: Remove Every Duplicate Invoice-Balance Formula
## F-001: Correct `InvoiceController::generateInvoice()`
### Defect
`app/Controllers/View/InvoiceController.php` still calculates:
```php
$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;
```
Cash refunds are incorrectly subtracted.
The same method writes display-style statuses:
```text
Paid
Partially Paid
Unpaid
```
instead of canonical machine statuses.
### Required change
Delete the local balance calculation.
After creating or updating invoice data, call only:
```php
$ledger = $invoiceLedgerService->recalculate($invoiceId);
```
Use the returned values:
```php
$ledger->totalAmountCents
$ledger->discountCents
$ledger->paidCents
$ledger->completedRefundCents
$ledger->rawBalanceCents
$ledger->balanceDueCents
$ledger->customerCreditCents
$ledger->status
```
Do not calculate invoice status in the controller.
### Required formula inside the ledger
```text
raw balance =
net charges
- valid payments
+ completed cash refunds
```
```text
amount due = max(0, raw balance)
customer credit = max(0, -raw balance)
```
### Acceptance criteria
* No invoice balance formula remains in `InvoiceController`.
* No invoice status is manually assigned in `InvoiceController`.
* Generated invoices and recalculated invoices return identical totals.
* Status values are lowercase canonical values.
### Required tests
* Invoice with no payment.
* Partially paid invoice.
* Fully paid invoice.
* Overpaid invoice.
* Partially refunded invoice.
* Fully refunded overpayment.
* Payment after refund.
* Invoice generated twice with identical source data.
---
## F-002: Remove financial calculations from manual-payment views
### Defect
`app/Views/payment/manual_pay.php` calculates balances in PHP and JavaScript using the wrong refund sign.
### Required change
The controller must supply each invoice with:
```php
[
'invoice_id' => ...,
'display_total' => ...,
'balance_due_cents' => ...,
'customer_credit_cents' => ...,
'status' => ...,
]
```
The view must only display those values.
Remove JavaScript calculations based on:
* Invoice total
* Discounts
* Payments
* Refunds
* Existing balance fields
JavaScript may calculate only the amount entered by the user versus the already-provided backend balance.
### Acceptance criteria
Repository search shows no invoice accounting formula in:
```text
app/Views
public JavaScript
inline JavaScript
```
The manual-payment screen shows exactly the same balance as the ledger API.
### Required tests
* Backend ledger balance equals rendered data attribute.
* Full-payment button uses backend-provided balance.
* Refunded invoice displays the corrected amount due.
* Customer credit is never displayed as a negative balance.
---
# Work Package 2: Establish One Canonical Financial Service
## F-003: Make `InvoiceLedgerService` the only invoice calculator
### Required change
All financial entry points must call the same service.
Required callers include:
* Invoice creation
* Invoice regeneration
* Payment creation
* Manual payment creation
* Payment reversal
* Discount application
* Discount removal
* Additional-charge application
* Refund approval
* Refund payout
* Refund reversal
* Financial reports
* Parent financial summaries
### Repository enforcement
Search for all occurrences of:
```text
refund_paid_amount
paid_amount
discount_amount
balance
total_amount
SUM(
Partially Paid
Unpaid
Paid
```
Classify every occurrence as:
* Canonical ledger implementation
* Database migration
* Display-only
* Invalid duplicate calculation
Delete or replace every invalid duplicate.
### Acceptance criteria
Outside the ledger and migrations:
* Controllers do not calculate balances.
* Views do not calculate balances.
* Reports do not calculate balances.
* JavaScript does not calculate balances.
* API controllers do not calculate balances.
---
## F-004: Define valid payment contribution centrally
### Required change
Create one method:
```php
InvoiceLedgerService::getValidPaymentTotalCents(int $invoiceId): int
```
Only include successful financial states.
Suggested included states:
```text
successful
completed
paid
```
Suggested excluded states:
```text
pending
failed
declined
cancelled
voided
reversed
refunded
chargeback
```
Normalize actual project status values before implementing the query.
All refund eligibility, invoice balances, parent summaries, and reports must use this method or its underlying canonical query.
### Acceptance criteria
No raw `SUM(paid_amount)` is used to determine refundable credit or invoice balance outside the ledger repository.
### Required tests
Create one payment in every supported status and verify that only valid final payments contribute.
---
# Work Package 3: Freeze Historical Invoice Values
## F-005: Stop recalculating issued invoices from live configuration
### Defect
The current ledger rebuilds historical invoices from current:
* Enrollments
* Tuition settings
* Student class assignments
* Event charges
* Refund deadlines
* Calculator versions
### Required schema
Create `invoice_lines`:
```text
id
invoice_id
line_type
source_type
source_id
description
quantity
unit_amount_cents
line_amount_cents
discount_eligible
calculation_version
metadata_json
created_at
updated_at
voided_at
```
### Required behavior
When an invoice is issued:
1. Calculate tuition and charges once.
2. Save immutable invoice lines.
3. Calculate totals only from those lines.
4. Never reload current enrollment or current pricing to recalculate that invoice.
Allowed post-issue changes:
* Credit memo
* Debit adjustment
* New additional-charge line
* Voiding and reissuing the invoice
Disallowed changes:
* Replacing historical tuition based on current configuration
* Replacing event fees based on current event records
* Rewriting original issued lines
### Legacy migration
For existing invoices:
* Where historical source details are trustworthy, backfill individual lines.
* Where they are not trustworthy, create a single `legacy_invoice_total` line preserving the issued total.
* Do not rebuild old invoices from current tuition settings.
### Acceptance criteria
Changing current tuition, enrollment, event configuration, or refund deadline does not alter an issued invoice.
### Required tests
* Issue invoice.
* Change tuition configuration.
* Recalculate after payment.
* Confirm invoice charges remain unchanged.
Repeat for enrollment and event changes.
---
# Work Package 4: Rebuild Refund Source and Eligibility Rules
## F-006: Require a specific refund source
### Required source types
```text
invoice_overpayment
payment_duplicate
payment_correction
credit_memo
administrative_credit
```
Each refund must store:
```text
source_type
source_id
invoice_id
parent_id
requested_amount_cents
approved_amount_cents
```
### Rules
#### Invoice overpayment
Must reference an invoice with current customer credit.
#### Duplicate payment
Must reference one specific valid payment.
#### Payment correction
Must reference the payment being corrected.
#### Credit memo
Must reference the approved credit memo.
#### Administrative credit
Must reference an approved administrative adjustment record.
### Prohibited behavior
* No source-free duplicate-payment refund.
* No refund created from only parent ID and amount.
* No fallback to the latest invoice.
* No attaching a payout to an invoice without credit.
### Acceptance criteria
Every refund can be traced to a specific source record.
---
## F-007: Create one refund-eligibility service
Create:
```php
RefundEligibilityService
```
Required methods:
```php
calculateAvailableCredit(
int $parentId,
?int $invoiceId,
string $sourceType,
int $sourceId
): RefundEligibilityResult
```
```php
validateRequestedAmount(
RefundEligibilityResult $eligibility,
int $requestedAmountCents
): void
```
### Calculation
```text
available refundable amount =
current source credit
- completed payouts for the source
- active approved unpaid refund reservations
```
For partially paid refunds, reserve only the unpaid remainder.
Pending requests should not automatically become payable. Approval must recalculate eligibility.
### Result object
```php
RefundEligibilityResult {
sourceCreditCents
completedPayoutCents
reservedAmountCents
availableAmountCents
reasonCodes
}
```
### Acceptance criteria
Refund request, approval, payout, and reports all use the same eligibility service.
---
# Work Package 5: Fix Refund Request Creation
## F-008: Validate refund requests transactionally
### Request flow
1. Validate authentication and permission.
2. Validate source type and source ID.
3. Load the source.
4. Verify source ownership.
5. Calculate current eligibility.
6. Reject zero or negative amounts.
7. Reject amounts above available credit.
8. Create the refund request.
9. Write an audit record.
### Required validation
* Parent exists.
* Invoice exists when required.
* Invoice belongs to parent.
* Payment exists when required.
* Payment belongs to the invoice and parent.
* Payment has a refundable status.
* Currency matches.
* Amount is positive.
* Amount does not exceed source availability.
### Acceptance criteria
A request cannot be saved with an unsupported or missing source.
---
# Work Package 6: Fix Refund Approval
## F-009: Lock and revalidate during approval
### Current defect
Approval reads refund state before transaction locking and does not revalidate current credit.
### Required transaction
```text
BEGIN
SELECT refund FOR UPDATE
verify current status = requested
SELECT source invoice/payment/credit FOR UPDATE
recalculate available refundable amount
verify requested amount <= available amount
verify no conflicting approved refund exists
set approved_amount
set status = approved
set approved_by
set approved_at
insert audit event
COMMIT
```
### Required state transitions
Allowed:
```text
requested → approved
requested → rejected
```
Rejected, cancelled, paid, and reversed refunds cannot be approved.
### Segregation of duties
For configured high-value thresholds:
* Requester cannot approve their own request.
* A second approver is required.
### Acceptance criteria
Two concurrent approvals against the same $100 credit cannot approve more than $100 total.
### Required concurrency test
Start two approval transactions for separate $100 refund requests against the same $100 credit.
Expected:
* One succeeds.
* One fails after locked revalidation.
---
# Work Package 7: Replace Mutable Refund Aggregates with Immutable Payouts
## F-010: Create `refund_payouts`
### Required schema
```text
id
refund_id
amount_cents
currency
payout_type
payment_method
status
external_reference
check_number
check_date
evidence_path
idempotency_key
processed_by
processed_at
reversed_payout_id
failure_code
failure_message
created_at
updated_at
```
### Required constraints
```text
UNIQUE(idempotency_key)
FOREIGN KEY(refund_id)
FOREIGN KEY(reversed_payout_id)
amount_cents > 0
```
### Payout types
```text
cash_out
account_credit
reversal
```
Only completed `cash_out` payouts increase invoice amount due by reducing retained cash.
An account credit must be represented as a credit balance or credit memo, not as cash leaving the organization.
### Migration
Convert existing `refund_paid_amount` values into legacy payout rows.
Preserve original:
* Payment method
* Check number
* External reference
* Date
* User
Where details are missing, mark the payout as `legacy_import` rather than inventing evidence.
### Aggregate fields
`refund_paid_amount` may remain temporarily as a cached projection, but it must not be the source of truth.
### Acceptance criteria
Every partial payout is a separate immutable row.
---
## F-011: Implement idempotent refund payout processing
### Required input
Every payout request must contain an idempotency key.
For browser requests, generate an operation UUID before submission and reuse it for retries.
### Required transaction
```text
BEGIN
check idempotency key
if payout exists:
return existing result
SELECT refund FOR UPDATE
verify status is approved or partially_paid
SELECT source records FOR UPDATE
recalculate current refundable amount
recalculate unpaid approved refund remainder
verify payout amount <= both values
insert refund_payout as processing
perform or register payout operation
update payout to completed
derive refund status from payout totals
recalculate invoice ledger
insert audit event
COMMIT
```
### Required status derivation
```text
completed payouts = 0
→ approved
0 < completed payouts < approved amount
→ partially_paid
completed payouts = approved amount
→ paid
```
Never allow:
```text
completed payouts > approved amount
```
### Acceptance criteria
Submitting the same payout request repeatedly produces one payout row and one financial effect.
---
## F-012: Remove invoice fallback during payout
### Required change
Delete logic that attaches an unassigned refund to the latest invoice.
If no eligible invoice or source credit exists:
```text
reject payout
```
Do not guess.
### Acceptance criteria
A refund with no valid credit source cannot be paid.
---
## F-013: Prevent refund target below already-paid amount
### Required rule
When recalculating an approved refund:
```text
new approved amount >= completed payout total
```
If current credit falls below already-paid amount:
* Do not reduce the approved amount below the paid amount.
* Mark the refund as an exception requiring reconciliation.
* Create a recovery or adjustment workflow if money was over-refunded.
### Acceptance criteria
The database cannot contain:
```text
completed payout total > approved amount
```
Add a reconciliation query and automated alert.
---
## F-014: Implement refund reversals
Paid payout rows cannot be edited or deleted.
To reverse:
1. Lock original payout.
2. Verify it is completed.
3. Verify unreversed amount.
4. Create a reversal payout referencing the original.
5. Recalculate refund status.
6. Recalculate invoice ledger.
7. Record reason and authorizer.
### Acceptance criteria
Original payout history remains unchanged.
---
# Work Package 8: Fix Refund Summaries and Parent Recalculation
## F-015: Replace raw refund summary queries
### Current defects
Refund summaries:
* Use `SUM(amount)` instead of canonical payment amount.
* Ignore discounts in some parent calculations.
* Use different formulas for parent-level and invoice-level credit.
### Required change
Delete direct summary arithmetic from `RefundController`.
Use:
```php
InvoiceLedgerService::getInvoiceProjection($invoiceId)
ParentLedgerService::getParentProjection($parentId, $schoolYear)
RefundEligibilityService::calculateAvailableCredit(...)
```
### Parent projection must include
```text
net invoice charges
valid payments
completed cash refunds
customer credit
approved refund reservations
available refundable credit
```
### Acceptance criteria
Parent summary, invoice summary, refund request screen, and refund payout validation show identical available-credit values.
---
# Work Package 9: Fix Refund Persistence and Error Handling
## F-016: Check every financial write
Every call to:
```php
insert()
update()
save()
delete()
```
must have its return value checked.
Required pattern:
```php
if (!$model->update($id, $data)) {
throw new FinancialPersistenceException(
'REFUND_UPDATE_FAILED',
$model->errors()
);
}
```
After all writes:
```php
if ($db->transStatus() === false) {
throw new FinancialPersistenceException('TRANSACTION_FAILED');
}
```
### Apply to
* Refund updates
* Refund payout inserts
* Invoice ledger projection update
* Discount usage
* Voucher usage count
* Expense insert/update
* Reimbursement insert/update
* Batch updates
* Purchase-order receiving
* Inventory updates
* Financial audit records
### Acceptance criteria
No financial endpoint returns success after a failed model write.
---
## F-017: Handle uploaded evidence safely
### Current defect
Refund evidence is stored before transaction success, leaving orphaned files after rollback.
### Required flow
1. Validate upload.
2. Store in temporary location.
3. Begin transaction.
4. Create payout.
5. Commit transaction.
6. Move file to permanent location.
7. If move fails, mark payout evidence as incomplete and alert operations.
Alternatively, use durable object storage with temporary status and finalize after commit.
On rollback, delete temporary upload.
### Acceptance criteria
Failed payout attempts leave no permanent orphaned evidence files.
---
## F-018: Stop leaking internal exceptions
### Required change
Client response:
```json
{
"success": false,
"code": "REFUND_PAYOUT_FAILED",
"message": "The refund could not be processed."
}
```
Server log:
* Full exception
* Stack trace
* Request ID
* Refund ID
* User ID
* Idempotency key
### Acceptance criteria
Database messages, SQL, file paths, and stack traces never appear in client responses.
---
# Work Package 10: Fix Routes, Authorization, and API Bypasses
## F-019: Protect every financial route explicitly
Every financial route must require:
* Authentication
* Exact permission
* Correct HTTP method
* CSRF protection for browser writes
* Ownership or organizational scope validation
### Permissions
```text
invoice.view
invoice.create
invoice.adjust
payment.view
payment.create
payment.reverse
refund.view
refund.request
refund.approve
refund.pay
refund.reverse
discount.apply
discount.manage
expense.manage
expense.approve
reimbursement.create
reimbursement.approve
reimbursement.pay
purchase_order.manage
financial_report.view
```
### High-risk routes to inspect immediately
* Invoice status changes
* Invoice generation
* Manual payment
* Payment balance update
* Refund request
* Refund approval
* Refund payout
* Refund status update
* Expense update
* Reimbursement process
* Reimbursement batch lock
* PO receiving
* Inventory adjustment
### Acceptance criteria
Anonymous users receive `401`.
Authenticated users without permission receive `403`.
---
## F-020: Remove generic CRUD bypasses
### Required repository audit
Inspect generic `/api/v1` endpoints and any CRUD controllers for direct access to:
* Payments
* Refunds
* Expenses
* Reimbursements
* Discounts
* Additional charges
* Invoices
### Required rule
Financial models cannot be mutated through generic CRUD.
Allowed approaches:
* Remove financial entities from generic CRUD routing.
* Make financial models read-only through generic APIs.
* Route commands through approved financial services.
### Acceptance criteria
There is no API endpoint that can directly set:
```text
invoice balance
invoice status
payment status
refund paid amount
refund approved amount
reimbursement amount
expense approval
discount applied amount
```
without domain validation.
---
## F-021: Add route integrity tests
Automated test must verify:
* Controller class exists.
* Method exists.
* Route parameters match method signature.
* State-changing routes do not use GET.
* Required filters are attached.
---
# Work Package 11: Fix Discounts and Vouchers
## F-022: Validate voucher eligibility inside a locked transaction
### Required checks
* Voucher exists.
* Voucher is active.
* Current date is within validity range.
* School year matches.
* Semester matches.
* Parent and invoice are eligible.
* Usage limit remains.
* Voucher has not already been applied to the invoice.
* Discount amount is positive.
### Transaction
```text
BEGIN
SELECT voucher FOR UPDATE
SELECT invoice FOR UPDATE
recheck validity
recheck usage count
calculate eligible invoice base
calculate actual applied amount
insert discount usage
increment voucher usage
recalculate ledger
COMMIT
```
### Acceptance criteria
Two simultaneous requests cannot consume the last voucher use twice.
---
## F-023: Store actual applied discount
Store:
```text
requested_discount_cents
eligible_base_cents
applied_discount_cents
```
Reports must use `applied_discount_cents`.
### Formula
```text
applied discount =
min(requested discount, remaining eligible base)
```
Percentage discounts must apply only to eligible lines.
### Acceptance criteria
The amount shown on the invoice, voucher usage, and financial report is identical.
---
## F-024: Define multiple-discount ordering
Recommended deterministic order:
1. Approved credit memo
2. Fixed discount
3. Percentage discount
4. Administrative adjustment
Every discount applies to the remaining eligible base.
### Database constraint
Prevent duplicate voucher use on the same invoice unless the voucher explicitly supports repeated application.
### Required tests
* Two fixed discounts.
* Fixed plus percentage.
* Two percentages.
* Discount exceeds eligible base.
* Event fees excluded from eligibility.
* Concurrent final voucher use.
---
## F-025: Dispatch events only after commit
Do not trigger notifications or integration events before the transaction commits.
Use an outbox record or post-commit dispatch.
---
# Work Package 12: Fix Additional Charges
## F-026: Derive ownership from invoice
When an `invoice_id` is provided:
1. Load invoice.
2. Derive parent, school year, and semester from invoice.
3. Ignore or reject conflicting request values.
### Acceptance criteria
A charge cannot be saved for parent A while recalculating parent Bs invoice.
---
## F-027: Use one amount-sign model
Recommended storage:
```text
amount_cents is always nonnegative
charge_type = add | deduct
```
Ledger interpretation:
```text
add → +amount
deduct → -amount
```
Creation and editing must use the same normalization.
### Database constraints
```text
amount_cents >= 0
charge_type IN ('add', 'deduct')
```
---
## F-028: Make applied charges immutable
Once a charge generates an issued invoice line:
* Do not edit it directly.
* Do not change its sign.
* Do not change its amount.
* Do not delete it.
Correction requires:
* Void original charge line.
* Create replacement adjustment line.
* Record reason and user.
---
## F-029: Complete pending-charge workflow
Statuses:
```text
pending
approved
applied
rejected
voided
```
Implement explicit commands for approval and application.
A pending charge must not remain indefinitely without a valid workflow.
---
# Work Package 13: Fix Expenses
## F-030: Make expense creation atomic with receipt persistence
### Required flow
1. Validate request.
2. Upload receipt to temporary storage.
3. Begin transaction.
4. Insert expense.
5. Commit.
6. Finalize receipt.
7. On failure, remove temporary file.
Check the insert result.
### School-year validation
Require one canonical format:
```text
YYYY-YYYY
```
Do not fall back to a single four-digit year.
---
## F-031: Make reimbursed expenses immutable
If an expense has a paid or active reimbursement:
Block direct changes to:
* Amount
* Category
* Purchaser
* School year
* Receipt
* Donation flag
Correction requires reversal and replacement.
### Donation conversion
When changing a donation into a reimbursable expense:
* Reset approval status to pending.
* Clear approver.
* Clear approval timestamp.
* Require the normal approval workflow.
---
# Work Package 14: Fix Reimbursements
## F-032: Make reimbursement creation atomic
### Transaction
```text
BEGIN
SELECT expense FOR UPDATE
verify expense exists
verify approved
verify positive amount
verify not donation
verify not already reimbursed
verify correct recipient
verify correct accounting period
insert reimbursement
link expense
insert audit event
COMMIT
```
### Database constraint
One active reimbursement per expense.
Use either:
* A unique `expense_id` in reimbursements, or
* A filtered/derived uniqueness strategy supported by the database.
---
## F-033: Disable the legacy reimbursement process endpoint
The weaker legacy `process()` route must be removed or rewritten to call the same reimbursement service.
No separate validation path may remain.
---
## F-034: Make paid reimbursements immutable
Paid reimbursement fields cannot be overwritten.
Correction requires:
* Reversal record
* Replacement reimbursement
* Link to original transaction
* Reason and authorization
---
## F-035: Make reimbursement batches all-or-nothing
### Required transaction
1. Lock batch.
2. Verify batch is open.
3. Lock all active batch items.
4. Lock all associated expenses.
5. Validate every item.
6. Create every reimbursement.
7. Verify every item succeeded.
8. Close batch.
9. Commit.
Do not skip invalid entries.
If one item fails, roll back the entire batch.
### Batch number
Replace `MAX + 1` without locking.
Use:
* Locked yearly sequence row, and
* Unique database constraint on year plus sequence.
---
# Work Package 15: Fix Purchase Orders and Inventory
## F-036: Validate purchase-order values
Required constraints:
```text
quantity > 0
unit_cost_cents >= 0
received_quantity >= 0
received_quantity <= ordered_quantity
```
Reject negative unit cost.
---
## F-037: Make receiving atomic
### Required transaction
1. Lock purchase order.
2. Lock every affected PO item.
3. Verify supply exists before changing received quantity.
4. Lock inventory supply row.
5. Increment inventory atomically.
6. Insert inventory transaction.
7. Update received quantity.
8. Query all PO lines.
9. Mark PO received only when all lines are complete.
10. Commit.
Do not calculate completion from submitted rows only.
### Acceptance criteria
Omitting one incomplete line from the request cannot mark the PO fully received.
---
## F-038: Handle cancellation after partial receipt
Do not allow direct cancellation after inventory was received.
Required options:
* Reject cancellation, or
* Create inventory reversal transactions before cancellation.
Never silently remove the financial or inventory history.
---
# Work Package 16: Normalize Statuses
## F-039: Create canonical machine statuses
### Invoice
```text
draft
issued
unpaid
partially_paid
paid
credited
voided
```
### Refund
```text
requested
approved
partially_paid
paid
rejected
cancelled
reversed
exception
```
### Reimbursement
```text
pending
approved
paid
rejected
reversed
```
### Payment
Use one documented status set and one normalization map.
### Required change
* Migrate legacy display-style values.
* Update every query.
* Update every model validation rule.
* Remove arbitrary status-update endpoints.
* Derive invoice status from ledger values.
### Database enforcement
Use enum or check constraints where supported.
---
# Work Package 17: Replace Independent Report Arithmetic
## F-040: Build report projections from the canonical ledger
Reports must not sum raw mutable fields independently.
Create queryable projections for:
```text
invoice gross charges
applied discounts
net charges
valid payments
completed cash refunds
amount due
customer credit
approved refund reservations
available refundable credit
reimbursed expense amount
outstanding reimbursement amount
```
### Required report corrections
* Use actual applied discount, not requested discount.
* Use completed payout rows, not `refund_paid_amount`.
* Use valid payment status filters.
* Use frozen invoice lines.
* Use canonical customer credit.
### Acceptance criteria
For every invoice:
```text
invoice screen total
payment screen total
refund screen total
financial report total
API total
```
must match exactly.
---
# Work Package 18: Resolve the Detached Payment-Transaction Subsystem
## F-041: Decide whether to remove or integrate it
Inspect `PaymentTransactionController` and `PaymentTransactionModel`.
Choose one:
### Remove
Delete unused controller, model, and routes if the subsystem is obsolete.
### Integrate
If required:
* Add explicit routes.
* Validate amount greater than zero.
* Validate referenced payment exists.
* Persist every accepted field.
* Restrict statuses.
* Exclude invalid statuses from totals.
* Route changes through the canonical payment service.
Do not leave unreachable or partially implemented financial code in the application.
---
# Work Package 19: Database Constraints and Indexes
## F-042: Add database-level financial invariants
Required constraints should include:
* Unique invoice identity.
* Unique refund payout idempotency key.
* One active reimbursement per expense.
* Unique voucher use per invoice where required.
* Unique batch sequence per year.
* Positive payout amounts.
* Nonnegative charge storage amounts.
* Valid status values.
* Foreign keys for financial relationships.
* Unique external payment/refund references where appropriate.
Application checks are not enough. Requests can race, imports can bypass controllers, and future developers will eventually discover confidence.
---
# Work Package 20: Historical Reconciliation
## F-043: Produce a correction report before migration
For every invoice calculate:
```text
stored total
canonical frozen charge total
valid payment total
completed refund payout total
applied discount total
expected balance due
expected customer credit
```
Flag:
* Wrong refund sign
* Invalid status
* Duplicate invoice
* Excess refund
* Refund without source
* Refund payout above approval
* Discount above eligible base
* Reimbursement mismatch
* Paid record edited later
* Negative or impossible values
### Correction policy
Do not silently overwrite history.
Use:
* Adjustment entry
* Credit memo
* Debit adjustment
* Reversal
* Replacement transaction
Record who approved every correction.
---
# Work Package 21: Automated Test Suite
## F-044: Unit tests
Required formula tests:
* No payment
* Partial payment
* Exact payment
* Overpayment
* Partial refund
* Full refund
* Multiple refund payouts
* Refund reversal
* Failed payment
* Reversed payment
* Chargeback
* Fixed discount
* Percentage discount
* Discount cap
* Multiple-discount ordering
* Additional charge
* Deduction
* Cent-rounding boundaries
---
## F-045: Integration tests
Required workflows:
* Invoice → payment → refund
* Invoice → discount → payment → refund
* Overpayment → request → approval → payout
* Duplicate payment refund
* Stale refund approval
* Stale refund payout
* Failed database write rollback
* Uploaded evidence rollback
* Historical invoice after tuition change
* Historical invoice after enrollment change
* Reimbursement creation
* Reimbursement reversal
* PO partial receiving
* Inventory update failure
* Report-to-ledger equality
---
## F-046: Concurrency tests
Run simultaneous requests for:
* Duplicate invoice generation
* Final voucher usage
* Refund approval
* Refund payout
* Reimbursement creation
* Reimbursement batch closing
* PO receiving
* Inventory increment
Expected outcomes:
* No duplicate financial event.
* No over-refund.
* No partial transaction.
* No lost inventory update.
* One valid winner where exclusivity is required.
---
## F-047: Authorization tests
Test every financial route for:
* Anonymous access
* Authenticated user without permission
* Wrong parent or organization
* Invalid CSRF token
* Wrong HTTP method
* Invalid status transition
* Invalid payment method
* Direct generic API mutation
---
## F-048: Repository regression checks
Add CI checks that fail when financial formulas appear outside approved services.
Search patterns should include:
```text
refund_paid_amount
SUM(paid_amount)
SUM(amount)
newBalance
balance =
total - discount
total - paid
Partially Paid
Unpaid
Paid
```
The check should use an allowlist for migrations and canonical repositories.
---
# Work Package 22: Production Acceptance Gate
The system is not production-ready until all answers below are yes.
## Accounting
* Is there one invoice balance formula?
* Do cash refunds reduce retained cash correctly?
* Are amount due and customer credit mutually exclusive?
* Are issued invoice charges immutable?
* Do reports match the ledger?
## Refunds
* Does every refund reference a valid source?
* Is eligibility revalidated at request, approval, and payout?
* Are approved amounts reserved?
* Can concurrent requests over-approve credit?
* Can concurrent requests pay twice?
* Are payouts immutable?
* Is payout retry idempotent?
* Can a paid refund be reversed without editing history?
* Is latest-invoice fallback removed?
## Persistence
* Does every failed write roll back?
* Are model return values checked?
* Are transaction results checked?
* Are database constraints present?
* Are uploads cleaned up after failure?
## Security
* Is every financial route authenticated?
* Is every write permission-protected?
* Are generic CRUD bypasses removed?
* Are ownership checks enforced?
* Are internal exceptions hidden?
## Other financial modules
* Are discounts locked and capped?
* Are applied charges immutable?
* Are reimbursed expenses immutable?
* Are reimbursements atomic?
* Are reimbursement batches all-or-nothing?
* Is PO receiving atomic?
* Can partially received orders be cancelled safely?
## Verification
* Do unit tests pass?
* Do integration tests pass?
* Do concurrency tests pass?
* Do authorization tests pass?
* Does historical reconciliation have zero unexplained differences?
---
# Required Implementation Order
Implement in this exact order:
1. F-001 through F-004: remove duplicate formulas and centralize payment status rules.
2. F-005: freeze issued invoice values.
3. F-006 through F-018: rebuild refund source, approval, payout, reversal, persistence, and summaries.
4. F-019 through F-021: secure routes and remove API bypasses.
5. F-022 through F-025: repair discounts.
6. F-026 through F-029: repair additional charges.
7. F-030 through F-035: repair expenses and reimbursements.
8. F-036 through F-038: repair purchase orders and inventory.
9. F-039 through F-042: normalize statuses and add database constraints.
10. F-043: reconcile historical data.
11. F-044 through F-048: complete automated verification.
12. Run the production acceptance gate.