AAOIFI COA seed + per-fund control account auto-provisioning (Phase 0 Task 0.2)
Status: ✅ Complete
Plan reference: Task 0.2 in 2026-04-30-FINANCE_MODULE_IMPLEMENTATION_PLAN.md (operations workspace root)
What it does
Phase 0 introduced a baseline Chart of Accounts for AAOIFI-compliant fund accounting:
- A global seed of 13 sub-group accounts (purification, zakat, fund/client control, FX, suspense) that downstream Phases 1–3 depend on. Surfaced as an admin-only endpoint, not normal CRUD UI.
- A per-fund auto-provision hook that creates two custom-account rows (
6001*Units Issued,6002*NAV Adjustment) every timeFundsService.create()is invoked.
This is foundational plumbing — without these accounts, Phase 1 subledger reconciliation and NAV publish have no control accounts to post to.
Files
- Spec —
ethica-api/src/finance/seeds/coa-aaoifi.seed.ts— the 13-rowAAOIFI_COA_SEEDarray + per-fund templates - Service —
ethica-api/src/finance/chart-of-accounts.service.ts:593(seedAaoifiDefaults) and:674(ensureFundControlAccounts) - Controller —
ethica-api/src/finance/chart-of-accounts.controller.ts:225—POST /finance/chart-of-accounts/seed-aaoifi - Fund hook —
ethica-api/src/finance/funds.service.ts:127— callsensureFundControlAccountsinsidecreate()
Account list (global seed)
| Code | Name | Group | Side |
|---|---|---|---|
| 3201 | Purification Reserve | Equity (3000) | Cr |
| 3202 | Zakat Payable Reserve | Equity (3000) | Cr |
| 5201 | Purification Expense | Expense (5000) | Dr |
| 5202 | Zakat Expense | Expense (5000) | Dr |
| 6101 | Client Ledger Control — Cash | Control (6000) | Dr |
| 6102 | Client Ledger Control — Securities | Control (6000) | Dr |
| 6201 | Suspended Income — Haram | Control (6000) | Cr |
| 6202 | Purification Allocation | Control (6000) | Dr |
| 6203 | Mudaraba/Wakala Profit Allocation | Control (6000) | Cr |
| 6301 | Realized FX Gain/Loss | Control (6000) | Cr |
| 6302 | Unrealized FX Gain/Loss | Control (6000) | Cr |
| 7001 | Suspense — Unallocated Trades | Suspense (7000) | Dr |
| 7002 | Suspense — Settlement Mismatches | Suspense (7000) | Dr |
Parent groups (3000/5000/6000/7000) must already exist — they are not created by this seed. If a parent is missing the spec is reported in errors and the seed continues (partial progress is acceptable).
Per-fund account scheme
Per-fund control accounts cannot reuse the 4-digit group/sub-group schema because the COA model stores code as INTEGER and the journal account number as STRING(10) with no hyphens. The scheme therefore is:
customAccountModelrows, 10-digit numeric codes6001+ 6-digit sequential suffix (e.g. fund #1 →6001000001, fund #2 →6001000002)6002+ 6-digit sequential suffix likewise- Human-readable identity (
Fund Units Issued — <fund name> [<fund code>]) is stored inname - Idempotency key: re-query by
namebefore insert; per-template errors collected, never thrown
Endpoint contract
POST /finance/chart-of-accounts/seed-aaoifi
- Auth:
PermissionsGuard+FINANCE_COA_MANAGEpermission + admin role check (controller throwsForbiddenExceptionotherwise) - Body: none
- Response:
{ created: number, skipped: number, errors: Array<{ code: string, error: string }> } - Idempotent: second run returns
created: 0, skipped: 13
Design rationale
- Why an admin endpoint, not a startup migration? Migrations run blindly; the seed reports per-account status. Operators in multi-tenant deployments may need to inspect or replay it.
- Why surface errors instead of throwing? Some downstream installs may have customised group codes. Partial seeds are recoverable; total rollback is not.
- Why hyphenless per-fund codes? The legacy
INTEGERcolumn onchart_of_account_sub_groupcannot store6001-FUND1. Migrating that column is out of Phase 0 scope.
Tests
seedAaoifiDefaultsidempotency — first run creates 13, second run skips 13.ensureFundControlAccountson a fresh fund creates two rows; re-running with same fund is a no-op.- A missing parent group is captured in
errors, not raised.