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
@@ -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());
}
}