From 937098c4ee47ff7fad734403f8980ab8349b7e2c Mon Sep 17 00:00:00 2001 From: Peter Bagrij Date: Fri, 20 Oct 2023 13:57:11 +0200 Subject: [PATCH] FINERACT-1905 Loan Charge Reverse Replay with Advanced Payment Allocation --- .../portfolio/loanaccount/domain/Loan.java | 4 +- .../LoanRepaymentScheduleInstallment.java | 19 + ...oanRepaymentScheduleProcessingWrapper.java | 148 ++---- ...rgeRepaymentScheduleProcessingWrapper.java | 216 ++++++++ ...epaymentScheduleProcessingWrapperTest.java | 154 ++++++ ...edPaymentScheduleTransactionProcessor.java | 97 ++-- .../impl/ChargeOrTransaction.java | 88 ++++ .../impl/ChargeOrTransactionTest.java | 107 ++++ ...eOffWithAdvancedPaymentAllocationTest.java | 430 ++++++++------- ...playWithAdvancedPaymentAllocationTest.java | 492 ++++++++++++++++++ 10 files changed, 1430 insertions(+), 325 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java create mode 100644 fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index bc698e2bd..15cf20fee 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -694,8 +694,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { // store Id's of existing loan transactions and existing reversed loan // transactions - final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); - wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), getActiveCharges()); + final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge); updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, this); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 18f305957..89f0e2587 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -419,6 +419,15 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.penaltyAccrued = null; } + public void resetChargesFields() { + this.feeChargesCharged = null; + this.feeChargesWaived = null; + this.feeChargesWrittenOff = null; + this.penaltyCharges = null; + this.penaltyChargesWaived = null; + this.penaltyChargesWrittenOff = null; + } + public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); @@ -639,6 +648,16 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount()); } + public void addToChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff, + final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) { + this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount()); + this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount()); + this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount()); + this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount()); + this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount()); + this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount()); + } + public void updateAccrualPortion(final Money interest, final Money feeCharges, final Money penalityCharges) { this.interestAccrued = defaultToNullIfZero(interest.getAmount()); this.feeAccrued = defaultToNullIfZero(feeCharges.getAmount()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 644ae0585..4c032e3cd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -22,9 +22,11 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.jetbrains.annotations.NotNull; /** * A wrapper around loan schedule related data exposing needed behaviour by loan. @@ -52,18 +54,20 @@ public class LoanRepaymentScheduleProcessingWrapper { final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), - loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), - loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + final Money feeChargesWaivedForRepaymentPeriod = cumulativeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges, + currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); + final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeChargesWrittenOffWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), loanCharges, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(), - loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, - period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + final Money penaltyChargesWaivedForRepaymentPeriod = cumulativeChargesWaivedWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, + LoanCharge::isPenaltyCharge); + final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativeChargesWrittenOffWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, + LoanCharge::isPenaltyCharge); period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, @@ -81,24 +85,9 @@ public class LoanRepaymentScheduleProcessingWrapper { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - if (loanCharge.getChargeCalculation().isPercentageBased()) { - BigDecimal amount = BigDecimal.ZERO; - if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { - amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()) - .add(period.getInterestCharged(monetaryCurrency).getAmount()); - } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { - amount = amount.add(period.getInterestCharged(monetaryCurrency).getAmount()); - } else { - amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()); - } - BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); - cumulative = cumulative.plus(loanChargeAmt); - } else { - cumulative = cumulative.plus(loanCharge.amountOrPercentage()); - } + cumulative = cumulative.plus(getInstallmentFee(monetaryCurrency, period, loanCharge)); } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = cumulative.plus(loanCharge.chargeAmount()); } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { @@ -134,16 +123,15 @@ public class LoanRepaymentScheduleProcessingWrapper { return cumulative; } - private Money cumulativeFeeChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, - boolean isFirstPeriod) { + private Money cumulativeChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, + final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, boolean isFirstPeriod, + Predicate predicate) { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + if (predicate.test(loanCharge)) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); if (loanChargePerInstallment != null) { @@ -158,16 +146,15 @@ public class LoanRepaymentScheduleProcessingWrapper { return cumulative; } - private Money cumulativeFeeChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, + private Money cumulativeChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, - boolean isFirstPeriod) { + boolean isFirstPeriod, Predicate chargePredicate) { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + if (chargePredicate.test(loanCharge)) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); if (loanChargePerInstallment != null) { @@ -182,6 +169,15 @@ public class LoanRepaymentScheduleProcessingWrapper { return cumulative; } + private Predicate feeCharge() { + return loanCharge -> loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement(); + } + + private boolean loanChargeIsDue(LocalDate periodStart, LocalDate periodEnd, boolean isFirstPeriod, LoanCharge loanCharge) { + return isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + } + private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable, boolean isFirstPeriod) { @@ -190,24 +186,9 @@ public class LoanRepaymentScheduleProcessingWrapper { for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isPenaltyCharge()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - if (loanCharge.getChargeCalculation().isPercentageBased()) { - BigDecimal amount = BigDecimal.ZERO; - if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { - amount = amount.add(period.getPrincipal(currency).getAmount()) - .add(period.getInterestCharged(currency).getAmount()); - } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { - amount = amount.add(period.getInterestCharged(currency).getAmount()); - } else { - amount = amount.add(period.getPrincipal(currency).getAmount()); - } - BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); - cumulative = cumulative.plus(loanChargeAmt); - } else { - cumulative = cumulative.plus(loanCharge.amountOrPercentage()); - } + cumulative = cumulative.plus(getInstallmentFee(currency, period, loanCharge)); } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = cumulative.plus(loanCharge.chargeAmount()); } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { @@ -230,51 +211,28 @@ public class LoanRepaymentScheduleProcessingWrapper { return cumulative; } - private Money cumulativePenaltyChargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, - boolean isFirstPeriod) { - - Money cumulative = Money.zero(currency); - - for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isPenaltyCharge()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); - if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - if (loanChargePerInstallment != null) { - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); - } - } else if (isDue) { - cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); - } - } + private BigDecimal getInstallmentFee(MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge) { + if (loanCharge.getChargeCalculation().isPercentageBased()) { + BigDecimal amount = BigDecimal.ZERO; + amount = getBaseAmount(currency, period, loanCharge, amount); + return amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + } else { + return loanCharge.amountOrPercentage(); } - - return cumulative; } - private Money cumulativePenaltyChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, - final boolean isFirstPeriod) { - - Money cumulative = Money.zero(currency); - - for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isPenaltyCharge()) { - boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); - if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); - if (loanChargePerInstallment != null) { - cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); - } - } else if (isDue) { - cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency)); - } - } + @NotNull + private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge, + BigDecimal amount) { + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()) + .add(period.getInterestCharged(monetaryCurrency).getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(period.getInterestCharged(monetaryCurrency).getAmount()); + } else { + amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()); } - - return cumulative; + return amount; } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java new file mode 100644 index 000000000..c119dfb49 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java @@ -0,0 +1,216 @@ +/** + * 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.domain; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.function.Predicate; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.jetbrains.annotations.NotNull; + +/** + * A wrapper around loan schedule related data exposing needed behaviour by loan. + */ +public class SingleLoanChargeRepaymentScheduleProcessingWrapper { + + public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate, + final List repaymentPeriods, LoanCharge loanCharge) { + + Money totalInterest = Money.zero(currency); + Money totalPrincipal = Money.zero(currency); + for (final LoanRepaymentScheduleInstallment installment : repaymentPeriods) { + totalInterest = totalInterest.plus(installment.getInterestCharged(currency)); + totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); + } + LocalDate startDate = disbursementDate; + for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { + + if (!period.isDownPayment()) { + + boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream() + .filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber()) + .allMatch(LoanRepaymentScheduleInstallment::isDownPayment); + + final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency, + period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + final Money feeChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, period.getDueDate(), loanCharge, currency, + !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); + final Money feeChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, period.getDueDate(), loanCharge, + currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); + + final Money penaltyChargesDueForRepaymentPeriod = penaltyChargesDueWithin(startDate, period.getDueDate(), loanCharge, + currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), + isFirstNonDownPaymentPeriod); + final Money penaltyChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, period.getDueDate(), loanCharge, + currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, LoanCharge::isPenaltyCharge); + final Money penaltyChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, period.getDueDate(), + loanCharge, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, + LoanCharge::isPenaltyCharge); + + period.addToChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, + feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, + penaltyChargesWrittenOffForRepaymentPeriod); + + startDate = period.getDueDate(); + } + } + } + + private Money feeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final LoanCharge loanCharge, + final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, final Money totalPrincipal, + final Money totalInterest, boolean isInstallmentChargeApplicable, boolean isFirstPeriod) { + + if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); + if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { + return Money.of(monetaryCurrency, getInstallmentFee(monetaryCurrency, period, loanCharge)); + } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + return Money.of(monetaryCurrency, loanCharge.chargeAmount()); + } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + BigDecimal amount = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(totalPrincipal.getAmount()).add(totalInterest.getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(totalInterest.getAmount()); + } else { + // If charge type is specified due date and loan is + // multi disburment loan. + // Then we need to get as of this loan charge due date + // how much amount disbursed. + if (loanCharge.getLoan() != null && loanCharge.isSpecifiedDueDate() && loanCharge.getLoan().isMultiDisburmentLoan()) { + for (final LoanDisbursementDetails loanDisbursementDetails : loanCharge.getLoan().getDisbursementDetails()) { + if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { + amount = amount.add(loanDisbursementDetails.principal()); + } + } + } else { + amount = amount.add(totalPrincipal.getAmount()); + } + } + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + return Money.of(monetaryCurrency, loanChargeAmt); + } else if (isDue) { + return Money.of(monetaryCurrency, loanCharge.amount()); + } + } + return Money.zero(monetaryCurrency); + } + + private Money chargesWaivedWithin(final LocalDate periodStart, final LocalDate periodEnd, final LoanCharge loanCharge, + final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, boolean isFirstPeriod, + Predicate predicate) { + + if (predicate.test(loanCharge)) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); + if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + if (loanChargePerInstallment != null) { + return loanChargePerInstallment.getAmountWaived(currency); + } + } else if (isDue) { + return loanCharge.getAmountWaived(currency); + } + } + + return Money.zero(currency); + } + + private Money loanChargesWrittenOffWithin(final LocalDate periodStart, final LocalDate periodEnd, final LoanCharge loanCharge, + final MonetaryCurrency currency, boolean isInstallmentChargeApplicable, boolean isFirstPeriod, + Predicate chargePredicate) { + if (chargePredicate.test(loanCharge)) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); + if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + if (loanChargePerInstallment != null) { + return loanChargePerInstallment.getAmountWrittenOff(currency); + } + } else if (isDue) { + return loanCharge.getAmountWrittenOff(currency); + } + } + return Money.zero(currency); + } + + private Predicate feeCharge() { + return loanCharge -> loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement(); + } + + private boolean loanChargeIsDue(LocalDate periodStart, LocalDate periodEnd, boolean isFirstPeriod, LoanCharge loanCharge) { + return isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + } + + private Money penaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final LoanCharge loanCharge, + final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, final Money totalPrincipal, final Money totalInterest, + boolean isInstallmentChargeApplicable, boolean isFirstPeriod) { + + if (loanCharge.isPenaltyCharge()) { + boolean isDue = loanChargeIsDue(periodStart, periodEnd, isFirstPeriod, loanCharge); + if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { + return Money.of(currency, getInstallmentFee(currency, period, loanCharge)); + } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + return Money.of(currency, loanCharge.chargeAmount()); + } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + BigDecimal amount = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(totalPrincipal.getAmount()).add(totalInterest.getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(totalInterest.getAmount()); + } else { + amount = amount.add(totalPrincipal.getAmount()); + } + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + return Money.of(currency, loanChargeAmt); + } else if (isDue) { + return Money.of(currency, loanCharge.amount()); + } + } + + return Money.zero(currency); + } + + private BigDecimal getInstallmentFee(MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge) { + if (loanCharge.getChargeCalculation().isPercentageBased()) { + BigDecimal amount = BigDecimal.ZERO; + amount = getBaseAmount(currency, period, loanCharge, amount); + return amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + } else { + return loanCharge.amountOrPercentage(); + } + } + + @NotNull + private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge, + BigDecimal amount) { + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()) + .add(period.getInterestCharged(monetaryCurrency).getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(period.getInterestCharged(monetaryCurrency).getAmount()); + } else { + amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()); + } + return amount; + } + +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java new file mode 100644 index 000000000..31c2ca10f --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java @@ -0,0 +1,154 @@ +/** + * 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.domain; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +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.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest { + + private final SingleLoanChargeRepaymentScheduleProcessingWrapper underTest = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + private static final MockedStatic MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); + + private MonetaryCurrency currency = MonetaryCurrency.fromCurrencyData(new CurrencyData("USD")); + + private ArgumentCaptor feeChargesDue = ArgumentCaptor.forClass(Money.class); + private ArgumentCaptor feeChargesWaived = ArgumentCaptor.forClass(Money.class); + private ArgumentCaptor feeChargesWrittenOff = ArgumentCaptor.forClass(Money.class); + private ArgumentCaptor penaltyChargesDue = ArgumentCaptor.forClass(Money.class); + private ArgumentCaptor penaltyChargesWaived = ArgumentCaptor.forClass(Money.class); + private ArgumentCaptor penaltyChargesWrittenOff = ArgumentCaptor.forClass(Money.class); + + @BeforeAll + public static void init() { + MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + } + + @Test + public void testOnePeriodWithFeeCharge() { + LocalDate disbursementDate = LocalDate.of(2023, 01, 1); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate))); + + LoanRepaymentScheduleInstallment period = createPeriod(1, LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 30)); + LoanCharge loanCharge = createCharge(false); + + underTest.reprocess(currency, disbursementDate, List.of(period), loanCharge); + + verify(period, "10.0", "0.0", "0.0", "0.0", "0.0", "0.0"); + } + + @Test + public void testOnePeriodWithPenaltyCharge() { + LocalDate disbursementDate = LocalDate.of(2023, 01, 1); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate))); + + LoanRepaymentScheduleInstallment period = createPeriod(1, LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 30)); + LoanCharge loanCharge = createCharge(true); + + underTest.reprocess(currency, disbursementDate, List.of(period), loanCharge); + + verify(period, "0.0", "0.0", "0.0", "10.0", "0.0", "0.0"); + } + + @Test + public void testTwoPeriodsWithPenaltyCharge() { + LocalDate disbursementDate = LocalDate.of(2023, 01, 1); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, disbursementDate))); + + LoanRepaymentScheduleInstallment period1 = createPeriod(1, LocalDate.of(2023, 01, 1), LocalDate.of(2023, 01, 31)); + LoanRepaymentScheduleInstallment period2 = createPeriod(1, LocalDate.of(2023, 02, 1), LocalDate.of(2023, 02, 28)); + + LoanCharge loanCharge = createCharge(true); + + underTest.reprocess(currency, disbursementDate, List.of(period1, period2), loanCharge); + + verify(period1, "0.0", "0.0", "0.0", "10.0", "0.0", "0.0"); + verify(period2, "0.0", "0.0", "0.0", "0.0", "0.0", "0.0"); + } + + private void verify(LoanRepaymentScheduleInstallment period, String expectedFeeChargesDue, String expectedFeeChargesWaived, + String expectedFeeChargesWrittenOff, String expectedPenaltyChargesDue, String expectedPenaltyChargesWaived, + String expectedPenaltyChargesWrittenOff) { + + Mockito.verify(period, times(1)).addToChargePortion(feeChargesDue.capture(), feeChargesWaived.capture(), + feeChargesWrittenOff.capture(), penaltyChargesDue.capture(), penaltyChargesWaived.capture(), + penaltyChargesWrittenOff.capture()); + + Assertions.assertTrue(new BigDecimal(expectedFeeChargesDue).compareTo(feeChargesDue.getValue().getAmount()) == 0); + Assertions.assertTrue(new BigDecimal(expectedFeeChargesWaived).compareTo(feeChargesWaived.getValue().getAmount()) == 0); + Assertions.assertTrue(new BigDecimal(expectedFeeChargesWrittenOff).compareTo(feeChargesWrittenOff.getValue().getAmount()) == 0); + Assertions.assertTrue(new BigDecimal(expectedPenaltyChargesDue).compareTo(penaltyChargesDue.getValue().getAmount()) == 0); + Assertions.assertTrue(new BigDecimal(expectedPenaltyChargesWaived).compareTo(penaltyChargesWaived.getValue().getAmount()) == 0); + Assertions.assertTrue( + new BigDecimal(expectedPenaltyChargesWrittenOff).compareTo(penaltyChargesWrittenOff.getValue().getAmount()) == 0); + } + + @NotNull + private static LoanCharge createCharge(boolean penalty) { + Charge charge = mock(Charge.class); + when(charge.getId()).thenReturn(1L); + when(charge.getName()).thenReturn("charge a"); + when(charge.getCurrencyCode()).thenReturn("UDS"); + when(charge.isPenalty()).thenReturn(penalty); + LoanCharge loanCharge = new LoanCharge(null, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE, + ChargeCalculationType.FLAT, LocalDate.of(2023, 01, 15), ChargePaymentMode.REGULAR, 1, null, null); + return loanCharge; + } + + @NotNull + private LoanRepaymentScheduleInstallment createPeriod(int periodId, LocalDate start, LocalDate end) { + LoanRepaymentScheduleInstallment period = Mockito.mock(LoanRepaymentScheduleInstallment.class); + Mockito.when(period.getInstallmentNumber()).thenReturn(periodId); + Mockito.when(period.getFromDate()).thenReturn(start); + Mockito.when(period.getDueDate()).thenReturn(end); + Money principal = Money.of(currency, new BigDecimal("1000.0")); + Money interest = Money.of(currency, BigDecimal.ZERO); + + Mockito.when(period.getPrincipal(eq(currency))).thenReturn(principal); + Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest); + return period; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 76bfb0ca6..0f71ee189 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -37,13 +38,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; +import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Profile; //TODO: remove `test` profile when it is finished @@ -53,6 +55,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; + private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + @Override public String getCode() { return ADVANCED_PAYMENT_ALLOCATION_STRATEGY; @@ -91,10 +95,44 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep throw new NotImplementedException(); } + private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Set charges, + ChangedTransactionDetail changedTransactionDetail) { + if (loanTransaction.getId() == null) { + processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency)); + loanTransaction.adjustInterestComponent(currency); + } else { + /* + * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has + * changed.
+ */ + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); + + // Reset derived component of new loan transaction and + // re-process transaction + processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency)); + newLoanTransaction.adjustInterestComponent(currency); + /* + * Check if the transaction amounts have changed. If so, reverse the original transaction and update + * changedTransactionDetail accordingly + */ + if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { + loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( + newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); + } else { + createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + } + } + } + + private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currency, List installments, + LocalDate disbursementDate) { + loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge); + } + @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, - List transactionsPostDisbursement, MonetaryCurrency currency, - List installments, Set charges) { + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { // TODO: rewrite this whole logic step by step if (charges != null) { @@ -107,44 +145,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { currentInstallment.resetDerivedComponents(); + currentInstallment.resetChargesFields(); currentInstallment.updateDerivedFields(currency, disbursementDate); } - // TODO: Remove this reprocess and add the charges to the installment in chronological order - final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); - wrapper.reprocess(currency, disbursementDate, installments, charges); - final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); - for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { - if (loanTransaction.getId() == null) { - processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency)); - loanTransaction.adjustInterestComponent(currency); - } else { - /** - * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has - * changed.
- **/ - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); + List chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges); - // Reset derived component of new loan transaction and - // re-process transaction - processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency)); - newLoanTransaction.adjustInterestComponent(currency); - /** - * Check if the transaction amounts have changed. If so, reverse the original transaction and update - * changedTransactionDetail accordingly - **/ - if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { - loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( - newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); - } else { - createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); - } - } + final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) { + chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency, + installments, charges, changedTransactionDetail)); + chargeOrTransaction.getLoanCharge() + .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } reprocessInstallments(installments, currency); return changedTransactionDetail; } + @NotNull + private List createSortedChargesAndTransactionsList(List loanTransactions, + Set charges) { + List chargeOrTransactions = new ArrayList<>(); + if (charges != null) { + chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList()); + } + if (loanTransactions != null) { + chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList()); + } + Collections.sort(chargeOrTransactions); + return chargeOrTransactions; + } + @Override public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Set charges, Money overpaidAmount) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java new file mode 100644 index 000000000..34f677e69 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java @@ -0,0 +1,88 @@ +/** + * 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.domain.transactionprocessor.impl; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Optional; +import lombok.Getter; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.jetbrains.annotations.NotNull; + +@Getter +public class ChargeOrTransaction implements Comparable { + + private final Optional loanCharge; + private final Optional loanTransaction; + + public ChargeOrTransaction(LoanCharge loanCharge) { + this.loanCharge = Optional.of(loanCharge); + this.loanTransaction = Optional.empty(); + } + + public ChargeOrTransaction(LoanTransaction loanTransaction) { + this.loanTransaction = Optional.of(loanTransaction); + this.loanCharge = Optional.empty(); + } + + private LocalDate getEffectiveDate() { + if (loanCharge.isPresent()) { + return loanCharge.get().getDueDate(); + } else if (loanTransaction.isPresent()) { + return loanTransaction.get().getTransactionDate(); + } else { + throw new RuntimeException("Either charge or transaction should be present"); + } + } + + private LocalDate getSubmittedOnDate() { + if (loanCharge.isPresent()) { + return loanCharge.get().getSubmittedOnDate(); + } else if (loanTransaction.isPresent()) { + return loanTransaction.get().getSubmittedOnDate(); + } else { + throw new RuntimeException("Either charge or transaction should be present"); + } + } + + private OffsetDateTime getCreatedDateTime() { + if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) { + return loanCharge.get().getCreatedDate().get(); + } else if (loanTransaction.isPresent()) { + return loanTransaction.get().getCreatedDateTime(); + } else { + throw new RuntimeException("Either charge with createdDate or transaction created datetime should be present"); + } + } + + @Override + public int compareTo(@NotNull ChargeOrTransaction o) { + int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate()); + if (datePortion == 0) { + int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate()); + if (submittedDate == 0) { + return this.getCreatedDateTime().compareTo(o.getCreatedDateTime()); + } + return submittedDate; + } + return datePortion; + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java new file mode 100644 index 000000000..892fc6547 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java @@ -0,0 +1,107 @@ +/** + * 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.domain.transactionprocessor.impl; + +import com.google.common.collect.Collections2; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class ChargeOrTransactionTest { + + @Test + public void testCompareToEqual() { + ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) == 0); + Assertions.assertTrue(transaction.compareTo(charge) == 0); + } + + @Test + public void testCompareToCreatedDateTime() { + ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); + ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testCompareToSubmittedOnDate() { + ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorEffectiveDate() { + ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorOnDifferentSubmittedDay() { + ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00"); + ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + @Test + public void testComparatorOnSameDay() { + ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) { + LoanCharge charge = Mockito.mock(LoanCharge.class); + Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); + Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime))); + return new ChargeOrTransaction(charge); + } + + private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) { + LoanTransaction transaction = Mockito.mock(LoanTransaction.class); + Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate)); + Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime)); + return new ChargeOrTransaction(transaction); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java index e6d32ab67..2523a58b6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.integrationtests; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -39,6 +42,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AllowAttributeOverrides; +import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.ChargeData; import org.apache.fineract.client.models.ChargeToGLAccountMapper; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; @@ -55,7 +59,9 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostPaymentTypesRequest; import org.apache.fineract.client.models.PostPaymentTypesResponse; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.PaymentTypeHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; @@ -88,6 +94,8 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest { private AccountHelper accountHelper; private LoanProductHelper loanProductHelper; private PaymentTypeHelper paymentTypeHelper; + private final BusinessDateHelper businessDateHelper = new BusinessDateHelper(); + private static final String DATETIME_PATTERN = "dd MMMM yyyy"; // asset private Account loansReceivable; private Account interestFeeReceivable; @@ -212,72 +220,76 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest { // Reverse Replay of Charge-Off @Test public void loanChargeOffReverseReplayWithAdvancedPaymentStrategyTest() { - String loanExternalIdStr = UUID.randomUUID().toString(); - final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); - final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); - final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + runAt("9 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); - // apply charges - Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, - ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); - LocalDate targetDate = LocalDate.of(2022, 9, 5); - final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); - Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, - LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); - // apply penalty - Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, - ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); - final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate); + final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate); - Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, - LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); - // make Repayment - final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, - new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en") - .transactionAmount(10.0)); + // make Repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en") + .transactionAmount(10.0)); - GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); - // set loan as chargeoff - String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); - Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); - String transactionExternalId = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan((long) loanId, - new PostLoansLoanIdTransactionsRequest().transactionDate("10 September 2022").locale("en").dateFormat("dd MMMM yyyy") - .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + // set loan as chargeoff + updateBusinessDate("10 September 2022"); + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + String transactionExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan((long) loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate("10 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); - loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - // verify amounts for charge-off transaction - verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff"); + // verify amounts for charge-off transaction + verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff"); - Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); + Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); - // reverse Repayment - loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "11 September 2022"); + // reverse Repayment + updateBusinessDate("11 September 2022"); + loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "11 September 2022"); - // verify chargeOffTransaction gets reverse replayed + // verify chargeOffTransaction gets reverse replayed - GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper - .getLoanTransactionDetails((long) loanId, transactionExternalId); - assertNotNull(getLoansTransactionResponse); - assertNotNull(getLoansTransactionResponse.getTransactionRelations()); + GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper + .getLoanTransactionDetails((long) loanId, transactionExternalId); + assertNotNull(getLoansTransactionResponse); + assertNotNull(getLoansTransactionResponse.getTransactionRelations()); - // test replayed relationship - GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); - assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); - assertEquals("REPLAYED", transactionRelation.getRelationType()); - - // verify amounts for charge-off transaction - verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 0.0f, 10.0f, 10.0f, loanId, "chargeoff"); + // test replayed relationship + GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); + assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); + assertEquals("REPLAYED", transactionRelation.getRelationType()); + // verify amounts for charge-off transaction + verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 0.0f, 10.0f, 10.0f, loanId, "chargeoff"); + }); } // undo Charge-Off @@ -329,202 +341,211 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest { // Backdated repayment transaction, Reverse replay of charge off @Test public void postChargeOffAddBackdatedTransactionAndReverseReplayTest() { - String loanExternalIdStr = UUID.randomUUID().toString(); - final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); - final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); - final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + runAt("3 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); - // apply charges - Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, - ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + // apply charges + updateBusinessDate("5 September 2022"); + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); - LocalDate targetDate = LocalDate.of(2022, 9, 5); - final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); - Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, - LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); - // set loan as chargeoff - String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); - Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); - String transactionExternalId = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId, - new PostLoansLoanIdTransactionsRequest().transactionDate("14 September 2022").locale("en").dateFormat("dd MMMM yyyy") - .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + // set loan as chargeoff + updateBusinessDate("14 September 2022"); + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + String transactionExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate("14 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); + Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); - // verify Journal Entries For ChargeOff Transaction - GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper - .getJournalEntries("L" + chargeOffTransaction.getResourceId().toString()); + // verify Journal Entries For ChargeOff Transaction + GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper + .getJournalEntries("L" + chargeOffTransaction.getResourceId().toString()); - assertNotNull(journalEntriesForChargeOff); - List journalEntries = journalEntriesForChargeOff.getPageItems(); - assertEquals(4, journalEntries.size()); + assertNotNull(journalEntriesForChargeOff); + List journalEntries = journalEntriesForChargeOff.getPageItems(); + assertEquals(4, journalEntries.size()); - verifyJournalEntry(journalEntries.get(3), 1000.0, LocalDate.of(2022, 9, 14), loansReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(2), 10.0, LocalDate.of(2022, 9, 14), interestFeeReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(1), 1000.0, LocalDate.of(2022, 9, 14), creditLossBadDebt, "DEBIT"); - verifyJournalEntry(journalEntries.get(0), 10.0, LocalDate.of(2022, 9, 14), feeChargeOff, "DEBIT"); + verifyJournalEntry(journalEntries.get(3), 1000.0, LocalDate.of(2022, 9, 14), loansReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(2), 10.0, LocalDate.of(2022, 9, 14), interestFeeReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(1), 1000.0, LocalDate.of(2022, 9, 14), creditLossBadDebt, "DEBIT"); + verifyJournalEntry(journalEntries.get(0), 10.0, LocalDate.of(2022, 9, 14), feeChargeOff, "DEBIT"); - // make Repayment before chargeoff date - final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, - new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") - .transactionAmount(100.0)); + // make Repayment before chargeoff date - business date is still on 14 September 2022 + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") + .transactionAmount(100.0)); - loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - // verify Journal Entries for Repayment transaction + // verify Journal Entries for Repayment transaction - GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper - .getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); - assertNotNull(journalEntriesForRepayment); + GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper + .getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); + assertNotNull(journalEntriesForRepayment); - journalEntries = journalEntriesForRepayment.getPageItems(); - assertEquals(3, journalEntries.size()); + journalEntries = journalEntriesForRepayment.getPageItems(); + assertEquals(3, journalEntries.size()); - verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); + verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); - // verify reverse replay of Charge-Off + // verify reverse replay of Charge-Off - GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper - .getLoanTransactionDetails((long) loanId, transactionExternalId); - assertNotNull(getLoansTransactionResponse); - assertNotNull(getLoansTransactionResponse.getTransactionRelations()); + GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper + .getLoanTransactionDetails((long) loanId, transactionExternalId); + assertNotNull(getLoansTransactionResponse); + assertNotNull(getLoansTransactionResponse.getTransactionRelations()); - // test replayed relationship - GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); - assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); - assertEquals("REPLAYED", transactionRelation.getRelationType()); + // test replayed relationship + GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); + assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); + assertEquals("REPLAYED", transactionRelation.getRelationType()); - // verify amounts for charge-off transaction - verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 0.0f, 0.0f, loanId, "chargeoff"); + // verify amounts for charge-off transaction + verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 0.0f, 0.0f, loanId, "chargeoff"); - // make Repayment after chargeoff date - final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, - new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("15 September 2022").locale("en") - .transactionAmount(100.0)); + // make Repayment after chargeoff date + updateBusinessDate("15 September 2022"); + final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("15 September 2022").locale("en") + .transactionAmount(100.0)); - loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - // verify Journal Entries for Repayment transaction - journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction_1.getResourceId().toString()); + // verify Journal Entries for Repayment transaction + journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction_1.getResourceId().toString()); - assertNotNull(journalEntriesForRepayment); + assertNotNull(journalEntriesForRepayment); - journalEntries = journalEntriesForRepayment.getPageItems(); - assertEquals(2, journalEntries.size()); + journalEntries = journalEntriesForRepayment.getPageItems(); + assertEquals(2, journalEntries.size()); - verifyJournalEntry(journalEntries.get(1), 100.0, LocalDate.of(2022, 9, 15), recoveries, "CREDIT"); - verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 15), suspenseClearingAccount, "DEBIT"); + verifyJournalEntry(journalEntries.get(1), 100.0, LocalDate.of(2022, 9, 15), recoveries, "CREDIT"); + verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 15), suspenseClearingAccount, "DEBIT"); + }); } // Repayment before charge off on charge off date, reverse replay of charge off @Test public void transactionOnChargeOffDateReverseTest() { - String loanExternalIdStr = UUID.randomUUID().toString(); - final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); - final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); - final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + runAt("7 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy(); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); - // apply charges - Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, - ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); - LocalDate targetDate = LocalDate.of(2022, 9, 5); - final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); - Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, - LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); - // make Repayment before charge-off on charge off date - final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, - new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") - .transactionAmount(100.0)); + // make Repayment before charge-off on charge off date + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") + .transactionAmount(100.0)); - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); - // verify Journal Entries for Repayment transaction - GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper - .getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); + // verify Journal Entries for Repayment transaction + GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper + .getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); - assertNotNull(journalEntriesForRepayment); + assertNotNull(journalEntriesForRepayment); - List journalEntries = journalEntriesForRepayment.getPageItems(); - assertEquals(3, journalEntries.size()); + List journalEntries = journalEntriesForRepayment.getPageItems(); + assertEquals(3, journalEntries.size()); - verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); + verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); - // set loan as chargeoff - String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); - Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); - String transactionExternalId = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId, - new PostLoansLoanIdTransactionsRequest().transactionDate("7 September 2022").locale("en").dateFormat("dd MMMM yyyy") - .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + // set loan as chargeoff + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + String transactionExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate("7 September 2022").locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); - loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); + Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId(); - // verify Journal Entries For ChargeOff Transaction - GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper - .getJournalEntries("L" + chargeOffTransaction.getResourceId().toString()); + // verify Journal Entries For ChargeOff Transaction + GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper + .getJournalEntries("L" + chargeOffTransaction.getResourceId().toString()); - assertNotNull(journalEntriesForChargeOff); - journalEntries = journalEntriesForChargeOff.getPageItems(); - assertEquals(2, journalEntries.size()); + assertNotNull(journalEntriesForChargeOff); + journalEntries = journalEntriesForChargeOff.getPageItems(); + assertEquals(2, journalEntries.size()); - verifyJournalEntry(journalEntries.get(1), 910.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(0), 910.0, LocalDate.of(2022, 9, 7), creditLossBadDebt, "DEBIT"); + verifyJournalEntry(journalEntries.get(1), 910.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(0), 910.0, LocalDate.of(2022, 9, 7), creditLossBadDebt, "DEBIT"); - // reverse Repayment - loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "7 September 2022"); - loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertTrue(loanDetails.getStatus().getActive()); - assertTrue(loanDetails.getChargedOff()); + // reverse Repayment + loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "7 September 2022"); + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); - // verify Journal Entries for Reversed Repayment transaction - journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); - assertNotNull(journalEntriesForRepayment); + // verify Journal Entries for Reversed Repayment transaction + journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction.getResourceId().toString()); + assertNotNull(journalEntriesForRepayment); - journalEntries = journalEntriesForRepayment.getPageItems(); - assertEquals(6, journalEntries.size()); + journalEntries = journalEntriesForRepayment.getPageItems(); + assertEquals(6, journalEntries.size()); - verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); - verifyJournalEntry(journalEntries.get(3), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); - verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "DEBIT"); - verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "DEBIT"); - verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "CREDIT"); + verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT"); + verifyJournalEntry(journalEntries.get(3), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT"); + verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "DEBIT"); + verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "DEBIT"); + verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "CREDIT"); - // verify reverse replay of Charge-Off + // verify reverse replay of Charge-Off - GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper - .getLoanTransactionDetails((long) loanId, transactionExternalId); - assertNotNull(getLoansTransactionResponse); - assertNotNull(getLoansTransactionResponse.getTransactionRelations()); + GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper + .getLoanTransactionDetails((long) loanId, transactionExternalId); + assertNotNull(getLoansTransactionResponse); + assertNotNull(getLoansTransactionResponse.getTransactionRelations()); - // test replayed relationship - GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); - assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); - assertEquals("REPLAYED", transactionRelation.getRelationType()); + // test replayed relationship + GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next(); + assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction()); + assertEquals("REPLAYED", transactionRelation.getRelationType()); - // verify amounts for charge-off transaction - verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff"); + // verify amounts for charge-off transaction + verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff"); + }); } @@ -756,4 +777,23 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest { return paymentAllocationOrder; }).toList(); } + + private void runAt(String date, Runnable runnable) { + try { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE); + businessDateHelper.updateBusinessDate( + new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); + runnable.run(); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false); + } + } + + private void updateBusinessDate(String date) { + businessDateHelper.updateBusinessDate( + new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java new file mode 100644 index 000000000..0334570cb --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest.java @@ -0,0 +1,492 @@ +/** + * 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.integrationtests; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.AllowAttributeOverrides; +import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.ChargeData; +import org.apache.fineract.client.models.ChargeToGLAccountMapper; +import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; +import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PaymentAllocationOrder; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostPaymentTypesRequest; +import org.apache.fineract.client.models.PostPaymentTypesResponse; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.PaymentTypeHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.funds.FundsHelper; +import org.apache.fineract.integrationtests.common.funds.FundsResourceHandler; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductHelper; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(LoanTestLifecycleExtension.class) +public class LoanAccountChargeReveseReplayWithAdvancedPaymentAllocationTest { + + private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private ClientHelper clientHelper; + private LoanTransactionHelper loanTransactionHelper; + private JournalEntryHelper journalEntryHelper; + private AccountHelper accountHelper; + private LoanProductHelper loanProductHelper; + private PaymentTypeHelper paymentTypeHelper; + private final BusinessDateHelper businessDateHelper = new BusinessDateHelper(); + private static final String DATETIME_PATTERN = "dd MMMM yyyy"; + // asset + private Account loansReceivable; + private Account interestFeeReceivable; + private Account suspenseAccount; + private Account fundReceivables; + // liability + private Account suspenseClearingAccount; + private Account overpaymentAccount; + // income + private Account interestIncome; + private Account feeIncome; + private Account feeChargeOff; + private Account recoveries; + private Account interestIncomeChargeOff; + // expense + private Account creditLossBadDebt; + private Account creditLossBadDebtFraud; + private Account writtenOff; + private Account goodwillExpenseAccount; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + this.loanProductHelper = new LoanProductHelper(); + this.paymentTypeHelper = new PaymentTypeHelper(); + + // Asset + this.loansReceivable = this.accountHelper.createAssetAccount(); + this.interestFeeReceivable = this.accountHelper.createAssetAccount(); + this.suspenseAccount = this.accountHelper.createAssetAccount(); + this.fundReceivables = this.accountHelper.createAssetAccount(); + + // Liability + this.suspenseClearingAccount = this.accountHelper.createLiabilityAccount(); + this.overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + // income + this.interestIncome = this.accountHelper.createIncomeAccount(); + this.feeIncome = this.accountHelper.createIncomeAccount(); + this.feeChargeOff = this.accountHelper.createIncomeAccount(); + this.recoveries = this.accountHelper.createIncomeAccount(); + this.interestIncomeChargeOff = this.accountHelper.createIncomeAccount(); + + // expense + this.creditLossBadDebt = this.accountHelper.createExpenseAccount(); + this.creditLossBadDebtFraud = this.accountHelper.createExpenseAccount(); + this.writtenOff = this.accountHelper.createExpenseAccount(); + this.goodwillExpenseAccount = this.accountHelper.createExpenseAccount(); + + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void testLoanChargeReverseReplayWithAdvancedPaymentStrategy() { + runAt("10 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccounting(true); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr, true, "02 September 2022", + "03 September 2022"); + + // make an in advance repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en") + .transactionAmount(100.0)); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 9); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + + final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "20")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertNotNull(loanDetails.getRepaymentSchedule()); + assertNotNull(loanDetails.getRepaymentSchedule().getPeriods()); + assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(20.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(10.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(900.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(930.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod()); + }); + } + + @Test + public void testLoanChargeReverseReplayWithStandardPaymentStrategy() { + runAt("10 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccounting(false); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr, false, "02 September 2022", + "03 September 2022"); + + // make an in advance repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en") + .transactionAmount(100.0)); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 9); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + + final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "20")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertNotNull(loanDetails.getRepaymentSchedule()); + assertNotNull(loanDetails.getRepaymentSchedule().getPeriods()); + assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(930.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(930.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod()); + }); + } + + @Test + public void testRepaymentReverseReplayedOnBackdatedChargeWithAdvancedPaymentStrategy() { + runAt("1 September 2022", () -> { + String loanExternalIdStr = UUID.randomUUID().toString(); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccounting(true); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr, true, "1 September 2022", + "1 September 2022"); + + // make a repayment on 3rd od Sept + updateBusinessDate("3 September 2022"); + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("3 September 2022").locale("en") + .transactionAmount(100.0)); + + // apply charges on 4th of Sept backdated to 2nd of Sept 2022 + updateBusinessDate("4 September 2022"); + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 2); + final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate); + Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + + final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "20")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertNotNull(loanDetails.getRepaymentSchedule()); + assertNotNull(loanDetails.getRepaymentSchedule().getPeriods()); + assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(930.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(930.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalOutstandingForPeriod()); + }); + } + + private Integer createLoanAccount(final Integer clientID, final Integer loanProductID, final String externalId, + final boolean advancedPaymentStrategy, String approveDate, String disbursementDate) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("30") + .withLoanTermFrequencyAsDays().withNumberOfRepayments("1").withRepaymentEveryAfter("30").withRepaymentFrequencyTypeAsDays() + .withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("03 September 2022") + .withSubmittedOnDate("01 September 2022").withLoanType("individual").withExternalId(externalId) + .withRepaymentStrategy(advancedPaymentStrategy ? "advanced-payment-allocation-strategy" : "mifos-standard-strategy") + .build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan(approveDate, "1000", loanId, null); + loanTransactionHelper.disburseLoanWithTransactionAmount(disbursementDate, loanId, "1000"); + return loanId; + } + + private Integer createLoanProductWithPeriodicAccrualAccounting(boolean advancedPaymentStrategy) { + + String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6); + String shortName = Utils.uniqueRandomStringGenerator("", 4); + + List principalVariationsForBorrowerCycle = new ArrayList<>(); + List numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>(); + List interestRateVariationsForBorrowerCycle = new ArrayList<>(); + List charges = new ArrayList<>(); + List penaltyToIncomeAccountMappings = new ArrayList<>(); + List feeToIncomeAccountMappings = new ArrayList<>(); + + String paymentTypeName = PaymentTypeHelper.randomNameGenerator("P_T", 5); + String description = PaymentTypeHelper.randomNameGenerator("PT_Desc", 15); + Boolean isCashPayment = false; + Integer position = 1; + + PostPaymentTypesResponse paymentTypesResponse = paymentTypeHelper.createPaymentType(new PostPaymentTypesRequest() + .name(paymentTypeName).description(description).isCashPayment(isCashPayment).position(position)); + Long paymentTypeIdOne = paymentTypesResponse.getResourceId(); + Assertions.assertNotNull(paymentTypeIdOne); + + List paymentChannelToFundSourceMappings = new ArrayList<>(); + GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings(); + loanPaymentChannelToFundSourceMappings.fundSourceAccountId(fundReceivables.getAccountID().longValue()); + loanPaymentChannelToFundSourceMappings.paymentTypeId(paymentTypeIdOne.longValue()); + paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings); + + // fund + FundsHelper fh = FundsHelper.create(Utils.uniqueRandomStringGenerator("", 10)).externalId(UUID.randomUUID().toString()).build(); + String jsonData = fh.toJSON(); + + final Long fundID = createFund(jsonData, this.requestSpec, this.responseSpec); + Assertions.assertNotNull(fundID); + + // Delinquency Bucket + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + + String futureInstallmentAllocationRule = "NEXT_INSTALLMENT"; + + PostLoanProductsRequest loanProductsRequest = new PostLoanProductsRequest().name(name)// + .shortName(shortName)// + .description("Loan Product Description")// + .fundId(fundID)// + .startDate(null)// + .closeDate(null)// + .includeInBorrowerCycle(false)// + .currencyCode("USD")// + .digitsAfterDecimal(2)// + .inMultiplesOf(0)// + .installmentAmountInMultiplesOf(1)// + .useBorrowerCycle(false)// + .minPrincipal(100.0)// + .principal(1000.0)// + .maxPrincipal(10000.0)// + .minNumberOfRepayments(1)// + .numberOfRepayments(1)// + .maxNumberOfRepayments(30)// + .isLinkedToFloatingInterestRates(false)// + .minInterestRatePerPeriod((double) 0)// + .interestRatePerPeriod((double) 0)// + .maxInterestRatePerPeriod((double) 0)// + .interestRateFrequencyType(2)// + .repaymentEvery(30)// + .repaymentFrequencyType(0)// + .principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)// + .numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)// + .interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)// + .amortizationType(1)// + .interestType(0)// + .isEqualAmortization(false)// + .interestCalculationPeriodType(1)// + .transactionProcessingStrategyCode("mifos-standard-strategy").daysInYearType(1)// + .daysInMonthType(1)// + .canDefineInstallmentAmount(true)// + .graceOnArrearsAgeing(3)// + .overdueDaysForNPA(179)// + .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)// + .principalThresholdForLastInstallment(50)// + .allowVariableInstallments(false)// + .canUseForTopup(false)// + .isInterestRecalculationEnabled(false)// + .holdGuaranteeFunds(false)// + .multiDisburseLoan(true)// + .allowAttributeOverrides(new AllowAttributeOverrides()// + .amortizationType(true)// + .interestType(true)// + .transactionProcessingStrategyCode(true)// + .interestCalculationPeriodType(true)// + .inArrearsTolerance(true)// + .repaymentEvery(true)// + .graceOnPrincipalAndInterestPayment(true)// + .graceOnArrearsAgeing(true))// + .allowPartialPeriodInterestCalcualtion(true)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .charges(charges)// + .accountingRule(3)// + .fundSourceAccountId(suspenseClearingAccount.getAccountID().longValue())// + .loanPortfolioAccountId(loansReceivable.getAccountID().longValue())// + .transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())// + .interestOnLoanAccountId(interestIncome.getAccountID().longValue())// + .incomeFromFeeAccountId(feeIncome.getAccountID().longValue())// + .incomeFromPenaltyAccountId(feeIncome.getAccountID().longValue())// + .incomeFromRecoveryAccountId(recoveries.getAccountID().longValue())// + .writeOffAccountId(writtenOff.getAccountID().longValue())// + .overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())// + .receivableInterestAccountId(interestFeeReceivable.getAccountID().longValue())// + .receivableFeeAccountId(interestFeeReceivable.getAccountID().longValue())// + .receivablePenaltyAccountId(interestFeeReceivable.getAccountID().longValue())// + .dateFormat("dd MMMM yyyy")// + .locale("en_GB")// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("percentage")// + .overAppliedNumber(50)// + .delinquencyBucketId(delinquencyBucketId.longValue())// + .goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())// + .incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOff.getAccountID().longValue())// + .incomeFromGoodwillCreditFeesAccountId(feeChargeOff.getAccountID().longValue())// + .incomeFromGoodwillCreditPenaltyAccountId(feeChargeOff.getAccountID().longValue())// + .paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)// + .penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)// + .feeToIncomeAccountMappings(feeToIncomeAccountMappings)// + .incomeFromChargeOffInterestAccountId(interestIncomeChargeOff.getAccountID().longValue())// + .incomeFromChargeOffFeesAccountId(feeChargeOff.getAccountID().longValue())// + .chargeOffExpenseAccountId(creditLossBadDebt.getAccountID().longValue())// + .chargeOffFraudExpenseAccountId(creditLossBadDebtFraud.getAccountID().longValue())// + .incomeFromChargeOffPenaltyAccountId(feeChargeOff.getAccountID().longValue());// + + if (advancedPaymentStrategy) { + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(futureInstallmentAllocationRule); + + loanProductsRequest // + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy")// + .addPaymentAllocationItem(defaultAllocation); + } + + PostLoanProductsResponse loanProductCreateResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + return loanProductCreateResponse.getResourceId().intValue(); + } + + private Long createFund(final String fundJSON, final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { + String fundId = String.valueOf(FundsResourceHandler.createFund(fundJSON, requestSpec, responseSpec)); + if (fundId.equals("null")) { + // Invalid JSON data parameters + return null; + } + + return Long.valueOf(fundId); + } + + private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) { + AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); + advancedPaymentData.setTransactionType("DEFAULT"); + advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule); + + List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, + PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, + PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, + PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, + PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST); + + advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); + return advancedPaymentData; + } + + private List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { + AtomicInteger integer = new AtomicInteger(1); + return Arrays.stream(paymentAllocationTypes).map(pat -> { + PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder(); + paymentAllocationOrder.setPaymentAllocationRule(pat.name()); + paymentAllocationOrder.setOrder(integer.getAndIncrement()); + return paymentAllocationOrder; + }).toList(); + } + + private void runAt(String date, Runnable runnable) { + try { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE); + businessDateHelper.updateBusinessDate( + new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); + runnable.run(); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false); + } + } + + private void updateBusinessDate(String date) { + businessDateHelper.updateBusinessDate( + new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); + } + +}