> ## Documentation Index
> Fetch the complete documentation index at: https://cona.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Accounting Engine

> How documents become general ledger entries — the work queue, the posting matrix, and deferred revenue

# Accounting Engine

Document ingestion and accounting are **decoupled through a Postgres queue**. Nothing in
the import path calls accounting directly; it enqueues, and a long-running workflow drains
the queue.

## The two phases

```mermaid theme={null}
flowchart TB
    subgraph Phase1["Phase 1 — ingestion"]
        Sync["syncIntegrationWorkflow<br/>see Temporal Orchestration"]
        Manual["manual document creation<br/>webapp server actions"]
    end

    Queue[("accounting_work_queue<br/>org_id · document_id<br/>version_hint · state · priority")]

    subgraph Phase2["Phase 2 — accounting"]
        QueueWF["syncAccountingQueueWorkflow<br/>queue: accounting-queue<br/>lanes, continueAsNew"]
        Impact["services/create-accounting-impact.ts<br/>the hub"]
    end

    GL[("general_ledger")]
    Memo[("gl_memo")]
    Deferred[("deferred_revenue_schedules")]

    Sync -->|enqueueAccountingJobsActivity| Queue
    Manual -->|releaseDocumentForAccounting| Queue
    Queue -->|"FOR UPDATE SKIP LOCKED<br/>lease batch"| QueueWF
    QueueWF --> Impact
    Impact --> GL
    Impact --> Memo
    Impact --> Deferred

    classDef boundary fill:#f8fafc,stroke:#94a3b8,color:#334155

    classDef async fill:#e3f7ea,stroke:#2f9e5c,color:#111827
    classDef data fill:#fde8e8,stroke:#c53b3b,color:#111827
    classDef pkg fill:#e8eafd,stroke:#4967E6,color:#111827

    class Sync,Manual,QueueWF async
    class Queue,GL,Memo,Deferred data
    class Impact pkg
    class Phase1,Phase2 boundary
```

The decoupling buys independent scaling, resilient retries, and idempotency: a failed
accounting run does not roll back the import.

## The queue

`accounting_work_queue` carries `version_hint`, `state`, `run_count`, `last_error`,
`priority`, and `document_type` alongside the document and org.

`syncAccountingQueueWorkflow` (`workflows/accounting/sync-accounting-queue-workflow.ts:48`)
drains it:

* **Leasing** — `fetchJobs` uses `FOR UPDATE SKIP LOCKED` and sets `state = 'leased'`
  (`:66`), so parallel lanes never collide.
* **Pipelining** — the next batch is prefetched while the current one processes (`:65`).
* **Bounded concurrency** — 6 documents per batch activity; total in-flight is
  `laneCount × 6`. The comment at `:44-45` says to raise this only after observing direct-pool
  utilisation.
* **Heartbeating** — the batch activity heartbeats after every concurrency wave, under a
  30-minute `startToCloseTimeout` (`:37-39`).
* **`continueAsNew`** only when Temporal suggests it (`:106-107`), and the prefetched
  leased batch is drained first so those rows are not stranded (`:108-110`).
* **Stale-lease recovery** — if draining fails, leases recover via the stale-lease clause.
* **Cancellation propagates deliberately** (`:82`); swallowing it would let a lane run on.

A second workflow, `recreateAccountingImpactViaQueueWorkflow`, runs on its own queue
(`recreate-accounting-impact-batch`) for bulk re-derivation.

## Inside `createAccountingImpact`

`packages/core/src/services/create-accounting-impact.ts:121` — the single function all
accounting converges on. It runs inside a transaction and touches five domains.

