This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 79a3bdb9ec FINERACT-2418: Attach Loan originator details to  events
79a3bdb9ec is described below

commit 79a3bdb9eccc9406af371718e38643f9e46261e5
Author: Attila Budai <[email protected]>
AuthorDate: Fri Feb 27 21:49:22 2026 +0100

    FINERACT-2418: Attach Loan originator details to  events
---
 .../src/main/avro/loan/v1/LoanChargeDataV1.avsc    |  12 ++
 .../LoanChargeDataV1OriginatorEnricher.java        |  72 ++++++++
 .../mapper/loan/LoanChargeDataMapper.java          |   1 +
 .../feign/helpers/FeignExternalEventHelper.java    |  59 +++++++
 .../feign/helpers/InternalExternalEventsApi.java   |  41 +++++
 .../FeignLoanChargeOriginatorEnricherTest.java     | 196 +++++++++++++++++++++
 6 files changed, 381 insertions(+)

diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc 
b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
index 9eaa4adbce..e4f8022e1e 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanChargeDataV1.avsc
@@ -267,6 +267,18 @@
                     "type": "map"
                 }
             ]
+        },
+        {
+            "default": null,
+            "name": "originators",
+            "doc": "List of originators attached to the parent loan for 
revenue sharing",
+            "type": [
+                "null",
+                {
+                    "type": "array",
+                    "items": 
"org.apache.fineract.avro.loan.v1.OriginatorDetailsV1"
+                }
+            ]
         }
     ]
 }
diff --git 
a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanChargeDataV1OriginatorEnricher.java
 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanChargeDataV1OriginatorEnricher.java
