Flow
ServicesAccounting Service

Architecture

Binaries, chart domain, outbox publisher, and service layout.

Binaries

BinaryPathRole
APIcmd/apiHTTP + background outbox publisher
Log consumercmd/logconsumerDev-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

PackageRole
internal/chartCreate account, classifications, hierarchy validation
internal/commandSame Execute pattern as template
internal/publisherPoll outbox, publish to Azure Service Bus
internal/ledgerTrial 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.

PackageRole
common/loggingProcess logger: JSON when deployed, console locally
common/httpapiRequest id, request log, panic recovery, error envelope, command and pagination contracts, API and spec construction, root and docs endpoints
common/responseThe HTTP {header, payload} envelope
common/envelopeThe event envelope on the bus, including optional why
common/reqmetaActor, request id, and matched route on the context
common/healthLiveness and readiness paths
common/swaggeruiVendored 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 tables
  • 0002_chart.sql — charts and accounts
  • 0003_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.

On this page