Architecture
Binaries, chart domain, outbox publisher, and service layout.
Binaries
| Binary | Path | Role |
|---|---|---|
| API | cmd/api | HTTP + background outbox publisher |
| Log consumer | cmd/logconsumer | Dev-only event logger |
The API binary runs the HTTP server and outbox publisher in the same process — see services/accounting/cmd/api/main.go.
Domain packages
| Package | Role |
|---|---|
internal/chart | Create account, classifications, hierarchy validation |
internal/command | Same Execute pattern as template |
internal/publisher | Poll outbox, publish to Azure Service Bus |
internal/ledger | Trial balances and balance sheets |
Handlers: services/accounting/internal/handlers/accounts.go.
Shared platform code
Most of this service's HTTP layer is not this service's code. It lives in common/ and
is shared with every service, so the behaviour a client sees cannot drift apart one
service at a time — see
docs/decisions/0018-shared-platform-code-in-common.md for the rule
and docs/decisions/0019-the-handler-layer.md for how much of
handlers/ it covers.
| Package | Role |
|---|---|
common/logging | Process logger: JSON when deployed, console locally |
common/httpapi | Request id, request log, panic recovery, error envelope, command and pagination contracts, API and spec construction, root and docs endpoints |
common/response | The HTTP {header, payload} envelope |
common/envelope | The event envelope on the bus, including optional why |
common/reqmeta | Actor, request id, and matched route on the context |
common/health | Liveness and readiness paths |
common/swaggerui | Vendored Swagger UI assets, served at /api-docs |
What stays here is the per-operation registration, the request and response types Huma reflects into the spec, the handler bodies, and this service's own error cases. Everything this service says about itself — name, version, title, paths, schema prefix — is a constant block in services/accounting/internal/handlers/api.go.
The router installs the shared chain in one call, so the ordering it depends on is decided once: services/accounting/internal/handlers/router.go.
Events
Payload structs and golden fixtures live in
services/accounting/events/.
Event envelope (including optional why): common/envelope/, shared
with every service.
Each event carries the correlation id of the request that produced it, written into the
outbox row inside the same transaction and copied onto the broker message, so one
requestId finds the request and everything downstream of it.
Migrations
0001_platform.sql— principals, command idempotency, and outbox tables0002_chart.sql— charts and accounts0003_why.sql— audit envelope column
Accounting uses a service-wide outbox, rather than the per-domain layout specified in docs/decisions/0015-domain-scoped-outbox-and-table-prefixes.md.
Authorization
Cedar policy: services/accounting/policies/policy.cedar. Local startup applies services/accounting/testdata/dev-seed.sql automatically. See docs/local-dev.md for manual seeding and reset behavior.
Entity calendars
Legal entities store timezone and books_start_month. The
calendar API
initializes an active entity once from verified values. The conditional update and
audit event commit together; concurrent requests cannot overwrite the configuration.
Current-period reads calculate the Open month from the server clock in the entity's timezone, without stored monthly rows or a rollover job. Local seeds configure only unconfigured example entities without closure history and preserve existing values.
A controller or platform admin can correct a timezone entered wrongly for an active,
configured entity. A nonblank why.detail is required before taking command or period
locks. The correction is serialized with period commands and audited with the previous
and new values and the reason. It leaves the books start month, closed periods, and
existing postings unchanged. Previously set dates, including created dates,
online-payment transaction dates, and rent dates on the 1st, are not restated.
Books start also supports a reasoned correction by a controller or platform admin.
The exclusive entity lock prevents a concurrent posting or close from invalidating
its checks. A later start cannot exclude posted journal months, and closed months
must form a continuous sequence beginning at the corrected start. Conflicting
closures must be reopened first; correction never rewrites postings or closures.
The before and after start and reason are recorded in
period.calendar_books_start_changed in the same transaction.
Near a month boundary, the timezone correction can move the Open month backward and make a soft-closed month Open again. Postings already made to the later month remain there. A correction that would make a hard-closed month current is rejected.
services/accounting/migrations/0023_entity_calendar.sql leaves existing entities unconfigured. Its rollback refuses to erase configuration or its audit.
Period controls
Close and reopen take an exclusive entity advisory lock; journal period checks take
a shared lock. Closing proceeds in month order from books_start_month. Reopen
removes the selected month and later closures, with one audit event per month.
Journal posting periods
Posting defaults journal_entries.post_month to the entity's current Open month
and holds the shared period lock through commit. Transaction dates stay unchanged.
Historical posted entries and imports use their original entry_date month.
Posting requires a configured calendar. Super Admin overrides require an active
person with an unexpired global grant in principal_grants; a role label alone is
insufficient. The posting API
describes manual period selection.
Report periods
Balances, trial balances, and balance sheets aggregate by posting month independently of transaction dates. Journal lists support separate transaction-date and posting-month filters. See report periods.