FINERACT-2311: Buy down fee - daily amortization
This commit is contained in:
+3
-2
@@ -50,8 +50,9 @@ public class WorkFlowJobHelper {
|
|||||||
new BusinessStep().stepName("ADD_PERIODIC_ACCRUAL_ENTRIES").order(7L), //
|
new BusinessStep().stepName("ADD_PERIODIC_ACCRUAL_ENTRIES").order(7L), //
|
||||||
new BusinessStep().stepName("ACCRUAL_ACTIVITY_POSTING").order(8L), //
|
new BusinessStep().stepName("ACCRUAL_ACTIVITY_POSTING").order(8L), //
|
||||||
new BusinessStep().stepName("CAPITALIZED_INCOME_AMORTIZATION").order(9L), //
|
new BusinessStep().stepName("CAPITALIZED_INCOME_AMORTIZATION").order(9L), //
|
||||||
new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(10L), //
|
new BusinessStep().stepName("BUY_DOWN_FEE_AMORTIZATION").order(10L), //
|
||||||
new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(11L)//
|
new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(11L), //
|
||||||
|
new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(12L)//
|
||||||
);
|
);
|
||||||
BusinessStepRequest request = new BusinessStepRequest().businessSteps(businessSteps);
|
BusinessStepRequest request = new BusinessStepRequest().businessSteps(businessSteps);
|
||||||
Response<Void> response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request)
|
Response<Void> response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request)
|
||||||
|
|||||||
+35
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -57,5 +57,7 @@ public interface LoanTransactionApiConstants {
|
|||||||
capitalizedIncomeAdjustment, //
|
capitalizedIncomeAdjustment, //
|
||||||
contractTermination, //
|
contractTermination, //
|
||||||
capitalizedIncomeAmortizationAdjustment, //
|
capitalizedIncomeAmortizationAdjustment, //
|
||||||
|
buyDownFeeAmortization, //
|
||||||
|
buyDownFeeAmortizationAdjustment, //
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -74,6 +74,8 @@ public class LoanTransactionEnumData implements Serializable {
|
|||||||
private final boolean contractTermination;
|
private final boolean contractTermination;
|
||||||
private final boolean buyDownFee;
|
private final boolean buyDownFee;
|
||||||
private final boolean buyDownFeeAdjustment;
|
private final boolean buyDownFeeAdjustment;
|
||||||
|
private final boolean buyDownFeeAmortization;
|
||||||
|
private final boolean buyDownFeeAmortizationAdjustment;
|
||||||
|
|
||||||
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -118,6 +120,9 @@ public class LoanTransactionEnumData implements Serializable {
|
|||||||
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
|
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
|
||||||
this.buyDownFee = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE.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.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() {
|
public boolean isRepaymentType() {
|
||||||
|
|||||||
+23
-1
@@ -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() {
|
public LoanTransaction copyTransactionPropertiesAndMappings() {
|
||||||
LoanTransaction newTransaction = copyTransactionProperties(this);
|
LoanTransaction newTransaction = copyTransactionProperties(this);
|
||||||
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
||||||
@@ -806,7 +826,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF
|
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF
|
||||||
|| type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE
|
|| type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE
|
||||||
|| type == LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION || type == LoanTransactionType.CONTRACT_TERMINATION
|
|| 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) {
|
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
|
||||||
|
|||||||
+14
-3
@@ -361,16 +361,27 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
|
|||||||
AND (lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION
|
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)
|
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("""
|
@Query("""
|
||||||
SELECT lt FROM LoanTransaction lt, LoanTransactionRelation ltr
|
SELECT lt FROM LoanTransaction lt, LoanTransactionRelation ltr
|
||||||
WHERE lt.reversed = false
|
WHERE lt.reversed = false
|
||||||
AND lt = ltr.fromTransaction
|
AND lt = ltr.fromTransaction
|
||||||
AND ltr.toTransaction = :capitalizedIncome
|
AND ltr.toTransaction = :transaction
|
||||||
AND ltr.relationType = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.ADJUSTMENT
|
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("""
|
@Query("""
|
||||||
SELECT lt FROM LoanTransaction lt
|
SELECT lt FROM LoanTransaction lt
|
||||||
|
|||||||
+4
@@ -76,6 +76,8 @@ public enum LoanTransactionType {
|
|||||||
|
|
||||||
BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), //
|
BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), //
|
||||||
BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), //
|
BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), //
|
||||||
|
BUY_DOWN_FEE_AMORTIZATION(42, "loanTransactionType.buyDownFeeAmortization"), //
|
||||||
|
BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT(43, "loanTransactionType.buyDownFeeAmortizationAdjustment"), //
|
||||||
;
|
;
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
@@ -133,6 +135,8 @@ public enum LoanTransactionType {
|
|||||||
case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
||||||
case 40 -> LoanTransactionType.BUY_DOWN_FEE;
|
case 40 -> LoanTransactionType.BUY_DOWN_FEE;
|
||||||
case 41 -> LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT;
|
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;
|
default -> LoanTransactionType.INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -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);
|
||||||
|
}
|
||||||
+6
@@ -342,6 +342,12 @@ public final class LoanEnumerations {
|
|||||||
LoanTransactionType.BUY_DOWN_FEE.getCode(), "Buy Down Fee");
|
LoanTransactionType.BUY_DOWN_FEE.getCode(), "Buy Down Fee");
|
||||||
case BUY_DOWN_FEE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue().longValue(),
|
case BUY_DOWN_FEE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue().longValue(),
|
||||||
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getCode(), "Buy Down Fee Adjustment");
|
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");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+159
@@ -153,6 +153,14 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
if (transactionType.isBuyDownFeeAdjustment()) {
|
if (transactionType.isBuyDownFeeAdjustment()) {
|
||||||
createJournalEntriesForBuyDownFeeAdjustment(loanDTO, loanTransactionDTO, office);
|
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,
|
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
|
||||||
Office office) {
|
Office office) {
|
||||||
final Long loanProductId = loanDTO.getLoanProductId();
|
final Long loanProductId = loanDTO.getLoanProductId();
|
||||||
|
|||||||
+60
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -537,6 +537,8 @@ public class LoanTransactionsApiResource {
|
|||||||
case capitalizedIncomeAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
|
case capitalizedIncomeAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
|
||||||
case contractTermination -> LoanTransactionType.CONTRACT_TERMINATION;
|
case contractTermination -> LoanTransactionType.CONTRACT_TERMINATION;
|
||||||
case capitalizedIncomeAmortizationAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
case capitalizedIncomeAmortizationAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
||||||
|
case buyDownFeeAmortization -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION;
|
||||||
|
case buyDownFeeAmortizationAdjustment -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT;
|
||||||
default ->
|
default ->
|
||||||
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
||||||
};
|
};
|
||||||
|
|||||||
+8
@@ -735,6 +735,14 @@ final class LoansApiResourceSwagger {
|
|||||||
public boolean capitalizedIncomeAdjustment;
|
public boolean capitalizedIncomeAdjustment;
|
||||||
@Schema(example = "false")
|
@Schema(example = "false")
|
||||||
public boolean contractTermination;
|
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 {
|
static final class GetLoansLoanIdPaymentDetailData {
|
||||||
|
|||||||
+113
@@ -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());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -125,7 +125,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
|
|
||||||
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
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;
|
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate;
|
||||||
final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||||
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency());
|
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency());
|
||||||
@@ -136,7 +136,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmount(loan);
|
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
|
||||||
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
|
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
|
||||||
if (MathUtil.isZero(totalUnrecognizedAmount)) {
|
if (MathUtil.isZero(totalUnrecognizedAmount)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -203,7 +203,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
|
|
||||||
Money totalAmortization = Money.zero(loan.getCurrency());
|
Money totalAmortization = Money.zero(loan.getCurrency());
|
||||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
|
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
|
||||||
Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||||
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency());
|
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency());
|
||||||
totalAmortization = totalAmortization.add(amortizationTillDate);
|
totalAmortization = totalAmortization.add(amortizationTillDate);
|
||||||
@@ -214,7 +214,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
|
|
||||||
loanCapitalizedIncomeBalanceRepository.saveAll(balances);
|
loanCapitalizedIncomeBalanceRepository.saveAll(balances);
|
||||||
|
|
||||||
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmount(loan);
|
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
|
||||||
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
|
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
|
||||||
|
|
||||||
if (!MathUtil.isZero(totalAmortizationAmount)) {
|
if (!MathUtil.isZero(totalAmortizationAmount)) {
|
||||||
|
|||||||
+24
@@ -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.LoanAssembler;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
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.LoanCalculateRepaymentPastDueService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
|
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.LoanChargeAssembler;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||||
@@ -549,4 +552,25 @@ public class LoanAccountConfiguration {
|
|||||||
loanCapitalizedIncomeAmortizationProcessingService);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+84
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -208,4 +208,5 @@
|
|||||||
<include file="parts/0187_add_Loan_modified_and_withdrawn_event_configuration.xml" relativeToChangelogFile="true" />
|
<include file="parts/0187_add_Loan_modified_and_withdrawn_event_configuration.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0188_create_loan_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
|
<include file="parts/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/0189_add_loan_buydown_fee_event.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+49
@@ -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>
|
||||||
+6
-2
@@ -110,7 +110,9 @@ public class ExternalEventConfigurationValidationServiceTest {
|
|||||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
||||||
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
||||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
|
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||||
|
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||||
|
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||||
|
|
||||||
List<FineractPlatformTenant> tenants = Arrays
|
List<FineractPlatformTenant> tenants = Arrays
|
||||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||||
@@ -202,7 +204,9 @@ public class ExternalEventConfigurationValidationServiceTest {
|
|||||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
||||||
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
||||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
|
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||||
|
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||||
|
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||||
|
|
||||||
List<FineractPlatformTenant> tenants = Arrays
|
List<FineractPlatformTenant> tenants = Arrays
|
||||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||||
|
|||||||
+148
@@ -23,22 +23,28 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
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.PostLoanProductsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
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.ClientHelper;
|
||||||
import org.apache.fineract.integrationtests.common.Utils;
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
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"));
|
.transactionAmount(amount).externalId(buyDownFeeExternalId).note("Buy Down Fee Transaction"));
|
||||||
return response.getResourceId();
|
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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -646,6 +646,17 @@ public class ExternalEventConfigurationHelper {
|
|||||||
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
|
loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
|
||||||
defaults.add(loanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent);
|
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;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -3083,6 +3083,11 @@ public class LoanTransactionHelper {
|
|||||||
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "buyDownFee"));
|
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) {
|
public List<AdvancedPaymentData> getAdvancedPaymentAllocationRules(final Integer loanId) {
|
||||||
return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
|
return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user