new file mode 100644
index 0000000000..df59ee046d
--- /dev/null
+++ 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanChargeDataV1OriginatorEnricher.java
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanorigination.enricher;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.avro.loan.v1.LoanChargeDataV1;
+import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1;
+import org.apache.fineract.infrastructure.core.service.DataEnricher;
+import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator;
+import 
org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping;
+import 
org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", 
havingValue = "true")
+public class LoanChargeDataV1OriginatorEnricher implements 
DataEnricher<LoanChargeDataV1> {
+
+    private final LoanOriginatorMappingRepository 
loanOriginatorMappingRepository;
+    private final LoanOriginatorAvroMapper loanOriginatorAvroMapper;
+
+    @Override
+    public boolean isDataTypeSupported(final Class<LoanChargeDataV1> dataType) 
{
+        return dataType.isAssignableFrom(LoanChargeDataV1.class);
+    }
+
+    @Override
+    public void enrich(final LoanChargeDataV1 data) {
+        if (data == null || data.getLoanId() == null) {
+            return;
+        }
+
+        final List<LoanOriginatorMapping> mappings = 
loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(data.getLoanId());
+        if (mappings == null || mappings.isEmpty()) {
+            return;
+        }
+
+        final List<OriginatorDetailsV1> originators = new ArrayList<>();
+        for (LoanOriginatorMapping mapping : mappings) {
+            final LoanOriginator originator = mapping.getOriginator();
+            if (originator != null) {
+                final OriginatorDetailsV1 originatorDetails = 
loanOriginatorAvroMapper.toAvro(originator);
+                if (originatorDetails != null) {
+                    originators.add(originatorDetails);
+                }
+            }
+        }
+
+        if (!originators.isEmpty()) {
+            data.setOriginators(originators);
+        }
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanChargeDataMapper.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanChargeDataMapper.java
index 049a488990..2406a34dbc 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanChargeDataMapper.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanChargeDataMapper.java
@@ -30,6 +30,7 @@ public interface LoanChargeDataMapper {
 
     @Mapping(target = "externalOwnerId", ignore = true)
     @Mapping(target = "customData", ignore = true)
+    @Mapping(target = "originators", ignore = true)
     LoanChargeDataV1 map(LoanChargeData source);
 
     LoanChargeDataRangeViewV1 mapRangeView(LoanChargeData source);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignExternalEventHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignExternalEventHelper.java
new file mode 100644
index 0000000000..fb7f41b5bb
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignExternalEventHelper.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.client.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import 
org.apache.fineract.client.models.ExternalEventConfigurationUpdateRequest;
+import 
org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse;
+
+public class FeignExternalEventHelper {
+
+    private final FineractFeignClient fineractClient;
+    private final InternalExternalEventsApi internalEventsApi;
+
+    public FeignExternalEventHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+        this.internalEventsApi = 
fineractClient.create(InternalExternalEventsApi.class);
+    }
+
+    public void enableBusinessEvent(String eventName) {
+        ok(() -> 
fineractClient.externalEventConfiguration().updateExternalEventConfigurations(
+                new 
ExternalEventConfigurationUpdateRequest().externalEventConfigurations(Map.of(eventName,
 true))));
+    }
+
+    public void disableBusinessEvent(String eventName) {
+        ok(() -> 
fineractClient.externalEventConfiguration().updateExternalEventConfigurations(
+                new 
ExternalEventConfigurationUpdateRequest().externalEventConfigurations(Map.of(eventName,
 false))));
+    }
+
+    public List<ExternalEventResponse> getExternalEventsByType(String type) {
+        return ok(() -> internalEventsApi.getAllExternalEvents(Map.of("type", 
type)));
+    }
+
+    public void deleteAllExternalEvents() {
+        ok(() -> {
+            internalEventsApi.deleteAllExternalEvents();
+            return null;
+        });
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/InternalExternalEventsApi.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/InternalExternalEventsApi.java
new file mode 100644
index 0000000000..5a118085f6
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/InternalExternalEventsApi.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.client.feign.helpers;
+
+import feign.Headers;
+import feign.QueryMap;
+import feign.RequestLine;
+import java.util.List;
+import java.util.Map;
+import 
org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse;
+
+/**
+ * Feign interface for the internal-only external events API. This endpoint is 
only available when the server runs with
+ * the TEST profile and is not part of the generated OpenAPI client. Check 
InternalExternalEventsApiResource.java for
+ * the server-side implementation.
+ */
+@Headers({ "Accept: application/json", "Content-Type: application/json" })
+public interface InternalExternalEventsApi {
+
+    @RequestLine("GET /v1/internal/externalevents")
+    List<ExternalEventResponse> getAllExternalEvents(@QueryMap(encoded = true) 
Map<String, Object> queryParams);
+
+    @RequestLine("DELETE /v1/internal/externalevents")
+    void deleteAllExternalEvents();
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanChargeOriginatorEnricherTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanChargeOriginatorEnricherTest.java
new file mode 100644
index 0000000000..793de4fdd8
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanChargeOriginatorEnricherTest.java
@@ -0,0 +1,196 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.client.feign.tests;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.ChargeRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
+import 
org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse;
+import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignExternalEventHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanOriginatorHelper;
+import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class FeignLoanChargeOriginatorEnricherTest extends 
FeignIntegrationTest {
+
+    private static final String ADD_CHARGE_EVENT = 
"LoanAddChargeBusinessEvent";
+
+    private static FineractFeignClient fineractClient;
+    private static FeignLoanOriginatorHelper originatorHelper;
+    private static FeignClientHelper clientHelper;
+    private static FeignLoanHelper loanHelper;
+    private static FeignExternalEventHelper externalEventHelper;
+
+    @BeforeAll
+    public static void setup() {
+        fineractClient = FineractFeignClientHelper.getFineractFeignClient();
+        originatorHelper = new FeignLoanOriginatorHelper(fineractClient);
+        clientHelper = new FeignClientHelper(fineractClient);
+        loanHelper = new FeignLoanHelper(fineractClient);
+        externalEventHelper = new FeignExternalEventHelper(fineractClient);
+    }
+
+    @Test
+    public void testLoanAddChargeEventContainsOriginators() {
+        externalEventHelper.enableBusinessEvent(ADD_CHARGE_EVENT);
+        try {
+            // Given: a loan with an originator attached
+            final String originatorExternalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+            final Long originatorId = 
originatorHelper.createOriginator(originatorExternalId, "Test Originator", 
"ACTIVE");
+            final Long clientId = clientHelper.createClient();
+            final Long loanId = loanHelper.createSubmittedLoan(clientId);
+            originatorHelper.attachOriginatorToLoan(loanId, originatorId);
+
+            final Long chargeId = createFlatFeeCharge(50.0);
+
+            externalEventHelper.deleteAllExternalEvents();
+
+            // When: a charge is added to the loan
+            ok(() -> fineractClient.loanCharges()
+                    .executeLoanCharge(loanId,
+                            new 
PostLoansLoanIdChargesRequest().chargeId(chargeId).amount(50.0).locale("en").dateFormat("dd
 MMMM yyyy")
+                                    
.dueDate(org.apache.fineract.integrationtests.common.Utils.dateFormatter
+                                            
.format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant())),
+                            (String) null));
+
+            // Then: the external event payload contains originator details
+            final List<ExternalEventResponse> events = 
externalEventHelper.getExternalEventsByType(ADD_CHARGE_EVENT);
+            assertThat(events).isNotEmpty();
+
+            final ExternalEventResponse event = events.stream().filter(e -> 
loanId.equals(extractLoanId(e))).findFirst().orElse(null);
+            assertThat(event).isNotNull();
+
+            final Object originators = event.getPayLoad().get("originators");
+            assertThat(originators).isNotNull().isInstanceOf(List.class);
+
+            @SuppressWarnings("unchecked")
+            final List<Map<String, Object>> originatorList = (List<Map<String, 
Object>>) originators;
+            assertThat(originatorList).hasSize(1);
+            
assertThat(originatorList.get(0).get("externalId")).isEqualTo(originatorExternalId);
+
+            // Cleanup
+            originatorHelper.detachOriginatorFromLoan(loanId, originatorId);
+            originatorHelper.deleteOriginator(originatorId);
+        } finally {
+            externalEventHelper.disableBusinessEvent(ADD_CHARGE_EVENT);
+        }
+    }
+
+    @Test
+    public void testLoanAddChargeEventContainsMultipleOriginators() {
+        externalEventHelper.enableBusinessEvent(ADD_CHARGE_EVENT);
+        try {
+            // Given: a loan with two originators attached
+            final String externalId1 = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+            final String externalId2 = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+            final Long originatorId1 = 
originatorHelper.createOriginator(externalId1, "Originator One", "ACTIVE");
+            final Long originatorId2 = 
originatorHelper.createOriginator(externalId2, "Originator Two", "ACTIVE");
+            final Long clientId = clientHelper.createClient();
+            final Long loanId = loanHelper.createSubmittedLoan(clientId);
+            originatorHelper.attachOriginatorToLoan(loanId, originatorId1);
+            originatorHelper.attachOriginatorToLoan(loanId, originatorId2);
+
+            final Long chargeId = createFlatFeeCharge(75.0);
+
+            externalEventHelper.deleteAllExternalEvents();
+
+            // When: a charge is added
+            ok(() -> fineractClient.loanCharges()
+                    .executeLoanCharge(loanId,
+                            new 
PostLoansLoanIdChargesRequest().chargeId(chargeId).amount(75.0).locale("en").dateFormat("dd
 MMMM yyyy")
+                                    
.dueDate(org.apache.fineract.integrationtests.common.Utils.dateFormatter
+                                            
.format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant())),
+                            (String) null));
+
+            // Then: both originators appear in the event
+            final List<ExternalEventResponse> events = 
externalEventHelper.getExternalEventsByType(ADD_CHARGE_EVENT);
+            final ExternalEventResponse event = events.stream().filter(e -> 
loanId.equals(extractLoanId(e))).findFirst().orElse(null);
+            assertThat(event).isNotNull();
+
+            @SuppressWarnings("unchecked")
+            final List<Map<String, Object>> originatorList = (List<Map<String, 
Object>>) event.getPayLoad().get("originators");
+            assertThat(originatorList).hasSize(2);
+
+            // Cleanup
+            originatorHelper.detachOriginatorFromLoan(loanId, originatorId1);
+            originatorHelper.detachOriginatorFromLoan(loanId, originatorId2);
+            originatorHelper.deleteOriginator(originatorId1);
+            originatorHelper.deleteOriginator(originatorId2);
+        } finally {
+            externalEventHelper.disableBusinessEvent(ADD_CHARGE_EVENT);
+        }
+    }
+
+    @Test
+    public void testLoanAddChargeEventWithNoOriginators() {
+        externalEventHelper.enableBusinessEvent(ADD_CHARGE_EVENT);
+        try {
+            // Given: a loan without originators
+            final Long clientId = clientHelper.createClient();
+            final Long loanId = loanHelper.createSubmittedLoan(clientId);
+
+            final Long chargeId = createFlatFeeCharge(50.0);
+
+            externalEventHelper.deleteAllExternalEvents();
+
+            // When: a charge is added
+            ok(() -> fineractClient.loanCharges()
+                    .executeLoanCharge(loanId,
+                            new 
PostLoansLoanIdChargesRequest().chargeId(chargeId).amount(50.0).locale("en").dateFormat("dd
 MMMM yyyy")
+                                    
.dueDate(org.apache.fineract.integrationtests.common.Utils.dateFormatter
+                                            
.format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant())),
+                            (String) null));
+
+            // Then: originators field is null in the event (no enrichment 
when loan has no originators)
+            final List<ExternalEventResponse> events = 
externalEventHelper.getExternalEventsByType(ADD_CHARGE_EVENT);
+            final ExternalEventResponse event = events.stream().filter(e -> 
loanId.equals(extractLoanId(e))).findFirst().orElse(null);
+            assertThat(event).isNotNull();
+            assertThat(event.getPayLoad().get("originators")).isNull();
+        } finally {
+            externalEventHelper.disableBusinessEvent(ADD_CHARGE_EVENT);
+        }
+    }
+
+    private Long createFlatFeeCharge(double amount) {
+        return ok(() -> fineractClient.charges().createCharge(new 
ChargeRequest()//
+                .name("Originator Test Fee " + System.currentTimeMillis())//
+                .currencyCode("USD")//
+                .chargeAppliesTo(1)//
+                .chargeTimeType(2)//
+                .chargeCalculationType(1)//
+                .chargePaymentMode(0)//
+                .amount(amount)//
+                .active(true)//
+                .locale("en"))).getResourceId();
+    }
+
+    private Long extractLoanId(ExternalEventResponse event) {
+        final Object loanId = event.getPayLoad().get("loanId");
+        if (loanId instanceof Number) {
+            return ((Number) loanId).longValue();
+        }
+        return null;
+    }
+}

Reply via email to