intent: a roll-up's status is relinquished when its sum returns to zero - #7022
Merged
Conversation
A `rollups:` entry with `statusWhenFull` / `statusWhenPartial` moved the parent to PAID / PARTIAL as allocations arrived and never moved it back: the recompute had no else branch, so deleting the only allocation of a PAID invoice left it PAID with Paid 0 / Balance = Payable, and invisible to the settlement. The first move into a roll-up-owned status now snapshots the status it displaces into a hidden, read-only INTEGER column on the parent (`Displaced<Status>`, emitted by the EDM generator for every local parent of a capacity roll-up with a status), and a sum back at zero restores it - only while the parent still holds one of the two roll-up-owned statuses, so a manual void is never undone - then clears the snapshot. Every handler variant (create/update/delete/rekey) carries the branch, and both writes ride the same derived map into one updateDerived. Remembering beats a declared statusWhenEmpty, which is wrong for an invoice paid straight from ISSUED. The column is hidden through a new `isHiddenProperty` flag, now the one thing the Harmonia templates consult to keep bookkeeping out of forms, lists and details blocks; ModelParameterProcessor sets it from the model and by name for ProcessIds, so a pre-flag .model still hides the stamps. isReadOnlyProperty puts it in the preserved-on-update set, so a full-row form save cannot null it. Covered by RollupAggregatesTest, GlueRollupStatusTest, EdmIntentGeneratorTest, ModelParameterProcessorTest, the IntentEngineIT shape assertions and a live IntentEmissionCoverageIT scenario (pay in full -> SETTLED; delete the payment -> OPEN again, memory cleared). Fixes #7016 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| "every variant of the status roll-up - create, update, delete AND rekey - must relinquish through the same column: " | ||
| + paid); | ||
| assertTrue(lines.stream() | ||
| .allMatch(r -> "".equals(r.get("statusField")) && "".equals(r.get("statusDisplacedField"))), |
| "every variant of the status roll-up - create, update, delete AND rekey - must relinquish through the same column: " | ||
| + paid); | ||
| assertTrue(lines.stream() | ||
| .allMatch(r -> "".equals(r.get("statusField")) && "".equals(r.get("statusDisplacedField"))), |
delchev
added a commit
that referenced
this pull request
Sep 6, 2026
A settlement bound its allocation to the payment's create, correction and re-key moments but not to its DELETE, and nothing else could take the junction rows with the payment: the FK to it never becomes a database constraint on this platform, so there is no cascade, and when the payment is cross-model its owner knows nothing of this settlement and does not own the rows. Deleting a CustomerPayment therefore left its SalesInvoiceCustomerPayment rows behind as orphans pointing at an id that no longer existed, and the invoice stayed PAID with a zero balance forever - the path a user actually takes to reverse a payment. #7016 / #7022 fixed the relinquish for a deleted ALLOCATION row; this binds the moment that reaches it. The settlements generator now emits a cleanup handler per settlement, <Name>OnPaymentDeleted, on the payment's -deleted topic. It deletes every junction row of that payment through the generated junction repository, so each row's delete event fires and the paid roll-up recomputes the invoice's paid / balance / status exactly as for a hand-deleted allocation. Unlike the re-key handler it needs no payment repository - only the key off the delete payload - so it is emitted for a cross-model payment too. The payment-side roll-ups are unaffected: their parent is gone, and the handler returns on a null parent. Fixes #7061 Co-authored-by: Claude Opus 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.
Fixes #7016
What
A
rollups:entry withstatus:/statusWhenFull:/statusWhenPartial:moved the parent to PAID / PARTIAL as allocations arrived, but never moved it back. Deleting the only allocation of a PAID invoice left it PAID with Paid 0 / Balance = Payable, and invisible to the auto-settlement.The roll-up now relinquishes the status it set, restoring the status it displaced (the second option in the issue, the one that is always right):
Displaced<Status>(IntentNaming.displacedStatusProperty; emitted byEdmIntentGeneratorfor every local parent of a capacity roll-up with a status, one per status relation).derivedmap into oneupdateDerived, so the parent's listeners see one-updated.statusWhenEmptykey: a declared return status is wrong for every invoice paid straight from ISSUED and never CONFIRMED.The column is hidden through a new
isHiddenPropertyflag, which is now the one thing the Harmonia templates consult to keep bookkeeping out of forms, lists and details blocks (replacing the literalProcessIdsname checks).ModelParameterProcessorsets it from the model and by name forProcessIds, so a.modelwritten before the flag existed still hides the stamps; the modeler's serializer carries unknown attributes through its generic pass.isReadOnlyPropertyputs the column in the preserved-on-update set, so a full-row form save cannot null it.Tests
RollupAggregatesTest- the emitted snapshot / restore / warn branches.GlueRollupStatusTest- the descriptor names the column on every variant of a status roll-up and on none of a count roll-up.EdmIntentGeneratorTest- the parent carries the hidden column; child and nomenclature do not.ModelParameterProcessorTest- the flag from the model and by name.IntentEngineIT- generated-code shape of all four handlers + the.modelcolumn.IntentEmissionCoverageIT- a live scenario: pledge of 1000 OPEN, payment 1000 -> Paid 1000 / Balance 0 / SETTLED / DisplacedStatus = OPEN; delete the payment -> Paid 0 / Balance 1000 / OPEN / DisplacedStatus null; and no generated surface renders the column.Locally: both ITs green (61 tests), full unit suites of
engine-intent(993) andide-template(87) green,formatter:validateand release-profile javadoc clean.Docs
engine-intentREADME,intent-assistant-guide.md, moduleCLAUDE.md,RollupIntentjavadoc.🤖 Generated with Claude Code