feat(intent): transitions - guarded on-demand status flips (void/cancel/close) - #6307
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A document whose create-time process has ended (an issued invoice, a posted entry) 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-leveltransitions:block adds one - a per-record button that moves the record into a designatedEntityStatus, guarded server-side:The classic consumers: void/cancel an issued document (анулиране keeps the number for the audit trail), cancel a confirmed order, close a case, reopen a ticket.
How
Two generated halves, mirroring
generates:TransitionsIntentGenerator(@Order(470)) contributes<name>-transition-action.extension/.jsto the app's<project>-custom-actionpoint; the descriptor carries anendpoint(always per-record).GlueIntentGenerator.buildTransitionspre-renders everything (the allowed-statuses expression over anint currentStatuslocal; thewhenguard as a fullCalc.eval(...).compareTo(...)expression) into thetransitionsglue collection, rendered byTransition.java.templateinto a@Controlleratgen/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 targetedupdateProperty(a workflow-style system write - no-updatedre-fire), and publishes-transitioned- the same channel the workflow setters andgenerates.sourceStatuspublish, sopostings:glue observes a manual void exactly like a workflow transition (the enabler for a follow-up red-storno reversal posting).Also:
ControllerInvokerno longer stampstext/plainover a content type a controller set explicitly on a String return (default unchanged when unset) - the transition controller returns JSON.Docs:
intent-assistant-guide.mdgains the### transitionssection + quick-reference and allowed-values rows;engine-intent/CLAUDE.mdgains the semantics bullet.Testing
TransitionsIntentTest(parse + 6 validation rejections),GlueTransitionsTest(pre-rendered guards) - full engine-intent suite green.ControllerInvokerBindingTest+2 content-type tests - full engine-java suite green.IntentEmissionCoverageITextended per the engine-intent testing contract (fixture transition + emission tokens + runtime: allowed cancel returns 200 with the status flipped and-transitionedobservable; wrong-status re-cancel 409;when-guard 409 leaving the record untouched) - green locally.🤖 Generated with Claude Code