Skip to main content

The two-book model

One codebase, two databases, one per legal entity.

Why not one database

  • Separate regulators (SCA vs IFSCA). Audit trails cleanly separable at handover time.
  • Separate custodians — different reconciliation partners.
  • Separate tax regimes.
  • Separate risk profiles — a data-loss incident on India shouldn't touch UAE.

What's shared

  • The FastAPI app — one image, one main.py, one router set.
  • The ledger schema — every table + column, driven by one alembic history.
  • The posting engine — services/posting.py, source-agnostic.
  • The recost dispatch — apply_recost_gtn and apply_recost_viewtrade; routed by Transaction.source.
  • Every reconciliation check — India-only checks live in the same registry as UAE checks and return ok when there's nothing to reconcile.
  • The frontend — one Vite build, book toggle in the sidebar.

What's per-book

Per-bookUAEIndia
Databaseledger_uaeledger_ind
Backend port (local)80778078
Env file.env.uae.local.env.ind.local
ENTITY_IDVALURA_UAEVALURA_IND_IFSC
Chart of accounts seedaccounts.json (32)accounts.india.json (19)
SourcesGTN, ZAG, Gold, PrivateViewTrade IFSC, GlomoPay LRS
India-only tables(empty)lrs_remittances, dividend_events
India-only endpoints(return safe zeros)16 /v1/india/* endpoints

The "degrade to OK on the wrong book" pattern

Every India check is registered on both books. When the check runs on UAE:

  • Short-circuits on empty state — e.g. lrs_viewtrade_tie_out queries lrs_remittances, gets empty, returns ok.
  • Detects the wrong ENTITY_ID — treasury / compliance checks read settings.entity_id and return an early "not applicable" ok.

The "no UAE code changes" hard lock

  • classify.py — added classify_viewtrade and classify_glomopay alongside classify_zag; existing functions untouched.
  • recost_writer.py — added apply_recost_viewtrade; apply_recost_gtn untouched.
  • posting.py — added the India-specific accounts to ALL_POSTING_ACCOUNT_CODES; no UAE posting rule modified.
  • config.py — added India settings; all UAE defaults preserved.

The migrations

One alembic history covers both books. Every migration MUST be a no-op on the book it doesn't concern (either targets shared tables, or targets tables that exist in both schemas but stay empty on the other book).

The frontend book toggle

The UI keeps a ledger_entity key in localStorage and reads it via apiBase():

const ENTITY_BASE: Record<EntityId, string> = {
UAE: import.meta.env.VITE_LEDGER_API_URL || "http://localhost:8077",
IND: import.meta.env.VITE_LEDGER_API_URL_IND || "http://localhost:8078",
}

Toggling calls window.location.reload() — clears react-query caches so no stale UAE data leaks into an India view.

Adding a third book

  1. Add accounts.<xx>.json to db/seeds/.
  2. Add ENTITY_ID=VALURA_<XX> support to the seeder + config.
  3. Stand up a third DB + backend + env file.
  4. Add per-book sources (adapters + classify + importer + sync).
  5. Add per-book endpoints with the same "degrade to OK on other books" pattern.

No shared-code fork.