FINERACT-1968: Enhanced charge handling

This commit is contained in:
Adam Saghy
2023-11-23 19:21:43 +01:00
parent 48300cd19d
commit 054903a952
11 changed files with 638 additions and 148 deletions
@@ -779,14 +779,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
.determineProcessor(this.transactionProcessingStrategyCode); .determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>(); final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanRepaymentScheduleInstallment installment : installments) {
boolean isDue = installment.isFirstPeriod() boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
? charge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate()) ? charge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate())
: charge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate()); : charge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate());
if (installmentNumber == null && isDue) { if (installmentNumber == null && isFirstNormalInstallment) {
chargePaymentInstallments.add(installment); chargePaymentInstallments.add(installment);
break; break;
} else if (installmentNumber != null && installment.getInstallmentNumber().equals(installmentNumber)) { } else if (installment.getInstallmentNumber().equals(installmentNumber)) {
chargePaymentInstallments.add(installment); chargePaymentInstallments.add(installment);
break; break;
} }
@@ -1347,7 +1349,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
LocalDate transactionDateForRange = isBasedOnSubmittedOnDate LocalDate transactionDateForRange = isBasedOnSubmittedOnDate
? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getDueDate() ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getDueDate()
: loanTransaction.getTransactionDate(); : loanTransaction.getTransactionDate();
if (installment.isInPeriod(transactionDateForRange)) { boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments);
if (isInPeriod) {
interest = interest.plus(loanTransaction.getInterestPortion(getCurrency())); interest = interest.plus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency())); fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency()));
penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency())); penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
@@ -6641,13 +6644,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
Money paidFromFutureInstallments = Money.zero(currency); Money paidFromFutureInstallments = Money.zero(currency);
Money fee = Money.zero(currency); Money fee = Money.zero(currency);
Money penalty = Money.zero(currency); Money penalty = Money.zero(currency);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) { if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) {
interest = interest.plus(installment.getInterestOutstanding(currency)); interest = interest.plus(installment.getInterestOutstanding(currency));
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency)); penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
fee = fee.plus(installment.getFeeChargesOutstanding(currency)); fee = fee.plus(installment.getFeeChargesOutstanding(currency));
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) { } 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])) { if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) {
interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]); interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]);
} else { } else {
@@ -6680,7 +6688,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency, private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency,
final LoanRepaymentScheduleInstallment installment) { final LoanRepaymentScheduleInstallment installment, boolean isFirstNormalInstallment) {
Money penaltyForCurrentPeriod = Money.zero(getCurrency()); Money penaltyForCurrentPeriod = Money.zero(getCurrency());
Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency()); Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency());
Money feeForCurrentPeriod = 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())); interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()).plus(installment.getInterestPaid(getCurrency()));
for (LoanCharge loanCharge : this.charges) { for (LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) { if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
boolean isDue = installment.isFirstPeriod() boolean isDue = isFirstNormalInstallment
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate); : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate);
if (isDue) { if (isDue) {
@@ -6734,7 +6742,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
Money[] balances = new Money[3]; Money[] balances = new Money[3];
final MonetaryCurrency currency = getCurrency(); final MonetaryCurrency currency = getCurrency();
balances[0] = balances[1] = balances[2] = Money.zero(currency); balances[0] = balances[1] = balances[2] = Money.zero(currency);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
if (DateUtils.isEqual(paymentDate, installment.getDueDate())) { if (DateUtils.isEqual(paymentDate, installment.getDueDate())) {
Money interest = installment.getInterestCharged(currency); Money interest = installment.getInterestCharged(currency);
Money fee = installment.getFeeChargesCharged(currency); Money fee = installment.getFeeChargesCharged(currency);
@@ -6745,7 +6756,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
break; break;
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate()) } else if (DateUtils.isAfter(paymentDate, installment.getFromDate())
&& DateUtils.isBefore(paymentDate, installment.getDueDate())) { && DateUtils.isBefore(paymentDate, installment.getDueDate())) {
balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment); balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment, isFirstNormalInstallment);
break; break;
} }
} }
@@ -960,15 +960,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.additional = true; 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<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() { public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
return this.loanTransactionToRepaymentScheduleMappings; return this.loanTransactionToRepaymentScheduleMappings;
} }
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -43,13 +44,14 @@ public class LoanRepaymentScheduleProcessingWrapper {
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
} }
LocalDate startDate = disbursementDate; LocalDate startDate = disbursementDate;
LoanRepaymentScheduleInstallment firstNormalPeriod = repaymentPeriods.stream()
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber))
.filter(repaymentPeriod -> !repaymentPeriod.isDownPayment()).findFirst().orElseThrow();
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
if (!period.isDownPayment()) { if (!period.isDownPayment()) {
boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream() boolean isFirstNonDownPaymentPeriod = period.equals(firstNormalPeriod);
.filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber())
.allMatch(LoanRepaymentScheduleInstallment::isDownPayment);
final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges,
currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(),
@@ -235,4 +237,18 @@ public class LoanRepaymentScheduleProcessingWrapper {
return amount; return amount;
} }
public static int fetchFirstNormalInstallmentNumber(List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> 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());
}
} }
@@ -42,13 +42,11 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
} }
LocalDate startDate = disbursementDate; LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods);
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
if (!period.isDownPayment()) { if (!period.isDownPayment()) {
boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber);
boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream()
.filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber())
.allMatch(LoanRepaymentScheduleInstallment::isDownPayment);
final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency, final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency,
period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
@@ -102,9 +102,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
} }
LocalDate startDate = disbursementDate; LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanRepaymentScheduleInstallment installment : installments) {
boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
for (final LoanCharge loanCharge : transferCharges) { for (final LoanCharge loanCharge : transferCharges) {
boolean isDue = installment.isFirstPeriod() boolean isDue = isFirstPeriod
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate()) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate()); : loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
if (isDue) { if (isDue) {
@@ -635,15 +637,15 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return penaltyCharges; return penaltyCharges;
} }
protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges, protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set<LoanCharge> charges,
final Integer installmentNumber) { final Integer installmentNumber) {
Money amountRemaining = feeCharges; Money amountRemaining = chargeAmount;
while (amountRemaining.isGreaterThanZero()) { while (amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency()); final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency());
Money feeAmount = feeCharges.zero(); Money feeAmount = chargeAmount.zero();
if (loanTransaction.isChargePayment()) { if (loanTransaction.isChargePayment()) {
feeAmount = feeCharges; feeAmount = chargeAmount;
} }
if (unpaidCharge == null) { if (unpaidCharge == null) {
break; // All are trache charges break; // All are trache charges
@@ -669,6 +671,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
public interface ChargesPaidByFunction {
void accept(LoanTransaction loanTransaction, Money feeCharges, Set<LoanCharge> 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<LoanCharge> charges, final MonetaryCurrency currency) { protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, final MonetaryCurrency currency) {
LoanCharge earliestUnpaidCharge = null; LoanCharge earliestUnpaidCharge = null;
LoanCharge installemntCharge = null; LoanCharge installemntCharge = null;
@@ -770,15 +784,15 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
} }
protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges, protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set<LoanCharge> charges,
final Integer installmentNumber) { final Integer installmentNumber) {
Money amountRemaining = feeCharges; Money amountRemaining = chargeAmount;
while (amountRemaining.isGreaterThanZero()) { while (amountRemaining.isGreaterThanZero()) {
final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency()); final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency());
if (paidCharge != null) { if (paidCharge != null) {
Money feeAmount = feeCharges.zero(); Money feeAmount = chargeAmount.zero();
final Money amountDeductedTowardsCharge = paidCharge.undoPaidOrPartiallyAmountBy(amountRemaining, installmentNumber, final Money amountDeductedTowardsCharge = paidCharge.undoPaidOrPartiallyAmountBy(amountRemaining, installmentNumber,
feeAmount); feeAmount);
@@ -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.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; 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.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
@@ -196,7 +197,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments,
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule,
transactionMappings, balances); transactionMappings, charges, balances);
if (!transactionAmountUnprocessed.isGreaterThanZero()) { if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break; break;
} }
@@ -205,7 +206,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero, transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
paymentAllocationType); paymentAllocationType);
if (!transactionAmountUnprocessed.isGreaterThanZero()) { if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break; break;
@@ -216,20 +217,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> 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, private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
@@ -350,44 +337,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment, private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances, LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set<LoanCharge> chargesOfInstallment,
LoanRepaymentScheduleInstallment.PaymentAction action) { Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) {
LocalDate transactionDate = loanTransaction.getTransactionDate(); LocalDate transactionDate = loanTransaction.getTransactionDate();
Money zero = transactionAmountUnprocessed.zero(); 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 -> { case PENALTY -> {
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion)); balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
yield portion; Set<LoanCharge> penalties = chargesOfInstallment.stream().filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet());
chargesPaidByFunction.accept(loanTransaction, portion, penalties, currentInstallment.getInstallmentNumber());
} }
case FEE -> { case FEE -> {
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion)); balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero);
yield portion; Set<LoanCharge> fees = chargesOfInstallment.stream().filter(LoanCharge::isFeeCharge).collect(Collectors.toSet());
chargesPaidByFunction.accept(loanTransaction, portion, fees, currentInstallment.getInstallmentNumber());
} }
case INTEREST -> { case INTEREST -> {
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion)); balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero);
yield portion;
} }
case PRINCIPAL -> { case PRINCIPAL -> {
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion)); balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero);
yield portion;
} }
}; }
return portion;
} }
private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping,
@@ -427,7 +409,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency)); principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency)); interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(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(); LocalDate startDate = loanTransaction.getLoan().getDisbursementDate();
Money unprocessed = loanTransaction.getAmount(currency); Money unprocessed = loanTransaction.getAmount(currency);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanRepaymentScheduleInstallment installment : installments) {
boolean isDue = installment.isFirstPeriod() boolean isDue = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate()) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate()); : loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
if (isDue) { if (isDue) {
@@ -487,7 +470,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) { List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
Money refundedPortion; Money refundedPortion;
outerLoop: do { outerLoop: do {
@@ -498,15 +481,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = getFutureInstallmentsForRefund(loanTransaction, currency, List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = getFutureInstallmentsForRefund(loanTransaction, currency,
installments, futureInstallmentAllocationRule, zero); installments, futureInstallmentAllocationRule, zero);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
switch (paymentAllocationType.getDueType()) { switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> { case PAST_DUE -> {
if (latestPastDueInstallment != null) { if (latestPastDueInstallment != null) {
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, latestPastDueInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, latestPastDueInstallment, currency); transactionMappings, loanTransaction, latestPastDueInstallment, currency);
refundedPortion = processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} else { } else {
break outerLoop; break outerLoop;
@@ -514,11 +500,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
case DUE -> { case DUE -> {
if (dueInstallment != null) { if (dueInstallment != null) {
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, dueInstallment, currency); transactionMappings, loanTransaction, dueInstallment, currency);
refundedPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} else { } else {
break outerLoop; break outerLoop;
@@ -530,13 +518,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
firstNormalInstallmentNumber);
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment); evenPortion = evenPortion.add(balanceAdjustment);
} }
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency); transactionMappings, loanTransaction, inAdvanceInstallment, currency);
refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} }
@@ -554,30 +544,35 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money zero, List<LoanRepaymentScheduleInstallment> installments, Money zero,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances, FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set<LoanCharge> charges, Balances balances,
PaymentAllocationType paymentAllocationType) { PaymentAllocationType paymentAllocationType) {
LoanRepaymentScheduleInstallment currentInstallment = null; LoanRepaymentScheduleInstallment currentInstallment = null;
Money refundedPortion = zero; Money refundedPortion = zero;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
do { do {
switch (paymentAllocationType.getDueType()) { switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> { case PAST_DUE -> {
currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero); currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero);
if (currentInstallment != null) { if (currentInstallment != null) {
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} }
} }
case DUE -> { case DUE -> {
currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero); currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero);
if (currentInstallment != null) { if (currentInstallment != null) {
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} }
@@ -592,14 +587,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) { for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
currentInstallment = internalCurrentInstallment; currentInstallment = internalCurrentInstallment;
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) { if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment); evenPortion = evenPortion.add(balanceAdjustment);
} }
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances, loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
if (internalUnpaidPortion.isGreaterThanZero()) { if (internalUnpaidPortion.isGreaterThanZero()) {
refundedPortion = internalUnpaidPortion; refundedPortion = internalUnpaidPortion;
} }
@@ -666,25 +663,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (LoanScheduleProcessingType.HORIZONTAL if (LoanScheduleProcessingType.HORIZONTAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed, transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed,
paymentAllocationRule, transactionMappings, balances); paymentAllocationRule, transactionMappings, charges, balances);
} else if (LoanScheduleProcessingType.VERTICAL } else if (LoanScheduleProcessingType.VERTICAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed, transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed,
paymentAllocationRule, transactionMappings, balances); paymentAllocationRule, transactionMappings, charges, balances);
} }
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
payAdditionalCharges(loanTransaction, currency, charges);
handleOverpayment(transactionAmountUnprocessed, loanTransaction); handleOverpayment(transactionAmountUnprocessed, loanTransaction);
} }
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Balances balances) { Set<LoanCharge> charges, Balances balances) {
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
mapping(Function.identity(), toList()))); mapping(Function.identity(), toList())));
@@ -692,7 +687,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances); paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances);
} }
return transactionAmountUnprocessed; return transactionAmountUnprocessed;
} }
@@ -700,7 +695,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) { List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
Money paidPortion; Money paidPortion;
boolean exit = false; boolean exit = false;
do { do {
@@ -727,15 +722,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} }
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
switch (paymentAllocationType.getDueType()) { switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> { case PAST_DUE -> {
if (oldestPastDueInstallment != null) { if (oldestPastDueInstallment != null) {
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, oldestPastDueInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, oldestPastDueInstallment, currency); transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else { } else {
exit = true; exit = true;
@@ -743,11 +742,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
case DUE -> { case DUE -> {
if (dueInstallment != null) { if (dueInstallment != null) {
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, dueInstallment, currency); transactionMappings, loanTransaction, dueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else { } else {
exit = true; exit = true;
@@ -762,6 +763,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// Adjustment might be needed due to the divide operation and the rounding mode // Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
firstNormalInstallmentNumber);
// Adjust the portion for the last installment // Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment); evenPortion = evenPortion.add(balanceAdjustment);
@@ -769,7 +772,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency); transactionMappings, loanTransaction, inAdvanceInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} }
@@ -787,10 +790,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return transactionAmountUnprocessed; return transactionAmountUnprocessed;
} }
@NotNull
private static Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> 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, private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Balances balances) { Set<LoanCharge> charges, Balances balances) {
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
LoanRepaymentScheduleInstallment currentInstallment = null; LoanRepaymentScheduleInstallment currentInstallment = null;
@@ -802,11 +816,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate())) currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) { if (currentInstallment != null) {
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} }
} }
@@ -814,11 +830,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate())) currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) { if (currentInstallment != null) {
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} }
} }
@@ -848,6 +866,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) { for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
currentInstallment = internalCurrentInstallment; currentInstallment = internalCurrentInstallment;
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
firstNormalInstallmentNumber);
// Adjust the portion for the last installment // Adjust the portion for the last installment
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) { if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment); evenPortion = evenPortion.add(balanceAdjustment);
@@ -855,8 +875,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances, loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping,
LoanRepaymentScheduleInstallment.PaymentAction.PAY); inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
// Some extra logic to allocate as much as possible across the installments if the // Some extra logic to allocate as much as possible across the installments if the
// outstanding balances are different // outstanding balances are different
if (internalPaidPortion.isGreaterThanZero()) { if (internalPaidPortion.isGreaterThanZero()) {
@@ -887,43 +907,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}; };
} }
private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) {
final Set<LoanCharge> paidFeeCharges = loanTransaction.getLoanChargesPaid().stream() //
.map(LoanChargePaidBy::getLoanCharge) //
.filter(LoanCharge::isFeeCharge).collect(Collectors.toSet());
final Set<LoanCharge> paidPenaltyCharges = loanTransaction.getLoanChargesPaid().stream() //
.map(LoanChargePaidBy::getLoanCharge) //
.filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet());
// TODO: rewrite to provide sorted list
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> 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 @AllArgsConstructor
@Getter @Getter
@Setter @Setter
@@ -44,7 +44,11 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
private LocalDate getEffectiveDate() { private LocalDate getEffectiveDate() {
if (loanCharge.isPresent()) { if (loanCharge.isPresent()) {
return loanCharge.get().getDueDate(); if (isBackdatedCharge()) {
return loanCharge.get().getDueDate();
} else {
return loanCharge.get().getSubmittedOnDate();
}
} else if (loanTransaction.isPresent()) { } else if (loanTransaction.isPresent()) {
return loanTransaction.get().getTransactionDate(); return loanTransaction.get().getTransactionDate();
} else { } else {
@@ -52,6 +56,10 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
} }
} }
private boolean isBackdatedCharge() {
return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate());
}
private LocalDate getSubmittedOnDate() { private LocalDate getSubmittedOnDate() {
if (loanCharge.isPresent()) { if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate(); return loanCharge.get().getSubmittedOnDate();
@@ -612,15 +612,17 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
CurrencyData currencyData = applicationCurrency.toData(); CurrencyData currencyData = applicationCurrency.toData();
Set<LoanCharge> loanCharges = loan.getActiveCharges(); Set<LoanCharge> loanCharges = loan.getActiveCharges();
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (LoanRepaymentScheduleInstallment installment : installments) { for (LoanRepaymentScheduleInstallment installment : installments) {
if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) { if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) {
accruedTill = DateUtils.getBusinessLocalDate(); accruedTill = DateUtils.getBusinessLocalDate();
} }
if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) { if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate, generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate,
repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges, repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges,
installment); installment, isFirstNormalInstallment);
} }
} }
@@ -639,7 +641,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas, final Long loanId, Long officeId, final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas, final Long loanId, Long officeId,
final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery,
final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency, final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency,
final CurrencyData currencyData, final Set<LoanCharge> loanCharges, final LoanRepaymentScheduleInstallment installment) { final CurrencyData currencyData, final Set<LoanCharge> loanCharges, final LoanRepaymentScheduleInstallment installment,
boolean isFirstNormalInstallment) {
if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate()) if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate())
&& !DateUtils.isAfter(accruedTill, installment.getDueDate()))) { && !DateUtils.isAfter(accruedTill, installment.getDueDate()))) {
@@ -651,7 +654,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
for (final LoanCharge loanCharge : loanCharges) { for (final LoanCharge loanCharge : loanCharges) {
boolean isDue = installment.isFirstPeriod() boolean isDue = isFirstNormalInstallment
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), chargesTillDate) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), chargesTillDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate); : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate);
if (isDue) { if (isDue) {
@@ -109,7 +109,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(installment.isFirstPeriod()).thenReturn(true);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true); .thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
@@ -154,7 +153,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(installment.isFirstPeriod()).thenReturn(true);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true); .thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
@@ -204,7 +202,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL); Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(installment.isFirstPeriod()).thenReturn(true);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
.thenReturn(true); .thenReturn(true);
Mockito.when(installment.getInstallmentNumber()).thenReturn(1); Mockito.when(installment.getInstallmentNumber()).thenReturn(1);
@@ -40,6 +40,14 @@ public class ChargeOrTransactionTest {
Assertions.assertTrue(transaction.compareTo(charge) == 0); 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 @Test
public void testCompareToCreatedDateTime() { public void testCompareToCreatedDateTime() {
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00");
@@ -77,7 +85,7 @@ public class ChargeOrTransactionTest {
} }
@Test @Test
public void testComparatorOnSameDay() { public void testComparatorOnSameDayBackdatedCharge() {
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); 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 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"); 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<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
for (List<ChargeOrTransaction> permutation : permutations) {
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
}
}
private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) { private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) {
LoanCharge charge = Mockito.mock(LoanCharge.class); LoanCharge charge = Mockito.mock(LoanCharge.class);
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
@@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.BusinessDateRequest; 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.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PaymentAllocationOrder; 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, private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment,
Double principalOutstanding, Double principalPaid, Double totalOverpaid) { Double principalOutstanding, Double principalPaid, Double totalOverpaid) {
assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());
@@ -2723,4 +3163,13 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion()); assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion());
assertEquals(loanBalance, loanDetails.getTransactions().get(index).getOutstandingLoanBalance()); 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());
}
} }