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/08 10:38:08 UTC
(fineract) branch develop updated: FINERACT-2311: Buy down fee - daily amortization
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 b8d6dfa794 FINERACT-2311: Buy down fee - daily amortization
b8d6dfa794 is described below
commit b8d6dfa794144b3a0971b8700eca6d6b36700610
Author: adam.magyari <ma...@gmail.com>
AuthorDate: Mon Jul 7 14:58:31 2025 +0200
FINERACT-2311: Buy down fee - daily amortization
---
.../fineract/test/helper/WorkFlowJobHelper.java | 5 +-
...nAdjustmentTransactionCreatedBusinessEvent.java | 35 +++++
...mortizationTransactionCreatedBusinessEvent.java | 35 +++++
.../api/LoanTransactionApiConstants.java | 2 +
.../loanaccount/data/LoanTransactionEnumData.java | 5 +
.../loanaccount/domain/LoanTransaction.java | 24 +++-
.../domain/LoanTransactionRepository.java | 17 ++-
.../loanaccount/domain/LoanTransactionType.java | 4 +
...oanBuyDownFeeAmortizationProcessingService.java | 28 ++++
.../loanproduct/service/LoanEnumerations.java | 6 +
.../AccrualBasedAccountingProcessorForLoan.java | 159 +++++++++++++++++++++
.../loan/BuyDownFeeAmortizationBusinessStep.java | 60 ++++++++
.../api/LoanTransactionsApiResource.java | 2 +
.../loanaccount/api/LoansApiResourceSwagger.java | 8 ++
...uyDownFeeAmortizationProcessingServiceImpl.java | 113 +++++++++++++++
...zedIncomeAmortizationProcessingServiceImpl.java | 8 +-
.../starter/LoanAccountConfiguration.java | 24 ++++
.../util/BuyDownFeeAmortizationUtil.java | 84 +++++++++++
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../parts/0190_buy_down_fee_amortization.xml | 49 +++++++
...nalEventConfigurationValidationServiceTest.java | 8 +-
.../integrationtests/LoanBuyDownFeeTest.java | 148 +++++++++++++++++++
.../common/ExternalEventConfigurationHelper.java | 11 ++
.../common/loans/LoanTransactionHelper.java | 5 +
24 files changed, 829 insertions(+), 12 deletions(-)
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
index fcaf44d892..1a4f2ece3c 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java
@@ -50,8 +50,9 @@ public class WorkFlowJobHelper {
new BusinessStep().stepName("ADD_PERIODIC_ACCRUAL_ENTRIES").order(7L), //
new BusinessStep().stepName("ACCRUAL_ACTIVITY_POSTING").order(8L), //
new BusinessStep().stepName("CAPITALIZED_INCOME_AMORTIZATION").order(9L), //
- new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(10L), //
- new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(11L)//
+ new BusinessStep().stepName("BUY_DOWN_FEE_AMORTIZATION").order(10L), //
+ new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(11L), //
+ new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(12L)//
);
BusinessStepRequest request = new BusinessStepRequest().businessSteps(businessSteps);
Response<Void> response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request)
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.java
new file mode 100644
index 0000000000..c8ef1b4121
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.java
@@ -0,0 +1,35 @@
+/**
+ * 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.infrastructure.event.business.domain.loan.transaction;
+
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
+
+ private static final String TYPE = "LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent";
+
+ public LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(LoanTransaction transaction) {
+ super(transaction);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.java
new file mode 100644
index 0000000000..6abe1a53d8
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.java
@@ -0,0 +1,35 @@
+/**
+ * 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.infrastructure.event.business.domain.loan.transaction;
+
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
+
+ private static final String TYPE = "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent";
+
+ public LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(LoanTransaction transaction) {
+ super(transaction);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
index 51d7df8d18..6617def508 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
@@ -57,5 +57,7 @@ public interface LoanTransactionApiConstants {
capitalizedIncomeAdjustment, //
contractTermination, //
capitalizedIncomeAmortizationAdjustment, //
+ buyDownFeeAmortization, //
+ buyDownFeeAmortizationAdjustment, //
}
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index 9de2b093dd..743143453f 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -74,6 +74,8 @@ public class LoanTransactionEnumData implements Serializable {
private final boolean contractTermination;
private final boolean buyDownFee;
private final boolean buyDownFeeAdjustment;
+ private final boolean buyDownFeeAmortization;
+ private final boolean buyDownFeeAmortizationAdjustment;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -118,6 +120,9 @@ public class LoanTransactionEnumData implements Serializable {
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
this.buyDownFee = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE.getValue()).equals(this.id);
this.buyDownFeeAdjustment = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue()).equals(this.id);
+ this.buyDownFeeAmortization = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION.getValue()).equals(this.id);
+ this.buyDownFeeAmortizationAdjustment = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT.getValue())
+ .equals(this.id);
}
public boolean isRepaymentType() {
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 9a1c48dd14..62c49d807b 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
@@ -348,6 +348,26 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
};
}
+ public static LoanTransaction buyDownFeeAmortization(final Loan loan, final Office office, final LocalDate dateOf,
+ final BigDecimal amount, final ExternalId externalId) {
+ return switch (loan.getLoanProductRelatedDetail().getBuyDownFeeIncomeType()) {
+ case FEE -> new LoanTransaction(loan, office, LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION, dateOf, amount, null, null, amount,
+ null, null, false, null, externalId);
+ case INTEREST -> new LoanTransaction(loan, office, LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION, dateOf, amount, null, amount,
+ null, null, null, false, null, externalId);
+ };
+ }
+
+ public static LoanTransaction buyDownFeeAmortizationAdjustment(final Loan loan, final Money amount, final LocalDate transactionDate,
+ final ExternalId externalId) {
+ return switch (loan.getLoanProductRelatedDetail().getBuyDownFeeIncomeType()) {
+ case FEE -> new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT,
+ transactionDate, amount.getAmount(), null, null, amount.getAmount(), null, null, false, null, externalId);
+ case INTEREST -> new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT,
+ transactionDate, amount.getAmount(), null, amount.getAmount(), null, null, null, false, null, externalId);
+ };
+ }
+
public LoanTransaction copyTransactionPropertiesAndMappings() {
LoanTransaction newTransaction = copyTransactionProperties(this);
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
@@ -806,7 +826,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF
|| type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE
|| type == LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION || type == LoanTransactionType.CONTRACT_TERMINATION
- || type == LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT);
+ || type == LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT
+ || type == LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION
+ || type == LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT);
}
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java
index 3934f0253f..a41b352ea1 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java
@@ -361,16 +361,27 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND (lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION
OR lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT)
""")
- BigDecimal getAmortizedAmount(@Param("loan") Loan loan);
+ BigDecimal getAmortizedAmountCapitalizedIncome(@Param("loan") Loan loan);
+
+ @Query("""
+ SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION THEN lt.amount
+ WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT THEN -lt.amount
+ ELSE 0 END), 0) FROM LoanTransaction lt
+ WHERE lt.loan = :loan
+ AND lt.reversed = false
+ AND (lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION
+ OR lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT)
+ """)
+ BigDecimal getAmortizedAmountBuyDownFee(@Param("loan") Loan loan);
@Query("""
SELECT lt FROM LoanTransaction lt, LoanTransactionRelation ltr
WHERE lt.reversed = false
AND lt = ltr.fromTransaction
- AND ltr.toTransaction = :capitalizedIncome
+ AND ltr.toTransaction = :transaction
AND ltr.relationType = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.ADJUSTMENT
""")
- List<LoanTransaction> findAdjustmentsForCapitalizedIncome(@Param("capitalizedIncome") LoanTransaction capitalizedIncome);
+ List<LoanTransaction> findAdjustments(@Param("transaction") LoanTransaction transaction);
@Query("""
SELECT lt FROM LoanTransaction lt
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index 0e014d0275..41489897c6 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -76,6 +76,8 @@ public enum LoanTransactionType {
BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), //
BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), //
+ BUY_DOWN_FEE_AMORTIZATION(42, "loanTransactionType.buyDownFeeAmortization"), //
+ BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT(43, "loanTransactionType.buyDownFeeAmortizationAdjustment"), //
;
private final Integer value;
@@ -133,6 +135,8 @@ public enum LoanTransactionType {
case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
case 40 -> LoanTransactionType.BUY_DOWN_FEE;
case 41 -> LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT;
+ case 42 -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION;
+ case 43 -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT;
default -> LoanTransactionType.INVALID;
};
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingService.java
new file mode 100644
index 0000000000..8c1cc6a043
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingService.java
@@ -0,0 +1,28 @@
+/**
+ * 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.service;
+
+import java.time.LocalDate;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.lang.NonNull;
+
+public interface LoanBuyDownFeeAmortizationProcessingService {
+
+ void processBuyDownFeeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate, boolean addJournal);
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
index 9d9f0b0b48..6ff0c86c38 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
@@ -342,6 +342,12 @@ public final class LoanEnumerations {
LoanTransactionType.BUY_DOWN_FEE.getCode(), "Buy Down Fee");
case BUY_DOWN_FEE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue().longValue(),
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getCode(), "Buy Down Fee Adjustment");
+ case BUY_DOWN_FEE_AMORTIZATION ->
+ new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION.getValue().longValue(),
+ LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION.getCode(), "Buy Down Fee Amortization");
+ case BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT ->
+ new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT.getValue().longValue(),
+ LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT.getCode(), "Buy Down Fee Amortization Adjustment");
};
}
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 9d5fbef252..481aefdb0b 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
@@ -153,6 +153,14 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
if (transactionType.isBuyDownFeeAdjustment()) {
createJournalEntriesForBuyDownFeeAdjustment(loanDTO, loanTransactionDTO, office);
}
+ // Handle Buy Down Fee Amortization
+ if (transactionType.isBuyDownFeeAmortization()) {
+ createJournalEntriesForBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
+ }
+ // Handle Buy Down Fee Amortization Adjustment
+ if (transactionType.isBuyDownFeeAmortizationAdjustment()) {
+ createJournalEntriesForBuyDownFeeAmortizationAdjustment(loanDTO, loanTransactionDTO, office);
+ }
}
}
@@ -462,6 +470,157 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
+ private void createJournalEntriesForBuyDownFeeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+ final Office office) {
+ final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
+ if (isMarkedAsChargeOff) {
+ createJournalEntriesForChargeOffLoanBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
+ } else {
+ createJournalEntriesForLoanBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
+ }
+ }
+
+ private void createJournalEntriesForLoanBuyDownFeeAmortization(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();
+ final boolean isLoanWrittenOff = loanDTO.isMarkedAsWrittenOff();
+
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
+ final BigDecimal interestAmount = loanTransactionDTO.getInterest();
+ final BigDecimal feesAmount = loanTransactionDTO.getFees();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+ final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
+
+ // interest payment
+ final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
+ : AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN;
+ if (MathUtil.isGreaterThanZero(interestAmount)) {
+ populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
+ }
+ // handle fees payment
+ if (MathUtil.isGreaterThanZero(feesAmount)) {
+ populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
+ }
+
+ // create credit entries
+ for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
+ this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ creditEntry.getValue(), glAccount);
+ }
+ }
+ // create debit entries
+ for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
+ this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ debitEntry.getValue(), glAccount);
+ }
+ }
+ }
+
+ private void createJournalEntriesForChargeOffLoanBuyDownFeeAmortization(final LoanDTO loanDTO,
+ final LoanTransactionDTO loanTransactionDTO, final Office office) {
+ // loan properties
+ final Long loanProductId = loanDTO.getLoanProductId();
+ final boolean isMarkedFraud = loanDTO.isMarkedAsFraud();
+ final Long loanId = loanDTO.getLoanId();
+ final String currencyCode = loanDTO.getCurrencyCode();
+
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
+ final BigDecimal interestAmount = loanTransactionDTO.getInterest();
+ final BigDecimal feesAmount = loanTransactionDTO.getFees();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+ final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
+ final Long chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
+
+ final ProductToGLAccountMapping mapping = chargeOffReasonCodeValue != null
+ ? helper.getChargeOffMappingByCodeValue(loanProductId, PortfolioProductType.LOAN, chargeOffReasonCodeValue)
+ : null;
+
+ if (mapping != null) {
+ final GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
+ // handle interest payment
+ if (MathUtil.isGreaterThanZero(interestAmount)) {
+ glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), interestAmount);
+ glAccountBalanceHolder.addToDebit(accountDebit, interestAmount);
+ }
+ // handle fees payment
+ if (MathUtil.isGreaterThanZero(feesAmount)) {
+ glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), feesAmount);
+ glAccountBalanceHolder.addToDebit(accountDebit, feesAmount);
+ }
+ } else {
+ final AccrualAccountsForLoan creditAccountType = isMarkedFraud ? AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE
+ : AccrualAccountsForLoan.CHARGE_OFF_EXPENSE;
+ // handle interest payment
+ if (MathUtil.isGreaterThanZero(interestAmount)) {
+ populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
+ }
+ // handle fees payment
+ if (MathUtil.isGreaterThanZero(feesAmount)) {
+ populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
+ }
+ }
+
+ // create credit entries
+ for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
+ this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ creditEntry.getValue(), glAccount);
+ }
+ }
+ // create debit entries
+ for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
+ this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ debitEntry.getValue(), glAccount);
+ }
+ }
+ }
+
+ private void createJournalEntriesForBuyDownFeeAmortizationAdjustment(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+ final Office office) {
+ GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
+ if (MathUtil.isGreaterThanZero(loanTransactionDTO.getAmount())) {
+ populateCreditDebitMaps(loanDTO.getLoanProductId(), loanTransactionDTO.getAmount(), loanTransactionDTO.getPaymentTypeId(),
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN.getValue(),
+ glAccountBalanceHolder);
+ }
+
+ // create credit entries
+ for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
+ this.helper.createCreditJournalEntryForLoan(office, loanDTO.getCurrencyCode(), loanDTO.getLoanId(),
+ loanTransactionDTO.getTransactionId(), loanTransactionDTO.getTransactionDate(), creditEntry.getValue(), glAccount);
+ }
+ }
+ // create debit entries
+ for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
+ this.helper.createDebitJournalEntryForLoan(office, loanDTO.getCurrencyCode(), loanDTO.getLoanId(),
+ loanTransactionDTO.getTransactionId(), loanTransactionDTO.getTransactionDate(), debitEntry.getValue(), glAccount);
+ }
+ }
+ }
+
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/cob/loan/BuyDownFeeAmortizationBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/BuyDownFeeAmortizationBusinessStep.java
new file mode 100644
index 0000000000..1f511c94a1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/BuyDownFeeAmortizationBusinessStep.java
@@ -0,0 +1,60 @@
+/**
+ * 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.cob.loan;
+
+import jakarta.transaction.Transactional;
+import java.time.LocalDate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingService;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class BuyDownFeeAmortizationBusinessStep implements LoanCOBBusinessStep {
+
+ private final LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService;
+
+ @Transactional
+ @Override
+ public Loan execute(Loan loan) {
+ if (!loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) {
+ return loan;
+ }
+
+ LocalDate businessDate = DateUtils.getBusinessLocalDate();
+
+ loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationTillDate(loan, businessDate, true);
+
+ return loan;
+ }
+
+ @Override
+ public String getEnumStyledName() {
+ return "BUY_DOWN_FEE_AMORTIZATION";
+ }
+
+ @Override
+ public String getHumanReadableName() {
+ return "Buy Down Fee amortization";
+ }
+}
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 039f7a70e4..366d1d022d 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
@@ -537,6 +537,8 @@ public class LoanTransactionsApiResource {
case capitalizedIncomeAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
case contractTermination -> LoanTransactionType.CONTRACT_TERMINATION;
case capitalizedIncomeAmortizationAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
+ case buyDownFeeAmortization -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION;
+ case buyDownFeeAmortizationAdjustment -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT;
default ->
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
};
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
index 44a8738b63..3d4b1cdbc2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
@@ -735,6 +735,14 @@ final class LoansApiResourceSwagger {
public boolean capitalizedIncomeAdjustment;
@Schema(example = "false")
public boolean contractTermination;
+ @Schema(example = "false")
+ public boolean buyDownFee;
+ @Schema(example = "false")
+ public boolean buyDownFeeAdjustment;
+ @Schema(example = "false")
+ public boolean buyDownFeeAmortization;
+ @Schema(example = "false")
+ public boolean buyDownFeeAmortizationAdjustment;
}
static final class GetLoansLoanIdPaymentDetailData {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java
new file mode 100644
index 0000000000..979bb4c065
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java
@@ -0,0 +1,113 @@
+/**
+ * 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.service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository;
+import org.apache.fineract.portfolio.loanaccount.util.BuyDownFeeAmortizationUtil;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyDownFeeAmortizationProcessingService {
+
+ private final LoanTransactionRepository loanTransactionRepository;
+ private final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository;
+ private final BusinessEventNotifierService businessEventNotifierService;
+ private final LoanJournalEntryPoster journalEntryPoster;
+ private final ExternalIdFactory externalIdFactory;
+
+ @Override
+ @Transactional
+ public void processBuyDownFeeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate, boolean addJournal) {
+ final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
+ final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
+
+ List<LoanBuyDownFeeBalance> balances = loanBuyDownFeesBalanceRepository.findAllByLoanId(loan.getId());
+
+ LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate()
+ : getFinalBuyDownFeeAmortizationTransactionDate(loan);
+ LocalDate tillDatePlusOne = tillDate.plusDays(1);
+ if (tillDatePlusOne.isAfter(maturityDate)) {
+ tillDatePlusOne = maturityDate;
+ }
+
+ Money totalAmortization = Money.zero(loan.getCurrency());
+ for (LoanBuyDownFeeBalance balance : balances) {
+ List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
+ Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, maturityDate,
+ loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), tillDatePlusOne, loan.getCurrency());
+ totalAmortization = totalAmortization.add(amortizationTillDate);
+
+ balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment()))
+ .subtract(amortizationTillDate.getAmount()));
+ }
+
+ loanBuyDownFeesBalanceRepository.saveAll(balances);
+
+ BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan);
+ BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
+
+ if (!MathUtil.isZero(totalAmortizationAmount)) {
+ LoanTransaction transaction = MathUtil.isGreaterThanZero(totalAmortizationAmount)
+ ? LoanTransaction.buyDownFeeAmortization(loan, loan.getOffice(), tillDate, totalAmortizationAmount,
+ externalIdFactory.create())
+ : LoanTransaction.buyDownFeeAmortizationAdjustment(loan,
+ Money.of(loan.getCurrency(), MathUtil.negate(totalAmortizationAmount)), tillDate, externalIdFactory.create());
+ loan.addLoanTransaction(transaction);
+
+ transaction = loanTransactionRepository.save(transaction);
+ loanTransactionRepository.flush();
+
+ if (addJournal) {
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
+ }
+
+ BusinessEvent<?> event = MathUtil.isGreaterThanZero(totalAmortizationAmount)
+ ? new LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(transaction)
+ : new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(transaction);
+ businessEventNotifierService.notifyPostBusinessEvent(event);
+ }
+ }
+
+ private LocalDate getFinalBuyDownFeeAmortizationTransactionDate(final Loan loan) {
+ return switch (loan.getStatus()) {
+ case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
+ case OVERPAID -> loan.getOverpaidOnDate();
+ case CLOSED_WRITTEN_OFF -> loan.getWrittenOffOnDate();
+ default -> throw new IllegalStateException("Unexpected value: " + loan.getStatus());
+ };
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java
index 17f0cf5343..e8bf678e13 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java
@@ -125,7 +125,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
for (LoanCapitalizedIncomeBalance balance : balances) {
- List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
+ List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate;
final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency());
@@ -136,7 +136,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
balance.setUnrecognizedAmount(BigDecimal.ZERO);
}
- BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmount(loan);
+ BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
if (MathUtil.isZero(totalUnrecognizedAmount)) {
return Optional.empty();
@@ -203,7 +203,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
Money totalAmortization = Money.zero(loan.getCurrency());
for (LoanCapitalizedIncomeBalance balance : balances) {
- List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
+ List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency());
totalAmortization = totalAmortization.add(amortizationTillDate);
@@ -214,7 +214,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
loanCapitalizedIncomeBalanceRepository.saveAll(balances);
- BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmount(loan);
+ BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
if (!MathUtil.isZero(totalAmortizationAmount)) {
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 00609a9999..a891a2aebf 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
@@ -123,9 +123,12 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
+import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingService;
+import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
+import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
@@ -549,4 +552,25 @@ public class LoanAccountConfiguration {
loanCapitalizedIncomeAmortizationProcessingService);
}
+ @Bean
+ @ConditionalOnMissingBean(LoanCapitalizedIncomeAmortizationProcessingService.class)
+ public LoanCapitalizedIncomeAmortizationProcessingService loanCapitalizedIncomeAmortizationProcessingService(
+ final ConfigurationDomainService configurationDomainService, final LoanTransactionRepository loanTransactionRepository,
+ final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
+ final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster,
+ final ExternalIdFactory externalIdFactory) {
+ return new LoanCapitalizedIncomeAmortizationProcessingServiceImpl(configurationDomainService, loanTransactionRepository,
+ loanCapitalizedIncomeBalanceRepository, businessEventNotifierService, journalEntryPoster, externalIdFactory);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(LoanBuyDownFeeAmortizationProcessingService.class)
+ public LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService(
+ final LoanTransactionRepository loanTransactionRepository,
+ final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository,
+ final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster,
+ final ExternalIdFactory externalIdFactory) {
+ return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeesBalanceRepository,
+ businessEventNotifierService, journalEntryPoster, externalIdFactory);
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/util/BuyDownFeeAmortizationUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/util/BuyDownFeeAmortizationUtil.java
new file mode 100644
index 0000000000..37459b5f0a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/util/BuyDownFeeAmortizationUtil.java
@@ -0,0 +1,84 @@
+/**
+ * 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.util;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Comparator;
+import java.util.List;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public final class BuyDownFeeAmortizationUtil {
+
+ private BuyDownFeeAmortizationUtil() {}
+
+ public static Money calculateTotalAmortizationTillDate(final LoanBuyDownFeeBalance buyDownFeeBalance,
+ final List<LoanTransaction> adjustmentTransactions, final LocalDate maturityDate,
+ final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LocalDate tillDate, final MonetaryCurrency currency) {
+ return switch (buyDownFeeStrategy) {
+ case EQUAL_AMORTIZATION -> calculateTotalAmortizationTillDateEqualAmortization(buyDownFeeBalance, adjustmentTransactions,
+ maturityDate, tillDate, currency);
+ };
+ }
+
+ private static Money calculateTotalAmortizationTillDateEqualAmortization(LoanBuyDownFeeBalance balance,
+ List<LoanTransaction> adjustmentTransactions, LocalDate maturityDate, LocalDate tillDate, MonetaryCurrency currency) {
+
+ BigDecimal unrecognizedAmount = balance.getAmount();
+ BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
+ BigDecimal overAmortizationCorrection = BigDecimal.ZERO;
+
+ List<LoanTransaction> sortedAdjustmentTransactions = adjustmentTransactions.stream()
+ .sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
+ LocalDate periodStart = balance.getDate();
+ for (LoanTransaction adjustmentTransaction : sortedAdjustmentTransactions) {
+ long daysUntilMaturity = DateUtils.getDifferenceInDays(periodStart, maturityDate);
+ long daysOfPeriod = DateUtils.getDifferenceInDays(periodStart, adjustmentTransaction.getDateOf());
+ BigDecimal periodAmortization = daysUntilMaturity == 0L ? BigDecimal.ZERO
+ : unrecognizedAmount.multiply(BigDecimal.valueOf(daysOfPeriod)).divide(BigDecimal.valueOf(daysUntilMaturity),
+ MoneyHelper.getMathContext());
+
+ totalAmortizationAmount = totalAmortizationAmount.add(periodAmortization);
+ unrecognizedAmount = unrecognizedAmount.subtract(periodAmortization).subtract(adjustmentTransaction.getAmount());
+ if (MathUtil.isLessThanZero(unrecognizedAmount)) {
+ overAmortizationCorrection = overAmortizationCorrection.add(unrecognizedAmount);
+ unrecognizedAmount = BigDecimal.ZERO;
+ }
+ periodStart = adjustmentTransaction.getDateOf();
+ }
+ if (periodStart.isBefore(tillDate)) {
+ long daysUntilMaturity = DateUtils.getDifferenceInDays(periodStart, maturityDate);
+ long daysOfPeriod = DateUtils.getDifferenceInDays(periodStart, tillDate);
+ BigDecimal periodAmortization = unrecognizedAmount.multiply(BigDecimal.valueOf(daysOfPeriod))
+ .divide(BigDecimal.valueOf(daysUntilMaturity), MoneyHelper.getMathContext());
+ totalAmortizationAmount = totalAmortizationAmount.add(periodAmortization);
+ } else if (balance.getDate().equals(maturityDate)) {
+ totalAmortizationAmount = totalAmortizationAmount.add(unrecognizedAmount);
+ }
+
+ return Money.of(currency, totalAmortizationAmount.add(overAmortizationCorrection));
+ }
+}
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 4307ea39ee..5e627b4d8d 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
@@ -208,4 +208,5 @@
<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" />
+ <include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0190_buy_down_fee_amortization.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0190_buy_down_fee_amortization.xml
new file mode 100644
index 0000000000..d9f53156b4
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0190_buy_down_fee_amortization.xml
@@ -0,0 +1,49 @@
+<?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.1.xsd">
+ <changeSet id="1" author="fineract">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="r_enum_value">
+ <column name="enum_name" value="transaction_type_enum"/>
+ <column name="enum_id" valueNumeric="42"/>
+ <column name="enum_message_property" value="Buy Down Fee Amortization"/>
+ <column name="enum_value" value="Buy Down Fee Amortization"/>
+ <column name="enum_type" valueBoolean="false"/>
+ </insert>
+ <insert tableName="r_enum_value">
+ <column name="enum_name" value="transaction_type_enum"/>
+ <column name="enum_id" valueNumeric="43"/>
+ <column name="enum_message_property" value="Buy Down Fee Amortization Adjustment"/>
+ <column name="enum_value" value="Buy Down Fee Amortization Adjustment"/>
+ <column name="enum_type" 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 9ac262fcab..3347b2162b 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
@@ -110,7 +110,9 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
- "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
+ "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
+ "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
+ "LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -202,7 +204,9 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
- "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
+ "LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
+ "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
+ "LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
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 786e207831..57288eaec0 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,22 +23,28 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
+import org.apache.fineract.client.models.PostClientsResponse;
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.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.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -425,4 +431,146 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
.transactionAmount(amount).externalId(buyDownFeeExternalId).note("Buy Down Fee Transaction"));
return response.getResourceId();
}
+
+ @Test
+ public void testBuyDownFeeDailyAmortization() {
+ new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
+ "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "CHECK_DUE_INSTALLMENTS", "UPDATE_LOAN_ARREARS_AGING",
+ "ADD_PERIODIC_ACCRUAL_ENTRIES", "ACCRUAL_ACTIVITY_POSTING", "CAPITALIZED_INCOME_AMORTIZATION", "BUY_DOWN_FEE_AMORTIZATION",
+ "LOAN_INTEREST_RECALCULATION", "EXTERNAL_ASSET_OWNER_TRANSFER");
+
+ final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+ final AtomicReference<Long> buyDownFeeTransactionIdIdRef = new AtomicReference<>();
+
+ final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()
+ .enableBuyDownFee(true).buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
+ .buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
+ .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
+ .buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
+ .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
+ .incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()));
+
+ runAt("1 January 2024", () -> {
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
+ 500.0, 7.0, 3, null);
+ loanIdRef.set(loanId);
+
+ disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+ PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.makeLoanBuyDownFee(loanId, "1 January 2024",
+ 50.0);
+ buyDownFeeTransactionIdIdRef.set(transactionsResponse.getResourceId());
+ });
+ runAt("31 January 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ // summarized amortization
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(0.55, "Accrual", "30 January 2024"), //
+ transaction(50.0, "Buy Down Fee", "01 January 2024"), //
+ transaction(16.48, "Buy Down Fee Amortization", "30 January 2024"));
+ });
+ runAt("1 February 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ // daily amortization
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(50.0, "Buy Down Fee", "01 January 2024"), //
+ transaction(0.55, "Accrual", "30 January 2024"), //
+ transaction(16.48, "Buy Down Fee Amortization", "30 January 2024"), //
+ transaction(0.01, "Accrual", "31 January 2024"), //
+ transaction(0.55, "Buy Down Fee Amortization", "31 January 2024"));
+
+ loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "1 February 2024", 10.0);
+ });
+ runAt("2 February 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ // not backdated and not large buy down fee adjustment -> lowered daily amount
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(50.0, "Buy Down Fee", "01 January 2024"), //
+ transaction(0.55, "Accrual", "30 January 2024"), //
+ transaction(16.48, "Buy Down Fee Amortization", "30 January 2024"), //
+ transaction(0.01, "Accrual", "31 January 2024"), //
+ transaction(0.55, "Buy Down Fee Amortization", "31 January 2024"), //
+ transaction(10.0, "Buy Down Fee Adjustment", "01 February 2024"), //
+ transaction(0.02, "Accrual", "01 February 2024"), //
+ transaction(0.39, "Buy Down Fee Amortization", "01 February 2024"));
+
+ loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "10 January 2024", 10.0);
+ });
+ runAt("3 February 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ // backdated buy down fee adjustment -> amortization adjustment
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(50.0, "Buy Down Fee", "01 January 2024"), //
+ transaction(10.0, "Buy Down Fee Adjustment", "10 January 2024"), //
+ transaction(0.55, "Accrual", "30 January 2024"), //
+ transaction(16.48, "Buy Down Fee Amortization", "30 January 2024"), //
+ transaction(0.01, "Accrual", "31 January 2024"), //
+ transaction(0.55, "Buy Down Fee Amortization", "31 January 2024"), //
+ transaction(10.0, "Buy Down Fee Adjustment", "01 February 2024"), //
+ transaction(0.02, "Accrual", "01 February 2024"), //
+ transaction(0.39, "Buy Down Fee Amortization", "01 February 2024"), //
+ transaction(0.02, "Accrual", "02 February 2024"), //
+ transaction(2.55, "Buy Down Fee Amortization Adjustment", "02 February 2024"));
+
+ loanTransactionHelper.buyDownFeeAdjustment(loanId, buyDownFeeTransactionIdIdRef.get(), "03 February 2024", 20.0);
+ });
+ runAt("4 February 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ // large (more than remaining unrecognized (15.13)) buy down fee adjustment -> amortization adjustment
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(50.0, "Buy Down Fee", "01 January 2024"), //
+ transaction(10.0, "Buy Down Fee Adjustment", "10 January 2024"), //
+ transaction(0.55, "Accrual", "30 January 2024"), //
+ transaction(16.48, "Buy Down Fee Amortization", "30 January 2024"), //
+ transaction(0.01, "Accrual", "31 January 2024"), //
+ transaction(0.55, "Buy Down Fee Amortization", "31 January 2024"), //
+ transaction(10.0, "Buy Down Fee Adjustment", "01 February 2024"), //
+ transaction(0.02, "Accrual", "01 February 2024"), //
+ transaction(0.39, "Buy Down Fee Amortization", "01 February 2024"), //
+ transaction(0.02, "Accrual", "02 February 2024"), //
+ transaction(2.55, "Buy Down Fee Amortization Adjustment", "02 February 2024"), //
+ transaction(20.0, "Buy Down Fee Adjustment", "03 February 2024"), //
+ transaction(0.02, "Accrual", "03 February 2024"), //
+ transaction(4.87, "Buy Down Fee Amortization Adjustment", "03 February 2024"));
+
+ // Check journal entries of amortization and amortization adjustment
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+
+ Optional<GetLoansLoanIdTransactions> amortizationTransactionOpt = loanDetails.getTransactions().stream()
+ .filter(transaction -> LocalDate.of(2024, 2, 1).equals(transaction.getDate())
+ && transaction.getType().getBuyDownFeeAmortization())
+ .findFirst();
+ Assertions.assertTrue(amortizationTransactionOpt.isPresent());
+
+ verifyTRJournalEntries(amortizationTransactionOpt.get().getId(), //
+ journalEntry(0.39, feeIncomeAccount, "CREDIT"), //
+ journalEntry(0.39, deferredIncomeLiabilityAccount, "DEBIT"));
+
+ Optional<GetLoansLoanIdTransactions> amortizationAdjustmentTransactionOpt = loanDetails.getTransactions().stream()
+ .filter(transaction -> LocalDate.of(2024, 2, 3).equals(transaction.getDate())
+ && transaction.getType().getBuyDownFeeAmortizationAdjustment())
+ .findFirst();
+ Assertions.assertTrue(amortizationAdjustmentTransactionOpt.isPresent());
+
+ verifyTRJournalEntries(amortizationAdjustmentTransactionOpt.get().getId(), //
+ journalEntry(4.87, deferredIncomeLiabilityAccount, "CREDIT"), //
+ journalEntry(4.87, feeIncomeAccount, "DEBIT"));
+ });
+ }
}
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 a8c4300cb5..bc4c92951b 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
@@ -646,6 +646,17 @@ public class ExternalEventConfigurationHelper {
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent);
+ Map<String, Object> loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent = new HashMap<>();
+ loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.put("type", "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent");
+ loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent.put("enabled", false);
+ defaults.add(loanBuyDownFeeAmortizationTransactionCreatedBusinessEvent);
+
+ Map<String, Object> loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent = new HashMap<>();
+ loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("type",
+ "LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+ loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
+ defaults.add(loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent);
+
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 bec059a9f3..96ac70a249 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
@@ -3083,6 +3083,11 @@ public class LoanTransactionHelper {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "buyDownFee"));
}
+ public PostLoansLoanIdTransactionsResponse makeLoanBuyDownFee(Long loanId, String date, double amount) {
+ return makeLoanBuyDownFee(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(date)
+ .locale("en").transactionAmount(amount));
+ }
+
public List<AdvancedPaymentData> getAdvancedPaymentAllocationRules(final Integer loanId) {
return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
}