Trial balance — architecture
The Tier-2 trial balance (bookkeeping-trial-balance, REQ-TB-001..REQ-TB-021)
produces a per-account, per-period view of opening balance, debit/credit
movements, and closing balance, computed live from the general ledger.
Schema declaration
The capability adds one read-only schema, TrialBalanceLine, as an
ADR-037 register fragment at
lib/Settings/register.d/bookkeeping-trial-balance.json — the monolith
shillinq_register.json is never edited. The fragment is deep-merged at load by
SettingsService::deepMergeConfig(), which unions components.schemas by key
and concatenates components.objects[], so concurrent change fragments never
collide.
TrialBalanceLine carries: periodId, accountNumber, accountName,
accountType, openingBalance, debitMovement, creditMovement,
closingBalance, currency, parentAccountNumber, administrationId. It is
marked readonly: true (and x-openregister.readonly: true) — operators never
author rows; the rows are materialised on demand.
The monolith already ships a complementary snapshot TrialBalance schema
(period totals + isBalanced, from add-shillinq-bookkeeping-operations,
REQ-FS-005). TrialBalanceLine is the per-account breakdown the Tier-2 spec
requires; the two coexist, and the fragment merge leaves the snapshot schema
untouched.
Declarative aggregation (ADR-031)
TrialBalanceLine.x-openregister-aggregations.trialBalanceByAccountPeriod
documents the canonical shape: group GLLine by (periodId, accountNumber)
within an administration, sum debit and credit movements, join Account for
name / type / currency / parent, and derive
closingBalance = openingBalance + (debitMovement − creditMovement).
PHP computation (the engine-side fallback)
Two parts of the report exceed what the declarative aggregation engine can
currently express: the prior-period opening-balance carry (REQ-TB-002) and
the cross-schema GLLine → GLTransaction → Account scoping. Per the ADR-031
exception path these are computed in PHP by TrialBalanceService::compute()
using only the real OpenRegister ObjectService API
(setRegister()->setSchema()->findAll(['filters' => …])):
- Fetch the administration's
Accountchart, keyed byaccountNumber. - Fetch the administration's
GLTransactionrows for the period to get the in-scope transaction ids (administration + period scoping, REQ-TB-017). - Fetch
GLLinerows for the period, keep only non-eliminated lines whose parent transaction is in scope, and sum debit/credit per account in integer cents. - If a prior period is supplied, compute its net per account and use it as the opening balance (REQ-TB-002); otherwise opening is zero.
- Derive
closingBalanceand per-type totals viaTrialBalanceCalculator.
TrialBalanceCalculator holds the side-effect-free arithmetic (cent conversion,
closing formula, opening-from-prior, parent roll-up, balanced check, totals) and
is unit-tested in isolation.
Performance characteristics
The computation is O(accounts + lines) over two-to-three findAll reads scoped
to one administration + period. Typical administrations (10K accounts, 100K GL
lines per period) compute well under the REQ-TB-014 two-second budget. For very
large administrations a materialised view can be added in a later tier without
changing the API contract.
Data flow
GLTransaction ─┐
GLLine ────────┼─▶ TrialBalanceService.compute() ─▶ TrialBalanceController
Account ───────┘ │ (per-account rows + totals) │ GET /api/trial-balance
└─ TrialBalanceCalculator ▼
manifest pages
(TrialBalanceLines)
API contract
GET /api/trial-balance?period_id=<id>&administration_id=<id>[&prior_period_id=<id>]
returns HTTP 200 with { data: [...], total, totals, isBalanced }, HTTP 400 on a
missing or malformed identifier (REQ-TB-015), and HTTP 500 (no stack trace) on a
GL fetch failure. The endpoint is #[NoAdminRequired]; administration scoping
plus OpenRegister multitenancy prevent cross-administration leakage
(REQ-TB-016/017). There is no write route — trial balance is read-only
(REQ-TB-007).