Skip to content

intent: a create-from is guarded by the source's status (#7068) - #7080

Merged
delchev merged 4 commits into
masterfrom
issue-7068-generates-from-status
Sep 6, 2026
Merged

intent: a create-from is guarded by the source's status (#7068)#7080
delchev merged 4 commits into
masterfrom
issue-7068-generates-from-status

Conversation

@delchev

@delchev delchev commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7068

The defect

A generates: create-from was unconditional. With a sourceStatus: completion hook it flipped the source once the target existed - and then went on offering the same button on the flipped record, and answering the same endpoint 200. So a second click minted a second document: on sta, ProformaInvoice PF00000001 produced SalesInvoice 10, flipped to INVOICED, and a second InvoiceFromProformaGenerate/run {id:1} produced SalesInvoice 11 for the same proforma - a duplicate invoice in the customer's hands.

sourceStatus: declared what "already done" looks like for this action. Nothing consulted it, which is why the gap survives review: both halves of the model read correctly and the duplicate is an ordinary-looking document with a number of its own.

The fix

One rule, resolved once (GeneratesGuardSupport), feeding both halves of the action:

  • the generated run() refuses with 409 naming the action and the current status, before anything is created (a guard asked after the document exists is not a guard);
  • the contributed action descriptor carries the same guard, so the shared customActions store stops offering the click on a record it would refuse - getActions(view, type, record) gained an optional record and the four entity-action views pass the one they already have (selected, and the document form). The 409 stays the contract for every other caller.

Two shapes:

generates:
  - name: invoice-from-proforma
    from: ProformaInvoice
    to: SalesInvoice
    forEntity: ProformaInvoice
    fromStatus: [CONFIRMED]     # the explicit allow-list
    sourceStatus: INVOICED

fromStatus: is the from: of a transitions: entry - spelled differently only because from: on a create-from already names the source entity, which is why the issue's suggested from: could not be taken literally. Absent it, a declared sourceStatus implies the deny-list of exactly that status: the minimal refusal, derived from what the author already wrote, so a model carrying the defect today (base-timesheets' invoice-from-timesheet included) is corrected with no authoring change.

The guard is on the click. An event-driven create-from already carries the at-most-once back-reference guard and qualifies its moment with event.when, so fromStatus on an event-only rule is refused at parse rather than silently ignored - as are a page scope (no record whose status could be read), a source declaring no function: EntityStatus relation, and an allow-list containing the sourceStatus the action itself writes (it re-opens exactly the duplicate the guard removes). Statuses are symbolic like every other status site.

Emission is gated on a new hasStatusGuard key, so a .glue written before it renders the unguarded run() it always did.

Tests

  • GeneratesIntentTest - the declared list parses; the four refusals.
  • GlueGeneratesTest - the implied deny-list, the authored allow-list, and a create-from with no status at all keeping no guard.
  • IntentEngineIT.generates_completion_hook_flips_the_source_via_targeted_update - the rendered 409 branch, its ordering before the create, and the descriptor's guard. Green locally (64 tests).
  • engine-intent 1080 + ide-template 97 unit tests green; formatter:validate clean; javadoc clean under -P release.

Docs

A `generates:` was unconditional. With a `sourceStatus:` completion hook it
flipped the source once the target existed - and then went on offering the same
button on the flipped record and answering the same endpoint 200, so a second
click minted a second document: a proforma already INVOICED produced a second
invoice, in the customer's hands. The hook DECLARED what "already done" looks
like; nothing consulted it, and both halves of the model read correctly, which
is why the gap survived review.

One rule, resolved once (`GeneratesGuardSupport`), feeds both halves of the
action: the generated `run()` refuses with 409 before anything is created, and
the contributed action descriptor carries the same guard so the shared
`customActions` store stops OFFERING the click on a record it would refuse
(`getActions(view, type, record)` takes the record the four entity-action views
already have). Two shapes: `fromStatus: [...]` is the explicit allow-list - the
`from:` of a `transitions:` entry, spelled differently only because `from:` on a
create-from already names the source ENTITY - and absent it a declared
`sourceStatus` implies the deny-list of exactly that status, so a model carrying
the defect today is corrected with no authoring change.

The guard is on the CLICK: an event-driven create-from keeps its own at-most-once
back-reference guard and qualifies its moment with `event.when`, so `fromStatus`
on an event-only rule is refused at parse rather than silently ignored - as are a
`page` scope, a source with no `function: EntityStatus` relation, and an
allow-list containing the `sourceStatus` the action itself writes. The statuses
are symbolic like every other status site. Emission is gated on a new
`hasStatusGuard` key, so a `.glue` written before it renders the unguarded
`run()` it always did.
…s-from-status

# Conflicts:
#	tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java
…s-from-status

Both sides taught the same store not to offer an action a record's status
cannot accept - #7073 for a transition's `from:`, this branch for a
create-from's `fromStatus:` - so the two predicates now stand side by side and
getActions applies both (`appliesTo` for the transition guard, `isAvailable`
for the create-from guard), each still failing open on a missing guard, record
or status value. The generated views take master's call shape, which passes the
document page's stored `record` rather than the edit buffer.
…s-from-status

Both sides added a rule to the same generates validation block: master refuses
`unique:` on an on-demand create-from (#7070), this branch validates
`fromStatus:`. They are independent checks and both now run.
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.

intent: generates has no from-status guard - Generate Invoice on an already INVOICED proforma/project-month creates a second invoice

1 participant