Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public void generate(IntentGenerationContext context) {
List<Map<String, Object>> expansionCleanups = expansionHandlers.cleanups();
List<Map<String, Object>> settlements = buildSettlements(model, byName, compositionParents, settings, context);
List<Map<String, Object>> settlementListeners = buildSettlementListeners(settlements);
List<Map<String, Object>> settlementCleanups = buildSettlementCleanups(settlements);
List<Map<String, Object>> generates = buildGenerates(model, byName, compositionParents, settings, context);
List<Map<String, Object>> transitions = buildTransitions(model, byName, compositionParents, settings, context);
List<Map<String, Object>> sends = buildSends(model, byName, compositionParents, settings, context);
Expand Down Expand Up @@ -172,6 +173,7 @@ public void generate(IntentGenerationContext context) {
glue.put("expansionCleanups", expansionCleanups);
glue.put("settlements", settlements);
glue.put("settlementListeners", settlementListeners);
glue.put("settlementCleanups", settlementCleanups);
glue.put("generates", generates);
// The event-driven subset (issue #6711) - the SAME descriptors, filtered, so the listener and
// the create-from it calls can never be built from divergent data. A create-from with no event
Expand Down Expand Up @@ -845,6 +847,31 @@ private static List<Map<String, Object>> buildSettlementListeners(List<Map<Strin
return listeners;
}

/**
* One cleanup listener per settlement, bound to the payment's <b>delete</b> moment (issue #7061):
* it gives the whole allocation back by removing the payment's junction rows, so the invoice's paid
* roll-up recomputes and the parent relinquishes PAID / PARTIAL through the ordinary
* allocation-delete path.
*
* <p>
* Nothing else does it: the junction FK to the payment 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 cannot delete rows it does not own. Left unbound, the allocation rows
* outlived the payment as orphans pointing at an id that no longer existed and kept the invoice
* settled forever. Unlike the re-key handler this one needs no payment repository - only the
* payment's key, off the delete payload - so it is emitted for a cross-model payment too.
*
* @param settlements the settlement descriptors
* @return one entry per settlement
*/
private static List<Map<String, Object>> buildSettlementCleanups(List<Map<String, Object>> settlements) {
List<Map<String, Object>> cleanups = new ArrayList<>();
for (Map<String, Object> settlement : settlements) {
cleanups.add(rollupEntry(settlement, String.valueOf(settlement.get("name")) + "OnPaymentDeleted", "-deleted"));
}
return cleanups;
}

/**
* One glue entry per {@link GeneratesIntent}: resolves the source entity's perspective/genFolder
* (in this project) and the target's - possibly cross-model, via {@link CrossModelSupport} - plus
Expand Down Expand Up @@ -1626,6 +1653,14 @@ static List<Map<String, Object>> buildSettlementListenersForTest(IntentModel mod
IntentSettings.parse("{}"), context));
}

/** Test hook: build the {@code settlementCleanups} glue collection without a repository. */
static List<Map<String, Object>> buildSettlementCleanupsForTest(IntentModel model) {
IntentGenerationContext context =
new IntentGenerationContext(model, "/" + model.getName(), model.getName(), "workspace", model.getName(), null);
return buildSettlementCleanups(buildSettlements(model, IntentEntities.byName(model), IntentEntities.compositionParents(model),
IntentSettings.parse("{}"), context));
}

/** Test hook: build the {@code waits} glue collection without a repository. */
static List<Map<String, Object>> buildWaitsForTest(IntentModel model) {
return buildWaits(model, IntentSettings.parse("{}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,35 @@ void aLocalPaymentGetsTheRekeyListenerBesideCreateAndUpdated() {
.toList());
}

/**
* The payment's DELETE moment (issue #7061). The junction FK to the payment never becomes a
* database constraint on this platform, so a deleted payment left its allocation rows behind and
* the invoice stayed settled forever. The cleanup handler needs only the key off the delete payload
* - no payment repository - so it exists for a cross-model payment too.
*/
@Test
void everySettlementGetsACleanupListenerOnThePaymentsDeleteTopic() {
List<Map<String, Object>> cleanups = GlueIntentGenerator.buildSettlementCleanupsForTest(IntentParser.parse(YAML));

assertEquals(1, cleanups.size());
assertEquals("-deleted", cleanups.get(0)
.get("topicSuffix"));
assertEquals("AutoSettleOnPaymentDeleted", cleanups.get(0)
.get("className"));
}

@Test
void aCrossModelPaymentStillGetsTheCleanupListener() {
String yaml = YAML.replace("name: settle\n", "name: settle\nuses:\n - { model: treasury, project: treasury }\n")
.replace("- { name: Payment, kind: manyToOne, to: Payment, required: true }",
"- { name: Payment, kind: manyToOne, to: Payment, required: true, model: treasury }");
List<Map<String, Object>> cleanups = GlueIntentGenerator.buildSettlementCleanupsForTest(IntentParser.parse(yaml));

assertEquals(1, cleanups.size(), "the owner of a cross-model payment cannot delete junction rows it does not own");
assertEquals("treasury-Payment-Payment", cleanups.get(0)
.get("paymentTopic"));
}

/**
* A cross-model payment's DAO is generated by the OWNER model, which knows nothing of this
* settlement - no "-rekeyed" is ever published for it here, and the store-driven re-allocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ class GlueGenerator {
/** The names of the collections this generator handles. */
private static final List<String> COLLECTIONS = List.of("triggers", "resolvers", "fieldLoaders", "assignees", "timerLoaders", "waits",
"aborts", "setters", "writers", "notifications", "schedules", "integrations", "inbound", "inboundMessages", "inboundFiles",
"outbound", "stepEvents", "rollups", "expansions", "expansionCleanups", "settlements", "settlementListeners", "generates",
"generateEvents", "generateReopens", "transitions", "sends", "posts", "aggregates", "postings", "printFeeders", "snapshots",
"numbering", "resolves");
"outbound", "stepEvents", "rollups", "expansions", "expansionCleanups", "settlements", "settlementListeners",
"settlementCleanups", "generates", "generateEvents", "generateReopens", "transitions", "sends", "posts", "aggregates",
"postings", "printFeeders", "snapshots", "numbering", "resolves");

/** The renderer. */
private final ModelTemplateRenderer renderer;
Expand Down Expand Up @@ -112,6 +112,9 @@ List<GeneratedFile> generate(String collection, GenerationTemplateMetadataSource
case "expansionCleanups" -> each(collection, source, content, model, parameters, GlueGenerator::bindExpansionCleanup);
case "settlements" -> each(collection, source, content, model, parameters, GlueGenerator::bindSettlement);
case "settlementListeners" -> each(collection, source, content, model, parameters, GlueGenerator::bindSettlementListener);
// The payment's delete moment (issue #7061) - the same descriptor, rendered by its own
// template, so a settlement contributes exactly one cleanup handler per collection entry.
case "settlementCleanups" -> each(collection, source, content, model, parameters, GlueGenerator::bindSettlementListener);
// All three collections carry the SAME create-from descriptors (generateEvents is the
// event-driven subset, generateReopens the declared-reopen one), so they share one binding -
// the listeners and the create-from they surround cannot be rendered from divergent data.
Expand Down Expand Up @@ -603,7 +606,7 @@ private static void bindSettlement(Map<String, Object> item, Map<String, Object>

/**
* Binds one payment listener of an auto-settlement - the same descriptor as the settlement itself,
* rendered once per bound payment event (create, correction).
* rendered once per bound payment event (create, correction, re-key, delete).
*
* @param item the descriptor
* @param context the template context
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package gen.events.${javaGenFolderName};

import org.eclipse.dirigible.components.data.store.java.repository.Criteria;
import org.eclipse.dirigible.sdk.component.Component;
import org.eclipse.dirigible.sdk.log.Logger;
import org.eclipse.dirigible.sdk.log.Logging;
import org.eclipse.dirigible.sdk.messaging.ListenerKind;
import org.eclipse.dirigible.sdk.messaging.MessageHandler;
import org.eclipse.dirigible.sdk.utils.Json;

import gen.${javaGenFolderName}.data.${junctionJavaPerspective}.${junctionEntity}Entity;
import gen.${javaGenFolderName}.data.${junctionJavaPerspective}.${junctionEntity}Repository;
import gen.${paymentGenFolder}.data.${paymentJavaPerspective}.${paymentEntity}Entity;

/**
* Auto-settlement (on ${paymentEntity} delete): gives back the whole allocation of a deleted
* ${paymentEntity} by removing its ${junctionEntity} rows.
*
* Generated from the intent settlements block - do not edit; it is re-generated with the application.
* A foreign key never becomes a database constraint on this platform - referential integrity is a
* business-layer check - and the payment may be owned by another model entirely, so nothing else
* would take the allocation with the payment: the rows survived as orphans pointing at an id that no
* longer exists and kept the ${invoiceEntity} settled forever. The rows are removed through the
* junction repository, so each one's delete event fires and the paid roll-up recomputes the
* ${invoiceEntity}'s paid / balance / status exactly as for a hand-deleted allocation.
*
* The delete event is published after the payment row is gone, so a re-delivery finds an empty
* allocation set and is a no-op - the handler is idempotent without a guard of its own.
*/
@Component("${javaGenFolderName}_${className}")
public class ${className} implements MessageHandler {

private static final Logger LOG = Logging.getLogger("gen.events.${javaGenFolderName}.${className}");

@Override
public String destination() {
return "${paymentTopic}${topicSuffix}";
}

@Override
public ListenerKind kind() {
return ListenerKind.TOPIC;
}

@Override
public void onMessage(String message) {
${paymentEntity}Entity payment = Json.parse(message, ${paymentEntity}Entity.class);
if (payment == null || payment.${paymentPk} == null) {
return;
}
${junctionEntity}Repository rows = new ${junctionEntity}Repository();
for (${junctionEntity}Entity row : rows.findAll(Criteria.create()
.eq("${junctionFkPayment}", payment.${paymentPk}))) {
rows.delete(row);
}
}

@Override
public void onError(String error) {
LOG.error("Settlement ${name}: the listener on [{}] failed - [{}]", destination(), error);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ export function getTemplate(parameters) {
engine: "velocity",
collection: "settlementListeners"
},
{
location: "/template-application-events-java/events/SettlementCleanup.java.template",
action: "generate",
rename: "gen/events/{{javaGenFolderName}}/{{className}}.java",
engine: "velocity",
collection: "settlementCleanups"
},
{
location: "/template-application-events-java/events/SettlementOnInvoice.java.template",
action: "generate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,18 @@ void settlement_generates_on_payment_listener_and_on_invoice_delegate() {
assertTrue(onPaymentRekeyed.contains("release(payment.Id, allocated(payment.Id))"),
"the re-key recompute must release the whole allocation before re-allocating");

// Deleting the PAYMENT must take its allocation with it (#7061): the junction FK to the payment
// is never a database constraint here, so without this handler the rows outlived the payment as
// orphans and the invoice stayed PAID forever. Removing them through the junction repository is
// what makes the paid roll-up recompute and the invoice relinquish PAID (#7022).
String onPaymentDeleted = contentOf("gen/events/settle/AutoSettleOnPaymentDeleted.java");
assertTrue(onPaymentDeleted.contains("class AutoSettleOnPaymentDeleted implements MessageHandler"),
"a cleanup listener should be generated for the payment's delete event");
assertTrue(onPaymentDeleted.contains("return \"" + PROJECT + "-Payment-Payment-deleted\";"),
"it should bind the payment's delete topic");
assertTrue(onPaymentDeleted.contains(".eq(\"Payment\", payment.Id)") && onPaymentDeleted.contains("rows.delete(row)"),
"it should delete every allocation row of that payment through the junction repository");

String onInvoice = codeOf("gen/events/settle/AutoSettleOnInvoice.java");
assertTrue(onInvoice.contains("class AutoSettleOnInvoice implements JavaDelegate"),
"the onInvoice settlement delegate should be generated");
Expand Down
Loading