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