Skip to content

feat(intent): postings reverses - red-storno reversal on a source void - #6308

Merged
delchev merged 4 commits into
masterfrom
feat/intent-postings-reverses
Jul 20, 2026
Merged

feat(intent): postings reverses - red-storno reversal on a source void#6308
delchev merged 4 commits into
masterfrom
feat/intent-postings-reverses

Conversation

@delchev

@delchev delchev commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6307 (transitions) - this branch contains that commit; review the top commit (9e1db5eb6a). The void transition is the event that triggers the reversal.

What

The postings glue could post a document when a source reaches a status, but not UN-post it: a voided/cancelled source left its journal entry standing. The new reverses: mode completes the pair:

postings:
  - name: invoicePosting
    event: { onTransition: Invoice, when: "Status == 3" }
    creates: JournalEntry
    backReference: Invoice
    rule: { entity: PostingRule, match: { documentType: "Invoice" } }
    items:
      - { Account: rule(receivableAccount), debit: "Net + Vat" }
      - { Account: rule(revenueAccount),    credit: "Net + Vat" }
  - name: invoiceStorno
    event: { onTransition: Invoice, when: "Status == 8" }   # the void transition (#6307)
    reverses: invoicePosting
    storno: Storno            # the created entity's to-one SELF-relation to the original

Semantics (red storno - the accounting correction convention)

  • The reversal inherits creates/backReference/rule/map/items from the reversed sibling (declaring them is a validation error) and re-derives them from the source with every item amount expression negated on the SAME debit/credit side (Calc.eval("-(<expr>)", ...)) - never swapped sides, so turnover columns stay honest.
  • It locates the original through the empty storno link (back-reference = this source, link null) and skips fail-soft when none exists - the source was never posted, nothing to reverse.
  • Its creation stamps the storno link; both handlers' idempotency guards discriminate by it (the reversal counts only linked rows; the reversed sibling counts only unlinked ones - stornoProperty/stornoFilterProperty in the glue), so redeliveries of either event stay at-most-once.
  • The reversal lands as a normal new document: DRAFT status init:, number placeholder, checks:, the review-and-Post task - corrections post into the open period.

Testing

  • PostingsReversesIntentTest (parse + unknown-sibling / inherited-keys / missing-and-non-self-storno / storno-without-reverses rejections), GluePostingsReversesTest (negated expressions, inherited coordinates, storno keys on both entries) - full engine-intent suite green.
  • IntentEmissionCoverageIT extended per the engine-intent testing contract: a Doc + PostDoc/VoidDoc transitions + docPosting/docStorno fixture; emission tokens (negation, fail-soft, storno stamp, sibling filter) and the full runtime chain (post a Doc → the balanced Entry appears; void it → the reversal appears with negative debit AND credit lines, linked to the original) - green locally.

🤖 Generated with Claude Code

delchev and others added 4 commits July 20, 2026 12:54
…el/close)

A document whose create-time process has ended (invoice ISSUED, entry POSTED)
has no declarative affordance left to change its status: process triggers fire
only on create/update/delete, and actions: only opens a custom page. The new
top-level transitions: block adds one - a per-record button that moves the
record into a designated EntityStatus, guarded server-side:

  transitions:
    - name: VoidInvoice
      forEntity: Invoice          # must declare a function: EntityStatus relation
      from: [3, 4]                # allowed source status seed ids
      setStatus: 8                # target status seed id
      when: "Paid == 0"           # optional <Field> ==|!= <number> guard (Calc semantics)
      label: Void
      icon: ban

Two halves, the generates pattern: TransitionsIntentGenerator (@order(470))
contributes the per-record button to <project>-custom-action (descriptor
carries the endpoint); GlueIntentGenerator.buildTransitions pre-renders the
allowed-statuses expression and the Calc-backed when guard into the
transitions glue collection, rendered by Transition.java.template into a
@controller at gen/events/<ClassName>Transition/run. The controller re-loads
the record, returns 409 with the reason when a guard fails, flips ONLY the
status column via the targeted updateProperty (no -updated re-fire), and
publishes -transitioned - the same channel workflow setters publish, so
postings:/integrations observe a manual void exactly like a workflow
transition (the enabler for red-storno reversal postings).

ControllerInvoker: a CharSequence return no longer stamps text/plain over a
content type the controller set explicitly (the transition controller returns
JSON); default unchanged when unset.

Verified:
- unit: TransitionsIntentTest (7) + GlueTransitionsTest (2) + full
  engine-intent suite green; ControllerInvokerBindingTest +2 content-type
  tests, full engine-java suite green
- IntentEmissionCoverageIT extended (fixture transition + emission tokens +
  runtime: cancel 200 with the status flipped, wrong-status 409, when-guard
  409 leaving the record untouched) and green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The postings glue could post a document when a source reaches a status, but
not UN-post it: a voided/cancelled source left its journal entry standing.
The new reverses: mode pairs with the transitions: void primitive:

  postings:
    - name: invoicePosting
      event: { onTransition: Invoice, when: "Status == 3" }
      creates: JournalEntry
      backReference: Invoice
      rule: { ... }
      items: [ ... ]
    - name: invoiceStorno
      event: { onTransition: Invoice, when: "Status == 8" }   # the void transition
      reverses: invoicePosting
      storno: Storno            # the created entity's to-one SELF-relation to the original

Semantics (red storno - the accounting correction convention): the reversal
inherits creates/backReference/rule/map/items from the reversed sibling and
re-derives them from the source with every item amount expression NEGATED on
the SAME debit/credit side (never swapped - turnovers stay honest). It locates
the ORIGINAL through the empty storno link (back-reference set, link null) and
skips fail-soft when none exists (the source was never posted); its creation
stamps the link. Both handlers' idempotency guards discriminate by that link:
the reversal counts only linked rows, the reversed sibling counts only
unlinked ones (stornoProperty/stornoFilterProperty in the glue). The reversal
lands as a normal new document - DRAFT status init, number placeholder,
checks, the accountant review-and-Post task.

Verified:
- unit: PostingsReversesIntentTest (5: parse + sibling/inherited-keys/storno
  validations) + GluePostingsReversesTest (negated exprs, inherited
  coordinates, storno keys on both entries); full engine-intent suite green
- IntentEmissionCoverageIT extended (Doc + PostDoc/VoidDoc transitions +
  docPosting/docStorno fixture; emission tokens for negation/fail-soft/link;
  runtime: post -> balanced Entry appears, void -> the reversal appears with
  negative debit AND credit lines and the storno link to the original) - green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heir FK columns

A report filter like `Status != 8` (a to-one RELATION of the source, not a
field) passed through buildWhere untranslated - fields rewrote to their
qualified physical columns but relation names did not, so the generated query
carried a nonexistent column and failed at SQL time. Bare to-one relation
names now rewrite to the FK column (Invoice."INVOICE_STATUS"), with guards
so join-alias tokens from the dotted-ref pass (Customer."CUSTOMER_NAME")
and already-quoted columns are left intact.

Found by a suite emission audit: an overdue-invoices report excluding a
VOIDED status generated 'AND Status != 8' verbatim into the WHERE.

Verified: ReportIntentGeneratorTest +1 (bare relation translated, dotted-ref
alias unmangled), full class green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s-reverses

# Conflicts:
#	tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java
@delchev
delchev merged commit 90ee58a into master Jul 20, 2026
8 of 9 checks passed
@delchev
delchev deleted the feat/intent-postings-reverses branch July 20, 2026 11:32
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