Journal — Posting, Validation & Sourced JVs
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Platform Engineering | Internal | 1.0 | 2026-09-15 | 2026-12-15 | Draft |
Purpose. Operating guide for the journal module's write path — how a journal voucher (JV) is created, validated, routed, and posted through the one canonical spine (processjournal), how governed callers (Finni, close-step automations) submit JVs without ever writing a table directly, and how a JV links back to the source record that produced it. It is honest about where the pipeline is fully wired (✅), where it depends on per-tenant configuration that may be absent (⚠️), and what is still roadmap (◯). Evidence paths are relative to blitz/src.
1. The one intake spine
Every JV — email/xlsx upload, the manual Create screen, Finni, or a close-step automation — funnels through the same intake and the same processing workflow. There is no forked path, no "quick insert," no direct write to journalvoucherheader.
documentreceipt → extractedjournalvoucher (+ extractedjvlineitems) → processjournal
(no file) (staging, all-string fields) (the spine)| Entry point | Who | How it reaches the spine | Evidence |
|---|---|---|---|
| Manual Create / edit form | Preparer (UI) | POST /jvprocessingdata/worklist/manual → createManualJv; on submit=true → submitManualJv | packages/finance/journal/journala/controllers/journal/v1/jvprocessingdata.worklist.controller.ts |
| Finni ("submit it") | Agent, governed | submitJournalVoucher tool → submitManualJvViaApi → the same /manual endpoint | packages/finance/financeagents/src/agents/finni-agent/journal-tools.ts, journal-client.ts |
| Email / xlsx | Ingestion | extract razor → extractedjournalvoucher → processjournal | see Email & Document Ingestion |
| Module automation | closehub / recon / invoice | createsourcedjournal → processjournal | packages/finance/journal/journalwf/src/workflows/createsourcedjournal/workflow.ts |
createsourcedjournal is the shared journal-creation primitive: it materialises the documentreceipt (no file, sourcetype "Manual"), numbers the document via the platform receiptprefixes convention (JV-<padded id>; see Document Numbering), derives the header summary from balanced Dr/Cr lines, and then runs processjournal. Before it existed, postcutoffaccrual and postreconadjustment each hand-rolled ~70 lines of that intake.
2. processjournal — the lifecycle (create → validate → route → post)
processjournal(tenantId, documentreceiptid, stages?) runs staged, idempotent processing. It is guarded: a second run for the same extractedjvid reuses the existing jvprocessingdata instead of double-inserting. Evidence: packages/finance/journal/journalwf/src/workflows/processjournal/workflow.ts.
| Stage | What it does | Notes |
|---|---|---|
| 1. Insert | Promotes extractedjournalvoucher (+ lines) → journalvoucherheader + journalvoucherlineitems + jvprocessingdata | ✅ Stamps the source triple (§4) onto the header |
| 2. Idempotency guard | Skips re-insert if a jvprocessingdata already exists for this extractedjvid | ✅ |
| 3. BPD routing | Runs the journal_businessprocess ruleset (FirstPassed); falls back to applicationsettings.defaultjournalprocessid | ✅ ⚠️ Depends on a compiled routing ruleset existing |
| 4. Transform | Child runjournaltransformations — account/entity/cost-centre/posting-date/doc-type resolution | Skipped if no BPD matched |
| 5. Validate | Child runjournalvalidations — writes journalvalidationchecks (§3) | Skipped if no BPD matched |
| 6. Assign | AssignDocumentActivity → sets assignedtoid / assignedteamid | ownership only, not the lifecycle action |
| 6.5. Intelligence | Inline materiality tier (finbase.materialityconfig, applicationcode "JOURNAL") + series-deviation anomaly | ✅ writes materialitytier / anomalyreasons on the header |
| 7. Approval | ApproveDocumentActivity → auto-clear (immaterial) or route (create approval chain) | ✅ persists approvalrequest rows |
JV status model
status, jvstatus, and action move together (a single source-of-truth map). action is the next expected step; terminal states carry null. Evidence: packages/finance/journal/journala/services/journal/jvprocessingdata.worklist.service.ts (STATUS_NEXT_ACTION).
| Status | Next action | Meaning |
|---|---|---|
| Draft / New / In Progress | Submit | Not yet in the pipeline (Draft = local only) |
| Pending Approval | Approve | Routed to an approver (currentapproverid) |
| Rejected | Resubmit | Returned |
| Approved | Post | Cleared approval; awaiting posting |
| Posted | — (terminal) | Booked to the GL |
| Not a JV Document / Discarded | — (terminal) |
There is deliberately no "Validated" status — validation is a gate, not a state.
⚠️ Post-to-ERP is a status transition, not (yet) a workflow. Approved → Posted is modelled as an action, and journalvoucherheader.erpdocumentnumber exists for the ERP-assigned number, but the journal package does not contain a dedicated post-to-ERP / connector workflow that flips the status and stamps erpdocumentnumber. Treat the actual GL-posting integration as ◯ roadmap; this doc describes the pipeline up to and including the Approved state and the intended Post action.
3. Validation — defined, running when wired ⚠️
runjournalvalidations does execute and does write journal.journalvalidationchecks (one row per rule, with pass/fail, dashboardstatus, exceptionmessage, enriched additionalinfo). This is real, not a stub. Evidence: packages/finance/journal/journalwf/src/workflows/runjournalvalidations/workflow.ts.
Rule (servicename switch case) | Mechanism |
|---|---|
| Journal Header Validations | Runs a compiled ruleset via jvvalidationruleset (Process Name param) |
| Journal Mandatory Field Check | Same ruleset mechanism |
| Journal Line Validation | DuckDB export → customruleprocessor script → import |
| Master Data Lookup | DuckDB export (accounts / cost / profit centres) → script |
| Duplicate Journal Check | In-workflow fingerprint (posting date + currency + totals + entity/accounts) |
| Debit/Credit Balance Check | In-workflow, against line items (≥2 lines, non-zero, nets to zero) |
⚠️ The honest gap — "rules defined ≠ rules running." The two ruleset-driven cases (Header, Mandatory) only produce checks when the tenant's BPD has the wiring in place: finbase.validationrule rows for that businessprocessdefinitionid, matching journal.jvvalidationruleset records, and a compiled sys.ruleset + sys.rule set. If any link is missing, the case logs and skips, and journalvalidationchecks stays empty at runtime for those rules — the classic "validation rules exist in setup but the checks table is empty." This is a configuration/seeding gap, not missing code. The in-workflow checks (Duplicate, Balance) always run regardless. Verification: after a JV runs, confirm rows exist in journal.journalvalidationchecks for that jvprocessingdataid; an empty result for the ruleset rules means the BPD/ruleset wiring (and the engine rebuild, POST .../jvvalidationrulesets/rebuild) has not been applied.
Additional known gaps (⚠️): the validation score bar normalises against a fixed max (misleading across rulesets), and failed sub-rules can surface raw jvr_* codes rather than friendly names. journal.journaledithistory exists but nothing writes it yet (◯).
4. Sourced / automation JVs and the JV ↔ source link
A JV carries a provenance triple on journalvoucherheader so it can be traced back to whatever produced it (a close task, a payment, an invoice accrual, a recon variance). Evidence: packages/finance/journal/journald/prisma/models/journal.prisma (model journalvoucherheader).
| Field | Purpose | Example |
|---|---|---|
sourcemodule | Which module created the JV | closehub, cash_app, invoice |
sourcereferencetype | What the source record is | task, clearing_transaction, payment_refund |
sourcereferenceid | The source record's id | the closehub task id |
createsourcedjournal stamps the triple on the intake; processjournal propagates it to the promoted header. The journal "What to Post" cockpit reads sourcemodule / sourcereferenceid to show, e.g., the close task that requires each due JV.
The two shipped automation generators
Both are close-step workflows that read a source fact, build a balanced JV, and delegate to createsourcedjournal — they never write journal tables directly.
| Workflow | Bound close step | Reads | Governance |
|---|---|---|---|
postcutoffaccrual | "Post AP Accrual" (AP Cutoff task) | the prior step's accrual proposal (computeapaccrual) | assignApprove: true → routes for approval (immaterial auto-clears) |
postreconadjustment | "Post Adjustment JE" (recon/sub-ledger task) | the recon variance for the resolved control periodSummaryId | assignApprove: false → lands as a preparer Draft for review + confirm |
Evidence: postcutoffaccrual/workflow.ts, postreconadjustment/workflow.ts. Both stamp source: { module: "closehub", referencetype: "task", referenceid: taskId }, so the JV links back to the exact close task. The recon true-up carries a data-derived reason (account, period, variance direction, magnitude, recommended offset treatment) and is advisory — Finni proposes, a human confirms. See Connected Close — GL Stitching for the recon side.
⚠️ Single-childInput dispatch convention for close steps
The close orchestrator (runTaskActivitySequence) dispatches a step workflow with one input object, i.e. executeChild(ref, { args: [childInput] }) — not a (tenantId, input) positional pair. A workflow that declares a two-arg positional signature will silently receive an empty tenantId and skip. Both shipped generators therefore accept a single object and defensively merge a stray second arg:
export async function postreconadjustment(a: Input | string, b?: Input) {
const input = { ...(typeof a === "object" ? a : {}), ...(b ?? {}) };
const tenantId = input.tenantId ?? (typeof a === "string" ? a : "");
// ...
}Any new close-step journal workflow must follow this convention.
5. Governed execution — never a direct table write
The invariant: no external actor writes journalvoucherheader directly. Finni is the worked example. When a user confirms a drafted JV, Finni's submitJournalVoucher tool calls submitManualJvViaApi, which POSTs to the same /jvprocessingdata/worklist/manual endpoint the Create screen uses (submit: true), which hands the JV to processjournal. Evidence: packages/finance/financeagents/src/agents/finni-agent/journal-tools.ts (journalSubmitJv), journal-client.ts (submitManualJvViaApi).
| Guardrail | Where | Effect |
|---|---|---|
| Balance check before submit | Finni tool (Dr = Cr) | Refuses to submit an unbalanced JV |
| RBAC permission | /manual endpoint (journal:voucher:manage) | Finni surfaces a permission error, offers to draft for someone who can |
| One spine | submit=true → processjournal | Finni-born JVs get the same validation + routing as any other |
| Advise, not auto-post | tool description + assignApprove | The tool submits for approval; it does not post/approve |
The same principle governs the automations in §4: they produce a JV through createsourcedjournal, so the entry is validated and routed like any other — the automation proposes, the pipeline (and a human, where material) commits.
6. Verify
| Question | Check |
|---|---|
| Did the JV promote? | journal.journalvoucherheader + journal.jvprocessingdata rows exist for the receipt |
| Did it route? | jvprocessingdata.businessprocessid set; status = Pending Approval or Approved |
| Did validation run? | rows in journal.journalvalidationchecks for the jvprocessingdataid — empty (for ruleset rules) = wiring gap, §3 |
| Is it linked to its source? | journalvoucherheader.sourcemodule / sourcereferencetype / sourcereferenceid populated |
| Intelligence assessed? | journalvoucherheader.materialitytier / intelligenceassessedat set |
Related
- Connected Close — GL Stitching — the recon side of
postreconadjustment(account key, control-variant resolution, the true-up). - Close Automation Tasks — how a close step dispatches a module workflow (the orchestrator that hands the single
input). - Module Inventory — Journal — where the journal module sits in the platform inventory.
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-09-15 | Platform Engineering | Initial page — the one intake spine, processjournal lifecycle + status model, validation (wired-vs-empty gap), sourced/automation JVs + the JV↔source triple, single-childInput dispatch convention, and Finni as the governed-execution example. |