Skip to content

feat(intent): postings - declarative source-document-to-ledger posting#6224

Merged
delchev merged 1 commit into
masterfrom
feat/intent-postings
Jul 10, 2026
Merged

feat(intent): postings - declarative source-document-to-ledger posting#6224
delchev merged 1 commit into
masterfrom
feat/intent-postings

Conversation

@delchev

@delchev delchev commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

Fifth accounting-vertical platform PR — postings:, the declarative "documents → ledger" capability the Wave-0 spike was run to shape:

postings:
  - name: salesInvoicePosting
    event: { onTransition: SalesInvoice, model: kf-mod-sales-invoices, when: "Status == 3" }
    creates: JournalEntry              # a LOCAL document entity owning a composition items child
    backReference: SalesInvoice        # the at-most-once guard + the audit link
    map: { entryDate: date, customer: Customer, reason: "Sales invoice {number}" }
    rule: { entity: PostingRule, match: { documentType: "Sales Invoice" } }
    items:
      - { Account: rule(receivableAccount), debit: "Net + Vat" }
      - { Account: rule(revenueAccount),    credit: "Net" }
      - { Account: rule(vatAccount),        credit: "Vat", when: "Vat != 0" }

Semantics (each verified):

  • Binds the source's -transitioned topic (feat(intent): -transitioned event channel - workflow transitions become observable #6220) — reactions never see it, so no loops — and re-loads the source by id: the payload is as-of the transition, so the number stamped by a later generateNumber step is correctly picked up by the {number} interpolation.
  • At-most-once via the back-reference; event redelivery cannot double-post.
  • Fail-soft determination: a missing rule row or a null referenced rule column skips the posting — the document simply stays on the unposted worklist (final-status documents with no back-referencing target, derivable as a lens/report) — never a throw, never a half-post.
  • All writes go through the generated repositories → numbering, status init:, and checks: (feat(intent): checks - declarative cross-field and cross-line validations #6223 — the balance invariant) fire on the created document.
  • Amounts run through the SDK Calc evaluator with the local item field's scale; row when guards are null-safe; the glue pre-renders every expression Java-side (the expansions convention — the template stays shape-only).

Verification

Unit: GluePostingsTest (full pre-rendering: topic coordinates, guard, ruleRow.* refs, Calc.eval exprs with scale, {placeholder} concat, used-column set) + parser negative tests. Live: the Wave-0 spike's ~100-line hand-written posting listener replaced by the block above — a real invoice flow (approve → issue via the Inbox) produced the identical balanced entry (411 D 1200 / 702 C 1000 / 4532 C 200, customer mapped, VAT row's when passed, re-loaded number in the header) — 6/6 checks. Two shape bugs found and fixed by the live run: the rule entity's Settings perspective package and case-insensitive authored-name matching for map/item/rule references.

Deliberate follow-up (documented): the storno/negation mode + explicit Reverse action — needs the void-document event from the source side first.

🤖 Generated with Claude Code

`postings:` (top-level): when a (usually cross-model) source document reaches
a status, create ONE local document with computed multi-line content - the
accounting "source document -> balanced journal entry" capability,
generalized. Derived from a concrete spike (shape-first, per the Wave-0
plan); rides the -transitioned channel (#6220).

The generated MessageHandler re-loads the source by id (the transition
payload lacks later-step data such as the stamped number), guards on the
status, enforces at-most-once via the backReference, resolves the
account-determination rule row (a missing row or null referenced column
SKIPS the posting - the unposted worklist - never throws), and writes the
target + items through the generated repositories, so numbering/status
init/checks fire on the created document. Header map supports copy, literal
and {sourceProperty} templates; item amounts run through the SDK Calc
evaluator with the local field's scale; row `when` guards are null-safe.

Everything is pre-rendered by GlueIntentGenerator.buildPostings (the
expansions convention - the Velocity template stays shape-only); the rule
entity's Settings perspective is resolved correctly; map/item/rule column
references match case-insensitively against authored names.

Verified live: a hand-written posting listener replaced by the declarative
block produced the identical balanced entry (411 D 1200 / 702 C 1000 /
4532 C 200) off a real invoice flow - 6/6 checks incl. re-loaded-number
interpolation and the at-most-once guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@delchev delchev merged commit 59c8dba into master Jul 10, 2026
10 checks passed
@delchev delchev deleted the feat/intent-postings branch July 10, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant