FINERACT-1905 Loan Charge Reverse Replay with Advanced Payment Allocation
This commit is contained in:
+2
-2
@@ -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);
|
||||
|
||||
+19
@@ -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());
|
||||
|
||||
+53
-95
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+216
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+154
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+64
-33
@@ -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);
|
||||
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);
|
||||
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
|
||||
|
||||
// Reset derived component of new loan transaction and
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency));
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
/**
|
||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||
* changedTransactionDetail accordingly
|
||||
**/
|
||||
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
|
||||
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
|
||||
} else {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
|
||||
installments, charges, changedTransactionDetail));
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
}
|
||||
reprocessInstallments(installments, currency);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<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) {
|
||||
|
||||
+88
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+235
-195
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -39,6 +42,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||
import org.apache.fineract.client.models.ChargeData;
|
||||
import org.apache.fineract.client.models.ChargeToGLAccountMapper;
|
||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
@@ -55,7 +59,9 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||
import org.apache.fineract.client.models.PostPaymentTypesRequest;
|
||||
import org.apache.fineract.client.models.PostPaymentTypesResponse;
|
||||
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
|
||||
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
@@ -88,6 +94,8 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
|
||||
private AccountHelper accountHelper;
|
||||
private LoanProductHelper loanProductHelper;
|
||||
private PaymentTypeHelper paymentTypeHelper;
|
||||
private final BusinessDateHelper businessDateHelper = new BusinessDateHelper();
|
||||
private static final String DATETIME_PATTERN = "dd MMMM yyyy";
|
||||
// asset
|
||||
private Account loansReceivable;
|
||||
private Account interestFeeReceivable;
|
||||
@@ -212,72 +220,76 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
|
||||
// Reverse Replay of Charge-Off
|
||||
@Test
|
||||
public void loanChargeOffReverseReplayWithAdvancedPaymentStrategyTest() {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
runAt("9 September 2022", () -> {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
|
||||
// apply charges
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
// apply charges
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
|
||||
// apply penalty
|
||||
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true));
|
||||
// apply penalty
|
||||
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true));
|
||||
|
||||
final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
final String penaltyCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
|
||||
Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10"));
|
||||
Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10"));
|
||||
|
||||
// make Repayment
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en")
|
||||
.transactionAmount(10.0));
|
||||
// make Repayment
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en")
|
||||
.transactionAmount(10.0));
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
|
||||
// set loan as chargeoff
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("10 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
// set loan as chargeoff
|
||||
updateBusinessDate("10 September 2022");
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6)
|
||||
+ Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("10 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff");
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 10), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff");
|
||||
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
|
||||
// reverse Repayment
|
||||
loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "11 September 2022");
|
||||
// reverse Repayment
|
||||
updateBusinessDate("11 September 2022");
|
||||
loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "11 September 2022");
|
||||
|
||||
// verify chargeOffTransaction gets reverse replayed
|
||||
// verify chargeOffTransaction gets reverse replayed
|
||||
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 0.0f, 10.0f, 10.0f, loanId, "chargeoff");
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 10), 1020.0f, 1000.0f, 0.0f, 10.0f, 10.0f, loanId, "chargeoff");
|
||||
});
|
||||
}
|
||||
|
||||
// undo Charge-Off
|
||||
@@ -329,202 +341,211 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
|
||||
// Backdated repayment transaction, Reverse replay of charge off
|
||||
@Test
|
||||
public void postChargeOffAddBackdatedTransactionAndReverseReplayTest() {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
runAt("3 September 2022", () -> {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
|
||||
// apply charges
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
// apply charges
|
||||
updateBusinessDate("5 September 2022");
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
|
||||
// set loan as chargeoff
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("14 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
// set loan as chargeoff
|
||||
updateBusinessDate("14 September 2022");
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6)
|
||||
+ Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("14 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
|
||||
// verify Journal Entries For ChargeOff Transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper
|
||||
.getJournalEntries("L" + chargeOffTransaction.getResourceId().toString());
|
||||
// verify Journal Entries For ChargeOff Transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper
|
||||
.getJournalEntries("L" + chargeOffTransaction.getResourceId().toString());
|
||||
|
||||
assertNotNull(journalEntriesForChargeOff);
|
||||
List<JournalEntryTransactionItem> journalEntries = journalEntriesForChargeOff.getPageItems();
|
||||
assertEquals(4, journalEntries.size());
|
||||
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");
|
||||
verifyJournalEntry(journalEntries.get(3), 1000.0, LocalDate.of(2022, 9, 14), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(2), 10.0, LocalDate.of(2022, 9, 14), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 1000.0, LocalDate.of(2022, 9, 14), creditLossBadDebt, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 10.0, LocalDate.of(2022, 9, 14), feeChargeOff, "DEBIT");
|
||||
|
||||
// make Repayment before chargeoff date
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
// make Repayment before chargeoff date - business date is still on 14 September 2022
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
// verify Journal Entries for Repayment transaction
|
||||
// verify Journal Entries for Repayment transaction
|
||||
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper
|
||||
.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper
|
||||
.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(3, journalEntries.size());
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(3, journalEntries.size());
|
||||
|
||||
verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
|
||||
|
||||
// verify reverse replay of Charge-Off
|
||||
// verify reverse replay of Charge-Off
|
||||
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 0.0f, 0.0f, loanId, "chargeoff");
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 14), 910.0f, 910.0f, 0.0f, 0.0f, 0.0f, loanId, "chargeoff");
|
||||
|
||||
// make Repayment after chargeoff date
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("15 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
// make Repayment after chargeoff date
|
||||
updateBusinessDate("15 September 2022");
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("15 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
// verify Journal Entries for Repayment transaction
|
||||
journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction_1.getResourceId().toString());
|
||||
// verify Journal Entries for Repayment transaction
|
||||
journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction_1.getResourceId().toString());
|
||||
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(2, journalEntries.size());
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(2, journalEntries.size());
|
||||
|
||||
verifyJournalEntry(journalEntries.get(1), 100.0, LocalDate.of(2022, 9, 15), recoveries, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 15), suspenseClearingAccount, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 100.0, LocalDate.of(2022, 9, 15), recoveries, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 15), suspenseClearingAccount, "DEBIT");
|
||||
});
|
||||
}
|
||||
|
||||
// Repayment before charge off on charge off date, reverse replay of charge off
|
||||
@Test
|
||||
public void transactionOnChargeOffDateReverseTest() {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
runAt("7 September 2022", () -> {
|
||||
String loanExternalIdStr = UUID.randomUUID().toString();
|
||||
final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingAndAdvancedPaymentAllocationStrategy();
|
||||
final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
|
||||
final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr);
|
||||
|
||||
// apply charges
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
// apply charges
|
||||
Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false));
|
||||
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
LocalDate targetDate = LocalDate.of(2022, 9, 5);
|
||||
final String feeCharge1AddedDate = DATE_FORMATTER.format(targetDate);
|
||||
Integer feeLoanChargeId = loanTransactionHelper.addChargesForLoan(loanId,
|
||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10"));
|
||||
|
||||
// make Repayment before charge-off on charge off date
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
// make Repayment before charge-off on charge off date
|
||||
final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en")
|
||||
.transactionAmount(100.0));
|
||||
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
|
||||
// verify Journal Entries for Repayment transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper
|
||||
.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
// verify Journal Entries for Repayment transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForRepayment = journalEntryHelper
|
||||
.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
|
||||
List<JournalEntryTransactionItem> journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(3, journalEntries.size());
|
||||
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");
|
||||
verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
|
||||
|
||||
// set loan as chargeoff
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("7 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
// set loan as chargeoff
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6)
|
||||
+ Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
String transactionExternalId = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan((long) loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionDate("7 September 2022").locale("en").dateFormat("dd MMMM yyyy")
|
||||
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
|
||||
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
Long reversedAndReplayedTransactionId = chargeOffTransaction.getResourceId();
|
||||
|
||||
// verify Journal Entries For ChargeOff Transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper
|
||||
.getJournalEntries("L" + chargeOffTransaction.getResourceId().toString());
|
||||
// verify Journal Entries For ChargeOff Transaction
|
||||
GetJournalEntriesTransactionIdResponse journalEntriesForChargeOff = journalEntryHelper
|
||||
.getJournalEntries("L" + chargeOffTransaction.getResourceId().toString());
|
||||
|
||||
assertNotNull(journalEntriesForChargeOff);
|
||||
journalEntries = journalEntriesForChargeOff.getPageItems();
|
||||
assertEquals(2, journalEntries.size());
|
||||
assertNotNull(journalEntriesForChargeOff);
|
||||
journalEntries = journalEntriesForChargeOff.getPageItems();
|
||||
assertEquals(2, journalEntries.size());
|
||||
|
||||
verifyJournalEntry(journalEntries.get(1), 910.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 910.0, LocalDate.of(2022, 9, 7), creditLossBadDebt, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 910.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 910.0, LocalDate.of(2022, 9, 7), creditLossBadDebt, "DEBIT");
|
||||
|
||||
// reverse Repayment
|
||||
loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "7 September 2022");
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
// reverse Repayment
|
||||
loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction.getResourceId().intValue(), "7 September 2022");
|
||||
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetails.getStatus().getActive());
|
||||
assertTrue(loanDetails.getChargedOff());
|
||||
|
||||
// verify Journal Entries for Reversed Repayment transaction
|
||||
journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
// verify Journal Entries for Reversed Repayment transaction
|
||||
journalEntriesForRepayment = journalEntryHelper.getJournalEntries("L" + repaymentTransaction.getResourceId().toString());
|
||||
assertNotNull(journalEntriesForRepayment);
|
||||
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(6, journalEntries.size());
|
||||
journalEntries = journalEntriesForRepayment.getPageItems();
|
||||
assertEquals(6, journalEntries.size());
|
||||
|
||||
verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(3), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(5), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(4), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "CREDIT");
|
||||
verifyJournalEntry(journalEntries.get(3), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(2), 90.0, LocalDate.of(2022, 9, 7), loansReceivable, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(1), 10.0, LocalDate.of(2022, 9, 7), interestFeeReceivable, "DEBIT");
|
||||
verifyJournalEntry(journalEntries.get(0), 100.0, LocalDate.of(2022, 9, 7), suspenseClearingAccount, "CREDIT");
|
||||
|
||||
// verify reverse replay of Charge-Off
|
||||
// verify reverse replay of Charge-Off
|
||||
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = loanTransactionHelper
|
||||
.getLoanTransactionDetails((long) loanId, transactionExternalId);
|
||||
assertNotNull(getLoansTransactionResponse);
|
||||
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
|
||||
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
// test replayed relationship
|
||||
GetLoanTransactionRelation transactionRelation = getLoansTransactionResponse.getTransactionRelations().iterator().next();
|
||||
assertEquals(reversedAndReplayedTransactionId, transactionRelation.getToLoanTransaction());
|
||||
assertEquals("REPLAYED", transactionRelation.getRelationType());
|
||||
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff");
|
||||
// verify amounts for charge-off transaction
|
||||
verifyTransaction(LocalDate.of(2022, 9, 7), 1010.0f, 1000.0f, 0.0f, 10.0f, 0.0f, loanId, "chargeoff");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -756,4 +777,23 @@ public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest {
|
||||
return paymentAllocationOrder;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void runAt(String date, Runnable runnable) {
|
||||
try {
|
||||
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true);
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE);
|
||||
businessDateHelper.updateBusinessDate(
|
||||
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
|
||||
runnable.run();
|
||||
} finally {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE);
|
||||
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBusinessDate(String date) {
|
||||
businessDateHelper.updateBusinessDate(
|
||||
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+492
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user