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 a8614c9a4..796ccd37f 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 @@ -779,14 +779,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .determineProcessor(this.transactionProcessingStrategyCode); final List chargePaymentInstallments = new ArrayList<>(); List installments = getRepaymentScheduleInstallments(); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper + .fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments); for (final LoanRepaymentScheduleInstallment installment : installments) { - boolean isDue = installment.isFirstPeriod() + boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber) ? charge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate()) : charge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate()); - if (installmentNumber == null && isDue) { + if (installmentNumber == null && isFirstNormalInstallment) { chargePaymentInstallments.add(installment); break; - } else if (installmentNumber != null && installment.getInstallmentNumber().equals(installmentNumber)) { + } else if (installment.getInstallmentNumber().equals(installmentNumber)) { chargePaymentInstallments.add(installment); break; } @@ -1347,7 +1349,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LocalDate transactionDateForRange = isBasedOnSubmittedOnDate ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getDueDate() : loanTransaction.getTransactionDate(); - if (installment.isInPeriod(transactionDateForRange)) { + boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); + if (isInPeriod) { interest = interest.plus(loanTransaction.getInterestPortion(getCurrency())); fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency())); penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency())); @@ -6641,13 +6644,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money paidFromFutureInstallments = Money.zero(currency); Money fee = Money.zero(currency); Money penalty = Money.zero(currency); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper + .fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments); + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) { interest = interest.plus(installment.getInterestOutstanding(currency)); penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency)); fee = fee.plus(installment.getFeeChargesOutstanding(currency)); } else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) { - Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment); + Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment, + isFirstNormalInstallment); if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) { interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]); } else { @@ -6680,7 +6688,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency, - final LoanRepaymentScheduleInstallment installment) { + final LoanRepaymentScheduleInstallment installment, boolean isFirstNormalInstallment) { Money penaltyForCurrentPeriod = Money.zero(getCurrency()); Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency()); Money feeForCurrentPeriod = Money.zero(getCurrency()); @@ -6694,7 +6702,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()).plus(installment.getInterestPaid(getCurrency())); for (LoanCharge loanCharge : this.charges) { if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = installment.isFirstPeriod() + boolean isDue = isFirstNormalInstallment ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate); if (isDue) { @@ -6734,7 +6742,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money[] balances = new Money[3]; final MonetaryCurrency currency = getCurrency(); balances[0] = balances[1] = balances[2] = Money.zero(currency); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper + .fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments); for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); if (DateUtils.isEqual(paymentDate, installment.getDueDate())) { Money interest = installment.getInterestCharged(currency); Money fee = installment.getFeeChargesCharged(currency); @@ -6745,7 +6756,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { break; } else if (DateUtils.isAfter(paymentDate, installment.getFromDate()) && DateUtils.isBefore(paymentDate, installment.getDueDate())) { - balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment); + balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment, isFirstNormalInstallment); break; } } 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 0e46345cf..1431c8a8f 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 @@ -960,15 +960,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.additional = true; } - public boolean isFirstPeriod() { - return (this.installmentNumber == 1); - } - - public boolean isInPeriod(LocalDate date) { - return (isFirstPeriod() ? !DateUtils.isBefore(date, getFromDate()) : DateUtils.isAfter(date, getFromDate())) - && !DateUtils.isAfter(date, getDueDate()); - } - public Set getLoanTransactionToRepaymentScheduleMappings() { return this.loanTransactionToRepaymentScheduleMappings; } 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 4c032e3cd..6444da3e0 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 @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -43,13 +44,14 @@ public class LoanRepaymentScheduleProcessingWrapper { totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } LocalDate startDate = disbursementDate; + LoanRepaymentScheduleInstallment firstNormalPeriod = repaymentPeriods.stream() + .sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)) + .filter(repaymentPeriod -> !repaymentPeriod.isDownPayment()).findFirst().orElseThrow(); for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { if (!period.isDownPayment()) { - boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream() - .filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber()) - .allMatch(LoanRepaymentScheduleInstallment::isDownPayment); + boolean isFirstNonDownPaymentPeriod = period.equals(firstNormalPeriod); final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), @@ -235,4 +237,18 @@ public class LoanRepaymentScheduleProcessingWrapper { return amount; } + public static int fetchFirstNormalInstallmentNumber(List installments) { + return installments.stream().sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)) + .filter(repaymentPeriod -> !repaymentPeriod.isDownPayment()).findFirst().orElseThrow().getInstallmentNumber(); + } + + public static boolean isInPeriod(LocalDate transactionDate, LoanRepaymentScheduleInstallment targetInstallment, + List installments) { + int firstPeriod = fetchFirstNormalInstallmentNumber(installments); + return (targetInstallment.getInstallmentNumber().equals(firstPeriod) + ? !DateUtils.isBefore(transactionDate, targetInstallment.getFromDate()) + : DateUtils.isAfter(transactionDate, targetInstallment.getFromDate())) + && !DateUtils.isAfter(transactionDate, targetInstallment.getDueDate()); + } + } 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 index c119dfb49..506ae8a10 100644 --- 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 @@ -42,13 +42,11 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper { totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } LocalDate startDate = disbursementDate; + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods); for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { if (!period.isDownPayment()) { - - boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream() - .filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber()) - .allMatch(LoanRepaymentScheduleInstallment::isDownPayment); + boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber); final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 331fd8878..0f8dc6bfb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -102,9 +102,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } LocalDate startDate = disbursementDate; + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (final LoanRepaymentScheduleInstallment installment : installments) { + boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); for (final LoanCharge loanCharge : transferCharges) { - boolean isDue = installment.isFirstPeriod() + boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate()) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate()); if (isDue) { @@ -635,15 +637,15 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen return penaltyCharges; } - protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, + protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set charges, final Integer installmentNumber) { - Money amountRemaining = feeCharges; + Money amountRemaining = chargeAmount; while (amountRemaining.isGreaterThanZero()) { - final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency()); - Money feeAmount = feeCharges.zero(); + final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency()); + Money feeAmount = chargeAmount.zero(); if (loanTransaction.isChargePayment()) { - feeAmount = feeCharges; + feeAmount = chargeAmount; } if (unpaidCharge == null) { break; // All are trache charges @@ -669,6 +671,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } + public interface ChargesPaidByFunction { + + void accept(LoanTransaction loanTransaction, Money feeCharges, Set charges, Integer installmentNumber); + } + + public ChargesPaidByFunction getChargesPaymentFunction(LoanRepaymentScheduleInstallment.PaymentAction action) { + return switch (action) { + case PAY -> this::updateChargesPaidAmountBy; + case UNPAY -> this::undoChargesPaidAmountBy; + }; + } + protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set charges, final MonetaryCurrency currency) { LoanCharge earliestUnpaidCharge = null; LoanCharge installemntCharge = null; @@ -770,15 +784,15 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); } - protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, + protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set charges, final Integer installmentNumber) { - Money amountRemaining = feeCharges; + Money amountRemaining = chargeAmount; while (amountRemaining.isGreaterThanZero()) { - final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency()); + final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency()); if (paidCharge != null) { - Money feeAmount = feeCharges.zero(); + Money feeAmount = chargeAmount.zero(); final Money amountDeductedTowardsCharge = paidCharge.undoPaidOrPartiallyAmountBy(amountRemaining, installmentNumber, feeAmount); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index cedbadfe5..9d3adeedc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -49,6 +49,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; 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; @@ -196,7 +197,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, - transactionMappings, balances); + transactionMappings, charges, balances); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } @@ -205,7 +206,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero, - transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, + transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances, paymentAllocationType); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; @@ -216,20 +217,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); - - final Set loanFees = extractFeeCharges(charges); - final Set loanPenalties = extractPenaltyCharges(charges); - Integer installmentNumber = null; - - final Money feeCharges = loanTransaction.getFeeChargesPortion(currency); - if (feeCharges.isGreaterThanZero()) { - undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber); - } - - final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); - if (penaltyCharges.isGreaterThanZero()) { - undoChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber); - } } private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, @@ -350,44 +337,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances, - LoanRepaymentScheduleInstallment.PaymentAction action) { + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set chargesOfInstallment, + Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) { LocalDate transactionDate = loanTransaction.getTransactionDate(); Money zero = transactionAmountUnprocessed.zero(); - return switch (paymentAllocationType.getAllocationType()) { + + LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment + .getPaymentFunction(paymentAllocationType.getAllocationType(), action); + ChargesPaidByFunction chargesPaidByFunction = getChargesPaymentFunction(action); + Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed); + + switch (paymentAllocationType.getAllocationType()) { case PENALTY -> { - LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); - Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed); balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion); - yield portion; + Set penalties = chargesOfInstallment.stream().filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet()); + chargesPaidByFunction.accept(loanTransaction, portion, penalties, currentInstallment.getInstallmentNumber()); } case FEE -> { - LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); - Money portion = functional.accept(transactionDate, transactionAmountUnprocessed); balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero); - yield portion; + Set fees = chargesOfInstallment.stream().filter(LoanCharge::isFeeCharge).collect(Collectors.toSet()); + chargesPaidByFunction.accept(loanTransaction, portion, fees, currentInstallment.getInstallmentNumber()); } case INTEREST -> { - LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); - Money portion = functional.accept(transactionDate, transactionAmountUnprocessed); balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero); - yield portion; } case PRINCIPAL -> { - LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); - Money portion = functional.accept(transactionDate, transactionAmountUnprocessed); balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero); - yield portion; } - }; + } + return portion; } private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, @@ -427,7 +409,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency)); interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency)); feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency)); - penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency)); + penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesOutstanding(currency)); } } @@ -450,8 +432,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep LocalDate startDate = loanTransaction.getLoan().getDisbursementDate(); Money unprocessed = loanTransaction.getAmount(currency); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (final LoanRepaymentScheduleInstallment installment : installments) { - boolean isDue = installment.isFirstPeriod() + boolean isDue = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate()) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate()); if (isDue) { @@ -487,7 +470,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Balances balances) { + List transactionMappings, Set charges, Balances balances) { Money zero = Money.zero(currency); Money refundedPortion; outerLoop: do { @@ -498,15 +481,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep List inAdvanceInstallments = getFutureInstallmentsForRefund(loanTransaction, currency, installments, futureInstallmentAllocationRule, zero); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { if (latestPastDueInstallment != null) { + Set oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, latestPastDueInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, latestPastDueInstallment, currency); refundedPortion = processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, + oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); } else { break outerLoop; @@ -514,11 +500,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } case DUE -> { if (dueInstallment != null) { + Set dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, dueInstallment, currency); refundedPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, + balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); } else { break outerLoop; @@ -530,13 +518,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { + Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, + firstNormalInstallmentNumber); if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { evenPortion = evenPortion.add(balanceAdjustment); } LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, inAdvanceInstallment, currency); refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, balances, + evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); } @@ -554,30 +544,35 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money zero, List transactionMappings, Money transactionAmountUnprocessed, - FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set charges, Balances balances, PaymentAllocationType paymentAllocationType) { LoanRepaymentScheduleInstallment currentInstallment = null; Money refundedPortion = zero; + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); do { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero); if (currentInstallment != null) { + Set oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges, + balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); } } case DUE -> { currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero); if (currentInstallment != null) { + Set dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); } @@ -592,14 +587,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) { currentInstallment = internalCurrentInstallment; + Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) { evenPortion = evenPortion.add(balanceAdjustment); } LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, - loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, + balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); if (internalUnpaidPortion.isGreaterThanZero()) { refundedPortion = internalUnpaidPortion; } @@ -666,25 +663,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed, - paymentAllocationRule, transactionMappings, balances); + paymentAllocationRule, transactionMappings, charges, balances); } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed, - paymentAllocationRule, transactionMappings, balances); + paymentAllocationRule, transactionMappings, charges, balances); } loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); - payAdditionalCharges(loanTransaction, currency, charges); - handleOverpayment(transactionAmountUnprocessed, loanTransaction); } private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Balances balances) { + Set charges, Balances balances) { LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); @@ -692,7 +687,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), - paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances); + paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances); } return transactionAmountUnprocessed; } @@ -700,7 +695,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Balances balances) { + List transactionMappings, Set charges, Balances balances) { Money paidPortion; boolean exit = false; do { @@ -727,15 +722,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); } + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); + for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { if (oldestPastDueInstallment != null) { + Set oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, oldestPastDueInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, oldestPastDueInstallment, currency); paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, + oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } else { exit = true; @@ -743,11 +742,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } case DUE -> { if (dueInstallment != null) { + Set dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, dueInstallment, currency); paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, + balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } else { exit = true; @@ -762,6 +763,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep // Adjustment might be needed due to the divide operation and the rounding mode Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { + Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, + firstNormalInstallmentNumber); // Adjust the portion for the last installment if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { evenPortion = evenPortion.add(balanceAdjustment); @@ -769,7 +772,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, inAdvanceInstallment, currency); paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, balances, + evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } @@ -787,10 +790,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return transactionAmountUnprocessed; } + @NotNull + private static Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, + int firstNormalInstallmentNumber) { + return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber) + ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), + currentInstallment.getDueDate()) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(currentInstallment.getFromDate(), currentInstallment.getDueDate())) + .collect(Collectors.toSet()); + } + private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Balances balances) { + Set charges, Balances balances) { + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); LoanRepaymentScheduleInstallment currentInstallment = null; @@ -802,11 +816,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate())) .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); if (currentInstallment != null) { + Set oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, + oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } } @@ -814,11 +830,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate())) .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); if (currentInstallment != null) { + Set dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, + balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } } @@ -848,6 +866,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) { currentInstallment = internalCurrentInstallment; + Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment, + firstNormalInstallmentNumber); // Adjust the portion for the last installment if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) { evenPortion = evenPortion.add(balanceAdjustment); @@ -855,8 +875,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, - loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, + inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); // Some extra logic to allocate as much as possible across the installments if the // outstanding balances are different if (internalPaidPortion.isGreaterThanZero()) { @@ -887,43 +907,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep }; } - private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set charges) { - final Set paidFeeCharges = loanTransaction.getLoanChargesPaid().stream() // - .map(LoanChargePaidBy::getLoanCharge) // - .filter(LoanCharge::isFeeCharge).collect(Collectors.toSet()); - final Set paidPenaltyCharges = loanTransaction.getLoanChargesPaid().stream() // - .map(LoanChargePaidBy::getLoanCharge) // - .filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet()); - // TODO: rewrite to provide sorted list - final Set loanFees = extractFeeCharges(charges); - final Set loanPenalties = extractPenaltyCharges(charges); - loanFees.removeAll(paidFeeCharges); - loanPenalties.removeAll(paidPenaltyCharges); - - BigDecimal sumFeePaidAmount = paidFeeCharges.stream() // - .map(paidCharge -> paidCharge.getAmountPaid(currency)) // - .map(Money::getAmount) // - .reduce(BigDecimal.ZERO, BigDecimal::add); - - BigDecimal sumPenaltyPaidAmount = paidPenaltyCharges.stream() // - .map(paidCharge -> paidCharge.getAmountPaid(currency)) // - .map(Money::getAmount) // - .reduce(BigDecimal.ZERO, BigDecimal::add); - - if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) { - Money feeCharges = loanTransaction.getFeeChargesPortion(currency).minus(sumFeePaidAmount); - Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency).minus(sumPenaltyPaidAmount); - - if (feeCharges.isGreaterThanZero()) { - updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null); - } - - if (penaltyCharges.isGreaterThanZero()) { - updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null); - } - } - } - @AllArgsConstructor @Getter @Setter diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java index 34f677e69..5c5a4a043 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java @@ -44,7 +44,11 @@ public class ChargeOrTransaction implements Comparable { private LocalDate getEffectiveDate() { if (loanCharge.isPresent()) { - return loanCharge.get().getDueDate(); + if (isBackdatedCharge()) { + return loanCharge.get().getDueDate(); + } else { + return loanCharge.get().getSubmittedOnDate(); + } } else if (loanTransaction.isPresent()) { return loanTransaction.get().getTransactionDate(); } else { @@ -52,6 +56,10 @@ public class ChargeOrTransaction implements Comparable { } } + private boolean isBackdatedCharge() { + return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate()); + } + private LocalDate getSubmittedOnDate() { if (loanCharge.isPresent()) { return loanCharge.get().getSubmittedOnDate(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 7b0416230..48a13bed6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -612,15 +612,17 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); CurrencyData currencyData = applicationCurrency.toData(); Set loanCharges = loan.getActiveCharges(); + int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (LoanRepaymentScheduleInstallment installment : installments) { if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) { accruedTill = DateUtils.getBusinessLocalDate(); } if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) { + boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate, repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges, - installment); + installment, isFirstNormalInstallment); } } @@ -639,7 +641,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final Collection loanScheduleAccrualDatas, final Long loanId, Long officeId, final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency, - final CurrencyData currencyData, final Set loanCharges, final LoanRepaymentScheduleInstallment installment) { + final CurrencyData currencyData, final Set loanCharges, final LoanRepaymentScheduleInstallment installment, + boolean isFirstNormalInstallment) { if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate()) && !DateUtils.isAfter(accruedTill, installment.getDueDate()))) { @@ -651,7 +654,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } for (final LoanCharge loanCharge : loanCharges) { - boolean isDue = installment.isFirstPeriod() + boolean isDue = isFirstNormalInstallment ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), chargesTillDate) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate); if (isDue) { diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 3856ced6e..7cffe275e 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -109,7 +109,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest { Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); Mockito.when(loanTransaction.getLoan()).thenReturn(loan); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(installment.isFirstPeriod()).thenReturn(true); Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) .thenReturn(true); Mockito.when(installment.getInstallmentNumber()).thenReturn(1); @@ -154,7 +153,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest { Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); Mockito.when(loanTransaction.getLoan()).thenReturn(loan); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(installment.isFirstPeriod()).thenReturn(true); Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) .thenReturn(true); Mockito.when(installment.getInstallmentNumber()).thenReturn(1); @@ -204,7 +202,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest { Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); - Mockito.when(installment.isFirstPeriod()).thenReturn(true); Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) .thenReturn(true); Mockito.when(installment.getInstallmentNumber()).thenReturn(1); 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 index 892fc6547..20ac2238b 100644 --- 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 @@ -40,6 +40,14 @@ public class ChargeOrTransactionTest { Assertions.assertTrue(transaction.compareTo(charge) == 0); } + @Test + public void testCompareToEqualBackdatedCharge() { + ChargeOrTransaction charge = createCharge("2023-10-16", "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 testCompareToCreatedDateTime() { ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); @@ -77,7 +85,7 @@ public class ChargeOrTransactionTest { } @Test - public void testComparatorOnSameDay() { + public void testComparatorOnSameDayBackdatedCharge() { 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"); @@ -88,6 +96,18 @@ public class ChargeOrTransactionTest { } } + @Test + public void testComparatorOnSameDay() { + ChargeOrTransaction cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChargeOrTransaction cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChargeOrTransaction cot3 = createCharge("2023-10-24", "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)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index baa5bf31b..64c51a6c0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PaymentAllocationOrder; @@ -2522,6 +2523,445 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { } } + // UC112: Advanced payment allocation, horizontal repayment processing + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan (1000) + // 2. Add charge after maturity date + // 3. Pay 1st installment + // 4. Pay 2nd installment + // 5. Add charge to 3rd installment + // 6. Add charge to 4th installment + // 7. Do goodwill credit (in advance payment) + @Test + public void uc112() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE, + LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, + BigDecimal.valueOf(1000.0), 45, 15, 3, 0, "01 September 2023", "01 September 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 September 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + // Add Charge Penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en")); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20")); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20")); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(50.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 510.0, 550.0, 490.0, 510.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 10.0, 240.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 30.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0, + 0.0, 20.0, 0.0); + + validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 0.0, 20.0); + validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 20.0, 0.0); + validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 20.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC113: Advanced payment allocation, vertical repayment processing + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan (1000) + // 2. Add charge after maturity date + // 3. Pay 1st installment + // 4. Pay 2nd installment + // 5. Add charge to 3rd installment + // 6. Add charge to 4th installment + // 7. Do goodwill credit (in advance payment) + @Test + public void uc113() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE, + LoanScheduleProcessingType.VERTICAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, + BigDecimal.valueOf(1000.0), 45, 15, 3, 0, "01 September 2023", "01 September 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 September 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + // Add Charge Penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en")); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20")); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20")); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(50.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 510.0, 550.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0, + 0.0, 10.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0, + 0.0, 20.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0, + 0.0, 20.0, 0.0); + validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 10.0, 10.0); + validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 20.0, 0.0); + validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 20.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC114: Advanced payment allocation, horizontal repayment processing + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan (1000) + // 2. Add charge after maturity date + // 3. Pay 1st installment + // 4. Pay 2nd installment + // 5. Add charge to 3rd installment + // 6. Add charge to 4th installment + // 7. Do merchant issued refund (in advance payment) + @Test + public void uc114() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE, + LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, + BigDecimal.valueOf(1000.0), 45, 15, 3, 0, "01 September 2023", "01 September 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 September 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + // Add Charge Penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en")); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20")); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20")); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(30.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 530.0, 530.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0, + 0.0, 10.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, + 0.0, 0.0, 10.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0, + 0.0, 10.0, 0.0); + validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 10.0, 10.0); + validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 10.0, 10.0); + validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 10.0, 10.0); + assertTrue(loanDetails.getStatus().getActive()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC115: Advanced payment allocation, vertical repayment processing + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan (1000) + // 2. Add charge after maturity date + // 3. Pay 1st installment + // 4. Pay 2nd installment + // 5. Add charge to 3rd installment + // 6. Add charge to 4th installment + // 7. Do merchant issued refund (in advance payment) + @Test + public void uc115() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE, + LoanScheduleProcessingType.VERTICAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, + BigDecimal.valueOf(1000.0), 45, 15, 3, 0, "01 September 2023", "01 September 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 September 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + // Add Charge Penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true)); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en")); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20")); + loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(), + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20")); + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(30.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 530.0, 530.0, 500.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0, + 0.0, 10.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, + 0.0, 0.0, 10.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0, + 0.0, 10.0, 0.0); + validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 10.0, 10.0); + validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 10.0, 10.0); + validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 10.0, 10.0); + assertTrue(loanDetails.getStatus().getActive()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, Double principalOutstanding, Double principalPaid, Double totalOverpaid) { assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); @@ -2723,4 +3163,13 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion()); assertEquals(loanBalance, loanDetails.getTransactions().get(index).getOutstandingLoanBalance()); } + + private void validateLoanCharge(GetLoansLoanIdResponse loanDetails, int index, LocalDate dueDate, double charged, double paid, + double outstanding) { + GetLoansLoanIdLoanChargeData chargeData = loanDetails.getCharges().get(index); + assertEquals(dueDate, chargeData.getDueDate()); + assertEquals(charged, chargeData.getAmount()); + assertEquals(paid, chargeData.getAmountPaid()); + assertEquals(outstanding, chargeData.getAmountOutstanding()); + } }