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;
+ }
+}