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:

  1. 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.
  2. A per-fund auto-provision hook that creates two custom-account rows (6001* Units Issued, 6002* NAV Adjustment) every time FundsService.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

  • Specethica-api/src/finance/seeds/coa-aaoifi.seed.ts — the 13-row AAOIFI_COA_SEED array + per-fund templates
  • Serviceethica-api/src/finance/chart-of-accounts.service.ts:593 (seedAaoifiDefaults) and :674 (ensureFundControlAccounts)
  • Controllerethica-api/src/finance/chart-of-accounts.controller.ts:225POST /finance/chart-of-accounts/seed-aaoifi
  • Fund hookethica-api/src/finance/funds.service.ts:127 — calls ensureFundControlAccounts inside create()

Account list (global seed)

CodeNameGroupSide
3201Purification ReserveEquity (3000)Cr
3202Zakat Payable ReserveEquity (3000)Cr
5201Purification ExpenseExpense (5000)Dr
5202Zakat ExpenseExpense (5000)Dr
6101Client Ledger Control — CashControl (6000)Dr
6102Client Ledger Control — SecuritiesControl (6000)Dr
6201Suspended Income — HaramControl (6000)Cr
6202Purification AllocationControl (6000)Dr
6203Mudaraba/Wakala Profit AllocationControl (6000)Cr
6301Realized FX Gain/LossControl (6000)Cr
6302Unrealized FX Gain/LossControl (6000)Cr
7001Suspense — Unallocated TradesSuspense (7000)Dr
7002Suspense — Settlement MismatchesSuspense (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:

  • customAccountModel rows, 10-digit numeric codes
  • 6001 + 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 in name
  • Idempotency key: re-query by name before insert; per-template errors collected, never thrown

Endpoint contract

POST /finance/chart-of-accounts/seed-aaoifi

  • Auth: PermissionsGuard + FINANCE_COA_MANAGE permission + admin role check (controller throws ForbiddenException otherwise)
  • 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 INTEGER column on chart_of_account_sub_group cannot store 6001-FUND1. Migrating that column is out of Phase 0 scope.

Tests

  • seedAaoifiDefaults idempotency — first run creates 13, second run skips 13.
  • ensureFundControlAccounts on 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.