-
Mahmoud Aglan authored
S5. The academy publishes a handle, the member transfers and records what they sent, and staff turn that claim into a payment only after matching it against the academy's own statement. An unverified screenshot must never create a Payment. `transactions` are immutable and recordPayment() forces status = Confirmed and posts to the ledger immediately, so a proof is a separate object with its own lifecycle and only approval calls recordPayment() — double-entry happens exactly once and nothing in the ledger is ever edited. **A screenshot is not evidence.** It is a convenience. The control is `sender_reference`, unique per academy per method behind a partial index, which kills replay, cross-invoice reuse and "someone else's transfer against my invoice" in one constraint. The reviewer types the amount from the statement; `amount_claimed` is what the payer said and is never what gets posted. Concurrency is a conditional UPDATE, not a disabled button. Two reviewers open the queue and both see an enabled Approve; the second one's UPDATE matches zero rows and raises InvalidStatusTransitionException. A row that has left `pending` is frozen by a BEFORE UPDATE trigger — approving a proof is the moral equivalent of taking cash, and Auditable::createAuditLog() takes its user from auth() at boot and silently writes nothing when it cannot resolve an academy, so the approval facts are columns on the row rather than an audit-log dependency. Overpayment is capped at what is due and the excess is deposited to the member's wallet in the same transaction. InvoiceStatus::Overpaid exists but nothing consumes it and it drives due_amount negative, after which getCollectionRate() and ParticipantBillingService start summing negatives. branch_id is NOT NULL on a proof. Revenue is branch-attributed only through payments, so a NULL-branch payment lands in the all-branches total and in no branch — the columns stop summing with no error anywhere. E6 decided as recommended: all five method CHECKs that lacked `instapay` get it, the till included. Reception will take an InstaPay transfer within a month of launch, and the failure mode of leaving the POS out is a Postgres 23514 at the till in front of a customer. pos_transactions and pos_split_payments also gain `bank_transfer`, which they never had. The review queue ships before the member-facing upload, on purpose: a proof that can be submitted and never reviewed is a promise to a member that nobody is keeping. Proof files go to the private disk and are streamed by a controller that authorises the submitter, a co-guardian of the same member, and staff holding payments.approve_proof — Content-Disposition: attachment, nosniff, no-store. The parent excuse form wrote its medical attachments to the public disk; that is the mistake not to repeat. Verified against a restored copy of backups/oc_sport-20260831-081053.dump: a 600 EGP transfer against a 500 EGP invoice posts 500 to the invoice (Dr 1010 Bank / Cr 4000 Training Revenue, branch attributed) and 100 to the wallet; duplicate reference, self-approval, zero amount, second approval and editing a settled row are all refused. Suite: 87 passed, 3 skipped locally; 11 InstaPay tests pass against the restored tenant. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
2c95f617
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| POSItemType.php | ||
| POSPaymentMethod.php | ||
| POSPaymentStatus.php |