# 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 B’s 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.