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 <[email protected]>
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()));
}