FINERACT-2232: Capitalized income - Business events

This commit is contained in:
adam.magyari
2025-06-13 18:01:14 +02:00
committed by Adam Saghy
parent de580c2cef
commit 6cd634cb2f
15 changed files with 563 additions and 262 deletions
@@ -0,0 +1,35 @@
/**
* 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.infrastructure.event.business.domain.loan.transaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanCapitalizedIncomeTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanCapitalizedIncomeTransactionCreatedBusinessEvent";
public LoanCapitalizedIncomeTransactionCreatedBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -34,6 +34,10 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
@@ -61,6 +65,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanBalanceService loanBalanceService;
private final BusinessEventNotifierService businessEventNotifierService;
@Transactional
@Override
@@ -101,6 +106,9 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
// Post journal entries
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeTransactionCreatedBusinessEvent(capitalizedIncomeTransaction));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withEntityId(capitalizedIncomeTransaction.getId()) //
.withEntityExternalId(capitalizedIncomeTransaction.getExternalId()) //
@@ -153,6 +161,10 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
MathUtil.negativeToZero(capitalizedIncomeBalance.getUnrecognizedAmount().subtract(transactionAmount)));
capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance);
businessEventNotifierService.notifyPostBusinessEvent(
new LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent(savedCapitalizedIncomeAdjustment));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withEntityId(savedCapitalizedIncomeAdjustment.getId()) //
.withEntityExternalId(savedCapitalizedIncomeAdjustment.getExternalId()) //
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
@@ -50,10 +51,11 @@ public class ProgressiveLoanAccountConfiguration {
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService) {
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
BusinessEventNotifierService businessEventNotifierService) {
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService);
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, businessEventNotifierService);
}
@Bean
@@ -201,4 +201,5 @@
<include file="parts/0180_add_version_field_to_loan_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0181_add_capitalized_income_amortization_adjustment_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0182_transaction_summary_with_asset_owner_report_fix_charge_reason_and_add_buyback_intermediate.xml" relativeToChangelogFile="true" />
<include file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanCapitalizedIncomeTransactionCreatedBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -108,7 +108,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -197,7 +198,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -87,6 +87,7 @@ import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.client.util.Calls;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.BatchHelper;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
@@ -99,6 +100,9 @@ import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.error.ErrorResponse;
import org.apache.fineract.integrationtests.common.externalevents.BusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
import org.apache.fineract.integrationtests.common.loans.LoanAccountLockHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
@@ -119,7 +123,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith;
@Slf4j
@ExtendWith(LoanTestLifecycleExtension.class)
@ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class })
public abstract class BaseLoanIntegrationTest extends IntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
@@ -171,6 +175,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
protected GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper();
protected final CodeHelper codeHelper = new CodeHelper();
protected final ChargesHelper chargesHelper = new ChargesHelper();
protected final ExternalEventHelper externalEventHelper = new ExternalEventHelper();
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
@@ -1269,6 +1274,42 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
new PostLoansLoanIdRequest().rejectedOnDate(rejectedOnDate).locale("en").dateFormat(DATETIME_PATTERN));
}
protected void verifyBusinessEvents(BusinessEvent... businessEvents) {
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
logBusinessEvents(allExternalEvents);
Assertions.assertNotNull(businessEvents);
Assertions.assertNotNull(allExternalEvents);
Assertions.assertTrue(businessEvents.length <= allExternalEvents.size(),
"Expected business event count is less than actual. Expected: " + businessEvents.length + " Actual: "
+ allExternalEvents.size());
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
for (BusinessEvent businessEvent : businessEvents) {
long count = allExternalEvents.stream().filter(externalEvent -> businessEvent.verify(externalEvent, formatter)).count();
Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent);
}
}
protected void logBusinessEvents(List<ExternalEventDTO> allExternalEvents) {
allExternalEvents.forEach(externalEventDTO -> {
Object amount = externalEventDTO.getPayLoad().get("amount");
Object outstandingLoanBalance = externalEventDTO.getPayLoad().get("outstandingLoanBalance");
Object principalPortion = externalEventDTO.getPayLoad().get("principalPortion");
Object interestPortion = externalEventDTO.getPayLoad().get("interestPortion");
Object feePortion = externalEventDTO.getPayLoad().get("feeChargesPortion");
Object penaltyPortion = externalEventDTO.getPayLoad().get("penaltyChargesPortion");
log.info("Event Received\n type:'{}'\n businessDate:'{}'", externalEventDTO.getType(), externalEventDTO.getBusinessDate());
log.info(
"Values\n amount: {}\n outstandingLoanBalance: {}\n principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: {}",
amount, outstandingLoanBalance, principalPortion, interestPortion, feePortion, penaltyPortion);
});
}
protected void deleteAllExternalEvents() {
ExternalEventHelper.deleteAllExternalEvents(requestSpec, createResponseSpecification(Matchers.is(204)));
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
}
@RequiredArgsConstructor
public static class BatchRequestBuilder {
@@ -37,7 +37,6 @@ import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -305,12 +304,6 @@ public class CustomSnapshotEventIntegrationTest extends BaseLoanIntegrationTest
});
}
private void deleteAllExternalEvents() {
ExternalEventHelper.deleteAllExternalEvents(requestSpec, createResponseSpecification(Matchers.is(204)));
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
}
private void enableCOBBusinessStep(String... steps) {
new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", steps);
@@ -25,24 +25,12 @@ import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
@@ -66,8 +54,10 @@ import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -1012,12 +1002,6 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
externalEventHelper.disableBusinessEvent("LoanBalanceChangedBusinessEvent");
}
private void deleteAllExternalEvents() {
ExternalEventHelper.deleteAllExternalEvents(requestSpec, createResponseSpecification(Matchers.is(204)));
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
}
private static Long createLoanProductPeriodicWithInterest() {
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
String shortName = Utils.uniqueRandomStringGenerator("", 4);
@@ -1070,21 +1054,6 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
return loanId;
}
private void logBusinessEvents(List<ExternalEventDTO> allExternalEvents) {
allExternalEvents.forEach(externalEventDTO -> {
Object amount = externalEventDTO.getPayLoad().get("amount");
Object outstandingLoanBalance = externalEventDTO.getPayLoad().get("outstandingLoanBalance");
Object principalPortion = externalEventDTO.getPayLoad().get("principalPortion");
Object interestPortion = externalEventDTO.getPayLoad().get("interestPortion");
Object feePortion = externalEventDTO.getPayLoad().get("feeChargesPortion");
Object penaltyPortion = externalEventDTO.getPayLoad().get("penaltyChargesPortion");
log.info("Event Received\n type:'{}'\n businessDate:'{}'", externalEventDTO.getType(), externalEventDTO.getBusinessDate());
log.info(
"Values\n amount: {}\n outstandingLoanBalance: {}\n principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: {}",
amount, outstandingLoanBalance, principalPortion, interestPortion, feePortion, penaltyPortion);
});
}
private void enableLoanInterestRefundPstBusinessEvent(boolean enabled) {
externalEventHelper.configureBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent", enabled);
}
@@ -1096,220 +1065,4 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
private void configureLoanAccrualTransactionCreatedBusinessEvent(boolean enabled) {
externalEventHelper.configureBusinessEvent("LoanAccrualTransactionCreatedBusinessEvent", enabled);
}
public void verifyBusinessEvents(BusinessEvent... businessEvents) {
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
logBusinessEvents(allExternalEvents);
Assertions.assertNotNull(businessEvents);
Assertions.assertNotNull(allExternalEvents);
Assertions.assertTrue(businessEvents.length <= allExternalEvents.size(),
"Expected business event count is less than actual. Expected: " + businessEvents.length + " Actual: "
+ allExternalEvents.size());
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
for (BusinessEvent businessEvent : businessEvents) {
long count = allExternalEvents.stream().filter(externalEvent -> businessEvent.verify(externalEvent, formatter)).count();
Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class BusinessEvent {
String type;
String businessDate;
boolean verify(@NotNull ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
var businessDate = LocalDate.parse(getBusinessDate(), formatter);
return Objects.equals(externalEvent.getType(), getType()) && Objects.equals(externalEvent.getBusinessDate(), businessDate);
}
}
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
public static class LoanTransactionBusinessEvent extends BusinessEvent {
private Double amount;
private Double outstandingLoanBalance;
private Double principalPortion;
private Double interestPortion;
private Double feeChargesPortion;
private Double penaltyChargesPortion;
public LoanTransactionBusinessEvent(String type, String businessDate, Double amount, Double outstandingLoanBalance,
Double principalPortion, Double interestPortion, Double feeChargesPortion, Double penaltyChargesPortion) {
super(type, businessDate);
this.amount = amount;
this.outstandingLoanBalance = outstandingLoanBalance;
this.principalPortion = principalPortion;
this.interestPortion = interestPortion;
this.feeChargesPortion = feeChargesPortion;
this.penaltyChargesPortion = penaltyChargesPortion;
}
@Override
boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
Object amount = externalEvent.getPayLoad().get("amount");
Object outstandingLoanBalance = externalEvent.getPayLoad().get("outstandingLoanBalance");
Object principalPortion = externalEvent.getPayLoad().get("principalPortion");
Object interestPortion = externalEvent.getPayLoad().get("interestPortion");
Object feePortion = externalEvent.getPayLoad().get("feeChargesPortion");
Object penaltyPortion = externalEvent.getPayLoad().get("penaltyChargesPortion");
return super.verify(externalEvent, formatter) && Objects.equals(amount, getAmount())
&& Objects.equals(outstandingLoanBalance, getOutstandingLoanBalance())
&& Objects.equals(principalPortion, getPrincipalPortion()) && Objects.equals(interestPortion, getInterestPortion())
&& Objects.equals(feePortion, getFeeChargesPortion()) && Objects.equals(penaltyPortion, getPenaltyChargesPortion());
}
}
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
public static class LoanBusinessEvent extends BusinessEvent {
private Integer statusId;
private Double principalDisbursed;
private Double principalOutstanding;
private List<String> loanTermVariationType;
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed,
Double principalOutstanding) {
super(type, businessDate);
this.statusId = statusId;
this.principalDisbursed = principalDisbursed;
this.principalOutstanding = principalOutstanding;
}
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding,
List<String> loanTermVariationType) {
super(type, businessDate);
this.statusId = statusId;
this.principalDisbursed = principalDisbursed;
this.principalOutstanding = principalOutstanding;
this.loanTermVariationType = loanTermVariationType;
}
@Override
public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
Object summaryRes = externalEvent.getPayLoad().get("summary");
Object statusRes = externalEvent.getPayLoad().get("status");
Map<String, Object> summary = summaryRes instanceof Map ? (Map<String, Object>) summaryRes : Map.of();
Map<String, Object> status = statusRes instanceof Map ? (Map<String, Object>) statusRes : Map.of();
var principalDisbursed = summary.get("principalDisbursed");
var principalOutstanding = summary.get("principalOutstanding");
Double statusId = (Double) status.get("id");
return super.verify(externalEvent, formatter) && Objects.equals(statusId, getStatusId().doubleValue())
&& Objects.equals(principalDisbursed, getPrincipalDisbursed())
&& Objects.equals(principalOutstanding, getPrincipalOutstanding()) && loanTermVariationsMatch(
(List<Map<String, Object>>) externalEvent.getPayLoad().get("loanTermVariations"), loanTermVariationType);
}
private boolean loanTermVariationsMatch(final List<Map<String, Object>> loanTermVariations, final List<String> expectedTypes) {
if (CollectionUtils.isEmpty(expectedTypes)) {
return true;
}
final long numberOfMatches = expectedTypes.stream().filter(expectedType -> loanTermVariations.stream().anyMatch(
variation -> StringUtils.equals((String) ((Map<String, Object>) variation.get("termType")).get("value"), expectedType)))
.count();
return numberOfMatches == expectedTypes.size();
}
}
public static class LoanAdjustTransactionBusinessEvent extends BusinessEvent {
private String transactionTypeCode;
private String transactionDate;
private Double oldAmount;
private Double newAmount;
private Double oldPrincipalPortion;
private Double newPrincipalPortion;
private Double oldInterestPortion;
private Double newInterestPortion;
private Double oldFeePortion;
private Double newFeePortion;
private Double oldPenaltyPortion;
private Double newPenaltyPortion;
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
}
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate,
Double oldAmount, Double newAmount) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
this.oldAmount = oldAmount;
this.newAmount = newAmount;
}
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate,
Double oldAmount, Double newAmount, Double oldPrincipalPortion, Double newPrincipalPortion, Double oldInterestPortion,
Double newInterestPortion, Double oldFeePortion, Double newFeePortion, Double oldPenaltyPortion, Double newPenaltyPortion) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
this.oldAmount = oldAmount;
this.newAmount = newAmount;
this.oldPrincipalPortion = oldPrincipalPortion;
this.newPrincipalPortion = newPrincipalPortion;
this.oldInterestPortion = oldInterestPortion;
this.newInterestPortion = newInterestPortion;
this.oldFeePortion = oldFeePortion;
this.newFeePortion = newFeePortion;
this.oldPenaltyPortion = oldPenaltyPortion;
this.newPenaltyPortion = newPenaltyPortion;
}
@Override
boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
final Object transactionToAdjust = externalEvent.getPayLoad().get("transactionToAdjust");
final Map<?, Object> transActionToAdjustMap = transactionToAdjust instanceof Map ? (Map<String, Object>) transactionToAdjust
: Collections.emptyMap();
Object actualOldAmount = transActionToAdjustMap.get("amount");
Object actualOldPrincipalPortion = transActionToAdjustMap.get("principalPortion");
Object actualOldInterestPortion = transActionToAdjustMap.get("interestPortion");
Object actualOldFeePortion = transActionToAdjustMap.get("feeChargesPortion");
Object actualOldPenaltyPortion = transActionToAdjustMap.get("penaltyChargesPortion");
final Object newTransactionDetail = externalEvent.getPayLoad().get("newTransactionDetail");
final Map<?, Object> newTransactionDetailMap = newTransactionDetail instanceof Map ? (Map<String, Object>) newTransactionDetail
: Collections.emptyMap();
Object actualNewAmount = newTransactionDetailMap.get("amount");
Object actualNewPrincipalPortion = newTransactionDetailMap.get("principalPortion");
Object actualNewInterestPortion = newTransactionDetailMap.get("interestPortion");
Object actualNewFeePortion = newTransactionDetailMap.get("feeChargesPortion");
Object actualNewPenaltyPortion = newTransactionDetailMap.get("penaltyChargesPortion");
final Object actualTransactionDate = transActionToAdjustMap.get("date");
final Object transactionType = transActionToAdjustMap.get("type");
final Map<?, Object> transactionTypeMap = transactionType instanceof Map ? (Map<String, Object>) transactionType
: Collections.emptyMap();
final Object actualTransactionTypeCode = transactionTypeMap.get("code");
return super.verify(externalEvent, formatter)//
&& Objects.equals(actualTransactionTypeCode, transactionTypeCode)
&& Objects.equals(actualTransactionDate, transactionDate)//
&& (oldAmount == null || Objects.equals(actualOldAmount, oldAmount))//
&& (newAmount == null || Objects.equals(actualNewAmount, newAmount))//
&& (oldPrincipalPortion == null || Objects.equals(actualOldPrincipalPortion, oldPrincipalPortion))//
&& (newPrincipalPortion == null || Objects.equals(actualNewPrincipalPortion, newPrincipalPortion))//
&& (oldInterestPortion == null || Objects.equals(actualOldInterestPortion, oldInterestPortion))//
&& (newInterestPortion == null || Objects.equals(actualNewInterestPortion, newInterestPortion))//
&& (oldFeePortion == null || Objects.equals(actualOldFeePortion, oldFeePortion))//
&& (newFeePortion == null || Objects.equals(actualNewFeePortion, newFeePortion))//
&& (oldPenaltyPortion == null || Objects.equals(actualOldPenaltyPortion, oldPenaltyPortion))//
&& (newPenaltyPortion == null || Objects.equals(actualNewPenaltyPortion, newPenaltyPortion));
}
}
}
@@ -36,6 +36,9 @@ import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -780,4 +783,114 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
Assertions.assertEquals(thousandFiveHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP));
Assertions.assertEquals(zero, loanDetails.getSummary().getPrincipalOutstanding().setScale(0, RoundingMode.HALF_UP));
}
@Test
public void testCapitalizedIncomeEvents() {
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanAdjustTransactionBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
final AtomicReference<Long> capitalizedIncomeTransactionIdRef = new AtomicReference<>();
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
500.0, 7.0, 3, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
deleteAllExternalEvents();
Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 100.0)
.getResourceId();
capitalizedIncomeTransactionIdRef.set(capitalizedIncomeTransactionId);
verifyBusinessEvents(
new LoanTransactionBusinessEvent("LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "01 January 2024", 100.0,
200.0, 100.0, 0.0, 0.0, 0.0),
new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "01 January 2024", 300, 100.0, 200.0));
});
runAt("2 January 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024") //
);
verifyBusinessEvents(new LoanTransactionBusinessEvent("LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"01 January 2024", 1.10, 0.0, 0.0, 0.0, 1.10, 0.0));
});
runAt("3 January 2024", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
deleteAllExternalEvents();
Long capitalizedIncomeAdjustmentTransactionId = loanTransactionHelper
.capitalizedIncomeAdjustment(loanId, capitalizedIncomeTransactionIdRef.get(), "3 January 2024", 50.0).getResourceId();
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
transaction(0.04, "Accrual", "02 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
transaction(50.0, "Capitalized Income Adjustment", "03 January 2024") //
);
verifyBusinessEvents(
new LoanTransactionBusinessEvent("LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "03 January 2024",
50.0, 150.0, 50.0, 0.0, 0.0, 0.0),
new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "03 January 2024", 300, 100.0, 150.0));
deleteAllExternalEvents();
loanTransactionHelper.reverseLoanTransaction(loanId, capitalizedIncomeAdjustmentTransactionId, "3 January 2024");
verifyBusinessEvents(new LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "03 January 2024",
"loanTransactionType.capitalizedIncomeAdjustment", "2024-01-03"));
});
runAt("4 January 2024", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
deleteAllExternalEvents();
loanTransactionHelper.reverseLoanTransaction(loanId, capitalizedIncomeTransactionIdRef.get(), "3 January 2024");
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
transaction(0.04, "Accrual", "02 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
transaction(0.04, "Accrual", "03 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
transaction(50.0, "Capitalized Income Adjustment", "03 January 2024") //
);
verifyBusinessEvents(new LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "04 January 2024",
"loanTransactionType.capitalizedIncome", "2024-01-01") //
);
});
}
}
@@ -606,6 +606,11 @@ public class ExternalEventConfigurationHelper {
loanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent);
Map<String, Object> loanCapitalizedIncomeTransactionCreatedBusinessEvent = new HashMap<>();
loanCapitalizedIncomeTransactionCreatedBusinessEvent.put("type", "LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
loanCapitalizedIncomeTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeTransactionCreatedBusinessEvent);
return defaults;
}
@@ -0,0 +1,43 @@
/**
* 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.common.externalevents;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BusinessEvent {
protected String type;
protected String businessDate;
public boolean verify(@NotNull ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
var businessDate = LocalDate.parse(getBusinessDate(), formatter);
return Objects.equals(externalEvent.getType(), getType()) && Objects.equals(externalEvent.getBusinessDate(), businessDate);
}
}
@@ -0,0 +1,122 @@
/**
* 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.common.externalevents;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
@EqualsAndHashCode(callSuper = true)
@Data
public class LoanAdjustTransactionBusinessEvent extends BusinessEvent {
private String transactionTypeCode;
private String transactionDate;
private Double oldAmount;
private Double newAmount;
private Double oldPrincipalPortion;
private Double newPrincipalPortion;
private Double oldInterestPortion;
private Double newInterestPortion;
private Double oldFeePortion;
private Double newFeePortion;
private Double oldPenaltyPortion;
private Double newPenaltyPortion;
// minimum data for checking if transaction was reversed
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
}
// minimum data for checking if transaction was adjusted
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate,
Double oldAmount, Double newAmount) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
this.oldAmount = oldAmount;
this.newAmount = newAmount;
}
public LoanAdjustTransactionBusinessEvent(String type, String businessDate, String transactionTypeCode, String transactionDate,
Double oldAmount, Double newAmount, Double oldPrincipalPortion, Double newPrincipalPortion, Double oldInterestPortion,
Double newInterestPortion, Double oldFeePortion, Double newFeePortion, Double oldPenaltyPortion, Double newPenaltyPortion) {
super(type, businessDate);
this.transactionTypeCode = transactionTypeCode;
this.transactionDate = transactionDate;
this.oldAmount = oldAmount;
this.newAmount = newAmount;
this.oldPrincipalPortion = oldPrincipalPortion;
this.newPrincipalPortion = newPrincipalPortion;
this.oldInterestPortion = oldInterestPortion;
this.newInterestPortion = newInterestPortion;
this.oldFeePortion = oldFeePortion;
this.newFeePortion = newFeePortion;
this.oldPenaltyPortion = oldPenaltyPortion;
this.newPenaltyPortion = newPenaltyPortion;
}
@Override
public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
final Object transactionToAdjust = externalEvent.getPayLoad().get("transactionToAdjust");
final Map<?, Object> transActionToAdjustMap = transactionToAdjust instanceof Map ? (Map<String, Object>) transactionToAdjust
: Collections.emptyMap();
Object actualOldAmount = transActionToAdjustMap.get("amount");
Object actualOldPrincipalPortion = transActionToAdjustMap.get("principalPortion");
Object actualOldInterestPortion = transActionToAdjustMap.get("interestPortion");
Object actualOldFeePortion = transActionToAdjustMap.get("feeChargesPortion");
Object actualOldPenaltyPortion = transActionToAdjustMap.get("penaltyChargesPortion");
final Object newTransactionDetail = externalEvent.getPayLoad().get("newTransactionDetail");
final Map<?, Object> newTransactionDetailMap = newTransactionDetail instanceof Map ? (Map<String, Object>) newTransactionDetail
: Collections.emptyMap();
Object actualNewAmount = newTransactionDetailMap.get("amount");
Object actualNewPrincipalPortion = newTransactionDetailMap.get("principalPortion");
Object actualNewInterestPortion = newTransactionDetailMap.get("interestPortion");
Object actualNewFeePortion = newTransactionDetailMap.get("feeChargesPortion");
Object actualNewPenaltyPortion = newTransactionDetailMap.get("penaltyChargesPortion");
final Object actualTransactionDate = transActionToAdjustMap.get("date");
final Object transactionType = transActionToAdjustMap.get("type");
final Map<?, Object> transactionTypeMap = transactionType instanceof Map ? (Map<String, Object>) transactionType
: Collections.emptyMap();
final Object actualTransactionTypeCode = transactionTypeMap.get("code");
return super.verify(externalEvent, formatter)//
&& Objects.equals(actualTransactionTypeCode, transactionTypeCode) && Objects.equals(actualTransactionDate, transactionDate)//
&& (oldAmount == null || Objects.equals(actualOldAmount, oldAmount))//
&& (newAmount == null || Objects.equals(actualNewAmount, newAmount))//
&& (oldPrincipalPortion == null || Objects.equals(actualOldPrincipalPortion, oldPrincipalPortion))//
&& (newPrincipalPortion == null || Objects.equals(actualNewPrincipalPortion, newPrincipalPortion))//
&& (oldInterestPortion == null || Objects.equals(actualOldInterestPortion, oldInterestPortion))//
&& (newInterestPortion == null || Objects.equals(actualNewInterestPortion, newInterestPortion))//
&& (oldFeePortion == null || Objects.equals(actualOldFeePortion, oldFeePortion))//
&& (newFeePortion == null || Objects.equals(actualNewFeePortion, newFeePortion))//
&& (oldPenaltyPortion == null || Objects.equals(actualOldPenaltyPortion, oldPenaltyPortion))//
&& (newPenaltyPortion == null || Objects.equals(actualNewPenaltyPortion, newPenaltyPortion));
}
}
@@ -0,0 +1,85 @@
/**
* 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.common.externalevents;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
@EqualsAndHashCode(callSuper = true)
@Data
public class LoanBusinessEvent extends BusinessEvent {
private Integer statusId;
private Double principalDisbursed;
private Double principalOutstanding;
private List<String> loanTermVariationType;
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding) {
super(type, businessDate);
this.statusId = statusId;
this.principalDisbursed = principalDisbursed;
this.principalOutstanding = principalOutstanding;
}
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding,
List<String> loanTermVariationType) {
super(type, businessDate);
this.statusId = statusId;
this.principalDisbursed = principalDisbursed;
this.principalOutstanding = principalOutstanding;
this.loanTermVariationType = loanTermVariationType;
}
@Override
public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
Object summaryRes = externalEvent.getPayLoad().get("summary");
Object statusRes = externalEvent.getPayLoad().get("status");
Map<String, Object> summary = summaryRes instanceof Map ? (Map<String, Object>) summaryRes : Map.of();
Map<String, Object> status = statusRes instanceof Map ? (Map<String, Object>) statusRes : Map.of();
var principalDisbursed = summary.get("principalDisbursed");
var principalOutstanding = summary.get("principalOutstanding");
Double statusId = (Double) status.get("id");
return super.verify(externalEvent, formatter) && Objects.equals(statusId, getStatusId().doubleValue())
&& Objects.equals(principalDisbursed, getPrincipalDisbursed())
&& Objects.equals(principalOutstanding, getPrincipalOutstanding()) && loanTermVariationsMatch(
(List<Map<String, Object>>) externalEvent.getPayLoad().get("loanTermVariations"), loanTermVariationType);
}
private boolean loanTermVariationsMatch(final List<Map<String, Object>> loanTermVariations, final List<String> expectedTypes) {
if (CollectionUtils.isEmpty(expectedTypes)) {
return true;
}
final long numberOfMatches = expectedTypes
.stream().filter(
expectedType -> loanTermVariations.stream()
.anyMatch(variation -> StringUtils
.equals((String) ((Map<String, Object>) variation.get("termType")).get("value"), expectedType)))
.count();
return numberOfMatches == expectedTypes.size();
}
}
@@ -0,0 +1,63 @@
/**
* 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.common.externalevents;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
@EqualsAndHashCode(callSuper = true)
@Data
public class LoanTransactionBusinessEvent extends BusinessEvent {
private Double amount;
private Double outstandingLoanBalance;
private Double principalPortion;
private Double interestPortion;
private Double feeChargesPortion;
private Double penaltyChargesPortion;
public LoanTransactionBusinessEvent(String type, String businessDate, Double amount, Double outstandingLoanBalance,
Double principalPortion, Double interestPortion, Double feeChargesPortion, Double penaltyChargesPortion) {
super(type, businessDate);
this.amount = amount;
this.outstandingLoanBalance = outstandingLoanBalance;
this.principalPortion = principalPortion;
this.interestPortion = interestPortion;
this.feeChargesPortion = feeChargesPortion;
this.penaltyChargesPortion = penaltyChargesPortion;
}
@Override
public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
Object amount = externalEvent.getPayLoad().get("amount");
Object outstandingLoanBalance = externalEvent.getPayLoad().get("outstandingLoanBalance");
Object principalPortion = externalEvent.getPayLoad().get("principalPortion");
Object interestPortion = externalEvent.getPayLoad().get("interestPortion");
Object feePortion = externalEvent.getPayLoad().get("feeChargesPortion");
Object penaltyPortion = externalEvent.getPayLoad().get("penaltyChargesPortion");
return super.verify(externalEvent, formatter) && Objects.equals(amount, getAmount())
&& Objects.equals(outstandingLoanBalance, getOutstandingLoanBalance())
&& Objects.equals(principalPortion, getPrincipalPortion()) && Objects.equals(interestPortion, getInterestPortion())
&& Objects.equals(feePortion, getFeeChargesPortion()) && Objects.equals(penaltyPortion, getPenaltyChargesPortion());
}
}