# Feature: Accounting & the Posting Engine

> **Status:** Specified, not built · **Phase:** 2.1 — **first**, before any money feature
> **Regulatory basis:** GN 679 Reg 24–26 (books of accounts, preparation and disclosure of accounts); IFRS; BoT prudential returns

---

## Why this comes first

This is the hardest sequencing constraint in the project.

Every value movement — a deposit, a disbursement, a fee, an interest accrual, a
provision — must produce balanced double-entry journal entries. If savings ships
before the posting engine, those transactions exist only as rows in a savings
table with no ledger behind them. Adding the ledger afterwards means
reconstructing months of history from transaction logs that were never designed
to carry the information a journal entry needs.

That reconstruction is not a migration. It is forensic accounting on your own
product, and it is the single most expensive mistake available in this codebase.

**Nothing that touches money is built until this is done.**

---

## Chart of accounts

Five account classes, following the standard Tanzanian SACCOS structure:

| Range | Class | Normal balance |
|---|---|---|
| 1000–1999 | Assets | Debit |
| 2000–2999 | Liabilities | Credit |
| 3000–3999 | Equity | Credit |
| 4000–4999 | Income | Credit |
| 5000–5999 | Expenses | Debit |

A representative default set, seeded per tenant and editable:

```
1010  Cash in hand                    2010  Member savings — compulsory
1020  Bank accounts                   2020  Member savings — voluntary
1030  Mobile money — M-Pesa           2030  Fixed deposits
1031  Mobile money — Airtel           2040  Interest payable on savings
1100  Loans receivable — principal    2050  Trust account (mobile money float)
1110  Interest receivable
1190  Provision for loan loss (contra) 3010  Member share capital
1200  Prepayments                     3020  Statutory reserve
1300  Fixed assets                    3030  Retained earnings
1310  Accumulated depreciation (contra)
1900  Suspense                        4010  Interest income on loans
                                      4020  Loan fees and commissions
5010  Interest expense on savings     4030  Penalty income
5020  Loan loss provision expense     4040  Recovery of written-off loans
5030  Staff costs
5040  Depreciation
```

Two entries above are easy to overlook and both are required:

- **1190 Provision for loan loss** is a *contra-asset*. It sits in the asset
  range but carries a credit balance, netting against loans receivable. A chart
  that treats it as an ordinary asset will report gross portfolio as net.
- **2050 Trust account** holds customer mobile-money float. Under the NPS
  regulations customer funds must be **segregated** — this is not the
  institution's cash, and merging it into 1030 is a regulatory breach, not a
  presentation choice.

---

## Data model

### `ledger_accounts`

`tenant_id`, `code`, `name`, `class`, `type`, `parent_id` (accounts are a tree),
`is_contra`, `normal_balance`, `is_system` (system accounts cannot be deleted —
the posting engine references them by role), `is_active`.

### `journal_entries`

`tenant_id`, `entry_number`, `entry_date`, `posted_at`, `description`,
`source_type` + `source_id` (polymorphic — the loan repayment, deposit or
accrual that caused it), `posted_by`, `reversed_by_entry_id`, `is_reversal`.

### `journal_lines`

`journal_entry_id`, `ledger_account_id`, `debit` (bigint cents), `credit`
(bigint cents), `member_id` (nullable — sub-ledger attribution), `branch_id`.

**A line carries either a debit or a credit, never both.** Allowing both invites
a line that nets to zero and hides an error.

---

## The engine

```php
$posting = Posting::make()
    ->on($date)
    ->describedAs('Savings deposit — MBR/2026/00042')
    ->causedBy($transaction)
    ->debit(LedgerAccount::CASH_IN_HAND, $amountInCents)
    ->credit(LedgerAccount::MEMBER_SAVINGS_VOLUNTARY, $amountInCents, member: $member)
    ->post();
```

Rules the engine enforces, without exception:

1. **Debits equal credits.** Refuses to post otherwise. This is not a validation
   message shown to a user — it is an exception, because an unbalanced entry
   means a bug in the calling code.
