-
Mahmoud Aglan authored
S1 of the mobile-portal programme. Every item here is a live defect, and each one blocks the portal's money path rather than merely preceding it. The ledger. PaymentService::resolveDebitAccount() returned the literal 1 and resolveCreditAccount() returned 2, both with a `// TODO`. Seeder order made that Dr Cash / Cr Bank on every payment the product has ever taken — 627 of 701 rows on the restored oc_sport copy — so no revenue account was ever credited and FinancialOverview::getRevenueBySource(), which groups transactions by credit_account_id restricted to revenue accounts, could only ever return []. Accounts now resolve by code within the academy and hard-fail when absent, and a payment is split across revenue accounts in proportion to the invoice's own lines, floored with intdiv() and the remainder on the last row. Routing InstaPay into the old ledger would have multiplied a broken ledger across a new channel. The guards. Every rule 05-financial-integrity.md names lived in the UI, in two hand-copied Livewire components, so any new caller inherited none of them. amount > 0, amount <= due re-read under lockForUpdate inside the transaction, invoice not cancelled/paid, academy and currency agreement all sit in the service now. Draft is deliberately still payable: the POS issues an invoice as a draft and settles it in the same transaction. updatePaidAmount() was a read-modify-write on money with no lock — two settlements landing together each read the old paid_amount and one increment was lost. Paymob confirmed callbacks inline: no lock, no Transaction row at all, and an idempotency guard that was dead code because the finder already filtered status = Pending, so a retried webhook credited the invoice twice. It goes through PaymentService::confirmPending() now, which asserts the captured amount matches. POS cash sales double-counted the drawer: POSService incremented total_cash_in and UpdateCashSessionTotals incremented it again, inflating the expected drawer 2x and producing phantom variance at close. One writer each now. A split tendered above the total (cash handed over, change given) capped at the amount due instead of producing an overpaid invoice. RefundService refunded the full payment only, so an over-approved amount could not be corrected; it also hardcoded accounts 2/1 with a comment claiming A/R, which is account 3, and debited the refunding user's own drawer rather than the one that took the money. Migrations, all guarded and all verified against a restored copy of backups/oc_sport-20260831-081053.dump: - chart of accounts seeded for every academy, not just Academy::first(). The verified tenant was missing 4060, and db:seed only runs on first deploy — so a hard-failing resolver had to be preceded by this. - invoices.branch_id and transactions.branch_id, backfilled. Revenue was branch-attributed only through payments.branch_id, and getCollectionRate() scopes invoices through whereHas('payments'), so an invoice with no payment yet belonged to no branch. Portal invoices awaiting a proof would have vanished from every branch's overdue figure. 588/713 invoices and 644 transactions attributed. - academy_id on invoice_items, installments and notification_preferences, participant_id on event_registrations — four tenant tables that broke the tenancy invariant, all reachable from the portal. - notification channel CHECK widened to push and whatsapp. PushNotificationService writes 'push' and the CHECK allowed only in_app|email|sms, so every push delivery log insert raises 23514 today and the catch block writes another failing insert. - guardians and guardian_participant relationship_type CHECKs reconciled to their union. NewRegistrationWizard validates one field against the pivot's vocabulary and writes it to both tables, so picking أخ / أخت / وصي crashes registration on the guardians CHECK right now. - invoice_number_counters replaces generateNumber()'s count()+1 against a UNIQUE(academy_id, number) index — a guaranteed collision the moment members can check out without a receptionist serialising them, and it reissued numbers soft-deleted invoices still hold. - the deleted mobile API's INV-MOB lines repaired: it wrote line_total, which is not a column, so total_amount defaulted to 0 and every downstream allocation read the sale as worthless. tests/Feature/ExampleTest.php deleted: the stock Laravel scaffold test has failed since `init` (it GETs / with no tenant database), permanently red-lighting the suite and masking real failures. Suite: 61 passed, 0 failed. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
8d251d14