This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 5df6af6a23e5a9a25f8b0959b3584bcc4816e1c8 Author: Oleksii Novikov <[email protected]> AuthorDate: Fri Sep 12 10:59:05 2025 +0300 FINERACT-2354: Merge re-aged installment with the next (N+1) installment if they overlap --- .../test/resources/features/LoanReAging.feature | 120 +++++++++++++++++++++ ...dvancedPaymentScheduleTransactionProcessor.java | 98 +++++++++++++---- 2 files changed, 195 insertions(+), 23 deletions(-) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 0da3fbbd4e..c32e07bc0f 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -1652,3 +1652,123 @@ Feature: LoanReAging | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 01 May 2025 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | + Scenario: Verify merging re-aging transaction with N+1 installment in the same bucket(YEARS) + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 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 + When Admin sets the business date to "05 April 2024" + And Admin adds "LOAN_SNOOZE_FEE" due date charge with "03 May 2024" due date and 100 EUR transaction amount + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | YEARS | 01 April 2024 | 3 | + Then Loan Repayment schedule has 7 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 | 0 | 01 January 2024 | 01 January 2024 | 75.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 01 February 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 3 | 29 | 01 March 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 4 | 31 | 01 April 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 5 | 0 | 01 April 2024 | | 50.0 | 25.0 | 0.0 | 100.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | + | 6 | 365 | 01 April 2025 | | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 7 | 365 | 01 April 2026 | | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + + Scenario: Verify merging re-aging transaction with N+1 installment in the same bucket(MONTHS) + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 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 + When Admin sets the business date to "05 April 2024" + And Admin adds "LOAN_SNOOZE_FEE" due date charge with "03 May 2024" due date and 100 EUR transaction amount + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 01 April 2024 | 3 | + Then Loan Repayment schedule has 7 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 | 0 | 01 January 2024 | 01 January 2024 | 75.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 01 February 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 3 | 29 | 01 March 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 4 | 31 | 01 April 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 5 | 0 | 01 April 2024 | | 50.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 6 | 30 | 01 May 2024 | | 25.0 | 25.0 | 0.0 | 100.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | + | 7 | 31 | 01 June 2024 | | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + + Scenario: Verify merging re-aging transaction with N+1 installment in the same bucket(WEEKS) + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 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 + When Admin sets the business date to "05 April 2024" + And Admin adds "LOAN_SNOOZE_FEE" due date charge with "09 April 2024" due date and 100 EUR transaction amount + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | WEEKS | 01 April 2024 | 3 | + Then Loan Repayment schedule has 7 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 | 0 | 01 January 2024 | 01 January 2024 | 75.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 01 February 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 3 | 29 | 01 March 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 4 | 31 | 01 April 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 5 | 0 | 01 April 2024 | | 50.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 6 | 7 | 08 April 2024 | | 25.0 | 25.0 | 0.0 | 100.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | + | 7 | 7 | 15 April 2024 | | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + + Scenario: Verify merging re-aging transaction with N+1 installment in the same bucket(DAYS) + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 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 + When Admin sets the business date to "05 April 2024" + And Admin adds "LOAN_SNOOZE_FEE" due date charge with "03 April 2024" due date and 100 EUR transaction amount + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | DAYS | 01 April 2024 | 3 | + Then Loan Repayment schedule has 7 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 | 0 | 01 January 2024 | 01 January 2024 | 75.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 01 February 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 3 | 29 | 01 March 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 4 | 31 | 01 April 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 5 | 0 | 01 April 2024 | | 50.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 6 | 1 | 02 April 2024 | | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 7 | 1 | 03 April 2024 | | 0.0 | 25.0 | 0.0 | 100.0 | 0.0 | 125.0 | 0.0 | 0.0 | 0.0 | 125.0 | + + Scenario: Verify re-aging transaction with N+1 installment outside bucket + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 100 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 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 + When Admin sets the business date to "05 April 2024" + And Admin adds "LOAN_SNOOZE_FEE" due date charge with "03 July 2024" due date and 100 EUR transaction amount + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 01 April 2024 | 3 | + Then Loan Repayment schedule has 8 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 | 0 | 01 January 2024 | 01 January 2024 | 75.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 01 February 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 3 | 29 | 01 March 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 4 | 31 | 01 April 2024 | 05 April 2024 | 75.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + | 5 | 0 | 01 April 2024 | | 50.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 6 | 30 | 01 May 2024 | | 25.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 7 | 31 | 01 June 2024 | | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | 0.0 | 0.0 | 0.0 | 25.0 | + | 8 | 32 | 03 July 2024 | | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | \ No newline at end of file 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 b0d82d1184..85970a72f7 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 @@ -68,6 +68,7 @@ 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.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; @@ -2847,12 +2848,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep MonetaryCurrency currency = ctx.getCurrency(); List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); - AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); + final LocalDate startDate = loanTransaction.getLoanReAgeParameter().getStartDate(); + final LocalDate endDate = calculateReAgedInstallmentEndDate(loanTransaction.getLoanReAgeParameter()); + final AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); installments.forEach(i -> { - Money principalOutstanding = i.getPrincipalOutstanding(currency); - if (principalOutstanding.isGreaterThanZero()) { - outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); - i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); + final boolean shouldInclude = !i.isAdditional() || i.getDueDate().isBefore(startDate) + || (!i.getDueDate().isBefore(startDate) && i.getDueDate().isBefore(endDate)); + if (shouldInclude) { + Money principalOutstanding = i.getPrincipalOutstanding(currency); + if (principalOutstanding.isGreaterThanZero()) { + outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); + i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); + } } }); @@ -2873,25 +2880,26 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())); } final LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream() // - .filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())) // + .filter(i -> !i.isDownPayment() && i.getDueDate().isBefore(loanTransaction.getTransactionDate())) // .reduce((first, second) -> second) // .orElseThrow(); - LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment( - lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(), - loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount()); - installments.add(reAgedInstallment); + + LoanRepaymentScheduleInstallment reAgedInstallment = createOrConvertReAgedInstallment(installments, startDate, + loanTransaction.getLoanReAgeParameter(), lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, + lastNormalInstallment.getDueDate(), calculatedPrincipal.getAmount()); reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate()); for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { - LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), + final LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), reAgedInstallment.getDueDate()); - reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), - reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate, + + reAgedInstallment = createOrConvertReAgedInstallment(installments, calculatedDueDate, loanTransaction.getLoanReAgeParameter(), + reAgedInstallment.getLoan(), reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedPrincipal.getAmount()); - installments.add(reAgedInstallment); reAgedInstallment.updateObligationsMet(currency, loanTransaction.getTransactionDate()); } reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); + reprocessInstallments(installments); } @@ -2905,21 +2913,65 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate).forEachOrdered(i -> { i.updateInstallmentNumber(counter.getAndIncrement()); final LocalDate prev = previousDueDate.get(); - - if (prev != null && i.isAdditional()) { + if (prev != null && (i.isAdditional() || i.isReAged())) { i.updateFromDate(prev); } previousDueDate.set(i.getDueDate()); }); } - private LocalDate calculateReAgedInstallmentDueDate(LoanReAgeParameter reAgeParameter, LocalDate dueDate) { - return switch (reAgeParameter.getFrequencyType()) { - case DAYS -> dueDate.plusDays(reAgeParameter.getFrequencyNumber()); - case WEEKS -> dueDate.plusWeeks(reAgeParameter.getFrequencyNumber()); - case MONTHS -> dueDate.plusMonths(reAgeParameter.getFrequencyNumber()); - case YEARS -> dueDate.plusYears(reAgeParameter.getFrequencyNumber()); - default -> throw new UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode()); + private LoanRepaymentScheduleInstallment convertAdditionalToReAged(final LoanRepaymentScheduleInstallment installment, + final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principalAmount) { + installment.setAdditional(false); + installment.setReAged(true); + installment.updateFromDate(fromDate); + installment.updateDueDate(dueDate); + installment.updatePrincipal(principalAmount); + return installment; + } + + private Optional<LoanRepaymentScheduleInstallment> findAdditionalForFrequency(final List<LoanRepaymentScheduleInstallment> installments, + final LocalDate targetDueDate, final LoanReAgeParameter reAgeParameter) { + final LocalDate nextReAgedDate = calculateReAgedInstallmentDueDate(reAgeParameter, targetDueDate); + + return installments.stream() // + .filter(LoanRepaymentScheduleInstallment::isAdditional) // + .filter(ai -> !ai.getDueDate().isBefore(targetDueDate) && ai.getDueDate().isBefore(nextReAgedDate)).findAny(); + } + + private LoanRepaymentScheduleInstallment createOrConvertReAgedInstallment(final List<LoanRepaymentScheduleInstallment> installments, + final LocalDate targetDueDate, final LoanReAgeParameter reAgeParameter, final Loan loan, final int installmentNumber, + final LocalDate fromDate, final BigDecimal principalAmount) { + final Optional<LoanRepaymentScheduleInstallment> additionalInstallment = findAdditionalForFrequency(installments, targetDueDate, + reAgeParameter); + + if (additionalInstallment.isPresent()) { + return convertAdditionalToReAged(additionalInstallment.get(), fromDate, targetDueDate, principalAmount); + } else { + final LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, + installmentNumber, fromDate, targetDueDate, principalAmount); + installments.add(reAgedInstallment); + return reAgedInstallment; + } + } + + private LocalDate calculateReAgedInstallmentEndDate(final LoanReAgeParameter reAgeParameter) { + return calculateReAgedNextDate(reAgeParameter.getFrequencyType(), reAgeParameter.getStartDate(), + reAgeParameter.getFrequencyNumber(), reAgeParameter.getNumberOfInstallments()); + } + + private LocalDate calculateReAgedInstallmentDueDate(final LoanReAgeParameter reAgeParameter, final LocalDate dueDate) { + return calculateReAgedNextDate(reAgeParameter.getFrequencyType(), dueDate, reAgeParameter.getFrequencyNumber(), 1); + } + + private LocalDate calculateReAgedNextDate(final PeriodFrequencyType frequencyType, final LocalDate dueDate, + final Integer frequencyNumber, final Integer numberOfInstallments) { + return switch (frequencyType) { + case DAYS -> dueDate.plusDays((long) frequencyNumber * numberOfInstallments); + case WEEKS -> dueDate.plusWeeks((long) frequencyNumber * numberOfInstallments); + case MONTHS -> dueDate.plusMonths((long) frequencyNumber * numberOfInstallments); + case YEARS -> dueDate.plusYears((long) frequencyNumber * numberOfInstallments); + default -> throw new UnsupportedOperationException(); }; }
