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

34 KiB
Raw Blame History

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:

$newBalance = $newTotal - $invDiscount - $invRefunds - $paidOnInv;

Cash refunds are incorrectly subtracted.

The same method writes display-style statuses:

Paid
Partially Paid
Unpaid

instead of canonical machine statuses.

Required change

Delete the local balance calculation.

After creating or updating invoice data, call only:

$ledger = $invoiceLedgerService->recalculate($invoiceId);

Use the returned values:

$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

raw balance =
    net charges
  - valid payments
  + completed cash refunds
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:

[
    '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:

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:

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:

InvoiceLedgerService::getValidPaymentTotalCents(int $invoiceId): int

Only include successful financial states.

Suggested included states:

successful
completed
paid

Suggested excluded states:

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:

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

invoice_overpayment
payment_duplicate
payment_correction
credit_memo
administrative_credit

Each refund must store:

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:

RefundEligibilityService

Required methods:

calculateAvailableCredit(
    int $parentId,
    ?int $invoiceId,
    string $sourceType,
    int $sourceId
): RefundEligibilityResult
validateRequestedAmount(
    RefundEligibilityResult $eligibility,
    int $requestedAmountCents
): void

Calculation

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

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

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:

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

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

UNIQUE(idempotency_key)
FOREIGN KEY(refund_id)
FOREIGN KEY(reversed_payout_id)
amount_cents > 0

Payout types

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

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

completed payouts = 0
→ approved

0 < completed payouts < approved amount
→ partially_paid

completed payouts = approved amount
→ paid

Never allow:

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:

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:

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:

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:

InvoiceLedgerService::getInvoiceProjection($invoiceId)
ParentLedgerService::getParentProjection($parentId, $schoolYear)
RefundEligibilityService::calculateAvailableCredit(...)

Parent projection must include

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:

insert()
update()
save()
delete()

must have its return value checked.

Required pattern:

if (!$model->update($id, $data)) {
    throw new FinancialPersistenceException(
        'REFUND_UPDATE_FAILED',
        $model->errors()
    );
}

After all writes:

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:

{
  "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

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:

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

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:

requested_discount_cents
eligible_base_cents
applied_discount_cents

Reports must use applied_discount_cents.

Formula

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:

amount_cents is always nonnegative
charge_type = add | deduct

Ledger interpretation:

add    → +amount
deduct → -amount

Creation and editing must use the same normalization.

Database constraints

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:

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:

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

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:

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

draft
issued
unpaid
partially_paid
paid
credited
voided

Refund

requested
approved
partially_paid
paid
rejected
cancelled
reversed
exception

Reimbursement

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:

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:

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:

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:

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.