40 KiB
Remaining Financial Issues Remediation Plan
Project baseline: Archive_2.zip
Purpose: Fix every remaining financial defect identified in the latest audit.
Release status until completion: NO-GO
This plan is intentionally implementation-specific. Each item names the affected component, the required code behavior, database work, tests, and acceptance evidence. An item is not complete because the code was changed. It is complete only when its tests pass and the acceptance checks are demonstrated.
1. Delivery Rules
Every remediation item must be delivered with:
- The code change.
- Any required migration.
- Unit tests.
- Integration tests.
- Concurrency or retry tests where relevant.
- Authorization tests for routes and commands.
- A repository search proving the old implementation no longer exists.
- Before-and-after reconciliation evidence for affected financial records.
Do not combine unrelated fixes into one oversized pull request. Each pull request must be independently reviewable and reversible.
Recommended branch sequence:
finance/additional-charge-ledger-linesfinance/discount-eligible-basefinance/refund-eligibility-correctionsfinance/refund-state-and-idempotencyfinance/invoice-snapshot-atomicityfinance/payment-idempotencyfinance/expense-reimbursement-hardeningfinance/reporting-projectionsfinance/status-route-migrationfinance/migration-enforcement-tests
Phase 1: Fix Invoice-Line Integrity
FIN-001: Persist applied additional charges as immutable invoice lines
Current defect
New invoices use invoice_lines as the authoritative charge source, but applying an additional charge only changes the additional_charges row and recalculates the invoice. No invoice line is created. The charge can be marked applied while the invoice total remains unchanged.
Primary files
app/Controllers/View/ExtraChargesController.phpapp/Libraries/InvoiceLedgerService.php- Additional charge and invoice-line models
- Related migrations
Required implementation
Create one domain method:
InvoiceAdjustmentService::applyAdditionalCharge(
int $chargeId,
int $invoiceId,
int $actorId
): InvoiceLedgerResult
Inside one transaction:
- Lock the additional charge row with
FOR UPDATE. - Lock the invoice row with
FOR UPDATE. - Verify the invoice exists and is not voided.
- Verify the charge belongs to the same parent, school year, and semester as the invoice.
- Verify the charge status is
approved. - Verify no active invoice line already references this charge.
- Convert the charge to a signed amount:
addbecomes positive.deductbecomes negative.
- Insert an immutable
invoice_linesrow:line_type = additional_chargeoradditional_deductionsource_type = additional_chargesource_id = charge.idline_amount_cents = signed amountdiscount_eligibleaccording to the approved business rule
- Update the charge to
applied. - Save
applied_invoice_line_id,applied_by, andapplied_at. - Recalculate the invoice projection.
- Insert an audit event.
- Check every write result and transaction status.
- Commit.
Required schema changes
Add to additional_charges if missing:
applied_invoice_line_id
applied_by
applied_at
voided_by
voided_at
void_reason
Add a unique constraint preventing more than one active invoice line for the same additional charge source.
Reversal behavior
Do not change an applied charge back to pending.
Create:
InvoiceAdjustmentService::reverseAdditionalCharge(
int $chargeId,
string $reason,
int $actorId
): InvoiceLedgerResult
The reversal must:
- Lock the original charge and invoice.
- Verify the original charge is applied.
- Insert a reversing invoice line with the opposite amount.
- Preserve the original line unchanged.
- Mark the charge
reversed. - Link the reversing line to the original line.
- Recalculate the invoice.
- Record an audit event.
Fallback ledger correction
When legacy invoices have no invoice lines, additional-charge fallback logic must:
- Include only
status = applied. - Include only charges attached to the invoice.
- Apply
charge_typecorrectly. - Exclude pending, approved, rejected, voided, and reversed charges.
Tests
- Apply positive charge and verify invoice increases.
- Apply deduction and verify invoice decreases.
- Apply same charge twice and verify second request fails.
- Reverse applied charge and verify a reversing line is created.
- Verify original line remains unchanged.
- Verify pending and approved charges do not affect fallback totals.
- Concurrently apply the same charge twice; exactly one succeeds.
Acceptance evidence
- Every applied charge has exactly one source invoice line.
- Every reversed charge has an original line and one reversal line.
- No applied charge changes invoice total by zero unless the approved amount is zero, which must itself be rejected.
FIN-002: Make invoice creation and invoice-line creation atomic
Current defect
Invoice creation, invoice-line insertion, and ledger recalculation are separate operations. An invoice can be saved without authoritative invoice lines.
Primary files
app/Controllers/View/InvoiceController.phpapp/Libraries/InvoiceLedgerService.php
Required implementation
Wrap invoice issuance in one service transaction:
InvoiceIssuanceService::issueInvoice(IssueInvoiceCommand $command)
Required sequence:
- Begin transaction.
- Lock or enforce the invoice uniqueness key.
- Insert invoice in
draftstatus. - Build all line snapshots.
- Insert every line.
- Verify the number and total of inserted lines.
- Change invoice to
issued. - Recalculate projection from invoice lines.
- Verify invoice projection save.
- Insert audit event.
- Commit.
On any failure, roll back the invoice and all lines.
Required code changes
issueInitialInvoiceLines()must return a typed result or throw.- Check
insertBatch()return value. - Reject issuance when zero lines are created unless zero invoices are explicitly supported.
- Do not silently fall back to live configuration for a newly issued invoice.
- If an issued invoice has no lines, raise an integrity exception.
Tests
- Force line insertion failure and verify invoice is not saved.
- Force projection update failure and verify invoice and lines roll back.
- Issue the same invoice concurrently and verify the unique rule permits one.
- Verify issued invoice always has at least one authoritative line.
Acceptance evidence
Query returns zero rows:
SELECT i.id
FROM invoices i
LEFT JOIN invoice_lines l ON l.invoice_id = i.id
WHERE i.status = 'issued'
GROUP BY i.id
HAVING COUNT(l.id) = 0;
FIN-003: Correct invoice snapshot composition
Current defects
Initial snapshots do not fully capture existing approved/applied adjustments. Event charges can be loaded without the invoice semester. Legacy invoice backfill can make event fees discount eligible.
Required implementation
When issuing an invoice, snapshot:
- Tuition lines.
- Event-fee lines for the exact school year and semester.
- Existing approved adjustments that are meant to be included at issuance.
- Existing approved deductions.
- Any other authorized invoice charge types.
Each line must store the correct discount_eligible value.
Legacy migration strategy
Do not classify a full legacy invoice total as tuition.
For each legacy invoice:
- Recover tuition, event, and adjustment components where historical source data is trustworthy.
- Store separate lines with correct eligibility.
- If components cannot be reconstructed, create:
legacy_totalline.- A separate persisted
legacy_discount_eligible_base_cents.
- Do not infer that the whole legacy amount is discount eligible.
Tests
- Semester-one invoice excludes semester-two event charges.
- Event-only invoice has zero eligible discount base.
- Legacy invoice preserves its original total.
- Legacy event portion is not made discount eligible.
FIN-004: Render invoice documents from frozen invoice lines
Current defect
Invoice PDFs and document views reconstruct descriptions and charges from current enrollment and event data. The stored total may remain fixed while the rendered invoice changes.
Required implementation
Invoice rendering must use:
invoice_lines.descriptioninvoice_lines.quantityinvoice_lines.unit_amount_centsinvoice_lines.line_amount_cents- Snapshot metadata
Do not reload current:
- Enrollments
- Class assignments
- Event charges
- Withdrawal status
- Pricing configuration
Current data may be shown separately as non-financial context only if clearly labeled and not used to describe issued charges.
Tests
- Issue an invoice.
- Save its PDF hash or normalized rendered data.
- Change enrollment and event configuration.
- Regenerate the invoice document.
- Verify financial line descriptions and values are unchanged.
Phase 2: Correct Discount Calculations
FIN-005: Expose the actual eligible discount base from the ledger
Current defect
Discount application uses invoice balance due, which includes payments, refunds, and ineligible charges. The ledger later caps the discount differently.
Required ledger output
Add:
gross_charge_cents
discount_eligible_base_cents
requested_discount_cents
applied_discount_cents
net_charge_cents
Calculate eligible base directly from invoice lines:
discount eligible base =
sum(line_amount_cents where discount_eligible = true and line is active)
Do not use:
- Balance due
- Customer credit
- Total payments
- Refunds
- Full invoice total as a fallback
If eligible base is zero, applied discount must be zero.
Tests
- Event-only invoice has zero eligible base.
- Tuition plus event invoice includes tuition only.
- Deduction lines reduce the eligible base only when the business rule says they do.
- Payments do not change discount eligibility.
FIN-006: Remove duplicate discount recalculation from InvoiceController
Current defect
recalculateAndUpdateDiscount() recalculates discounts from live tuition, processes one usage row, updates legacy fields, and ignores some write results.
Required implementation
Delete the method or replace its body with a call to the canonical discount service.
No invoice controller method may independently:
- Calculate percentage discounts.
- Calculate fixed discount caps.
- Query live tuition to update an issued invoice.
- Select one discount usage as though it is the only usage.
- Write
discount_amountdirectly.
Repository check
The following concepts must only exist in the discount domain service and ledger:
percentage discount calculation
eligible base calculation
discount cap calculation
discount ordering
FIN-007: Store and consume the actual applied discount
Required fields
For every discount usage:
requested_discount_cents
eligible_base_before_cents
applied_discount_cents
application_order
The authoritative invoice discount total must be:
SUM(applied_discount_cents for active usages)
The ledger must stop using legacy discount_amount except during migration compatibility.
Required migration
- Backfill cents fields.
- Recalculate active usages against historical invoice eligibility where reliable.
- Flag ambiguous records for reconciliation.
- Move application reads to cents fields.
- Make legacy fields read-only or remove them after migration.
Tests
- Stored usage amount equals invoice-applied amount.
- Reports use applied amount.
- Discount cannot exceed eligible base.
- Multiple discounts apply in deterministic order.
FIN-008: Enforce voucher locking, ordering, and uniqueness
Required transaction
- Lock voucher.
- Lock invoice.
- Lock active discount usages for that invoice.
- Revalidate voucher dates, active state, school year, semester, parent eligibility, and remaining uses.
- Calculate remaining eligible base.
- Calculate actual applied amount.
- Insert usage.
- Increment voucher usage.
- Recalculate invoice.
- Commit.
Required constraints
- Unique voucher usage per invoice unless repeat use is explicitly supported.
- Applied amount nonnegative.
- Usage count cannot exceed maximum through application logic and locked transaction.
Tests
- Concurrent final voucher use; one succeeds.
- Fixed then percentage discount.
- Percentage then fixed is rejected if ordering is fixed.
- Event-only invoice receives zero discount.
- Expired or inactive voucher is rejected inside the transaction.
Phase 3: Correct Refund Eligibility and Source Rules
FIN-009: Fix invoice-overpayment eligibility double subtraction
Current defect
Current customer credit already reflects completed cash refunds, but completed payouts are subtracted again.
Required source-specific formulas
Invoice overpayment
available refundable amount =
current invoice customer credit
- active approved unpaid reservations for that invoice source
Do not subtract completed payouts again.
Payment duplicate or payment correction
available refundable amount =
approved refundable portion of the payment
- completed payouts tied to that payment source
- active approved unpaid reservations tied to that payment source
Credit memo or administrative credit
available refundable amount =
remaining cash-refundable amount on the approved credit source
- completed payouts
- active reservations
Implement separate strategy methods rather than one generic subtraction formula.
Tests
- $100 credit, $40 already refunded, no reservations gives $60 available.
- $60 current credit and $20 reserved gives $40 available.
- Payment-source completed payout is subtracted exactly once.
FIN-010: Implement or remove unsupported refund types
Current defect
Tuition or administrative refund requests can map to source types whose eligibility is always zero.
Required decision
Choose one of the following and enforce it consistently:
Option A: Implement real source records
Create:
credit_memos
administrative_credits
Each source must have:
- Parent
- Invoice
- Approved amount
- Cash-refundable amount
- Status
- Reason
- Approver
- Audit history
Option B: Remove unsupported types
Remove tuition and administrative-credit refund options from:
- Validation rules
- Request forms
- Controller mapping
- Reports
- Status logic
Do not leave a selectable type that can never pass eligibility.
Acceptance evidence
Every exposed refund type has a complete request, approval, payout, reversal, and reporting path.
FIN-011: Prove duplicate-payment eligibility
Current defect
Any recorded payment can be treated as a duplicate without proving duplication or overpayment.
Required implementation
A duplicate-payment refund must reference:
- The duplicate payment.
- The invoice.
- An approved duplicate-payment determination or correction record.
Create a record such as:
payment_corrections
- id
- payment_id
- correction_type
- approved_refundable_cents
- status
- reason
- approved_by
- approved_at
Allowed correction types may include:
duplicate
wrong_parent
wrong_invoice
wrong_amount
processor_duplicate
The eligibility service must verify the approved correction and remaining refundable amount.
Alternative: allow invoice-credit-based refund only when the payment demonstrably caused current invoice customer credit. This still requires allocation logic linking the excess to that payment.
Required behavior
During refund request creation:
- Derive
invoice_idandparent_idfrom the payment. - Reject conflicting client-provided values.
- Require a valid successful payment status.
- Reject a payment already fully refunded or reversed.
Tests
- Normal payment required to settle an invoice cannot be refunded as duplicate.
- Approved duplicate payment can be refunded.
- Payment on wrong invoice follows correction workflow.
- Payment-derived refund always stores its invoice.
FIN-012: Lock every refund source during approval and payout
Required locking matrix
| Source type | Rows to lock |
|---|---|
| Invoice overpayment | refund, invoice, active source reservations |
| Payment duplicate/correction | refund, payment, invoice, correction record, reservations |
| Credit memo | refund, credit memo, invoice, reservations |
| Administrative credit | refund, administrative credit, invoice, reservations |
Locks must occur before eligibility is recalculated.
Concurrency tests
- Two refund approvals against one payment source.
- Two approvals against one invoice credit.
- Approval racing with payment reversal.
- Payout racing with a new invoice adjustment.
Only valid remaining credit may be approved or paid.
Phase 4: Fix Refund State, Payout, and Retry Behavior
FIN-013: Lock refund rejection and enforce state transitions
Required rejection transaction
- Begin transaction.
- Lock refund row.
- Reload normalized status.
- Permit rejection only from
requested. - Save rejection reason, actor, and timestamp.
- Insert audit event.
- Commit.
Reject attempts from:
- Approved
- Partially paid
- Paid
- Cancelled
- Reversed
- Exception
State machine
Allowed transitions:
requested -> approved
requested -> rejected
requested -> cancelled
approved -> partially_paid
approved -> paid
approved -> cancelled
partially_paid -> paid
paid -> reversal_pending
paid -> partially_reversed
paid -> reversed
partially_paid -> partially_reversed
Every transition must be performed through one state service.
FIN-014: Validate idempotency against a request fingerprint
Current defect
Any existing payout with the same idempotency key can be returned as success even when it belongs to another refund or different operation.
Required implementation
Persist a fingerprint containing:
operation_type
refund_id
amount_cents
payment_method
currency
external_reference where applicable
On repeated key:
- Identical fingerprint: return original result.
- Different fingerprint: return
409 IDEMPOTENCY_CONFLICT. - Never report unrelated payout as successful.
Use separate operation types for:
- Refund payout
- Refund reversal
- Payment creation
- Inventory receipt
Required database fields
Add a request fingerprint hash or the normalized fields required to compare.
Tests
- Same key and same request returns original payout.
- Same key and different amount returns 409.
- Same key on another refund returns 409.
- Payout key cannot be reused for reversal.
FIN-015: Make payout rows authoritative
Current defect
Payout eligibility still reads mutable refund_paid_amount.
Required implementation
Calculate:
completed payout total =
sum(completed cash_out payouts)
- sum(completed reversal payouts)
Use payout rows for:
- Remaining approved amount.
- Refund status.
- Invoice ledger effect.
- Reports.
- Reconciliation.
- Reversal limits.
refund_paid_amount may remain only as a cached projection updated from payout rows. It must never be trusted as input.
Integrity check
Add a daily query comparing the cache to payout-row totals. Differences must alert and block further payout until reconciled.
FIN-016: Implement external payout states for online refunds
Current defect
Online refunds are marked completed without external confirmation.
Required state flow
created -> processing -> completed
created -> processing -> failed
processing -> unknown
unknown -> completed
unknown -> failed
Required behavior
For online or gateway-backed refunds:
- Insert payout as
processing. - Call provider using the idempotency key.
- Store provider operation ID.
- Mark completed only after confirmed provider success.
- On timeout, mark
unknown, not failed. - Reconcile unknown status with the provider.
- Apply invoice ledger effect only when completed.
For manual cash or check payouts:
- Require explicit staff confirmation.
- Require check number for checks.
- Require external or receipt reference according to policy.
Tests
- Provider success.
- Provider decline.
- Timeout after provider success.
- Retry with same idempotency key.
- Provider webhook or reconciliation confirmation.
FIN-017: Restrict overpayment refund recalculation to its own source
Current defect
Invoice overpayment recalculation can rewrite unrelated open refunds and can block legitimate future overpayment refunds because an old paid refund exists.
Required implementation
Only update or create a refund where:
source_type = invoice_overpayment
source_id = invoice_id
Never modify refunds from another source.
The recalculation transaction must:
- Lock invoice.
- Calculate current customer credit.
- Lock active invoice-overpayment refund reservations.
- Update only the active overpayment reservation for this exact source.
- Never reduce approved amount below completed payout total.
- Permit a new future refund cycle when new credit appears after an earlier refund was fully paid.
- Preserve all previous paid refunds as immutable history.
Suggested model
Use separate refund requests per credit event rather than reopening old paid rows.
Tests
- Duplicate-payment refund remains unchanged during invoice overpayment recalculation.
- New overpayment after a prior paid refund creates a new request.
- Recalculation racing with payout remains consistent.
- Approved amount never falls below net completed payout.
Phase 5: Fix Payment Retry and Evidence Handling
FIN-018: Add client-to-server idempotency for manual payments
Current defect
The server creates a new transaction ID on every request. A retry can create a second partial payment.
Required implementation
- Generate an operation UUID when the payment form is loaded or immediately before first submission.
- Store it in the form.
- Reuse it for all retries.
- Add unique
idempotency_keyto the payment record or payment operation table. - Store request fingerprint:
- Parent
- Invoice
- Amount
- Method
- Currency
- Same key plus identical request returns original payment.
- Same key plus different request returns 409.
Tests
- Response lost after commit, retry creates no second payment.
- Same key with different amount returns conflict.
- Two concurrent submissions with same key create one payment.
FIN-019: Stage payment evidence safely
Required flow
- Validate evidence.
- Save to temporary storage.
- Begin transaction.
- Create payment.
- Allocate payment.
- Recalculate invoice.
- Commit.
- Move evidence to permanent storage.
- On rollback, remove temporary evidence.
If permanent file finalization fails after commit:
- Preserve payment.
- Mark evidence status
incomplete. - Alert operations.
- Do not silently delete the payment.
Tests
- Payment validation failure cleans temporary file.
- Database rollback cleans temporary file.
- File finalization failure produces an operational exception state.
FIN-020: Use ledger discount values in payment notifications
Payment notifications and receipts must use:
- Applied discount from ledger projection.
- Net invoice charges from ledger.
- Valid payment total.
- Completed refund total.
- Amount due and customer credit.
Remove raw discount sums from payment notification code.
Phase 6: Enforce Expense and Reimbursement Integrity
FIN-021: Enforce expense immutability in the write endpoint
Current defect
The edit form checks reimbursement state, but direct POST to the update method can bypass it.
Required implementation
Inside the update transaction:
- Lock expense.
- Lock active reimbursements for that expense.
- Reject changes to protected fields when any active or paid reimbursement exists.
- Check update result.
- Commit only on success.
Protected fields:
- Amount
- Category
- Purchaser
- School year
- Receipt
- Donation flag
- Currency
Allow only non-financial notes if explicitly approved.
Receipt replacement
Use temporary storage and transaction-safe finalization.
Status updates
updateStatus() must:
- Lock expense.
- Validate legal transition.
- Prevent approval changes after reimbursement without reversal.
- Record actor and reason.
Tests
- Direct POST cannot edit reimbursed expense.
- Paid expense amount cannot change.
- Receipt replacement rolls back safely.
- Donation-to-reimbursable transition resets approval.
FIN-022: Make reimbursement batches use one domain service
Current defect
Batch processing can bypass the safer individual reimbursement service and can resurrect rejected or reversed records.
Required implementation
Create one service:
ReimbursementService::payApprovedExpense(
int $expenseId,
int $recipientId,
int $batchId,
PaymentDetails $payment,
int $actorId
)
Both individual and batch workflows must call it.
The service must:
- Lock expense.
- Lock existing reimbursement rows.
- Reject active, paid, rejected, or reversed records unless the exact transition is allowed.
- Verify amount equals approved reimbursable amount.
- Verify recipient.
- Verify accounting period.
- Verify batch relationship.
- Insert or transition reimbursement safely.
- Link expense.
- Record audit event.
Batch rules
- Lock batch and all active items.
- Validate every item before creating any payment.
- If one item fails, roll back all.
- Never skip invalid items.
- Close batch only after every active item has a valid paid reimbursement.
Tests
- Rejected reimbursement cannot become paid.
- Reversed reimbursement cannot be resurrected.
- Invalid batch item rolls back the whole batch.
- Two batch-close requests cannot double pay.
FIN-023: Implement reimbursement reversal
Required model
Create immutable reimbursement transaction or reversal records.
A reversal must:
- Lock original reimbursement.
- Verify paid status and unreversed amount.
- Insert reversal record.
- Update expense reimbursement availability according to policy.
- Preserve original reimbursement.
- Record reason, actor, and timestamp.
- Reconcile any external payment reference.
Do not edit a paid reimbursement amount to zero.
Tests
- Full reversal.
- Partial reversal if allowed.
- Duplicate reversal rejected.
- Expense correction after reversal follows approval workflow.
Phase 7: Finish Reporting and Status Normalization
FIN-024: Migrate every financial report and export to canonical data
Current defects
Some reports still use raw:
discount_amountrefund_paid_amountSUM(paid_amount)- Legacy status variants
Required implementation
Create dedicated read models or SQL views for:
invoice gross charges
discount eligible base
applied discounts
net charges
valid payments
completed refund payouts
refund reversals
amount due
customer credit
reserved refundable credit
available refundable credit
approved expense amount
paid reimbursement amount
inventory receipt totals
Every report and export must consume these projections.
Required replacements
- Discount export uses
applied_discount_cents. - Refund export lists payout rows and reversal rows.
- Payment totals use canonical successful-status query.
- Invoice reports use invoice-line projections.
- Parent summaries use the parent ledger projection.
- No report independently reconstructs accounting formulas.
Cross-surface test
For a selected invoice, verify exact equality across:
- Invoice page
- Manual payment page
- Refund page
- Financial report
- CSV/PDF export
- API response
FIN-025: Normalize all statuses to lowercase machine values
Required canonical values
Invoice
draft
issued
unpaid
partially_paid
paid
credited
voided
Refund
requested
approved
partially_paid
paid
rejected
cancelled
partially_reversed
reversed
exception
Reimbursement
pending
approved
paid
rejected
reversed
Payout
created
processing
unknown
completed
failed
reversed
Required implementation
- Migrate existing mixed-case values.
- Update controllers.
- Update models.
- Update queries.
- Update reports.
- Update form values.
- Add check constraints.
- Remove compatibility writes.
- Permit legacy aliases only in a one-time migration mapping.
Fix InvoiceModel::getUnpaidInvoices() to include canonical partially_paid.
Remove direct invoice mutation methods
Remove or make private any model methods that directly modify:
- Total amount
- Balance
- Discount total
- Refund total
- Additional-charge totals
All mutations must pass through invoice lines and ledger recalculation.
Phase 8: Secure Routes and Remove Bypasses
FIN-026: Protect every financial route explicitly
Required filters
Every route must have:
- Authentication.
- Exact permission.
- Correct method.
- CSRF for browser writes.
- Parent/organization ownership enforcement.
Routes requiring immediate review
- Invoice parent lookup.
- Invoice creation.
- Invoice listing.
- Parent invoice payment pages.
- Payment parent lookup.
- Payment creation.
- Payment details.
- Financial report download.
- Refund actions.
- Expense actions.
- Reimbursement batch actions.
- PO receipt and cancellation.
Permission examples
invoice.view
invoice.issue
invoice.adjust
payment.view
payment.create
payment.reverse
refund.view
refund.request
refund.approve
refund.pay
refund.reverse
discount.apply
expense.view
expense.edit
expense.approve
reimbursement.pay
reimbursement.reverse
purchase_order.receive
financial_report.view
Tests
- Anonymous request returns 401.
- Authenticated user without permission returns 403.
- Wrong parent or organization scope returns 403 or 404.
- State changes through GET are impossible.
- CSRF failure blocks browser writes.
FIN-027: Add route and domain-bypass tests
Automated checks must verify:
- Every configured controller method exists.
- Route parameters match method signatures.
- Financial write routes call an approved domain service.
- Generic CRUD cannot mutate financial models.
- No command, scheduled job, or admin utility writes financial totals directly.
- No model callback silently changes settled financial records.
Phase 9: Make Database Hardening Fail Loudly
FIN-028: Stop swallowing required migration failures
Current defect
Some hardening migrations catch constraint errors, log warnings, and continue. Deployment can appear successful without required safeguards.
Required implementation
For every required constraint:
- Pre-clean or reconcile conflicting data.
- Attempt constraint creation.
- Verify the constraint exists.
- Throw and fail the migration if it does not exist.
Required constraints include:
- Invoice identity uniqueness.
- Refund payout idempotency uniqueness.
- Discount usage uniqueness.
- One active reimbursement per expense.
- Batch sequence uniqueness.
- Positive payout amount.
- Valid additional-charge type.
- PO quantity and cost checks.
- Foreign keys for financial source relationships.
Database support
Declare the supported production database and minimum version.
If MySQL-only:
- Document the exact supported MySQL version.
- Run migration tests against that version.
- Do not imply portability to unsupported engines.
Migration test
After migrations, query metadata and assert every required index, foreign key, generated column, and check constraint exists.
Phase 10: Add Inventory Receipt Idempotency and Reversal
FIN-029: Make PO receiving retry-safe
Required schema
Create:
inventory_receipt_operations
- id
- idempotency_key
- purchase_order_id
- request_fingerprint
- status
- actor_id
- created_at
Create immutable receipt transaction rows per PO line.
Required behavior
- Same key and same request returns the original receipt result.
- Same key and different quantities returns 409.
- Retry after committed response loss does not add stock again.
- Completion status is derived from all receipt rows.
Tests
- Concurrent same-key receipts create one inventory effect.
- Retry after commit creates no additional quantity.
- Different request with reused key conflicts.
FIN-030: Implement inventory receipt reversal
A receiving mistake must be corrected with a reversal transaction.
Required checks:
- Original receipt exists.
- Reversal does not exceed unreversed quantity.
- Current inventory is sufficient unless negative inventory is explicitly supported.
- PO received quantities are recalculated.
- PO status is recalculated.
- Original receipt remains unchanged.
Phase 11: Logging and Sensitive Data
FIN-031: Remove full financial request logging
Do not log entire POST bodies for refunds, payments, reimbursements, or expenses.
Log structured fields only:
request_id
actor_id
operation
entity_id
amount_cents
currency
idempotency_key hash or truncated value
result_code
Never log:
- Full check number.
- Bank account data.
- Card data.
- Uploaded document contents.
- Full raw request payload.
- Sensitive external references unless redacted.
Add tests or static checks for prohibited logging patterns in financial controllers.
Phase 12: Automated Verification
FIN-032: Add unit tests
Minimum formula coverage:
- Exact payment.
- Partial payment.
- Multiple payments.
- Overpayment.
- Partial cash refund.
- Full cash refund.
- Multiple payouts.
- Payout reversal.
- Invalid payment statuses.
- Discount eligible base.
- Event-only invoice discount.
- Additional charge and deduction.
- Additional-charge reversal.
- Refund reservation.
- Invoice-overpayment eligibility after prior payout.
- Payment-source refund eligibility.
- Reimbursement amount and reversal.
- PO receipt and reversal.
FIN-033: Add integration tests
Required workflows:
- Issue invoice with frozen lines.
- Apply additional charge.
- Apply deduction.
- Apply discounts in order.
- Record payment.
- Create overpayment.
- Request refund.
- Approve refund.
- Pay refund.
- Reverse refund.
- Verify every report and export.
Additional workflows:
- Invoice issuance rollback.
- Evidence upload rollback.
- Payment retry.
- Online refund timeout and reconciliation.
- Expense edit after reimbursement.
- Batch reimbursement rollback.
- PO receipt retry.
- Migration constraint verification.
FIN-034: Add concurrency tests
Use the production database engine.
Required races:
- Same additional charge applied twice.
- Last voucher use requested twice.
- Two refund approvals against one credit.
- Refund payout and invoice adjustment.
- Refund rejection and approval.
- Same payment submitted twice.
- Two reimbursements for one expense.
- Two batch-close requests.
- Two PO receipts for the same lines.
Expected invariants:
no duplicate financial event
no over-refund
no over-discount
no double reimbursement
no duplicate inventory receipt
no settled record overwritten
FIN-035: Add repository guardrails
CI must fail if financial arithmetic appears outside approved services.
Search for suspicious patterns including:
refund_paid_amount
SUM(paid_amount)
SUM(amount)
discount_amount
newBalance
total - discount
total - paid
balance =
Partially Paid
Unpaid
Paid
Use a narrow allowlist for:
- Migrations.
- Compatibility readers scheduled for removal.
- Canonical financial repositories and services.
CI must also fail when:
- Financial routes lack auth/permission filters.
- New direct model mutations are introduced.
- Required tests are absent for a changed financial service.
Phase 13: Historical Reconciliation
FIN-036: Reconcile existing records after the fixes
For every invoice produce:
frozen charge total
discount eligible base
applied discount total
valid payment total
completed payout total
reversal total
amount due
customer credit
reserved refund amount
available refund amount
Flag:
- Applied additional charge without invoice line.
- Invoice line without source.
- Discount usage above eligible base.
- Refund payout above approval.
- Refund without valid source.
- Aggregate refund amount not matching payout rows.
- Mixed-case or invalid status.
- Issued invoice without lines.
- Reimbursed expense modified after payment.
- Reimbursement amount mismatch.
- PO receipt total mismatch.
- Missing required database constraint.
Correction method
Do not silently overwrite settled records.
Use:
- Adjustment line.
- Credit memo.
- Debit adjustment.
- Refund reversal.
- Reimbursement reversal.
- Inventory receipt reversal.
- Explicit reconciliation record.
Every correction needs:
- Reason.
- Actor.
- Approval.
- Timestamp.
- Reference to the original record.
Final Release Gate
Production approval requires every answer below to be yes.
Invoice integrity
- Does every issued invoice have authoritative lines?
- Are invoice issuance and line creation atomic?
- Do applied additional charges create immutable lines?
- Are reversals represented by reversing lines?
- Are invoice PDFs rendered from frozen lines?
- Do semester filters match the issued invoice?
Discounts
- Is the eligible base calculated from eligible lines?
- Is zero eligible base respected?
- Is actual applied discount stored and reported?
- Are voucher uses locked and uniquely constrained?
- Is duplicate discount recalculation removed?
Refunds
- Does invoice-overpayment eligibility avoid double subtraction?
- Does every refund type have a real source?
- Is duplicate-payment status proven?
- Is the source locked during approval and payout?
- Is rejection locked and state-safe?
- Does idempotency validate the request fingerprint?
- Are payout rows authoritative?
- Are online payouts completed only after confirmation?
- Does overpayment recalculation modify only its own source?
- Can new later overpayments create new refunds?
Payments
- Are manual payments idempotent?
- Is evidence staged and cleaned safely?
- Do notifications use ledger values?
Expenses and reimbursements
- Is reimbursed-expense immutability enforced in the write method?
- Do batch and individual reimbursements use one service?
- Can rejected or reversed reimbursements remain non-payable?
- Is reimbursement reversal implemented?
Reports and statuses
- Do all reports and exports use canonical projections?
- Are statuses normalized to lowercase machine values?
- Are direct invoice-total mutation methods removed?
Security and database
- Is every financial route explicitly authenticated and authorized?
- Are generic mutation bypasses absent?
- Do required migrations fail when constraints fail?
- Does metadata verification prove constraints exist?
Inventory
- Are receipt operations idempotent?
- Is receipt reversal available?
- Can retries avoid double stock increases?
Verification
- Do all unit tests pass?
- Do all integration tests pass?
- Do all concurrency tests pass on the production database engine?
- Do authorization tests pass?
- Does reconciliation report zero unexplained financial differences?
Required Execution Order
Implement in this order because later fixes depend on earlier invariants:
FIN-001throughFIN-004: invoice-line and snapshot integrity.FIN-005throughFIN-008: discount base and persistence.FIN-009throughFIN-017: refund eligibility, state, payouts, and recalculation.FIN-018throughFIN-020: payment idempotency and evidence.FIN-021throughFIN-023: expenses and reimbursements.FIN-024throughFIN-027: reports, statuses, routes, and bypasses.FIN-028: migration enforcement.FIN-029andFIN-030: inventory retry and reversal.FIN-031: logging cleanup.FIN-032throughFIN-035: automated verification and guardrails.FIN-036: historical reconciliation.- Run the final release gate.
Do not release a partial phase when it leaves two sources of truth active. In particular, do not switch reports to new projections while writes still update legacy aggregates independently. That would produce a more modern disagreement, which remains a disagreement.