FINERACT-2311: Add Buy Down Fees transaction support

This commit is contained in:
Jose Alberto Hernandez
2025-07-08 13:50:24 +02:00
committed by Adam Saghy
parent b8d6dfa794
commit 53108227ae
8 changed files with 74 additions and 20 deletions
@@ -70,10 +70,8 @@ public enum LoanTransactionType {
CAPITALIZED_INCOME(35, "loanTransactionType.capitalizedIncome"), // CAPITALIZED_INCOME(35, "loanTransactionType.capitalizedIncome"), //
CAPITALIZED_INCOME_AMORTIZATION(36, "loanTransactionType.capitalizedIncomeAmortization"), // CAPITALIZED_INCOME_AMORTIZATION(36, "loanTransactionType.capitalizedIncomeAmortization"), //
CAPITALIZED_INCOME_ADJUSTMENT(37, "loanTransactionType.capitalizedIncomeAdjustment"), // CAPITALIZED_INCOME_ADJUSTMENT(37, "loanTransactionType.capitalizedIncomeAdjustment"), //
CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT(39, "loanTransactionType.capitalizedIncomeAmortizationAdjustment"), //
// Kind of Final Transactions
CONTRACT_TERMINATION(38, "loanTransactionType.contractTermination"), // CONTRACT_TERMINATION(38, "loanTransactionType.contractTermination"), //
CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT(39, "loanTransactionType.capitalizedIncomeAmortizationAdjustment"), //
BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), // BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), //
BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), // BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), //
BUY_DOWN_FEE_AMORTIZATION(42, "loanTransactionType.buyDownFeeAmortization"), // BUY_DOWN_FEE_AMORTIZATION(42, "loanTransactionType.buyDownFeeAmortization"), //
@@ -61,4 +61,5 @@ public class LoanBuyDownFeeBalance extends AbstractAuditableWithUTCDateTimeCusto
@Column(name = "amount_adjustment", scale = 6, precision = 19) @Column(name = "amount_adjustment", scale = 6, precision = 19)
private BigDecimal amountAdjustment; private BigDecimal amountAdjustment;
} }
@@ -689,7 +689,8 @@ public class LoanTransactionsApiResource {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId, transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId); LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId);
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) { } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId); transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId, LoanTransactionType.BUY_DOWN_FEE,
transactionId);
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_ADJUSTMENT_COMMAND)) { } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_ADJUSTMENT_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId, transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT, transactionId); LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT, transactionId);
@@ -33,7 +33,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository; import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.util.BuyDownFeeAmortizationUtil; import org.apache.fineract.portfolio.loanaccount.util.BuyDownFeeAmortizationUtil;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -44,7 +44,7 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyDownFeeAmortizationProcessingService { public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyDownFeeAmortizationProcessingService {
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository; private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanJournalEntryPoster journalEntryPoster; private final LoanJournalEntryPoster journalEntryPoster;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
@@ -55,7 +55,7 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan); final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan); final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
List<LoanBuyDownFeeBalance> balances = loanBuyDownFeesBalanceRepository.findAllByLoanId(loan.getId()); List<LoanBuyDownFeeBalance> balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId());
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate()
: getFinalBuyDownFeeAmortizationTransactionDate(loan); : getFinalBuyDownFeeAmortizationTransactionDate(loan);
@@ -75,7 +75,7 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD
.subtract(amortizationTillDate.getAmount())); .subtract(amortizationTillDate.getAmount()));
} }
loanBuyDownFeesBalanceRepository.saveAll(balances); loanBuyDownFeeBalanceRepository.saveAll(balances);
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan); BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan);
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized); BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
@@ -484,16 +484,41 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
switch (transactionType) { switch (transactionType) {
case CAPITALIZED_INCOME: case CAPITALIZED_INCOME:
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
BigDecimal capitalizedIncomeBalance = BigDecimal.ZERO;
BigDecimal buyDownFeeBalance = BigDecimal.ZERO;
if (loan.getLoanProduct().getLoanProductRelatedDetail().isEnableIncomeCapitalization()) { if (loan.getLoanProduct().getLoanProductRelatedDetail().isEnableIncomeCapitalization()) {
final BigDecimal capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream() capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream()
.map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); .map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
transactionAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()).subtract(capitalizedIncomeBalance);
} }
// Calculate Buy Down Fee balance to subtract from Capitalized Income formula
buyDownFeeBalance = loanBuyDownFeeBalanceRepository.findAllByLoanId(loanId).stream().map(LoanBuyDownFeeBalance::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
transactionAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()).subtract(capitalizedIncomeBalance)
.subtract(buyDownFeeBalance);
paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate( loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount, LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId)); paymentOptions, retriveLoanCurrencyData(loanId));
break;
case BUY_DOWN_FEE:
final Loan buyDownLoan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
BigDecimal buyDownFeeBalanceForCalc = BigDecimal.ZERO;
BigDecimal capitalizedIncomeBalanceForCalc = BigDecimal.ZERO;
// Calculate existing Buy Down Fee balance
buyDownFeeBalanceForCalc = loanBuyDownFeeBalanceRepository.findAllByLoanId(loanId).stream()
.map(LoanBuyDownFeeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
// Calculate Capitalized Income balance to subtract from Buy Down Fee formula
if (buyDownLoan.getLoanProduct().getLoanProductRelatedDetail().isEnableIncomeCapitalization()) {
capitalizedIncomeBalanceForCalc = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream()
.map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
}
transactionAmount = buyDownLoan.getApprovedPrincipal().subtract(buyDownLoan.getDisbursedAmount())
.subtract(buyDownFeeBalanceForCalc).subtract(capitalizedIncomeBalanceForCalc);
paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId));
break; break;
case CAPITALIZED_INCOME_ADJUSTMENT: case CAPITALIZED_INCOME_ADJUSTMENT:
final LoanCapitalizedIncomeBalance loanCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository final LoanCapitalizedIncomeBalance loanCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
@@ -567,10 +567,10 @@ public class LoanAccountConfiguration {
@ConditionalOnMissingBean(LoanBuyDownFeeAmortizationProcessingService.class) @ConditionalOnMissingBean(LoanBuyDownFeeAmortizationProcessingService.class)
public LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService( public LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService(
final LoanTransactionRepository loanTransactionRepository, final LoanTransactionRepository loanTransactionRepository,
final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository, final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster, final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster,
final ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory) {
return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeesBalanceRepository, return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeeBalanceRepository,
businessEventNotifierService, journalEntryPoster, externalIdFactory); businessEventNotifierService, journalEntryPoster, externalIdFactory);
} }
} }
@@ -208,4 +208,33 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
}); });
} }
@Test
public void testGetLoanTransactionTemplateForBuyDownFee() {
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));
final String loanExternalIdStr = UUID.randomUUID().toString();
runAt("20 December 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024",
430.0, 7.0, 6, (request) -> request.externalId(loanExternalIdStr));
disburseLoan(loanId, BigDecimal.valueOf(230), "20 December 2024");
final GetLoansLoanIdTransactionsTemplateResponse transactionTemplate = loanTransactionHelper.retrieveTransactionTemplate(loanId,
buyDownFeeCommand, null, null, null);
assertNotNull(transactionTemplate);
assertEquals("loanTransactionType." + buyDownFeeCommand, transactionTemplate.getType().getCode());
assertEquals(transactionTemplate.getAmount(), 200);
assertThat(transactionTemplate.getPaymentTypeOptions().size() > 0);
});
}
} }
@@ -636,15 +636,15 @@ public class ExternalEventConfigurationHelper {
loanTransactionUndoContractTerminationBusinessEvent.put("enabled", false); loanTransactionUndoContractTerminationBusinessEvent.put("enabled", false);
defaults.add(loanTransactionUndoContractTerminationBusinessEvent); defaults.add(loanTransactionUndoContractTerminationBusinessEvent);
Map<String, Object> loanBuyDownFeeTransactionCreatedBusinessEvent = new HashMap<>(); Map<String, Object> loanTransactionBuyDownFeePostBusinessEvent = new HashMap<>();
loanBuyDownFeeTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeTransactionCreatedBusinessEvent"); loanTransactionBuyDownFeePostBusinessEvent.put("type", "LoanBuyDownFeeTransactionCreatedBusinessEvent");
loanBuyDownFeeTransactionCreatedBusinessEvent.put("enabled", false); loanTransactionBuyDownFeePostBusinessEvent.put("enabled", false);
defaults.add(loanBuyDownFeeTransactionCreatedBusinessEvent); defaults.add(loanTransactionBuyDownFeePostBusinessEvent);
Map<String, Object> loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent = new HashMap<>(); Map<String, Object> loanTransactionBuyDownFeeAdjustmentPostBusinessEvent = new HashMap<>();
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent"); loanTransactionBuyDownFeeAdjustmentPostBusinessEvent.put("type", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false); loanTransactionBuyDownFeeAdjustmentPostBusinessEvent.put("enabled", false);
defaults.add(loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent); defaults.add(loanTransactionBuyDownFeeAdjustmentPostBusinessEvent);
Map<String, Object> loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent = new HashMap<>(); Map<String, Object> loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent = new HashMap<>();
loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent"); loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent");