Skip to content

Accounting — Process flows

Architecture & lifecycle auto-generated from code (dispatch, CHECK status enums, SET status writes). Sequences & notes are authored.

Architecture

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
flowchart LR
  C["Client"] --> A["Pages Fn accounting<br/>bolt_session auth · company_id scope"]
  A --> D["dispatch<br/>void · reopen"]
  D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
  K --> DB[("D1 tables")]
  D --> DB
  D --> EV["events<br/>append audit trail"]
  EV --> DB
  DB -.-> T["accounting_payment<br/>accounting_open_item_settlement<br/>accounting_payment_allocation<br/>accounting_payment_void<br/>accounting_wht_certificate<br/>budget<br/>posting_lock<br/>accounting_fx_reval_run"]

Lifecycle

Kernel document lifecycle (erp_document.docstatus)

Every money/stock-moving document in this module posts through the shared kernel, walking a fixed docstatus:

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
stateDiagram-v2
  [*] --> Draft: createDocument (docstatus 0)
  Draft --> Submitted: submitDocument (docstatus 1, journal posted)
  Submitted --> Cancelled: cancelDocument (docstatus 2, reversed)
  Submitted --> [*]
  Cancelled --> [*]

accounting_period.status

States: open (initial) · soft-closed · locked

status set by (grepped SET status='…')
locked closePeriod
open reopenPeriod

accounting_fx_reval_run.status

States: queued (initial) · running · posted · failed · cancelled

accounting_tax_return_export.status

States: preview (initial) · finalized · filed · amended

Key flow: submitPayment (receipt / payment)

One end-to-end call. Note that the GL journal posts through the kernel before the subledger batch; on any batch abort where no payment row committed, the orphaned journal is reversed via cancelDocumentIdempotent.

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","actorBkg":"#2d2d30","actorBorder":"#569cd6","actorTextColor":"#d4d4d4","signalColor":"#9cdcfe","signalTextColor":"#d4d4d4","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa","labelBoxBkgColor":"#3a3d41","labelBoxBorderColor":"#dcdcaa"},"sequence":{"useMaxWidth":false}}}%%
sequenceDiagram
  participant C as Client
  participant F as accounting Fn
  participant L as PostingLock
  participant K as Kernel GL
  participant D as Accounting subledger
  participant W as WHT

  C->>F: POST PAYMENT or RECEIPT with idempotency_key
  F->>F: require idempotency_key and functional currency
  F->>L: assertPostingOpen posting_date
  L-->>F: open or 409 locked
  F->>F: aggregate allocations per open item
  F->>D: openItemSnapshot check aggregate le open amount
  F->>F: build balanced dr cr lines assertBalanced
  F->>K: createDocument then submitDocument
  K->>K: post erp_journal_entry idempotent on key
  K-->>F: submitted doc id and journal entry id
  F->>F: validate wht_code read-only (no number drawn)
  F->>D: pre-check payment by kernel_document_id
  alt already posted replay
    D-->>F: prior payment
    F->>W: ensureWhtCertificate reuse existing
    F-->>C: ok payment idempotent true
  else fresh post
    F->>D: batch insert payment + allocations + open_item_settlement
    alt batch abort
      opt concurrent winner already committed
        F->>W: ensureWhtCertificate reuse existing
        F-->>C: ok payment idempotent true (no journal reversal)
      end
      F->>K: cancelDocumentIdempotent reverse orphan journal
      alt settlement CHECK abort
        F-->>C: 409 concurrently settled retry
      else other abort cause
        F-->>C: rethrow original error
      end
    else committed
      F->>W: ensureWhtCertificate draw gap-free cert_no
      F-->>C: ok payment and wht_certificate
    end
  end

Notes

  • Purpose. Kernel-based double-entry accounting for a multi-tenant Thai ERP: journal vouchers, AR/AP settlement (payment/receipt), period close & posting locks, budgeting, WHT certificates, FX unrealized reval, bank statement import + reconciliation, recurring journals, and PP30/PND tax reports. Auth requires capability finance, accounting, or manage; every query is company_id-scoped.
  • Kernel-first, subledger-second, atomic. Handlers build balanced GL lines (assertBalanced), post them via kernel.createDocument+submitDocument (which own the erp_journal_entry and idempotency dedup), then land their own rows in one env.DB.batch. If that batch aborts and no payment row committed, the already-posted journal is reversed with cancelDocumentIdempotent so the GL is never left with a journal and no subledger row (round-7 LL-2). Note: submitPayment emits no domain event on success — emitEvent fires only from period close, FX reval, and bank match/unmatch.
  • Idempotency is mandatory for money moves. submitPayment hard-rejects a missing idempotency_key (400) before any DB work — the key is what makes submitDocument dedup a network-retry instead of double-posting payment + journal + settlement. FX reval and recurring journals derive deterministic keys (fx-reval:period:runNo, recurring:template_id:run_date) so a crashed run replays the same journal rather than posting a second one.
  • Append-only ledgers, mutable reversal side-tables. erp_accounting_payment, payment_allocation, wht_certificate, vat_return_pp30, and the FX reval rows have BEFORE UPDATE/DELETE triggers that RAISE(ABORT). A void therefore never mutates the payment; it inserts into the mutable erp_accounting_payment_void and re-opens erp_accounting_open_item_settlement. The WHT certificate row and its statutory number are always retained (Thai law) even when voided.
  • Concurrency guards. Settlement over-allocation is serialized by a CHECK'd open_item_settlement ledger (loser's whole batch aborts and its journal is reversed → 409 retry). Single bank-line match is serialized by a mutable reconciliation_active PK anchor (append-only match table stays audit-only); unmatch deletes the anchor and flips the line back to unreconciled but does not reverse GL.
  • Gotchas seen in code. withTransaction is a dead no-op (guards env.db.exec but the binding is env.DB) — atomicity comes from env.DB.batch, not that wrapper. Multi-currency settlement is intentionally refused: a payment currency must equal the functional currency, otherwise it would book foreign numerals as THB. Period close is rejected (409, recorded as a period_close_run state rejected) if any posting-relevant draft exists; reopen expires the posting lock rather than deleting it, because the append-only period_close_run holds a hard FK to erp_posting_lock.