Commit 5d693197 authored by DevPilot's avatar DevPilot

docs: extreme-detail reference for the rest of Accounting

Chart of accounts, cost centers/budgets, dimensions, revenue mapping
manual screens, posting chains, accruals, gaps, opening entries/
reclassification, bank reconciliation, loans/documentary credits/
guarantees, settlements/billing, fiscal years/journal types/branch
fees, and the exact calculation logic behind every report screen
including all 5 financial statements — plus a status table for every
defect found and fixed or flagged along the way.
parent e6e92996
# Accounting Module — Extended Reference
Everything under المحاسبة المالية not already covered by the step-by-step tutorial PDF or the wizard references in this folder: chart of accounts, cost centers/budgets, dimensions, revenue mapping (manual screens), posting chains, accruals, gaps, opening entries/reclassification, bank reconciliation, loans/documentary credits/guarantees, settlements/billing, fiscal years/journal types/branch fees, and every report screen's exact calculation logic.
All routes in `app/Modules/Accounting/Routes.php`. All require `auth`; POSTs also require `csrf`.
---
## دليل الحسابات (Chart of Accounts)
`ChartOfAccountsController` · `accounting.coa.view`/`.manage`
**Create fields:** `account_code` (required, unique), `name_ar`/`name_en` (required), `account_type` (required: asset/liability/equity/revenue/expense — drives every balance-sheet/income-statement classification downstream), `account_nature` (required: debit/credit — determines the sign convention used by *every* balance/ledger/trial-balance calculation in the system), `parent_id` (optional, must be `is_header=1`; the create form does **not** restrict by account_type, so a leaf can technically be created under a parent of a different type at creation time even though moving one there later is blocked — see reparent below), `opening_balance` (**cosmetic only** — reports never read this column; real opening balances live as posted `reference_type='opening'` journal entries, so editing this field after the fact changes nothing anywhere), `cost_center_id` (default only, never enforced), `is_header` checkbox, `description_ar`.
**is_header vs postable:** header accounts can never receive a posted line (`JournalService::createEntry` refuses) except through the one deliberate escape hatch `AccountReclassificationService` uses to empty a stranded balance. Toggling header status is guarded: promoting is refused if the account has posted lines or is referenced by any revenue rule/voucher/tax profile/bank account; demoting is refused if it has active children.
**Edit form:** `account_code` and `parent_id` are rendered `disabled` — you cannot move an account from the edit screen. Moving requires the separate reparent action, which refuses: system accounts, moving under itself or its own descendant (cycle guard), a non-header target, or a target of a different `account_type` ("النقل سيخلط الأصول بالالتزامات في التقارير").
**Delete:** system accounts can never be deleted. An account with children or with any real reference (revenue rules, tax profiles, vouchers, bank accounts, treasuries) is refused outright. An account with posted journal lines is **archived, not deleted**, so historic entries keep resolving a name.
## مراكز التكلفة (Cost Centers) & الموازنات التقديرية (Budgets)
`CostCenterController` · `.cost_center.view`/`.manage``code` (unique), `name_ar`/`name_en`, `type` (cost_center/profit_center), `branch_id`, `parent_id` (**no cycle/type guard here**, unlike Chart of Accounts).
`BudgetController` · `.budget.view`/`.manage` — one fiscal year at a time, a 12-column entry grid. **The grid's 12 columns are hardcoded to the current calendar year**, regardless of which fiscal year you selected — if the club's fiscal year doesn't run January–December, the columns won't line up with that year's actual periods. Zero amounts are skipped on save; non-zero ones upsert.
**Variance report** (`/accounting/budgets/variance`): budget total vs. actual posted movement for the period, filtered `from_period`/`to_period` (month pickers). Actual sign convention: for expense/asset accounts, `actual = debit − credit`; everything else, `actual = credit − debit`. `variance = budget − actual`; negative (overrun) shown red. The cost-center variance equivalent exists in code but isn't wired to any route — unreachable from the UI.
## الأبعاد المحاسبية (Dimensions)
`DimensionController` · `.dimensions.view`/`.manage``code` (uppercased), `name_ar`/`name_en`, `is_required`, `is_active`; values under a dimension take `code`/`name_ar`/`name_en`/`parent_id` (self-scoped).
**Worth knowing before configuring this:** nothing in the codebase reads `accounting_dimension_values` back — no journal line, voucher, or report has a dimension column at all. `is_required` is stored but never enforced anywhere. This screen currently lets you define a taxonomy that nothing consumes yet.
## توزيع الإيرادات (Revenue Mapping — manual screens)
`RevenueMappingController` — the guided wizard is documented separately (`wizard-revenue-mapping.md`); this covers the manual screens.
**Index** (`.view`) — every stream with its active rule per stage. Filters: category, status (`unmapped`/`broken`/`catchall`/`split`/`review`), free-text search. **`/edit` + `update`** (`.manage`) is the manual line-by-line rule builder — same tables, same versioning as the wizard, but exposes `debit_source` options the wizard hides (`auto_treasury`/`fixed_account`/`accounts_receivable`/`accounts_payable`) and only ever touches one stream+stage at a time. Saving never edits in place — it always inserts a new version and marks the same-scope predecessor `superseded`; already-posted entries keep pointing at the old version.
**Diagnostics** (`.view`) — four independent panels: unmapped payment types (no active rule at all), amounts falling into the catch-all account 410515, rule lines pointing at accounts that became header/inactive after the rule was set, and a hardcoded check of 7 legacy account codes for existence/header/active/currency problems.
**مركز التوصيل / Connection Centre** (`.view`) — buckets every stream into `connected` (has a rule), `needsCode` (fires no event — a developer must fix it, not finance), `mappable` (fires an event but has no rule) — sorted by money-at-risk, with a running total. For `mappable` streams the CTA goes to `/edit`, not the wizard.
**Tax profiles** (what the tutorial calls "tax brackets"): `tax_code` (sanitized, unique), `name_ar`, `treatment`, `rate`, `is_price_inclusive`, `output_tax_account_id` (must not be a header), `legal_reference`, `effective_from`.
**Deferred revenue recognition** (`/recognition`, `/recognition/run`): `period` (`YYYY-MM`). The screen always previews (dry run) before you separately confirm the real run.
## مسار الفلوس (Posting Chains) & فين الفلوس دلوقتي (Parked)
`PostingChainController` + `Services/Chain/*`
**Health check** (no posting involved, pure static analysis of the chain configuration): flags a chain with no entry point, no terminal step, a "parks" step whose target account can't resolve, a "relieves" step referencing a non-existent or forward/self step, and — the specific bug class this tool exists to catch — **a hop where the money-in and money-out sides resolve to the exact same account**, which balances the entry but moves nothing. Also checks debit/credit shape consistency per hop type before anything is ever posted.
**فين الفلوس دلوقتي / Parked** — aging is **FIFO, not a balance snapshot**: every posted (and reversed) line on a clearing account is walked in date order, each credit consumes the oldest unconsumed debits first, and whatever's left is "still parked," aged in days. A credit with nothing left to consume against it is tracked separately as `unmatched_credit` — described in the code as "the signature of the bug this package fixes: a hop relieving an account the entry point never charged." **Deliberately counts both `posted` and `reversed` statuses**, unlike every ledger/statement report in the system which only counts `posted` — flagged in the code itself as an untested inconsistency risk ("no entry has been reversed in this database yet... but it will the first time someone reverses one"). Expected-clearing-days per account is the *loosest* deadline of any chain sharing that account — "the only one we can assert is breached."
**Sub-treasury reclassification** (`/reclassification`): a one-time, idempotent correction tool for a specific historical bug where sub-treasury cash was posted into a foreign-currency cash-box account instead of the safe's own account. Preview nets off anything already corrected (tagged via a self-referencing `reference_type`), so pressing the button twice cannot double-move money.
## الاستحقاقات (Accruals)
`AccrualController` + `AccrualRunner` (14 named scanners: subscriptions, subscription dev fees, subscription fines, sports bookings/subscriptions, locker rentals, reservations, rental invoices, tournament fees, academy deposits/rent, plus three **gated** runners — pool zone bookings, player cards, pool bookings — that only fire once a rate has been declared via سد الفجوات).
**"Run now" is the only way accruals get scanned in practice on this deployment** — the equivalent cron job ships disabled, so the button on this screen does the same work the nightly job would.
## سد الفجوات (Gaps)
`GapController` + `GapToolService`**exactly four gap streams**, each a genuinely different missing-data problem, not a generic template:
| Stream | The actual problem | Rate basis options |
|---|---|---|
| حجوزات مسابح (`sa:pool_zone_booking`) | Price/attendee columns exist but are always empty | per booking / per attendee |
| كروت اللاعبين (`sa:player_card`) | **No amount column exists at all** on the underlying table | per card only |
| حجوزات السباحة (`facility:pool_booking`) | The amount column exists but is always written as zero | per booking / per swimmer |
| مباريات خاصة (`facility:private_match`) | Amount **is** recorded, but with no receipt/payment behind it | uses the recorded amount, not a flat rate |
**"عرض التفاصيل"** on each gap runs that gap's own query and shows up to 300 real underlying rows (date, party, quantity, recorded amount or "غير مسجّل", status) — because, as the code puts it, "the counter on screen says '314 cases' — that isn't enough for the accountant to be sure" before setting a rate.
**Saving a rate writes nothing to the ledger by itself** — it only records the decision (mode/rate/basis/effective-from, plus a **mandatory reason note** whenever the mode isn't "off": "اكتب سبب/مرجع التسعيرة — ده اللي المراجع هيسأل عنه"). The next Accruals run is what actually books it.
**Two more panels on the same screen, unrelated to rates:**
- **Broken member links** — players whose `member_id` doesn't resolve to any real member row (often because a *membership number* was stored where a member row id belongs). The accrual still books correctly but can never appear on that member's own statement. The screen guesses the likely correct member and links to fix it on the player's own edit screen — described in the code as "the one thing on this screen the accountant cannot decide alone."
- **Academy contracts import** — copies (never moves) contracts from the table real academy contracts live in into the separate table the monthly settlement engine actually reads, filling in three settlement terms (settlement day, grace period, penalty rate) the settlement engine needs but the source table doesn't have. Already-imported contracts are skipped, not duplicated.
## القيود الافتتاحية (Opening Entries) & إعادة تبويب الحسابات (Reclassification)
**Opening Entries** — only fiscal years still `status='open'`. Posted through the normal `JournalService::createEntry()` — same closed-period/balance/postability checks as any other entry — dated to the fiscal year's start, `reference_type='opening'`. The separate "snapshot" action archives a point-in-time export of every account's balance for the year; it feeds nothing else — every report recomputes live from journal lines regardless.
**Reclassification** (the generic account tool, distinct from the sub-treasury-specific one under Posting Chains) — lists every account that's a header or inactive but still carries a non-zero posted balance (money that can no longer be reached by an ordinary entry). Direction is derived from the **source account's actual current balance sign**, not its declared nature. A **reason is required** — "ده بيفضل في الدفاتر." Cross-type moves (asset into revenue, etc.) are explicitly out of scope and refused.
## المطابقة البنكية (Bank Reconciliation)
**Create:** bank account, statement date, statement balance, reconciliation date, notes. `book_balance` is pulled live from the bank account's own running balance; `adjusted_balance` starts equal to it.
**How the numbers actually move** — this is a book-side worksheet, not a two-column bank/book comparison:
```
adjusted_balance = book_balance − Σ(bank charges) + Σ(bank interest) + Σ(book errors)
difference = statement_balance − adjusted_balance
```
**Outstanding checks, deposits-in-transit, and bank errors are logged and shown in the item list but contribute nothing to this arithmetic** — their case branches are empty. In a textbook reconciliation those would normally adjust the *bank* side; since this screen only tracks one book-side `adjusted_balance`, recording one today is documentation only, not a number that affects whether the reconciliation balances.
**Completing** requires `difference` to be exactly zero. On completion, every uncleared bank-charge/interest item posts its own journal entry automatically. If the bank account has no linked GL account, these entries are silently skipped — no error shown.
## القروض البنكية (Bank Loans), الاعتمادات المستندية (Documentary Credits), خطابات الضمان (Guarantees)
**Bank Loans:** amortization is standard equal-installment (annuity) math via bcmath; the schedule is generated up front with the *final* installment's principal forced to absorb whatever rounding remains, rather than trusting the formula's last computed figure. Recording a payment reduces the outstanding balance by the installment's **scheduled** principal, not the amount actually paid — a partial or over-payment doesn't reconcile the running balance against what was truly collected.
**Fixed this session** (both had been broken since they shipped — confirmed by testing live): the create forms for Documentary Credits, Letters of Guarantee, and the collateral field/loan-type list on Bank Loans previously posted field names the controllers didn't read and offered dropdown values that didn't exist in the live `ENUM` columns — submitting any of them threw a database error or silently discarded what was typed. All three now match the live schema and were verified end-to-end (`TEST-DOC` records exist in the demo data as evidence). The `interest_rate`/`commission_rate` columns were also widened from `DECIMAL(5,4)` (capped at 9.9999%) to `DECIMAL(7,4)`, since real Egyptian loan/guarantee rates routinely exceed that.
**Still true, not yet fixed (lower priority, no crash risk):** both Documentary Credits and Letters of Guarantee have a fully-built status state-machine service (`transition()`, with a proper allowed-transitions map) that the status-change action never actually calls — the live action writes the status column directly with no transition validation at all, so any status can currently be set from any other status.
## التسويات (Settlements) & المطالبات والتحصيل (Billing)
**Settlements** — a maker/checker flow: create (`status='draft'`, **no server-side field validation at all** — worth tightening) then a *separate* approve permission posts the actual party-statement entries (credit/debit depending on customer vs. supplier on each side) and flips to `posted`. Entity types other than customer/supplier get no statement posting at all — the settlement row exists but leaves no visible trail on either party's account.
**Billing** — a generic "collect any outstanding charge from any module's table" engine, configured entirely through `billing_sources` rows rather than code changes. Declaring a new billable source requires naming its table/amount column/date column, at least one of a member/player/name column ("a charge nobody owes cannot be collected against anyone"), and any filter conditions as **structured column+operator+value pairs — never a raw WHERE clause**, with every identifier checked against the live schema before use. Collecting always re-reads the amount from the live source row, never trusts the client. A source with any prior collections can't be deleted, only deactivated, to preserve the audit trail.
## السنوات المالية (Fiscal Years), أنواع اليومية (Journal Types), رسوم الفروع (Branch Fees)
**Fiscal Years** — overlap with any other non-archived year is blocked on create, though the index page still surfaces overlaps that pre-date the check (a legacy calendar-year/July-year overlap can exist from before this validation existed). **Closing a year** requires every calendar month inside it to already be individually closed; the closing entry zeroes every revenue/expense account into retained earnings and is deliberately allowed to post *inside* the very month it just required to be closed (a specific escape hatch exists only for this, to avoid an impossible chicken-and-egg deadlock).
**Journal Types****the entire numbering configuration on this screen (prefix, length, yearly/monthly, "is default") has no effect on anything.** Every journal entry actually created anywhere in the system numbers itself through a separate, unrelated mechanism. This screen's fields are stored and editable but functionally inert — worth knowing before spending time configuring them expecting a specific number format.
**Branch Fees** — four fee codes: martyr stamp (added to membership-form sales), cheque clearing fee, bounced cheque fee, card commission. Resolution is branch-specific first, falling back to a global (no-branch) row. **Turning any fee on requires a non-empty reason note** — "اكتب سبب أو مرجع الرسم — ده اللي المراجع هيسأل عنه." A "generalize to every branch" action copies one branch's active setting to the global row, auto-tagging the note with where it came from. Also on this screen: a single global government expense-withholding profile (three rates, all zero until explicitly activated with a reason) and a toggle for whether cash disbursement of expenses is allowed at all (default: not allowed, per the government-institution rule the club operates under).
---
## Report screens — what each one actually calculates
**Trial Balance** — opening balance for every account is derived from cumulative posted ledger movement before the date range, **explicitly not** from the account's `opening_balance` field — the code comment records a real historical bug where reading that column too caused retained earnings to report at exactly double.
**General Ledger** — same "derive from the ledger, never the stored column" rule; running balance is computed oldest-first internally, then the display order is reversed — the underlying math is always correct even though the screen shows newest-first.
**Income Statement** — revenue = credit − debit within the period; expense = debit − credit; only non-zero accounts listed; net income = revenue − expenses.
**Balance Sheet** — every account's balance is cumulative movement up to the as-of date. Because no year-end closing entry has ever actually been posted in this production data, revenue/expense accounts still carry their all-time balances, so **accumulated undistributed profit is injected into equity from the ledger's inception date**, not from the fiscal year start — the code comment documents the exact real discrepancy this fixes. This synthetic line is labeled "أرباح متراكمة غير مقفلة." The current fiscal year's own net income is shown alongside it separately, since "that's the figure the board actually asks about."
**Consolidated Balance Sheet** — runs the balance sheet once per branch plus once unfiltered for the total. Simple aggregation — no intercompany elimination of any kind.
**Cash Flow Statement (indirect method)** — cash accounts are identified by a **name-pattern heuristic** (`is_bank_account=1` or a name matching "نقد"/"صندوق"/"Cash"/"Bank" — there's no dedicated flag), likewise fixed-asset and loan-like-liability accounts are matched by name pattern rather than a schema tag. A **reconciling item is always shown explicitly** — the gap between the actual change in cash and what operating+investing+financing account for — an honest admission that name-based classification won't always explain 100% of real cash movement.
**Statement of Changes in Equity** — opening/closing per equity account, cross-checked to tie exactly to the Balance Sheet's own totals by construction (same accumulated-profit logic, same two dates).
**AR/AP Aging** — five fixed buckets purely by `due_date` vs. today: current, 1–30, 31–60, 61–90, 90+ days overdue. The optional member/supplier filter only narrows the list — the aging *summary* itself is always company-wide.
**Treasury & Payments Report** — a raw, unaggregated transaction-level list straight from `payments`, not derived from journal entries at all — useful specifically because it can't disagree with what a cashier actually took in.
**Revenue Analysis** — everything computed from `payments`, never journal entries. **Worth knowing:** every branch breakdown attributes revenue to the *paying member's home branch*, not the branch/till where the money was physically collected — a guest payment with no member falls into an unattributed "بدون فرع" bucket, and a member paying at a branch other than their own is counted against their home branch, not the till.
---
## Confirmed defects found while researching this chapter (status noted per item)
| # | Where | Issue | Status |
|---|---|---|---|
| 1 | Documentary Credit create form | Field names didn't match the controller; no field at all for the `NOT NULL` beneficiary_name; missing required `expiry_date` | **Fixed & verified live** |
| 2 | Letter of Guarantee create form | Same beneficiary_name gap; `guarantee_number` marked required in HTML while the controller ignores it, silently blocking every submit client-side; `guarantee_type` dropdown offered values not in the live ENUM | **Fixed & verified live** |
| 3 | Bank Loan create form | `collateral` field name didn't match `collateral_description`, silently discarding typed text; `loan_type` dropdown offered two values (`term_loan`, `equipment`) that don't exist in the ENUM | **Fixed & verified live** |
| 4 | `bank_loans.interest_rate`, `letters_of_guarantee.commission_rate` | `DECIMAL(5,4)` caps at 9.9999% — below any realistic Egyptian loan/guarantee rate | **Fixed** (widened to `DECIMAL(7,4)`) |
| 5 | Documentary Credit / Letter of Guarantee status changes | A fully-built transition state machine exists in the service layer but is never called — status can be set directly to anything from anything | Documented, not yet fixed |
| 6 | Accounting Dimensions | Entirely disconnected from posting — no consumer reads a dimension value anywhere | Documented, not yet fixed |
| 7 | `BudgetService::getCostCenterVariance()` | Implemented, unreferenced by any route | Documented (dead code) |
| 8 | Journal Types numbering configuration | Stored and editable but has no effect on any real entry number | Documented, not yet fixed |
| 9 | Bank Reconciliation | Outstanding-check / deposit-in-transit / bank-error items logged but don't affect the balancing arithmetic | Documented — likely intentional given the book-side-only model, flagged for a product decision rather than treated as a bug |
| 10 | Cross-Entity Settlements | No server-side field validation on create | Documented, not yet fixed |
| 11 | Revenue Analysis branch attribution | Attributes revenue to the paying member's home branch, not the collecting till | Documented — a modeling choice worth confirming with finance, not necessarily wrong |
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment