You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2025/07/07 13:29:33 UTC
(fineract) branch develop updated: FINERACT-2311: Add buydown fees adjustments
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new b2bce4a45d FINERACT-2311: Add buydown fees adjustments
b2bce4a45d is described below
commit b2bce4a45df3b0c56a43c241e49b93e37b43f3f6
Author: Attila Budai <bu...@gmail.com>
AuthorDate: Sun Jul 6 10:31:06 2025 +0200
FINERACT-2311: Add buydown fees adjustments
---
.../commands/service/CommandWrapperBuilder.java | 9 ++
...eAdjustmentTransactionCreatedBusinessEvent.java | 20 ++-
...nBuyDownFeeTransactionCreatedBusinessEvent.java | 20 ++-
.../loanaccount/domain/LoanTransaction.java | 6 +
.../BuyDownFeeAdjustmentCommandHandler.java | 53 +++++++
.../LoanBuyDownFeesBalanceRepository.java | 25 +++-
.../service/BuyDownFeePlatformService.java | 3 +
.../BuyDownFeeWritePlatformServiceImpl.java | 104 ++++++++++++-
.../ProgressiveLoanTransactionValidator.java | 2 +
.../ProgressiveLoanTransactionValidatorImpl.java | 96 ++++++++++++
.../ProgressiveLoanAccountConfiguration.java | 5 +-
.../AccrualBasedAccountingProcessorForLoan.java | 26 ++++
.../api/LoanTransactionsApiResource.java | 2 +
.../starter/LoanAccountConfiguration.java | 7 +-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../parts/0189_add_loan_buydown_fee_event.xml | 35 +++++
...nalEventConfigurationValidationServiceTest.java | 6 +-
.../integrationtests/LoanBuyDownFeeTest.java | 164 +++++++++++++++++++++
.../common/ExternalEventConfigurationHelper.java | 10 ++
.../common/loans/LoanTransactionHelper.java | 30 ++++
20 files changed, 602 insertions(+), 22 deletions(-)
diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index bbae94c105..a3f2c14c4b 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3841,6 +3841,15 @@ public class CommandWrapperBuilder {
return this;
}
+ public CommandWrapperBuilder buyDownFeeAdjustment(final Long loanId, final Long transactionId) {
+ this.actionName = "BUYDOWNFEEADJUSTMENT";
+ this.entityName = "LOAN";
+ this.entityId = transactionId;
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/transactions/" + transactionId;
+ return this;
+ }
+
public CommandWrapperBuilder applyContractTermination(final Long loanId) {
this.actionName = "CONTRACT_TERMINATION";
this.entityName = "LOAN";
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.java
similarity index 59%
copy from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
copy to fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.java
index 8ef5620ab6..ee91f4b17b 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.java
@@ -16,14 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.service;
+package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.springframework.transaction.annotation.Transactional;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
-public interface BuyDownFeePlatformService {
+public class LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
- @Transactional
- CommandProcessingResult makeLoanBuyDownFee(Long loanId, JsonCommand command);
+ private static final String TYPE = "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent";
+
+ public LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeTransactionCreatedBusinessEvent.java
similarity index 60%
copy from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
copy to fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeTransactionCreatedBusinessEvent.java
index 8ef5620ab6..ae9c7497df 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeTransactionCreatedBusinessEvent.java
@@ -16,14 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.service;
+package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.springframework.transaction.annotation.Transactional;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
-public interface BuyDownFeePlatformService {
+public class LoanBuyDownFeeTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
- @Transactional
- CommandProcessingResult makeLoanBuyDownFee(Long loanId, JsonCommand command);
+ private static final String TYPE = "LoanBuyDownFeeTransactionCreatedBusinessEvent";
+
+ public LoanBuyDownFeeTransactionCreatedBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 6c59aa39bb..9a1c48dd14 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -332,6 +332,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
amount.getAmount(), amount.getAmount(), null, null, null, null, false, paymentDetail, externalId);
}
+ public static LoanTransaction buyDownFeeAdjustment(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
+ final LocalDate transactionDate, final ExternalId externalId) {
+ return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT, transactionDate, amount.getAmount(),
+ amount.getAmount(), null, null, null, null, false, paymentDetail, externalId);
+ }
+
public static LoanTransaction capitalizedIncomeAmortizationAdjustment(final Loan loan, final Money amount,
final LocalDate transactionDate, final ExternalId externalId) {
return switch (loan.getLoanProductRelatedDetail().getCapitalizedIncomeType()) {
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/BuyDownFeeAdjustmentCommandHandler.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/BuyDownFeeAdjustmentCommandHandler.java
new file mode 100644
index 0000000000..27f5c937a9
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/BuyDownFeeAdjustmentCommandHandler.java
@@ -0,0 +1,53 @@
+/**
+ * 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.portfolio.loanaccount.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "BUYDOWNFEEADJUSTMENT")
+public class BuyDownFeeAdjustmentCommandHandler implements NewCommandSourceHandler {
+
+ private final BuyDownFeePlatformService buyDownFeePlatformService;
+ private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
+
+ @Transactional
+ @Override
+ public CommandProcessingResult processCommand(final JsonCommand command) {
+
+ try {
+ return this.buyDownFeePlatformService.buyDownFeeAdjustment(command.getLoanId(), command.entityId(), command);
+ } catch (final JpaSystemException | DataIntegrityViolationException dve) {
+ dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.buy.down.fee",
+ "Buy Down Fee");
+ return CommandProcessingResult.empty();
+ }
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeesBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeesBalanceRepository.java
index 5c160ece45..88dceec5d8 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeesBalanceRepository.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeesBalanceRepository.java
@@ -18,9 +18,32 @@
*/
package org.apache.fineract.portfolio.loanaccount.repository;
+import java.math.BigDecimal;
+import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
public interface LoanBuyDownFeesBalanceRepository
- extends JpaRepository<LoanBuyDownFeeBalance, Long>, JpaSpecificationExecutor<LoanBuyDownFeeBalance> {}
+ extends JpaRepository<LoanBuyDownFeeBalance, Long>, JpaSpecificationExecutor<LoanBuyDownFeeBalance> {
+
+ String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lbfb.loanTransaction.id, lbfb.loan.id, lbfb.loanTransaction.dateOf, lbfb.loanTransaction.reversed, lbfb.amount, lbfb.unrecognizedAmount, lbfb.loanTransaction.feeChargesPortion) FROM LoanBuyDownFeeBalance lbfb ";
+
+ List<LoanBuyDownFeeBalance> findAllByLoanId(Long loanId);
+
+ LoanBuyDownFeeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId);
+
+ @Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lbfb.loan.id = :loanId")
+ List<LoanTransactionRepaymentPeriodData> findRepaymentPeriodDataByLoanId(Long loanId);
+
+ @Query("SELECT SUM(lbfb.amount) FROM LoanBuyDownFeeBalance lbfb WHERE lbfb.loan.id = :loanId")
+ BigDecimal calculateBuydownFeeAmount(Long loanId);
+
+ @Query("SELECT SUM(lbfb.amountAdjustment) FROM LoanBuyDownFeeBalance lbfb WHERE lbfb.loan.id = :loanId")
+ BigDecimal calculateBuydownFeeAdjustment(Long loanId);
+
+ @Query("SELECT lbfb FROM LoanBuyDownFeeBalance lbfb, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lbfb.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lbfb.loanTransaction.id = lt.id")
+ LoanBuyDownFeeBalance findBalanceForAdjustment(Long transactionId);
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
index 8ef5620ab6..69180af13f 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeePlatformService.java
@@ -26,4 +26,7 @@ public interface BuyDownFeePlatformService {
@Transactional
CommandProcessingResult makeLoanBuyDownFee(Long loanId, JsonCommand command);
+
+ @Transactional
+ CommandProcessingResult buyDownFeeAdjustment(Long loanId, Long buyDownFeeTransactionId, JsonCommand command);
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java
index b54c18de23..902b0a46d1 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -31,7 +32,13 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionCreatedBusinessEvent;
+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;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
@@ -39,7 +46,10 @@ import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
@@ -59,6 +69,10 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
private final NoteWritePlatformService noteWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository;
+ private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
+ private final LoanBalanceService loanBalanceService;
+ private final LoanLifecycleStateMachine loanLifecycleStateMachine;
+ private final BusinessEventNotifierService businessEventNotifierService;
@Transactional
@Override
@@ -92,7 +106,7 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Save transaction
loanTransactionRepository.saveAndFlush(buyDownFeeTransaction);
- // Create Buy Down Fee balances
+ // Create Buy Down Fee balance
createBuyDownFeeBalance(buyDownFeeTransaction);
// Update loan derived fields
@@ -107,6 +121,10 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Post journal entries
loanJournalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
+ // Notify business events
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanBuyDownFeeTransactionCreatedBusinessEvent(buyDownFeeTransaction));
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
+
return new CommandProcessingResultBuilder() //
.withClientId(loan.getClientId()) //
.withOfficeId(loan.getOfficeId()) //
@@ -116,6 +134,90 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
.build();
}
+ @Override
+ @Transactional
+ public CommandProcessingResult buyDownFeeAdjustment(final Long loanId, final Long buyDownFeeTransactionId, final JsonCommand command) {
+ this.loanTransactionValidator.validateBuyDownFeeAdjustment(command, loanId, buyDownFeeTransactionId);
+ final Loan loan = this.loanAssembler.assembleFrom(loanId);
+ checkClientOrGroupActive(loan);
+
+ final List<Long> existingTransactionIds = new ArrayList<>(loanTransactionRepository.findTransactionIdsByLoan(loan));
+ final List<Long> existingReversedTransactionIds = new ArrayList<>(loanTransactionRepository.findReversedTransactionIdsByLoan(loan));
+ final Map<String, Object> changes = new LinkedHashMap<>();
+
+ // Create payment details
+ final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
+
+ // Extract transaction details
+ final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
+ final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
+ final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId");
+
+ // Find and validate original buy down fee transaction
+ Optional<LoanTransaction> originalBuyDownFee = loanTransactionRepository.findById(buyDownFeeTransactionId);
+ if (originalBuyDownFee.isEmpty() || !originalBuyDownFee.get().isBuyDownFee()) {
+ throw new IllegalArgumentException("Original transaction must be a valid Buy Down Fee transaction");
+ }
+
+ // Create buy down fee adjustment transaction
+ LoanTransaction buyDownFeeAdjustment = LoanTransaction.buyDownFeeAdjustment(loan, Money.of(loan.getCurrency(), transactionAmount),
+ paymentDetail, transactionDate, txnExternalId);
+
+ // Link to original transaction
+ buyDownFeeAdjustment.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(buyDownFeeAdjustment,
+ originalBuyDownFee.get(), LoanTransactionRelationTypeEnum.ADJUSTMENT));
+
+ // Add transaction to loan
+ loan.addLoanTransaction(buyDownFeeAdjustment);
+
+ // Recalculate loan transactions
+ recalculateLoanTransactions(loan, transactionDate, buyDownFeeAdjustment);
+
+ // Save transaction
+ LoanTransaction savedBuyDownFeeAdjustment = loanTransactionRepository.saveAndFlush(buyDownFeeAdjustment);
+
+ // Update buy down fee balance
+ LoanBuyDownFeeBalance buydownFeeBalance = loanBuyDownFeesBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
+ buyDownFeeTransactionId);
+ if (buydownFeeBalance != null) {
+ buydownFeeBalance.setAmountAdjustment(MathUtil.nullToZero(buydownFeeBalance.getAmountAdjustment()).add(transactionAmount));
+ buydownFeeBalance
+ .setUnrecognizedAmount(MathUtil.negativeToZero(buydownFeeBalance.getUnrecognizedAmount().subtract(transactionAmount)));
+ loanBuyDownFeesBalanceRepository.save(buydownFeeBalance);
+ }
+
+ // Update outstanding loan balances
+ loanBalanceService.updateLoanOutstandingBalances(loan);
+
+ // Create a note if provided
+ final String noteText = command.stringValueOfParameterNamed("note");
+ if (StringUtils.isNotBlank(noteText)) {
+ noteWritePlatformService.createLoanTransactionNote(savedBuyDownFeeAdjustment.getId(), noteText);
+ }
+ // Post journal entries
+ loanJournalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
+
+ // Determine loan lifecycle transition
+ loanLifecycleStateMachine.determineAndTransition(loan, transactionDate);
+
+ // Notify business events
+ businessEventNotifierService
+ .notifyPostBusinessEvent(new LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent(savedBuyDownFeeAdjustment));
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
+
+ return new CommandProcessingResultBuilder().withEntityId(savedBuyDownFeeAdjustment.getId())
+ .withEntityExternalId(savedBuyDownFeeAdjustment.getExternalId()).withOfficeId(loan.getOfficeId())
+ .withClientId(loan.getClientId()).withLoanId(loan.getId()).build();
+ }
+
+ private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) {
+ if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
+ reprocessLoanTransactionsService.reprocessTransactions(loan);
+ } else {
+ reprocessLoanTransactionsService.processLatestTransaction(transaction, loan);
+ }
+ }
+
private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null && client.isNotActive()) {
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
index 825a6e85fd..553c6adff6 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
@@ -30,4 +30,6 @@ public interface ProgressiveLoanTransactionValidator extends LoanTransactionVali
void validateContractTerminationUndo(JsonCommand command, Long loanId);
void validateBuyDownFee(JsonCommand command, Long loanId);
+
+ void validateBuyDownFeeAdjustment(JsonCommand command, Long loanId, Long buyDownFeeTransactionId);
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
index 73480a7a44..46bf83fdd1 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
@@ -48,6 +48,7 @@ import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
@@ -57,6 +58,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionProcessingException;
+import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
@@ -69,6 +71,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
private final LoanTransactionValidator loanTransactionValidator;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
+ private final LoanBuyDownFeesBalanceRepository loanBuydownFeeBalanceRepository;
private final LoanTransactionRepository loanTransactionRepository;
@Override
@@ -320,6 +323,94 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
}
}
+ @Override
+ public void validateBuyDownFeeAdjustment(JsonCommand command, Long loanId, Long buyDownFeeTransactionId) {
+ final String json = command.json();
+ if (StringUtils.isBlank(json)) {
+ throw new InvalidJsonException();
+ }
+
+ final JsonElement element = this.fromApiJsonHelper.parse(json);
+ final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
+ this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getBuyDownFeeAdjustmentParameters());
+
+ Validator.validateOrThrow("loan.buyDownFeeAdjustment", baseDataValidator -> {
+ final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
+ validateLoanClientIsActive(loan);
+ validateLoanGroupIsActive(loan);
+
+ // Validate loan is progressive
+ if (!loan.isProgressiveSchedule()) {
+ baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.progressive.loan");
+ }
+
+ // Validate buy down fee is enabled
+ if (!loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) {
+ baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("buy.down.fee.not.enabled");
+ }
+
+ // Validate loan is active, or closed or overpaid
+ final LoanStatus loanStatus = loan.getStatus();
+ if (!loanStatus.isActive() && !loanStatus.isClosed() && !loanStatus.isOverpaid()) {
+ baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.valid.loan.status");
+ }
+
+ final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
+ baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
+
+ // Validate transaction date is not before disbursement date
+ if (transactionDate != null && loan.getDisbursementDate() != null && transactionDate.isBefore(loan.getDisbursementDate())) {
+ baseDataValidator.reset().parameter("transactionDate").failWithCode("before.disbursement.date",
+ "Transaction date cannot be before disbursement date");
+ }
+
+ // Validate transaction date is not in the future
+ if (transactionDate != null && transactionDate.isAfter(DateUtils.getBusinessLocalDate())) {
+ baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.in.the.future",
+ "Transaction date cannot be in the future");
+ }
+
+ final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
+ baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
+
+ Optional<LoanTransaction> buyDownFeeTransactionOpt = loanTransactionRepository.findById(buyDownFeeTransactionId);
+ if (buyDownFeeTransactionOpt.isEmpty()) {
+ baseDataValidator.reset().parameter("buyDownFeeTransactionId").failWithCode("loan.transaction.not.found",
+ "Buy Down Fee transaction not found.");
+ } else {
+ // Validate that the transaction is actually a buy down fee transaction
+ if (!buyDownFeeTransactionOpt.get().isBuyDownFee()) {
+ baseDataValidator.reset().parameter("buyDownFeeTransactionId").failWithCode("not.buyDownFee.transaction",
+ "The specified transaction is not a Buy Down Fee transaction.");
+ }
+ // Validate not before buy down fee transaction
+ if (transactionDate != null && transactionDate.isBefore(buyDownFeeTransactionOpt.get().getTransactionDate())) {
+ baseDataValidator.reset().parameter("transactionDate").failWithCode("before.buyDownFee.transaction.date",
+ "Transaction date cannot be before buy down fee transaction date");
+
+ }
+ if (transactionAmount != null) {
+ LoanBuyDownFeeBalance buydownFeeBalance = loanBuydownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
+ buyDownFeeTransactionId);
+ if (buydownFeeBalance == null) {
+ baseDataValidator.reset().parameter("buyDownFeeTransactionId").failWithCode("buydown.fee.balance.not.found",
+ "Buy down fee balance not found for the specified transaction.");
+ } else if (MathUtil.isLessThan(
+ buydownFeeBalance.getAmount().subtract(MathUtil.nullToZero(buydownFeeBalance.getAmountAdjustment())),
+ transactionAmount)) {
+ baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).failWithCode(
+ "cannot.be.more.than.remaining.amount",
+ " Buy down fee adjustment amount cannot be more than remaining amount");
+ }
+ }
+ }
+
+ validatePaymentDetails(baseDataValidator, element);
+ validateNote(baseDataValidator, element);
+ validateExternalId(baseDataValidator, element);
+ });
+ }
+
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
@@ -495,6 +586,11 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
return new HashSet<>(Arrays.asList("note", "reversalExternalId"));
}
+ private Set<String> getBuyDownFeeAdjustmentParameters() {
+ return new HashSet<>(
+ Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
+ }
+
private BigDecimal getOverAppliedMax(final Loan loan) {
final LoanProduct loanProduct = loan.getLoanProduct();
if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) {
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
index 8eb7fbad22..6e6665ed6c 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
@@ -24,6 +24,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceService;
@@ -65,9 +66,9 @@ public class ProgressiveLoanAccountConfiguration {
public ProgressiveLoanTransactionValidator progressiveLoanTransactionValidator(FromJsonHelper fromApiJsonHelper,
LoanTransactionValidator loanTransactionValidator, LoanRepositoryWrapper loanRepositoryWrapper,
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
- LoanTransactionRepository loanTransactionRepository) {
+ LoanBuyDownFeesBalanceRepository loanBuydownFeesBalanceRepository, LoanTransactionRepository loanTransactionRepository) {
return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper,
- loanCapitalizedIncomeBalanceRepository, loanTransactionRepository);
+ loanCapitalizedIncomeBalanceRepository, loanBuydownFeesBalanceRepository, loanTransactionRepository);
}
@Bean
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 5397617ab9..9d5fbef252 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -149,6 +149,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
if (transactionType.isBuyDownFee()) {
createJournalEntriesForBuyDownFee(loanDTO, loanTransactionDTO, office);
}
+ // Handle Buy Down Fee Adjustment
+ if (transactionType.isBuyDownFeeAdjustment()) {
+ createJournalEntriesForBuyDownFeeAdjustment(loanDTO, loanTransactionDTO, office);
+ }
}
}
@@ -436,6 +440,28 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
+ private void createJournalEntriesForBuyDownFeeAdjustment(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+ final Office office) {
+ // loan properties
+ final Long loanProductId = loanDTO.getLoanProductId();
+ final Long loanId = loanDTO.getLoanId();
+ final String currencyCode = loanDTO.getCurrencyCode();
+
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
+ final BigDecimal amount = loanTransactionDTO.getAmount();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+
+ if (MathUtil.isGreaterThanZero(amount)) {
+ // Mirror of Buy Down Fee entries (as per PS-2574 requirements)
+ // Debit: Deferred Income Liability, Credit: Buy Down Expense
+ this.helper.createJournalEntriesForLoan(office, currencyCode, AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
+ AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(), loanProductId, paymentTypeId, loanId, transactionId,
+ transactionDate, amount);
+ }
+ }
+
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index 60441e2a78..e5f503de9c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -710,6 +710,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.chargebackTransaction(resolvedLoanId, resolvedTransactionId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
commandRequest = builder.capitalizedIncomeAdjustment(resolvedLoanId, resolvedTransactionId).build();
+ } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_ADJUSTMENT_COMMAND)) {
+ commandRequest = builder.buyDownFeeAdjustment(resolvedLoanId, resolvedTransactionId).build();
} else { // Default to adjust the Loan Transaction
commandRequest = builder.adjustTransaction(resolvedLoanId, resolvedTransactionId).build();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index 7e1a619d5a..1f94fa781c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -383,10 +383,13 @@ public class LoanAccountConfiguration {
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
- LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository) {
+ LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository,
+ ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
+ LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory,
- loanBuyDownFeesBalanceRepository);
+ loanBuyDownFeesBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine,
+ businessEventNotifierService);
}
@Bean
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 28ddd5e822..4307ea39ee 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -207,4 +207,5 @@
<include file="parts/0186_add_buy_down_fee_to_loan_product.xml" relativeToChangelogFile="true" />
<include file="parts/0187_add_Loan_modified_and_withdrawn_event_configuration.xml" relativeToChangelogFile="true" />
<include file="parts/0188_create_loan_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
+ <include file="parts/0189_add_loan_buydown_fee_event.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0189_add_loan_buydown_fee_event.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0189_add_loan_buydown_fee_event.xml
new file mode 100644
index 0000000000..b21f786905
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0189_add_loan_buydown_fee_event.xml
@@ -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="1" author="fineract">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanBuyDownFeeTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index 99f0284a66..9ac262fcab 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -109,7 +109,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
- "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
+ "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
+ "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -200,7 +201,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
- "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
+ "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
+ "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java
index 4ce6ed3085..786e207831 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java
@@ -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
*
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 4e3819e476..a8c4300cb5 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -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;
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 9b060bdd6c..bec059a9f3 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -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)