Skip to content

Phase 1, wave 1 — contracts for building Plans 3 and 4 in parallel

Plans 3 (workflow engine) and 4 (accounting core) depend only on Plans 2 and 2b, which are merged. They are built at the same time on two branches cut from phase1-wave1. This document fixes everything the two branches share, so neither waits on the other and the two merges do not conflict on meaning.

Authority: the architecture spec (2026-09-20-erp-architecture-design.md). §5 governs Plan 3 and §10–§11 govern Plan 4. This document only divides the work. It changes no design decision.

Already on phase1-wave1, with tests.

  • posting.Semantic and its nine constants mirror constants.yaml → workflowSemantics. A test pins them to vocab.Semantics.
  • vocab.ControlKinds mirrors constants.yaml → controlKinds (§10.1).
  • posting.Event describes one instance entering one state: OrgID, Kind, InstanceID, SourceID, Semantic, ControlKind, PastAccrual, BusinessDate, ActorMemberID.
  • Event.PostsToLedger() is true on entering accrual or payout, and on entering rejected or cancelled with PastAccrual set.
  • posting.Dispatch(ctx, tx, ev) ignores events with no ledger effect. For ledger events it calls the registered Poster, or returns ErrNoPoster if none is registered.

Plan 3 calls posting.Dispatch inside every transition’s transaction and aborts the transition on any error. It never imports accounting. Its tests use a fake Poster.

Plan 4 implements posting.Poster by resolving the posting rule for (Kind, Semantic) (§10.3) and registers it with posting.Register at boot. It never imports the workflow engine. Its tests drive Poster.Post with hand-built Events.

If a plan needs to change the seam, the change lands on phase1-wave1 first and both branches rebase onto it. Neither branch edits internal/platform/posting on its own.

Plan 3 — workflow Plan 4 — accounting
Go packages internal/platform/workflow/…, internal/modules/workflow/… internal/platform/accounting/…, internal/modules/accounting/…, internal/platform/countrypack/…
Migrations 00030–00049 00050–00069
Provisioning step order 60–69 70–79
Permissions (constants.yaml) workflow.* accounting.*
OpenAPI paths /v1/workflow/… /v1/accounting/…
sqlc query files db/queries/workflow*.sql db/queries/accounting*.sql, db/queries/fiscal*.sql

00024–00029 stay free for hotfixes on main.

These files are edited by both branches. Each branch only appends its own entries and never reorders or rewrites the other’s.

  • constants.yaml (permissions list), then re-run make generate
  • api/openapi.yaml, then regenerate internal/gen/api
  • internal/app/app.go and the module imports in cmd/api, cmd/migrate and cmd/orgadmin (internal/app/binaries_test.go keeps the three in step)
  • internal/kernel/db/retention_test.go lists
  • docs/ROADMAP.md: each branch ticks only its own plan’s lines

Conflicts in generated files are resolved by regenerating, never by hand-merging.

Plans 3 and 4 merge into main separately, and the user decides when. Goose applies migrations in version order. If Plan 4 merges first, Plan 3 renumbers its migrations above Plan 4’s before it merges. No deployed database exists yet, so a renumber costs only a rename. A renumber after the first deployment is forbidden.

  • Malaysian audit compliance (§11A). Every state change and every posting writes audit.Write in the same transaction. The ledger and the decision log are append-only: corrections post reversals, and DELETE is revoked.
  • Tenant isolation: an explicit org_id = $n predicate plus forced RLS, composite (org_id, id) foreign keys, and new tables in the retention lists.
  • Advisory locks: take every non-audit lock class before the first audit.Write.
  • Money is money.Amount end to end, and a string on the wire. The sqlc numeric override already exists.
  • The business date is kernel/clock’s UTC date (§20.1.2). This is the spec’s decision. Whether to move to Asia/Kuala_Lumpur is still open with the user. Keep every date derivation behind clock so a later switch touches one place.
  • Plan 3 must persist the resolved approver set at decision time. SuperiorOf reads reporting lines and unit heads as of now, and only assignments are dated.
  • Tests: run only the changed packages during development, and the full suite once before a merge.