FINERACT-2311: Add buydown fees adjustments

This commit is contained in:
Attila Budai
2025-07-07 15:29:27 +02:00
committed by Adam Saghy
parent c8b88c3664
commit b2bce4a45d
20 changed files with 646 additions and 8 deletions
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +33,7 @@ import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
@@ -243,6 +245,168 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy");
}
@Test
public void testBuyDownFeeAdjustment() {
runAt("06 September 2024", () -> {
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 500.0, "06 September 2024");
assertNotNull(buyDownFeeTransactionId);
// Create buy down fee adjustment (use same date as business date)
PostLoansLoanIdTransactionsResponse adjustmentResponse = loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionId, "06 September 2024", 100.0);
assertNotNull(adjustmentResponse);
assertNotNull(adjustmentResponse.getLoanId());
assertNotNull(adjustmentResponse.getClientId());
assertNotNull(adjustmentResponse.getOfficeId());
assertEquals(loanId, adjustmentResponse.getLoanId());
// Verify loan details show both transactions
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails);
List<GetLoansLoanIdTransactions> transactions = loanDetails.getTransactions();
assertTrue(transactions.size() >= 3); // Disbursement, Buy Down Fee, Buy Down Fee Adjustment
// Find the buy down fee adjustment transaction
GetLoansLoanIdTransactions adjustmentTransaction = transactions.stream()
.filter(txn -> "Buy Down Fee Adjustment".equals(txn.getType().getValue())).findFirst().orElse(null);
assertNotNull(adjustmentTransaction);
assertEquals(0, BigDecimal.valueOf(100.0).compareTo(adjustmentTransaction.getAmount()));
assertEquals("06 September 2024", adjustmentTransaction.getDate().format(DateTimeFormatter.ofPattern("dd MMMM yyyy")));
});
}
@Test
public void testBuyDownFeeAdjustmentValidations() {
runAt("08 September 2024", () -> {
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 300.0, "08 September 2024");
assertNotNull(buyDownFeeTransactionId);
// Test 1: Adjustment amount more than original amount (should fail)
try {
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId, "08 September 2024", 400.0);
assertTrue(false, "Expected validation error for adjustment amount exceeding original amount");
} catch (Exception e) {
log.info("Expected validation error for excessive adjustment amount: {}", e.getMessage());
assertTrue(e.getMessage().contains("amount") || e.getMessage().contains("exceed"));
}
// Test 2: Adjustment date before original transaction date (should fail)
try {
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId, "07 September 2024", 100.0);
assertTrue(false, "Expected validation error for adjustment date before original transaction date");
} catch (Exception e) {
log.info("Expected validation error for early adjustment date: {}", e.getMessage());
assertTrue(e.getMessage().contains("date") || e.getMessage().contains("before"));
}
// Test 3: Valid adjustment should succeed
PostLoansLoanIdTransactionsResponse validAdjustment = loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionId, "08 September 2024", 150.0);
assertNotNull(validAdjustment);
// Test 4: Second adjustment that would exceed total should fail
try {
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId, "08 September 2024", 200.0);
assertTrue(false, "Expected validation error for total adjustments exceeding original amount");
} catch (Exception e) {
log.info("Expected validation error for cumulative adjustment excess: {}", e.getMessage());
assertTrue(e.getMessage().contains("amount") || e.getMessage().contains("exceed"));
}
});
}
@Test
public void testBuyDownFeeAdjustmentAccountingEntries() {
runAt("10 September 2024", () -> {
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 400.0, "10 September 2024");
assertNotNull(buyDownFeeTransactionId);
// Verify initial buy down fee accounting entries
verifyTRJournalEntries(buyDownFeeTransactionId, debit(buyDownExpenseAccount, 400.0),
credit(deferredIncomeLiabilityAccount, 400.0));
// Create buy down fee adjustment
PostLoansLoanIdTransactionsResponse adjustmentResponse = loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionId, "10 September 2024", 120.0);
assertNotNull(adjustmentResponse);
});
}
@Test
public void testMultipleBuyDownFeeAdjustments() {
runAt("12 September 2024", () -> {
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 600.0, "12 September 2024");
assertNotNull(buyDownFeeTransactionId);
// First adjustment
PostLoansLoanIdTransactionsResponse adjustment1 = loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId,
"12 September 2024", 100.0);
assertNotNull(adjustment1);
// Second adjustment
PostLoansLoanIdTransactionsResponse adjustment2 = loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId,
"12 September 2024", 150.0);
assertNotNull(adjustment2);
// Verify both adjustments are recorded
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
assertNotNull(loanDetails);
List<GetLoansLoanIdTransactions> adjustmentTransactions = loanDetails.getTransactions().stream()
.filter(txn -> "Buy Down Fee Adjustment".equals(txn.getType().getValue())).toList();
assertEquals(2, adjustmentTransactions.size());
// Verify total adjustment amounts
BigDecimal totalAdjustments = adjustmentTransactions.stream().map(GetLoansLoanIdTransactions::getAmount).reduce(BigDecimal.ZERO,
BigDecimal::add);
assertEquals(0, BigDecimal.valueOf(250.0).compareTo(totalAdjustments));
// Third adjustment that would exceed limit should fail
try {
loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionId, "12 September 2024", 400.0);
assertTrue(false, "Expected validation error for total adjustments exceeding original amount");
} catch (Exception e) {
log.info("Expected validation error for cumulative adjustments exceeding limit: {}", e.getMessage());
}
});
}
@Test
public void testBuyDownFeeAdjustmentWithExternalId() {
runAt("16 September 2024", () -> {
// Add initial buy down fee
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 350.0, "16 September 2024");
assertNotNull(buyDownFeeTransactionId);
// Create adjustment with external ID
String adjustmentExternalId = UUID.randomUUID().toString();
PostLoansLoanIdTransactionsResponse adjustmentResponse = loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionId,
new PostLoansLoanIdTransactionsTransactionIdRequest().transactionDate("16 September 2024").transactionAmount(80.0)
.externalId(adjustmentExternalId).note("Buy Down Fee Adjustment with external ID").dateFormat("dd MMMM yyyy")
.locale("en"));
assertNotNull(adjustmentResponse);
assertEquals(adjustmentExternalId, adjustmentResponse.getResourceExternalId());
// Verify adjustment transaction details
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
GetLoansLoanIdTransactions adjustmentTransaction = loanDetails.getTransactions().stream()
.filter(txn -> adjustmentExternalId.equals(txn.getExternalId())).findFirst().orElse(null);
assertNotNull(adjustmentTransaction);
assertEquals(0, BigDecimal.valueOf(80.0).compareTo(adjustmentTransaction.getAmount()));
});
}
/**
* Helper method to add buy down fee for a loan
*
@@ -636,6 +636,16 @@ public class ExternalEventConfigurationHelper {
loanTransactionUndoContractTerminationBusinessEvent.put("enabled", false);
defaults.add(loanTransactionUndoContractTerminationBusinessEvent);
Map<String, Object> loanBuyDownFeeTransactionCreatedBusinessEvent = new HashMap<>();
loanBuyDownFeeTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeTransactionCreatedBusinessEvent");
loanBuyDownFeeTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanBuyDownFeeTransactionCreatedBusinessEvent);
Map<String, Object> loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent = new HashMap<>();
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent);
return defaults;
}
@@ -963,6 +963,36 @@ public class LoanTransactionHelper {
.transactionAmount(amount).transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en"));
}
public PostLoansLoanIdTransactionsResponse buyDownFeeAdjustment(final Long loanId, final Long buyDownFeeTransactionId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction(loanId, buyDownFeeTransactionId,
request, "buyDownFeeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse buyDownFeeAdjustment(final String loanExternalId, final Long transactionId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId,
request, "buyDownFeeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse buyDownFeeAdjustment(final String loanExternalId, final String transactionExternalId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction3(loanExternalId,
transactionExternalId, request, "buyDownFeeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse buyDownFeeAdjustment(final Long loanId, final String transactionExternalId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId,
request, "buyDownFeeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse buyDownFeeAdjustment(final Long loanId, final Long buyDownFeeTransactionId,
final String transactionDate, final double amount) {
return buyDownFeeAdjustment(loanId, buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest()
.transactionAmount(amount).transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en"));
}
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)