```mermaid theme={null}
flowchart TB
    Start["createAccountingImpact"] --> Idem["checkIdempotency<br/>:260"]
    Idem --> Actor["findOrCreateSystemActor<br/>:278"]
    Actor --> Doc["getDocumentForAccounting<br/>:286"]
    Doc --> Event["getTriggerEventSlug<br/>:379"]
    Event --> Debtor["resolveDocumentDebtorAccount<br/>:392"]
    Debtor --> Dims["fetchGlDimensions<br/>ensureRequiredGlDimensions<br/>:458-469"]
    Dims --> Prepaid["preparePrepaidInvoiceInterception<br/>:550"]
    Prepaid --> Rules["getPostingMatrixRulesCached<br/>debit + credit + custom dims<br/>:602-619"]
    Rules --> Journal["createJournalEntry<br/>:658"]
    Journal --> Lines["createEntriesForLineItems<br/>splitAndPost<br/>:735-877"]
    Lines --> Reclass["reclassification tax handling<br/>:895"]

    classDef pkg fill:#e8eafd,stroke:#4967E6,color:#111827
    class Start,Idem,Actor,Doc,Event,Debtor,Dims,Prepaid,Rules,Journal,Lines,Reclass pkg
```

Two things worth noting from the order: **idempotency is checked first**, so a redelivered
job is cheap; and **an actor is resolved before any write**, satisfying the attribution
rule.

## The posting matrix

Rules are looked up by dimension label and trigger event slug, then cached.

```mermaid theme={null}
erDiagram
    posting_matrix ||--o{ posting_matrix_column : has
    posting_matrix_column_type ||--o{ posting_matrix_column : types
    posting_matrix ||--o{ posting_matrix_rule : contains
    posting_matrix_rule ||--o{ posting_matrix_criteria : matched_by
    chart_of_accounts ||--o{ posting_matrix_rule : targets
    gl_dimensions ||--o{ posting_matrix_rule : scoped_by
```

`getPostingMatrixRulesCached` is called once per dimension per document — debit, credit,
and each custom dimension (`create-accounting-impact.ts:602-619`). This is a hot read path
and is Redis-cached; see [Redis Caching](/architecture/redis-caching).

## Related workflows

| Workflow                                   | Queue                              | Purpose                               |
| ------------------------------------------ | ---------------------------------- | ------------------------------------- |
| `syncAccountingQueueWorkflow`              | `accounting-queue`                 | drain the queue continuously          |
| `recreateAccountingImpactViaQueueWorkflow` | `recreate-accounting-impact-batch` | bulk re-derivation                    |
| `monthlyRevenueRecognitionWorkflow`        | `accounting-queue`                 | deferred revenue recognition          |
| `updateOpenDocumentsSubsidiaryWorkflow`    | `accounting-queue`                 | subsidiary reassignment               |
| `syncBankMatchingHandoffsWorkflow`         | `accounting-queue`                 | bank matching handoff                 |
| `datevExportWorkflow`                      | `datev-export`                     | DATEV file generation — 44 activities |

## Deferred revenue

`deferred-revenue` (17 files) produces `deferred_revenue_schedules`, and
`revenue_recognition_entries` links each recognised amount to a `general_ledger` row.

`revenue_recognition_entries` has **no `org_id`** — it is tenant-scoped only through
`schedule_id` and `general_ledger_id`. Any query against it must join a parent to stay
tenant-safe. See [Data Model](/architecture/data-model).

## Notes

**The queue workflow is self-healing.** `worker.ts:62` calls
`ensureAccountingQueueRunning` at startup, using a no-op ping signal (`:23`, `:55`) to
detect whether the long-lived workflow is already active.

**Connection pooling is the real constraint.** The accounting process uses a lazy
`prismaDirect` pool capped at `ACCOUNTING_QUEUE_LANES * 6 + 4` per Machine — 28 at four
lanes. `WORKER_GROUPS.md` normalises Supabase pooler URLs to transaction mode on port
`6543` for this client to avoid Supavisor's session-client cap.

**CONA-986/987 governs batch sizing.** The in-code comment ties the 6-per-batch limit to
those tickets — check them before tuning.

**DATEV is an export format, not an integration.** `datevExportWorkflow` generates files;
no CONA code calls a DATEV API.