2. **Atomic with its cause.** The posting and the business transaction commit in
   one database transaction. A deposit that succeeds while its journal entry
   fails is worse than a deposit that fails outright.
3. **Append-only.** No updates, no deletes, ever. A correction is a **reversing
   entry** that references the original. The original stays visible because it
   is evidence.
4. **Closed periods reject postings.** Once a month is closed, entries dated
   into it are refused; they go to the current period with an explanatory note.
5. **Integer cents throughout.** No floats anywhere in the path.

---

## Standard postings

| Event | Debit | Credit |
|---|---|---|
| Savings deposit | Cash / Mobile money | Member savings |
| Savings withdrawal | Member savings | Cash / Mobile money |
| Savings interest accrual | Interest expense on savings | Interest payable |
| Share purchase | Cash | Member share capital |
| Loan disbursement | Loans receivable | Cash / Mobile money |
| Loan repayment | Cash | Loans receivable (principal), Interest income, Fee income, Penalty income |
| Interest accrual on loans | Interest receivable | Interest income |
| Provision increase | Loan loss provision expense | Provision for loan loss |
| Write-off | Provision for loan loss | Loans receivable |
| Recovery after write-off | Cash | Recovery of written-off loans |
| Fee charged | Cash / Loans receivable | Loan fees and commissions |
| Depreciation | Depreciation expense | Accumulated depreciation |

**Write-off and recovery deserve attention.** A write-off consumes the provision
already raised — it does not hit the income statement again, because the loss
was recognised when the provision was made. A later recovery is *income*, not a
reversal of the receivable, because the receivable no longer exists.

---

## Financial statements

Generated from the ledger, never from parallel counters:

- **Statement of Financial Position** (balance sheet)
- **Statement of Comprehensive Income**
- **Statement of Cash Flows**
- **Statement of Changes in Equity**
- **Trial balance** — real-time, exportable

Accrual basis throughout, as Reg 24 requires. Interest is recognised as it
accrues, not when it is received — except on non-performing loans, where accrual
stops and previously accrued interest is reversed. Continuing to accrue income
on a loan that will not be repaid overstates earnings, which is precisely what
the classification rules exist to prevent.

---

## Reconciliation

Three separate reconciliations, each answering a different question:

1. **GL ↔ sub-ledger.** Does the savings control account equal the sum of member
   balances? Does loans receivable equal the sum of loan balances? A drift here
   means a transaction posted to one and not the other.
2. **Bank reconciliation.** Statement against cashbook, with auto-matching on
   amount and date, and manual matching for the remainder.
3. **Mobile money settlement.** Provider settlement report against our
   transaction records, daily. Discrepancies here are usually timing, but
   occasionally they are money.

A scheduled job asserts that the **trial balance balances** and alerts on any
drift. If the ledger goes out of balance, we want to know that day — not at the
annual audit, when it becomes a management letter point.

---

## Test checklist

- [ ] Posting refuses to write when debits ≠ credits
- [ ] Business transaction and its journal entry are atomic — failure rolls both back
- [ ] No code path updates or deletes a posted journal line
- [ ] Reversal creates a new entry and leaves the original intact
- [ ] Closed periods reject backdated postings
- [ ] Trial balance balances after every transaction type *(parameterised over all of them)*
- [ ] Contra accounts (provision, accumulated depreciation) net in the correct direction
- [ ] Trust account never merges with institutional cash
- [ ] Accrual stops on non-performing loans and prior accrual is reversed
- [ ] Write-off consumes provision without a second income-statement hit
- [ ] Recovery posts as income, not as a receivable reversal
- [ ] GL control accounts equal their sub-ledger sums
- [ ] Tenant isolation on every ledger query

---

## Build order

1. `ledger_accounts` + seeded Tanzanian SACCOS chart
2. `journal_entries`, `journal_lines`
3. `Posting` builder with the balance assertion
4. Account-role constants so the engine references accounts by meaning, not code
5. Reversal
6. Trial balance
7. Period close
8. Financial statements
9. Reconciliation tools
10. The daily balance-assertion job
