Skip to content

intent: a roll-up's status is relinquished when its sum returns to zero - #7022

Merged
delchev merged 1 commit into
masterfrom
issue-7016-rollup-status-relinquish
Sep 2, 2026
Merged

intent: a roll-up's status is relinquished when its sum returns to zero#7022
delchev merged 1 commit into
masterfrom
issue-7016-rollup-status-relinquish

Conversation

@delchev

@delchev delchev commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #7016

What

A rollups: entry with status: / 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):

  • The first move into a roll-up-owned status snapshots the status it displaces into a hidden, read-only INTEGER column on the parent, Displaced<Status> (IntentNaming.displacedStatusProperty; emitted by EdmIntentGenerator for every local parent of a capacity roll-up with a status, one per status relation).
  • A sum back at zero restores it, only while the parent still holds one of the two roll-up-owned statuses (a manual void of a partially paid document is never undone), then clears the snapshot. A roll-up-owned status with no recorded predecessor is logged and left alone, never guessed.
  • Every handler variant carries the branch - create, update, delete and rekey - since an allocation amended to 0 or re-parented away is the same situation as a deleted one. Both writes ride the same derived map into one updateDerived, so the parent's listeners see one -updated.
  • No statusWhenEmpty key: a declared return status is wrong for every invoice paid straight from ISSUED and never CONFIRMED.

The column is hidden through a new isHiddenProperty flag, which is now the one thing the Harmonia templates consult to keep bookkeeping out of forms, lists and details blocks (replacing the literal ProcessIds name checks). ModelParameterProcessor sets it from the model and by name for ProcessIds, so a .model written before the flag existed still hides the stamps; the modeler's serializer carries unknown attributes through its generic pass. isReadOnlyProperty puts 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 .model column.
  • 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) and ide-template (87) green, formatter:validate and release-profile javadoc clean.

Docs

🤖 Generated with Claude Code

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
delchev merged commit 1ce126c into master Sep 2, 2026
10 checks passed
@delchev
delchev deleted the issue-7016-rollup-status-relinquish branch September 2, 2026 14:49
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>
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: rollup status: never lets go - deleting the only allocation leaves the parent PAID with a zero sum

2 participants