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