Observed
BusinessIntents base-sales-invoices + base-customer-payments on the 14.46.0 line (staging, 2026-09-06), the settlements: block below. Twice, with a fresh payment each time:
- Create a CustomerPayment (Exadel / Acme / EUR, 1 200).
AutoAllocateOnPayment allocates it to the oldest open invoice: SalesInvoiceCustomerPayment row created, invoice → PAID, DisplacedStatus = 3 (ISSUED). Correct.
- Delete the CustomerPayment (UI trash → confirm;
DELETE .../CustomerPaymentController/3 → 200, subsequent GET → 404).
- The allocation row survives:
GET .../SalesInvoiceCustomerPaymentController still returns {Id: 5, SalesInvoice: 4, CustomerPayment: 3, Amount: 1200} and the invoice stays PAID, Paid 1 200, Balance 0, forever. The invoice's payments panel renders the orphan as a bare id (Customer Payment: 3, no date, no number).
This is the QA-report scenario "a deleted payment leaves the invoice PAID" (the six 2026-09 no-brainers). #7016 / #7022 modelled it as deleting the allocation and fixed that path (verified on the same instance: deleting the orphan row above does relinquish PAID → ISSUED and clears DisplacedStatus). The path a user actually takes - delete or void the payment - was never covered.
Cause
SalesInvoiceCustomerPayment.CustomerPayment is a cross-model reference (model: customer-payments), so there is no FK and no database cascade, and the generated gen/events of the owning model contain listeners only for its own child topics (...-SalesInvoiceCustomerPayment-deleted roll-ups) and for the payment's -created / -updated topics (AutoAllocateOnPayment, AutoAllocateOnPaymentUpdated). Nothing listens on customer-payments-CustomerPayment-CustomerPayment-deleted. The payment side cannot do it either: it does not own the junction rows.
Expected
The settlements: generator emits a third handler, AutoAllocateOnPaymentDeleted, bound to the payment's -deleted topic, that deletes every junction row of that payment through the generated repository so the existing RollupOnDelete handlers fire and the parent relinquishes PAID/PARTIAL via the #7022 path. Same primitive as release() in AutoAllocateOnPaymentUpdated with pot = everything.
Related, same block (lower priority - a UX trap, not data corruption)
Deleting a single allocation row while its payment still exists is silently undone: the delete shrinks CustomerPayment.allocated (roll-up #6930) → the payment's -updated topic fires → AutoAllocateOnPaymentUpdated finds the pot and re-allocates the same amount to the same invoice within a second (row id 3 → 4 → …). The UI shows the row vanish and reappear on reload; the server answered 200. Auto-settlement is by design, but with the bug above there is then no working way to reverse a payment except correcting its amount. Worth a decision: either the junction delete should mark the amount as "released, do not re-allocate", or the panel should not offer a delete the settlement will immediately revert.
settlements:
- { name: autoAllocate, junction: SalesInvoiceCustomerPayment, invoice: SalesInvoice, payment: CustomerPayment,
amount: amount, total: payable, paid: paid, pot: amount, order: date,
match: [Customer, Currency], status: Status, payableStatuses: [3, 4, 6] }
Found by the BusinessIntents release gate (aws#32) on the 14.46.0 train, 2026-09-06.
Observed
BusinessIntents
base-sales-invoices+base-customer-paymentson the 14.46.0 line (staging, 2026-09-06), thesettlements:block below. Twice, with a fresh payment each time:AutoAllocateOnPaymentallocates it to the oldest open invoice: SalesInvoiceCustomerPayment row created, invoice → PAID,DisplacedStatus= 3 (ISSUED). Correct.DELETE .../CustomerPaymentController/3→ 200, subsequent GET → 404).GET .../SalesInvoiceCustomerPaymentControllerstill returns{Id: 5, SalesInvoice: 4, CustomerPayment: 3, Amount: 1200}and the invoice stays PAID, Paid 1 200, Balance 0, forever. The invoice's payments panel renders the orphan as a bare id (Customer Payment: 3, no date, no number).This is the QA-report scenario "a deleted payment leaves the invoice PAID" (the six 2026-09 no-brainers). #7016 / #7022 modelled it as deleting the allocation and fixed that path (verified on the same instance: deleting the orphan row above does relinquish PAID → ISSUED and clears
DisplacedStatus). The path a user actually takes - delete or void the payment - was never covered.Cause
SalesInvoiceCustomerPayment.CustomerPaymentis a cross-model reference (model: customer-payments), so there is no FK and no database cascade, and the generatedgen/eventsof the owning model contain listeners only for its own child topics (...-SalesInvoiceCustomerPayment-deletedroll-ups) and for the payment's-created/-updatedtopics (AutoAllocateOnPayment,AutoAllocateOnPaymentUpdated). Nothing listens oncustomer-payments-CustomerPayment-CustomerPayment-deleted. The payment side cannot do it either: it does not own the junction rows.Expected
The
settlements:generator emits a third handler,AutoAllocateOnPaymentDeleted, bound to the payment's-deletedtopic, that deletes every junction row of that payment through the generated repository so the existingRollupOnDeletehandlers fire and the parent relinquishes PAID/PARTIAL via the #7022 path. Same primitive asrelease()inAutoAllocateOnPaymentUpdatedwith pot = everything.Related, same block (lower priority - a UX trap, not data corruption)
Deleting a single allocation row while its payment still exists is silently undone: the delete shrinks
CustomerPayment.allocated(roll-up #6930) → the payment's-updatedtopic fires →AutoAllocateOnPaymentUpdatedfinds the pot and re-allocates the same amount to the same invoice within a second (row id 3 → 4 → …). The UI shows the row vanish and reappear on reload; the server answered 200. Auto-settlement is by design, but with the bug above there is then no working way to reverse a payment except correcting its amount. Worth a decision: either the junction delete should mark the amount as "released, do not re-allocate", or the panel should not offer a delete the settlement will immediately revert.Found by the BusinessIntents release gate (aws#32) on the 14.46.0 train, 2026-09-06.