FINERACT-2311: Buy Down fees - External business events

This commit is contained in:
Soma Sörös
2025-08-06 13:53:04 +02:00
committed by Adam Saghy
parent 9c37ef437d
commit aa68f251d3
11 changed files with 301 additions and 39 deletions
@@ -20,11 +20,11 @@ package org.apache.fineract.infrastructure.event.business.domain.loan.transactio
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
public class LoanBuyDownFeeAdjustmentTransactionBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent";
private static final String TYPE = "LoanBuyDownFeeAdjustmentTransactionBusinessEvent";
public LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent(LoanTransaction value) {
public LoanBuyDownFeeAdjustmentTransactionBusinessEvent(LoanTransaction value) {
super(value);
}
@@ -20,11 +20,11 @@ package org.apache.fineract.infrastructure.event.business.domain.loan.transactio
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanBuyDownFeeTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
public class LoanBuyDownFeeTransactionBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanBuyDownFeeTransactionCreatedBusinessEvent";
private static final String TYPE = "LoanBuyDownFeeTransactionBusinessEvent";
public LoanBuyDownFeeTransactionCreatedBusinessEvent(LoanTransaction value) {
public LoanBuyDownFeeTransactionBusinessEvent(LoanTransaction value) {
super(value);
}
@@ -36,9 +36,8 @@ import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRu
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.LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAdjustmentTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.client.domain.Client;
@@ -125,8 +124,7 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
loanJournalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
// Notify business events
businessEventNotifierService.notifyPostBusinessEvent(new LoanBuyDownFeeTransactionCreatedBusinessEvent(buyDownFeeTransaction));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBuyDownFeeTransactionBusinessEvent(buyDownFeeTransaction));
return new CommandProcessingResultBuilder() //
.withClientId(loan.getClientId()) //
@@ -205,8 +203,7 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Notify business events
businessEventNotifierService
.notifyPostBusinessEvent(new LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent(savedBuyDownFeeAdjustment));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
.notifyPostBusinessEvent(new LoanBuyDownFeeAdjustmentTransactionBusinessEvent(savedBuyDownFeeAdjustment));
return new CommandProcessingResultBuilder().withEntityId(savedBuyDownFeeAdjustment.getId())
.withEntityExternalId(savedBuyDownFeeAdjustment.getExternalId()).withOfficeId(loan.getOfficeId())
@@ -37,6 +37,9 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAdjustmentTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -321,7 +324,11 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
if (optAmortizationAdjustmentTransaction.isPresent()) {
loan.addLoanTransaction(optAmortizationAdjustmentTransaction.get());
loanTransactionRepository.saveAndFlush(optAmortizationAdjustmentTransaction.get());
businessEventNotifierService
.notifyPostBusinessEvent(new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(
optAmortizationAdjustmentTransaction.get()));
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanBuyDownFeeTransactionBusinessEvent(transactionForAdjustment));
}
if (loan.isClosedWrittenOff()) {
@@ -355,6 +362,8 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
MathUtil.nullToZero(buyDownFeeBalance.getUnrecognizedAmount()).add(transactionForAdjustment.getAmount()));
loanBuyDownFeeBalanceRepository.save(buyDownFeeBalance);
}
businessEventNotifierService
.notifyPostBusinessEvent(new LoanBuyDownFeeAdjustmentTransactionBusinessEvent(transactionForAdjustment));
}
}
@@ -211,4 +211,5 @@
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
<include file="parts/0193_buy_down_fee_events.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?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.3.xsd">
<changeSet id="4" author="fineract">
<update tableName="m_external_event_configuration">
<column name="type" value="LoanBuyDownFeeTransactionBusinessEvent"/>
<where>type='LoanBuyDownFeeTransactionCreatedBusinessEvent'</where>
</update>
<update tableName="m_external_event_configuration">
<column name="type" value="LoanBuyDownFeeAdjustmentTransactionBusinessEvent"/>
<where>type='LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent'</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -110,7 +110,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeTransactionBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
@@ -204,7 +204,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeTransactionBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
@@ -1481,16 +1481,21 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
protected void logBusinessEvents(List<ExternalEventResponse> 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);
if ("org.apache.fineract.avro.loan.v1.LoanTransactionDataV1".equals(externalEventDTO.getSchema())) {
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");
Object reversed = externalEventDTO.getPayLoad().get("reversed");
log.info(
"Values\n amount: {}\n outstandingLoanBalance: {}\n principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: {}\n reversed: {}",
amount, outstandingLoanBalance, principalPortion, interestPortion, feePortion, penaltyPortion, reversed);
} else {
log.info("Schema: {}", externalEventDTO.getSchema());
}
});
}
@@ -47,17 +47,18 @@ import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.externalevents.BusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionMinimalBusinessEvent;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Integration tests for Buy Down Fee functionality in Progressive Loans
*/
@Slf4j
@ExtendWith({ LoanTestLifecycleExtension.class })
public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
private Long clientId;
@@ -72,8 +73,21 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
private final Account feeReceivableAccount = accountHelper.createAssetAccount();
private final Account penaltyReceivableAccount = accountHelper.createAssetAccount();
@AfterAll
public static void teardown() {
ExternalEventHelper externalEventHelper = new ExternalEventHelper();
externalEventHelper.disableBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent");
externalEventHelper.disableBusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent");
externalEventHelper.disableBusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent");
externalEventHelper.disableBusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
}
@BeforeEach
public void beforeEach() {
externalEventHelper.enableBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent");
externalEventHelper.enableBusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
runAt("01 September 2024", () -> {
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
@@ -96,8 +110,10 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
assertNotNull(loanDetailsBeforeTransaction);
log.info("Loan Product: {}", loanDetailsBeforeTransaction.getLoanProductName());
deleteAllExternalEvents();
// Create buy down fee transaction
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 500.0, "02 September 2024");
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "02 September 2024"));
assertNotNull(buyDownFeeTransactionId);
@@ -126,11 +142,14 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
String externalId = UUID.randomUUID().toString();
String noteText = "Buy Down Fee - Test Note";
deleteAllExternalEvents();
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId,
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("03 September 2024").locale("en")
.transactionAmount(250.0).externalId(externalId).note(noteText));
assertNotNull(response.getResourceId());
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "03 September 2024"));
// Verify transaction details
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
@@ -147,11 +166,17 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
@Test
public void testMultipleBuyDownFees() {
runAt("04 September 2024", () -> {
deleteAllExternalEvents();
// Add first buy down fee
Long firstBuyDownFeeId = addBuyDownFeeForLoan(loanId, 200.0, "04 September 2024");
Long firstBuyDownFeeId = addBuyDownFeeForLoan(loanId, 200.0, "02 September 2024");
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "04 September 2024"));
deleteAllExternalEvents();
// Add second buy down fee
Long secondBuyDownFeeId = addBuyDownFeeForLoan(loanId, 150.0, "04 September 2024");
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "04 September 2024"));
assertNotNull(firstBuyDownFeeId);
assertNotNull(secondBuyDownFeeId);
@@ -258,14 +283,21 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
@Test
public void testBuyDownFeeAdjustment() {
runAt("06 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 500.0, "06 September 2024");
assertNotNull(buyDownFeeTransactionId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "06 September 2024"));
deleteAllExternalEvents();
// Create buy down fee adjustment (use same date as business date)
PostLoansLoanIdTransactionsResponse adjustmentResponse = loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionId, "06 September 2024", 100.0);
verifyBusinessEvents(new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"06 September 2024", 100.0, false));
assertNotNull(adjustmentResponse);
assertNotNull(adjustmentResponse.getLoanId());
assertNotNull(adjustmentResponse.getClientId());
@@ -286,6 +318,12 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
assertNotNull(adjustmentTransaction);
assertEquals(0, BigDecimal.valueOf(100.0).compareTo(adjustmentTransaction.getAmount()));
assertEquals("06 September 2024", adjustmentTransaction.getDate().format(DateTimeFormatter.ofPattern("dd MMMM yyyy")));
deleteAllExternalEvents();
loanTransactionHelper.reverseLoanTransaction(loanId, adjustmentResponse.getResourceId(), "06 September 2024");
verifyBusinessEvents(new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"06 September 2024", 100.0, true));
});
}
@@ -351,20 +389,30 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
@Test
public void testMultipleBuyDownFeeAdjustments() {
runAt("12 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 600.0, "12 September 2024");
assertNotNull(buyDownFeeTransactionId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "12 September 2024"));
deleteAllExternalEvents();
// First adjustment
PostLoansLoanIdTransactionsResponse adjustment1 = loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId,
"12 September 2024", 100.0);
assertNotNull(adjustment1);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "12 September 2024"));
deleteAllExternalEvents();
// Second adjustment
PostLoansLoanIdTransactionsResponse adjustment2 = loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId,
"12 September 2024", 150.0);
assertNotNull(adjustment2);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "12 September 2024"));
// Verify both adjustments are recorded
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails);
@@ -392,10 +440,12 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
@Test
public void testBuyDownFeeAdjustmentWithExternalId() {
runAt("16 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 350.0, "16 September 2024");
assertNotNull(buyDownFeeTransactionId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "16 September 2024"));
deleteAllExternalEvents();
// Create adjustment with external ID
String adjustmentExternalId = UUID.randomUUID().toString();
PostLoansLoanIdTransactionsResponse adjustmentResponse = loanTransactionHelper.buyDownFeeAdjustment(loanId,
@@ -407,6 +457,8 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
assertNotNull(adjustmentResponse);
assertEquals(adjustmentExternalId, adjustmentResponse.getResourceExternalId());
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "16 September 2024"));
// Verify adjustment transaction details
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
GetLoansLoanIdTransactions adjustmentTransaction = loanDetails.getTransactions().stream()
@@ -462,13 +514,17 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
deleteAllExternalEvents();
PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.makeLoanBuyDownFee(loanId, "1 January 2024",
50.0);
buyDownFeeTransactionIdIdRef.set(transactionsResponse.getResourceId());
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "01 January 2024"));
});
runAt("31 January 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent", "30 January 2024"));
// summarized amortization
verifyTransactions(loanId, //
@@ -479,7 +535,9 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
});
runAt("1 February 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent", "31 January 2024"));
// daily amortization
verifyTransactions(loanId, //
@@ -490,11 +548,15 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
transaction(0.01, "Accrual", "31 January 2024"), //
transaction(0.55, "Buy Down Fee Amortization", "31 January 2024"));
deleteAllExternalEvents();
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "1 February 2024", 10.0);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "01 February 2024"));
});
runAt("2 February 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent", "01 February 2024"));
// not backdated and not large buy down fee adjustment -> lowered daily amount
verifyTransactions(loanId, //
@@ -508,11 +570,16 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
transaction(0.02, "Accrual", "01 February 2024"), //
transaction(0.39, "Buy Down Fee Amortization", "01 February 2024"));
deleteAllExternalEvents();
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "10 January 2024", 10.0);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "02 February 2024"));
});
runAt("3 February 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyBusinessEvents(
new BusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "02 February 2024"));
// backdated buy down fee adjustment -> amortization adjustment
verifyTransactions(loanId, //
@@ -529,11 +596,16 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
transaction(0.02, "Accrual", "02 February 2024"), //
transaction(2.55, "Buy Down Fee Amortization Adjustment", "02 February 2024"));
deleteAllExternalEvents();
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "03 February 2024", 20.0);
verifyBusinessEvents(new BusinessEvent("LoanBuyDownFeeAdjustmentTransactionBusinessEvent", "03 February 2024"));
});
runAt("4 February 2024", () -> {
Long loanId = loanIdRef.get();
deleteAllExternalEvents();
executeInlineCOB(loanId);
verifyBusinessEvents(
new BusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "03 February 2024"));
// large (more than remaining unrecognized (15.13)) buy down fee adjustment -> amortization adjustment
verifyTransactions(loanId, //
@@ -630,7 +702,52 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
}
@Test
public void testReverseBuyDownFeeTransaction() {
public void tesReverseBuyDownFeeTransactionWithAmortizationAdjustmentTransaction() {
AtomicReference<Long> buyDownFeeTransactionIdRef = new AtomicReference<>();
runAt("2 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 400.0, "02 September 2024");
buyDownFeeTransactionIdRef.set(buyDownFeeTransactionId);
assertNotNull(buyDownFeeTransactionId);
verifyBusinessEvents(
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "02 September 2024", 400.0, false));
});
runAt("10 September 2024", () -> {
deleteAllExternalEvents();
inlineLoanCOBHelper.executeInlineCOB(loanId);
verifyBusinessEvents(new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"09 September 2024", 8.79, false));
deleteAllExternalEvents();
Long buyDownFeeTransactionId = buyDownFeeTransactionIdRef.get();
// Reverse Buy Down Fee
PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.reverseLoanTransaction(loanId,
buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN)
.transactionDate("10 September 2024").note("Buy Down Fee reversed").transactionAmount(0.0).locale("en"));
Assertions.assertNotNull(transactionsResponse);
Assertions.assertNotNull(transactionsResponse.getResourceId());
Assertions.assertEquals(transactionsResponse.getResourceId(), buyDownFeeTransactionId);
verifyBusinessEvents(
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"10 September 2024", 8.79, false),
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "10 September 2024", 400.0, true));
verifyTransactions(loanId, //
transaction(1000.000000, "Disbursement", "01 September 2024", 1000.000000, 0, 0, 0, 0, 0, 0, false), //
transaction(400.000000, "Buy Down Fee", "02 September 2024", 0, 0, 0, 0, 0, 0, 0, true), //
transaction(2.220000, "Accrual", "09 September 2024", 0, 0, 2.220000, 0, 0, 0, 0, false), //
transaction(8.790000, "Buy Down Fee Amortization", "09 September 2024", 0, 0, 0, 8.790000, 0, 0, 0, false), //
transaction(8.790000, "Buy Down Fee Amortization Adjustment", "10 September 2024", 0, 0, 0, 8.790000, 0, 0, 0, false) //
);
});
}
@Test
public void testReverseBuyDownFeeTransactionWithoutAmortizationAdjustmentTransaction() {
final AtomicReference<Long> buyDownFeeTransactionIdIdRef = new AtomicReference<>();
runAt("10 September 2024", () -> {
@@ -646,11 +763,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 September 2024"), //
transaction(400.0, "Buy Down Fee", "10 September 2024"));
});
runAt("23 September 2024", () -> {
final Long buyDownFeeTransactionId = buyDownFeeTransactionIdIdRef.get();
executeInlineCOB(loanId);
// Reverse Buy Down Fee
PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.reverseLoanTransaction(loanId,
buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN)
@@ -659,15 +772,69 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(transactionsResponse.getResourceId());
Assertions.assertEquals(transactionsResponse.getResourceId(), buyDownFeeTransactionId);
verifyTransactions(loanId, //
transaction(1000.000000, "Disbursement", "01 September 2024", 1000.000000, 0, 0, 0, 0, 0, 0, false), //
transaction(400.000000, "Buy Down Fee", "10 September 2024", 0, 0, 0, 0, 0, 0, 0, true) //
);
});
}
@Test
public void testReverseBuyDownFeeTransaction() {
final AtomicReference<Long> buyDownFeeTransactionIdIdRef = new AtomicReference<>();
runAt("10 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
final Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 400.0, "10 September 2024");
assertNotNull(buyDownFeeTransactionId);
buyDownFeeTransactionIdIdRef.set(buyDownFeeTransactionId);
verifyBusinessEvents(
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "10 September 2024", 400.0, false));
// Verify initial buy down fee accounting entries
verifyTRJournalEntries(buyDownFeeTransactionId, debit(buyDownExpenseAccount, 400.0),
credit(deferredIncomeLiabilityAccount, 400.0));
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 September 2024"), //
transaction(400.0, "Buy Down Fee", "10 September 2024"));
});
runAt("23 September 2024", () -> {
deleteAllExternalEvents();
final Long buyDownFeeTransactionId = buyDownFeeTransactionIdIdRef.get();
executeInlineCOB(loanId);
verifyBusinessEvents(new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"22 September 2024", 14.61, false));
deleteAllExternalEvents();
// Reverse Buy Down Fee
PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.reverseLoanTransaction(loanId,
buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN)
.transactionDate("10 September 2024").note("Buy Down Fee reversed").transactionAmount(0.0).locale("en"));
Assertions.assertNotNull(transactionsResponse);
Assertions.assertNotNull(transactionsResponse.getResourceId());
Assertions.assertEquals(transactionsResponse.getResourceId(), buyDownFeeTransactionId);
verifyBusinessEvents(
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"23 September 2024", 14.61, false),
new LoanTransactionMinimalBusinessEvent("LoanBuyDownFeeTransactionBusinessEvent", "23 September 2024", 400.0, true));
// Verify initial buy down fee reversed accounting entries
verifyTRJournalEntries(buyDownFeeTransactionId, debit(buyDownExpenseAccount, 400.0),
credit(deferredIncomeLiabilityAccount, 400.0), credit(buyDownExpenseAccount, 400.0),
debit(deferredIncomeLiabilityAccount, 400.0));
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 September 2024"), //
reversedTransaction(400.0, "Buy Down Fee", "10 September 2024"), //
transaction(5.83, "Accrual", "22 September 2024"));
transaction(1000.000000, "Disbursement", "01 September 2024", 1000.000000, 0, 0, 0, 0, 0, 0, false), //
transaction(400.000000, "Buy Down Fee", "10 September 2024", 0, 0, 0, 0, 0, 0, 0, true), //
transaction(5.830000, "Accrual", "22 September 2024", 0, 0, 5.830000, 0, 0, 0, 0, false), //
transaction(14.610000, "Buy Down Fee Amortization", "22 September 2024", 0, 0, 0, 14.610000, 0, 0, 0, false), //
transaction(14.610000, "Buy Down Fee Amortization Adjustment", "23 September 2024", 0, 0, 0, 14.610000, 0, 0, 0, false) //
);
});
}
@@ -637,12 +637,12 @@ public class ExternalEventConfigurationHelper {
defaults.add(loanTransactionUndoContractTerminationBusinessEvent);
Map<String, Object> loanTransactionBuyDownFeePostBusinessEvent = new HashMap<>();
loanTransactionBuyDownFeePostBusinessEvent.put("type", "LoanBuyDownFeeTransactionCreatedBusinessEvent");
loanTransactionBuyDownFeePostBusinessEvent.put("type", "LoanBuyDownFeeTransactionBusinessEvent");
loanTransactionBuyDownFeePostBusinessEvent.put("enabled", false);
defaults.add(loanTransactionBuyDownFeePostBusinessEvent);
Map<String, Object> loanTransactionBuyDownFeeAdjustmentPostBusinessEvent = new HashMap<>();
loanTransactionBuyDownFeeAdjustmentPostBusinessEvent.put("type", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
loanTransactionBuyDownFeeAdjustmentPostBusinessEvent.put("type", "LoanBuyDownFeeAdjustmentTransactionBusinessEvent");
loanTransactionBuyDownFeeAdjustmentPostBusinessEvent.put("enabled", false);
defaults.add(loanTransactionBuyDownFeeAdjustmentPostBusinessEvent);
@@ -0,0 +1,48 @@
/**
* 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.data.ExternalEventResponse;
@EqualsAndHashCode(callSuper = true)
@Data
public class LoanTransactionMinimalBusinessEvent extends BusinessEvent {
public Double amount;
public Boolean reversed;
public LoanTransactionMinimalBusinessEvent(String type, String businessDate, Double amount, Boolean reversed) {
super(type, businessDate);
this.amount = amount;
this.reversed = reversed;
}
@Override
public boolean verify(ExternalEventResponse externalEvent, DateTimeFormatter formatter) {
Object actualAmount = externalEvent.getPayLoad().get("amount");
Object actualReversed = externalEvent.getPayLoad().get("reversed");
return super.verify(externalEvent, formatter) && Objects.equals(actualAmount, getAmount())
&& Objects.equals(actualReversed, getReversed());
}
}