From 20b542049eb64676624e98d8653364538df0d61f Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Thu, 5 Sep 2024 21:30:12 +0200 Subject: [PATCH] FINERACT-1981: Pay-off schedule handling --- .../data/loanproduct/DefaultLoanProduct.java | 2 +- .../factory/LoanProductsRequestFactory.java | 127 +++++++++++- .../LoanProductGlobalInitializerStep.java | 30 +++ .../stepdef/loan/LoanRepaymentStepDef.java | 15 ++ .../fineract/test/support/TestContextKey.java | 1 + .../src/test/resources/features/Loan.feature | 62 +++++- .../data/OutstandingAmountsDTO.java | 12 +- ...edPaymentScheduleTransactionProcessor.java | 118 ++++++++--- .../ProgressiveLoanScheduleGenerator.java | 99 +++------ .../ProgressiveLoanScheduleGeneratorTest.java | 192 ------------------ 10 files changed, 353 insertions(+), 305 deletions(-) delete mode 100644 fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java index d2e3fceaee7..58701b8683c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java @@ -20,7 +20,7 @@ public enum DefaultLoanProduct implements LoanProduct { - LP1, LP1_DUE_DATE, LP1_INTEREST_FLAT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_SAME_AS_PAYMENT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_DAILY, LP1_1MONTH_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_MONTHLY, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_REDUCE_NR_INST, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_RESCH_NEXT_REP, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_INTEREST_FLAT, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_INTEREST_FLAT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT_INTEREST, LP2_DOWNPAYMENT, LP2_DOWNPAYMENT_AUTO, LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_INTEREST, LP2_DOWNPAYMENT_INTEREST_AUTO, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_INSTALLMENT_LEVEL_DELINQUENCY, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH; + LP1, LP1_DUE_DATE, LP1_INTEREST_FLAT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_SAME_AS_PAYMENT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_DAILY, LP1_1MONTH_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_MONTHLY, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_REDUCE_NR_INST, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_RESCH_NEXT_REP, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_INTEREST_FLAT, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_INTEREST_FLAT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT_INTEREST, LP2_DOWNPAYMENT, LP2_DOWNPAYMENT_AUTO, LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_INTEREST, LP2_DOWNPAYMENT_INTEREST_AUTO, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_INSTALLMENT_LEVEL_DELINQUENCY, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH, LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE; @Override public String getName() { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java index 9752049944a..f997b1b57e1 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.test.factory; +import static org.apache.fineract.test.data.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -64,17 +66,20 @@ public class LoanProductsRequestFactory { public static final String NAME_PREFIX_INTEREST_FLAT_LP2 = "LP2InterestFlat-"; public static final String NAME_PREFIX_INTEREST_DECLINING = "LP1InterestDeclining-"; public static final String NAME_PREFIX_INTEREST_DECLINING_RECALCULATION = "LP1InterestDecliningRecalculation-"; + public static final String NAME_PREFIX_LP2_EMI = "LP2Emi-"; public static final String SHORT_NAME_PREFIX = "p"; public static final String SHORT_NAME_PREFIX_INTEREST = "i"; + public static final String SHORT_NAME_PREFIX_EMI = "e"; public static final String DATE_FORMAT = "dd MMMM yyyy"; public static final String LOCALE_EN = "en"; - public static final String DESCRIPTION = "Pay in 30 days product"; - public static final String DESCRIPTION_LP2 = "Pay in 4 product"; - public static final String DESCRIPTION_INTEREST_FLAT = "Pay in 30 days product with 12% interest - FLAT"; - public static final String DESCRIPTION_INTEREST_FLAT_LP2 = "Pay in 4 product with 12% interest - FLAT"; - public static final String DESCRIPTION_INTEREST_DECLINING = "Pay in 30 days product with 12% interest - DECLINING BALANCE"; + public static final String DESCRIPTION = "LP1 product"; + public static final String DESCRIPTION_LP2 = "LP2 product"; + public static final String DESCRIPTION_INTEREST_FLAT = "LP1 product with 12% interest - FLAT"; + public static final String DESCRIPTION_INTEREST_FLAT_LP2 = "LP2 product with 12% interest - FLAT"; + public static final String DESCRIPTION_INTEREST_DECLINING = "LP1 product with 12% interest - DECLINING BALANCE"; public static final String DESCRIPTION_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_MONTHLY = "LP1-1MONTH with 12% DECLINING BALANCE interest, interest period: Daily, Interest recalculation-Monthly, Compounding:Interest"; public static final String DESCRIPTION_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE = "LP1 with 12% DECLINING BALANCE interest, interest period: Daily, Interest recalculation-Daily, Compounding:none"; + public static final String DESCRIPTION_LP2_EMI = "LP2 product with EMI"; public static final Long FUND_ID = FundId.LENDER_A.value; public static final String CURRENCY_CODE = "EUR"; public static final Integer INTEREST_RATE_FREQUENCY_TYPE_MONTH = InterestRateFrequencyType.MONTH.value; @@ -87,6 +92,7 @@ public class LoanProductsRequestFactory { public static final Integer INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT = InterestCalculationPeriodTime.SAME_AS_REPAYMENT_PERIOD.value; public static final Integer INTEREST_CALCULATION_PERIOD_TYPE_DAILY = InterestCalculationPeriodTime.DAILY.value; public static final String TRANSACTION_PROCESSING_STRATEGY_CODE = TransactionProcessingStrategyCode.PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER.value; + public static final String TRANSACTION_PROCESSING_STRATEGY_CODE_ADVANCED = TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION.value; public static final Integer DAYS_IN_YEAR_TYPE = DaysInYearType.ACTUAL.value; public static final Integer DAYS_IN_MONTH_TYPE = DaysInMonthType.ACTUAL.value; public static final Integer LOAN_ACCOUNTING_RULE = AccountingRule.ACCRUAL_PERIODIC.value; @@ -874,4 +880,115 @@ public PostLoanProductsRequest defaultLoanProductsRequestLP2InterestFlat() { .chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))// .incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));// } + + public PostLoanProductsRequest defaultLoanProductsRequestLP2Emi() { + String name = Utils.randomNameGenerator(NAME_PREFIX_LP2_EMI, 4); + String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX_EMI, 3); + + List principalVariationsForBorrowerCycle = new ArrayList<>(); + List numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>(); + List interestRateVariationsForBorrowerCycle = new ArrayList<>(); + List charges = new ArrayList<>(); + List penaltyToIncomeAccountMappings = new ArrayList<>(); + List feeToIncomeAccountMappings = new ArrayList<>(); + + List paymentChannelToFundSourceMappings = new ArrayList<>(); + GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings(); + loanPaymentChannelToFundSourceMappings.fundSourceAccountId(accountTypeResolver.resolve(DefaultAccountType.FUND_RECEIVABLES)); + loanPaymentChannelToFundSourceMappings.paymentTypeId(paymentTypeResolver.resolve(DefaultPaymentType.MONEY_TRANSFER)); + paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings); + + return new PostLoanProductsRequest()// + .name(name)// + .shortName(shortName)// + .description(DESCRIPTION_LP2_EMI)// + .loanScheduleType("PROGRESSIVE") // + .interestCalculationPeriodType(InterestCalculationPeriodTime.DAILY.value)// + .transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())// + .fundId(FUND_ID)// + .startDate(null)// + .closeDate(null)// + .includeInBorrowerCycle(false)// + .currencyCode(CURRENCY_CODE)// + .digitsAfterDecimal(2)// + .inMultiplesOf(0)// + .useBorrowerCycle(false)// + .minPrincipal(10.0)// + .principal(1000.0)// + .maxPrincipal(10000.0)// + .minNumberOfRepayments(1)// + .numberOfRepayments(4)// + .maxNumberOfRepayments(30)// + .isLinkedToFloatingInterestRates(false)// + .minInterestRatePerPeriod((double) 0)// + .interestRatePerPeriod((double) 12)// + .maxInterestRatePerPeriod((double) 60)// + .interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_YEAR)// + .repaymentEvery(15)// + .repaymentStartDateType(1)// + .repaymentFrequencyType(REPAYMENT_FREQUENCY_TYPE_DAYS)// + .principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)// + .numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)// + .interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)// + .amortizationType(AMORTIZATION_TYPE)// + .interestType(INTEREST_TYPE_DECLINING_BALANCE)// + .isEqualAmortization(false)// + .interestCalculationPeriodType(INTEREST_CALCULATION_PERIOD_TYPE_DAILY)// + .transactionProcessingStrategyCode(TRANSACTION_PROCESSING_STRATEGY_CODE_ADVANCED)// + .daysInYearType(DAYS_IN_YEAR_TYPE)// + .daysInMonthType(DAYS_IN_MONTH_TYPE)// + .canDefineInstallmentAmount(true)// + .graceOnArrearsAgeing(3)// + .overdueDaysForNPA(179)// + .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)// + .principalThresholdForLastInstallment(50)// + .allowVariableInstallments(false)// + .canUseForTopup(false)// + .isInterestRecalculationEnabled(false)// + .holdGuaranteeFunds(false)// + .multiDisburseLoan(false)// + .allowAttributeOverrides(new AllowAttributeOverrides()// + .amortizationType(true)// + .interestType(true)// + .transactionProcessingStrategyCode(true)// + .interestCalculationPeriodType(true)// + .inArrearsTolerance(true)// + .repaymentEvery(true)// + .graceOnPrincipalAndInterestPayment(true)// + .graceOnArrearsAgeing(true))// + .allowPartialPeriodInterestCalcualtion(false)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .charges(charges)// + .accountingRule(LOAN_ACCOUNTING_RULE)// + .fundSourceAccountId(accountTypeResolver.resolve(DefaultAccountType.SUSPENSE_CLEARING_ACCOUNT))// + .loanPortfolioAccountId(accountTypeResolver.resolve(DefaultAccountType.LOANS_RECEIVABLE))// + .transfersInSuspenseAccountId(accountTypeResolver.resolve(DefaultAccountType.TRANSFER_IN_SUSPENSE_ACCOUNT))// + .interestOnLoanAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME))// + .incomeFromFeeAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_INCOME))// + .incomeFromPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_INCOME))// + .incomeFromRecoveryAccountId(accountTypeResolver.resolve(DefaultAccountType.RECOVERIES))// + .writeOffAccountId(accountTypeResolver.resolve(DefaultAccountType.WRITTEN_OFF))// + .overpaymentLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.OVERPAYMENT_ACCOUNT))// + .receivableInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))// + .receivableFeeAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))// + .receivablePenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))// + .dateFormat(DATE_FORMAT)// + .locale(LOCALE_EN)// + .disallowExpectedDisbursements(false)// + .delinquencyBucketId(DELINQUENCY_BUCKET_ID.longValue())// + .goodwillCreditAccountId(accountTypeResolver.resolve(DefaultAccountType.GOODWILL_EXPENSE_ACCOUNT))// + .incomeFromGoodwillCreditInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME_CHARGE_OFF))// + .incomeFromGoodwillCreditFeesAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))// + .incomeFromGoodwillCreditPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))// + .paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)// + .penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)// + .feeToIncomeAccountMappings(feeToIncomeAccountMappings)// + .incomeFromChargeOffInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME_CHARGE_OFF))// + .incomeFromChargeOffFeesAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))// + .chargeOffExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT))// + .chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))// + .incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));// + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 6468c7070aa..6670bda5ad5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -36,6 +36,8 @@ import org.apache.fineract.client.services.LoanProductsApi; import org.apache.fineract.test.data.AdvancePaymentsAdjustmentType; import org.apache.fineract.test.data.ChargeProductType; +import org.apache.fineract.test.data.DaysInMonthType; +import org.apache.fineract.test.data.DaysInYearType; import org.apache.fineract.test.data.InterestCalculationPeriodTime; import org.apache.fineract.test.data.RecalculationRestFrequencyType; import org.apache.fineract.test.data.TransactionProcessingStrategyCode; @@ -476,6 +478,34 @@ public void initialize() throws Exception { .createLoanProduct(loanProductsRequestDownPaymentAdvPmtAllocFixedLength).execute(); TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH, responseLoanProductsRequestDownPaymentAdvPmtAllocFixedLength); + + // LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 + // + interest recalculation, preClosureInterestCalculationStrategy= till preclose, + // interestRecalculationCompoundingMethod = none + // Frequency for recalculate Outstanding Principal: Daily, Frequency Interval for recalculation: 1 + // (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE) + String name38 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE.getName(); + PostLoanProductsRequest loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcTillPreclose = loanProductsRequestFactory + .defaultLoanProductsRequestLP2Emi()// + .name(name38)// + .daysInYearType(DaysInYearType.DAYS360.value)// + .daysInMonthType(DaysInMonthType.DAYS30.value)// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(4)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), // + createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), // + createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), // + createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));// + Response responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillPreCloese = loanProductsApi + .createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcTillPreclose).execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE, + responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillPreCloese); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRepaymentStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRepaymentStepDef.java index 8473d17f4d1..206abd1a014 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRepaymentStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRepaymentStepDef.java @@ -33,12 +33,14 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetUsersUserIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; @@ -66,6 +68,7 @@ import org.springframework.beans.factory.annotation.Autowired; import retrofit2.Response; +@Slf4j public class LoanRepaymentStepDef extends AbstractStepDef { public static final String DATE_FORMAT = "dd MMMM yyyy"; @@ -561,4 +564,16 @@ private void adjustNthRepaymentWithExternalOwnerCheck(String nthItemStr, String } } + + @When("Loan Pay-off is made on {string}") + public void makeLoanPayOff(String transactionDate) throws IOException { + Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId1 = loanResponse.body().getLoanId(); + Response response = loanTransactionsApi + .retrieveTransactionTemplate(loanId1, "prepayLoan", DATE_FORMAT, transactionDate, DEFAULT_LOCALE).execute(); + Double transactionAmount = response.body().getAmount(); + + log.info("--- Loan Pay-off with amount: {} ---", transactionAmount); + makeRepayment("AUTOPAY", transactionDate, transactionAmount, null); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 0227f95909c..213866d145d 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -77,6 +77,7 @@ public abstract class TestContextKey { public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_INSTALLMENT_LEVEL_DELINQUENCY = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleInstallmentLevelDelinquency"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleHorizontalInstallmentLevelDelinquencyCreditAllocation"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleFixedLength"; + public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationTillPreClose"; public static final String CHARGE_FOR_LOAN_PERCENT_LATE_CREATE_RESPONSE = "ChargeForLoanPercentLateCreateResponse"; public static final String CHARGE_FOR_LOAN_PERCENT_LATE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanPercentLateAmountPlusInterestCreateResponse"; public static final String CHARGE_FOR_LOAN_PERCENT_PROCESSING_CREATE_RESPONSE = "ChargeForLoanPercentProcessingCreateResponse"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index 38e437e3abf..4d97b23a87d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -5523,7 +5523,6 @@ Feature: Loan When Admin successfully disburse the loan on "01 February 2024" with "1000" EUR transaction amount Then LoanDetails has fixedLength field with int value: 60 - Scenario: Actual disbursement date is in the past with advanced payment allocation product + submitted on date repaymentStartDateType When Admin sets repaymentStartDateType for "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product to "SUBMITTED_ON_DATE" When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule @@ -5536,3 +5535,64 @@ Feature: Loan Then Loan status has changed to "Approved" Then Admin fails to disburse the loan on "31 December 2022" with "500" EUR transaction amount because disbursement date is earlier than "01 January 2023" When Admin sets repaymentStartDateType for "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product to "DISBURSEMENT_DATE" + + Scenario: Early pay-off loan with interest + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin sets the business date to "01 February 2024" + And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + When Admin sets the business date to "15 February 2024" + When Loan Pay-off is made on "15 February 2024" +# When Customer makes "AUTOPAY" repayment on "15 February 2024" with 83.81 EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 15 February 2024 | 66.8 | 16.77 | 0.24 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 3 | 31 | 01 April 2024 | 15 February 2024 | 49.79 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 4 | 30 | 01 May 2024 | 15 February 2024 | 32.78 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 5 | 31 | 01 June 2024 | 15 February 2024 | 15.77 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 17.01 | 17.01 | 0.0 | 0.0 | + | 6 | 30 | 01 July 2024 | | 0.0 | 15.77 | 0.0 | 0.0 | 0.0 | 15.77 | 15.77 | 15.77 | 0.0 | 0.0 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + | 15 February 2024 | Repayment | 83.81 | 83.57 | 0.24 | 0.0 | 0.0 | 0.0 | false | + When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java index aa1351880b3..265cc6a34fd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java @@ -46,20 +46,24 @@ public Money getTotalOutstanding() { .plus(penaltyCharges()); } - public void plusPrincipal(Money principal) { + public OutstandingAmountsDTO plusPrincipal(Money principal) { this.principal = this.principal.plus(principal); + return this; } - public void plusInterest(Money interest) { + public OutstandingAmountsDTO plusInterest(Money interest) { this.interest = this.interest.plus(interest); + return this; } - public void plusFeeCharges(Money feeCharges) { + public OutstandingAmountsDTO plusFeeCharges(Money feeCharges) { this.feeCharges = this.feeCharges.plus(feeCharges); + return this; } - public void plusPenaltyCharges(Money penaltyCharges) { + public OutstandingAmountsDTO plusPenaltyCharges(Money penaltyCharges) { this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges); + return this; } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index a9b5773b94e..3d9512c0091 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -55,6 +55,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -77,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -143,12 +145,13 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu throw new NotImplementedException(); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { + // only for progressive loans + public Pair reprocessProgressiveLoanTransactions( + LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, + List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, null); } if (charges != null) { for (final LoanCharge loanCharge : charges) { @@ -185,10 +188,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement chargeOrTransaction.getLoanCharge() .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } - List txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent) + List txs = chargeOrTransactions.stream() // + .map(ChargeOrTransaction::getLoanTransaction) // + .filter(Optional::isPresent) // .map(Optional::get).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, scheduleModel); + } + + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); } @Override @@ -281,7 +292,7 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio } return originalTransaction.get(); } else { // when there is no id, then it might be that the original transaction is changed, so we need to look - // it up from the Ctx. + // it up from the Ctx. Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); Collection updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values(); Optional updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations() @@ -738,19 +749,18 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() .filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); - Money transactionAmountUnprocessed = null; + Money transactionAmountUnprocessed = transactionCtx.getOverpaymentHolder().getMoneyObject(); Money zero = Money.zero(transactionCtx.getCurrency()); Balances balances = new Balances(zero, zero, zero, zero); if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(), + transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings, Set.of(), balances); } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(), - defaultPaymentAllocationRule, transactionMappings, Set.of(), balances); + transactionCtx.getInstallments(), transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings, + Set.of(), balances); } transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); @@ -1109,9 +1119,8 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings, - transactionCtx.getCharges(), balances); + transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, + paymentAllocationRule, transactionMappings, transactionCtx.getCharges(), balances); } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(), @@ -1125,26 +1134,31 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder()); } - private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Set charges, Balances balances) { + private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx, + Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, + List transactionMappings, Set charges, Balances balances) { LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { - transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments, - transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), - paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances); + transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, + paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, + charges, balances); } return transactionAmountUnprocessed; } - private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx, + Money transactionAmountUnprocessed, List paymentAllocationTypes, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, List transactionMappings, Set charges, Balances balances) { + if (transactionAmountUnprocessed.isZero()) { + return transactionAmountUnprocessed; + } + + MonetaryCurrency currency = transactionCtx.getCurrency(); + List installments = transactionCtx.getInstallments(); Money paidPortion; boolean exit = false; do { @@ -1214,15 +1228,55 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, firstNormalInstallmentNumber); - // Adjust the portion for the last installment - if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { - evenPortion = evenPortion.add(balanceAdjustment); - } + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, inAdvanceInstallment, currency); - paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + + Loan loan = loanTransaction.getLoan(); + if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing() + && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { + ProgressiveLoanInterestScheduleModel model = ctx.getModel(); + LocalDate transactionDate = loanTransaction.getTransactionDate(); + LocalDate payDate = inAdvanceInstallment.getFromDate().isAfter(transactionDate) + ? inAdvanceInstallment.getFromDate() + : transactionDate; + ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator + .getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate).orElseThrow(); + + switch (paymentAllocationType) { + case IN_ADVANCE_INTEREST -> + inAdvanceInstallment.updateInterestCharged(payableDetails.getInterestDue().getAmount()); + case IN_ADVANCE_PRINCIPAL -> + inAdvanceInstallment.updatePrincipal(payableDetails.getPrincipalDue().getAmount()); + default -> { + } + } + + paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, + inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); + + switch (paymentAllocationType) { + case IN_ADVANCE_PRINCIPAL -> { + emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getOutstandingBalance().multipliedBy(-1)); + emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getPrincipalDue().minus(paidPortion)); + } + case IN_ADVANCE_INTEREST -> emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getInterestDue().minus(paidPortion)); + default -> { + } + } + } else { + // Adjust the portion for the last installment + if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { + evenPortion = evenPortion.add(balanceAdjustment); + } + paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, + evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, + LoanRepaymentScheduleInstallment.PaymentAction.PAY); + } transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } } else { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index 5fd7d21c547..b1250fd457d 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -18,11 +18,10 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; -import static java.time.temporal.ChronoUnit.DAYS; +import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; import java.math.BigDecimal; import java.math.MathContext; -import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -30,14 +29,12 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; 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.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; @@ -46,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; @@ -247,77 +245,38 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> { - log.debug("calculating prepayment amount till pre closure date (Strategy A)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - AtomicBoolean firstAfterPayoff = new AtomicBoolean(true); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isInstallmentAfterPayoff) { - if (firstAfterPayoff.getAndSet(false)) { - outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate)); - } else { - log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(), - installment.getDueDate()); - } - } else { - log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(), - installment.getInterestOutstanding(currency)); - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - yield outstandingAmounts; - } - - case TILL_REST_FREQUENCY_DATE -> { - log.debug("calculating prepayment amount till rest frequency date (Strategy B)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isPayoffBeforeInstallment) { - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } else { - log.debug("Payoff after installment {}, not counting interest", installment.getDueDate()); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - - yield outstandingAmounts; - } - case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported"); + List installments = loan.getRepaymentScheduleInstallments(); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> // find due date of current installment + installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst() + .orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate(); + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; - } - private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) { - RoundingMode roundingMode = MoneyHelper.getRoundingMode(); - MonetaryCurrency currency = loan.getCurrency(); - Money originalInterest = installment.getInterestCharged(currency); - log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate()); - - LocalDate start = installment.getFromDate(); - Money payableInterest = Money.zero(currency); - - while (!start.isEqual(onDate)) { - long between = DAYS.between(start, installment.getDueDate()); - Money dailyInterest = originalInterest.minus(payableInterest).dividedBy(between, roundingMode); - log.debug("Daily interest is {}: {} / {}, total: {}", dailyInterest, originalInterest.minus(payableInterest), between, - payableInterest.add(dailyInterest)); - payableInterest = payableInterest.add(dailyInterest); - start = start.plusDays(1); + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); } + ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), + loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); + + LoanRepaymentScheduleInstallment actualInstallment = installments.stream() + .filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate())).findFirst() + .orElse(installments.get(0)); + + ProgressiveLoanInterestRepaymentModel result = emiCalculator + .getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow(); + + OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // + .principal(result.getOutstandingBalance()) // + .interest(result.getInterestDue()); - payableInterest = payableInterest.minus(installment.getInterestPaid(currency).minus(installment.getInterestWaived(currency))); + installments.forEach(installment -> amounts // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); - log.debug("Payable interest is {}", payableInterest); - return payableInterest; + return amounts; } // Private, internal methods diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java deleted file mode 100644 index 214bff051c9..00000000000 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/** - * 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.loanschedule.domain; - -import static java.math.BigDecimal.ZERO; -import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; -import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.time.LocalDate; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -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.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; -import org.springframework.test.util.ReflectionTestUtils; - -class ProgressiveLoanScheduleGeneratorTest { - - static class TestRow { - - LocalDate fromDate; - LocalDate dueDate; - BigDecimal balance; - BigDecimal principal; - BigDecimal interest; - BigDecimal fee; - BigDecimal penalty; - boolean paid; - - TestRow(LocalDate fromDate, LocalDate dueDate, BigDecimal balance, BigDecimal principal, BigDecimal interest, BigDecimal fee, - BigDecimal penalty, boolean paid) { - this.fromDate = fromDate; - this.dueDate = dueDate; - this.balance = balance; - this.principal = principal; - this.interest = interest; - this.fee = fee; - this.penalty = penalty; - this.paid = paid; - } - } - - private ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(null, null); - private MonetaryCurrency usd = new MonetaryCurrency("USD", 2, null); - private HolidayDetailDTO holidays = new HolidayDetailDTO(false, null, null); - LoanRepaymentScheduleTransactionProcessor processor = mock(LoanRepaymentScheduleTransactionProcessor.class); - - static { - ConfigurationDomainService domainService = mock(ConfigurationDomainService.class); - when(domainService.getRoundingMode()).thenReturn(RoundingMode.HALF_UP.ordinal()); - ReflectionTestUtils.setField(MoneyHelper.class, "staticConfigurationDomainService", domainService); - } - - @BeforeAll - public static void beforeAll() { - ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(ch.qos.logback.classic.Level.DEBUG); - } - - @AfterAll - public static void afterAll() { - ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(Level.INFO); - } - - public List testRows() { - return List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(83.57), BigDecimal.valueOf(16.43), - BigDecimal.valueOf(0.58), ZERO, ZERO, true), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(67.05), BigDecimal.valueOf(16.52), - BigDecimal.valueOf(0.49), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(50.43), BigDecimal.valueOf(16.62), - BigDecimal.valueOf(0.39), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(33.71), BigDecimal.valueOf(16.72), - BigDecimal.valueOf(0.29), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(16.90), BigDecimal.valueOf(16.81), - BigDecimal.valueOf(0.20), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(00.90), BigDecimal.valueOf(16.90), - BigDecimal.valueOf(0.10), ZERO, ZERO, false)); - } - - @Test - public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE); - Loan loan = prepareLoanWithInstallments(testRows()); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount()); - } - - @Test - public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); - Loan loan = prepareLoanWithInstallments(testRows()); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(84.06), amounts.getTotalOutstanding().getAmount()); - } - - @Test - public void calculateSameDayPayoff_TILL_PRE_CLOSURE_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE); - - Loan loan = prepareLoanWithInstallments(List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false))); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue()); - } - - @Test - public void calculateSameDayPayoff_TILL_REST_FREQUENCY_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); - - Loan loan = prepareLoanWithInstallments(List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false))); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue()); - } - - @NotNull - private Loan prepareLoanWithInstallments(List rows) { - Loan loan = mock(Loan.class); - List installments = createInstallments(rows, loan, usd); - when(loan.getRepaymentScheduleInstallments()).thenReturn(installments); - when(loan.getCurrency()).thenReturn(usd); - return loan; - } - - private List createInstallments(List rows, Loan loan, MonetaryCurrency usd) { - AtomicInteger count = new AtomicInteger(1); - return rows.stream().map(row -> { - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, count.incrementAndGet(), row.fromDate, - row.dueDate, row.principal, row.interest, row.fee, row.penalty, true, null, null, row.paid); - if (row.paid) { - installment.payPrincipalComponent(row.fromDate, Money.of(usd, row.principal)); - installment.payInterestComponent(row.fromDate, Money.of(usd, row.interest)); - installment.updateObligationMet(true); - } - return installment; - }).toList(); - } -}