FINERACT-2081: Payments after loan charge - COB accrual

This commit is contained in:
Marta Jankovics
2024-09-13 12:12:06 +02:00
committed by Adam Saghy
parent 889fc8d2cb
commit e94ce7f1df
9 changed files with 199 additions and 102 deletions
@@ -3166,3 +3166,79 @@ Feature: LoanRepayment
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1250.0 | 39.33 | 0.0 | 0.0 | 1289.33 | 0.0 | 0.0 | 0.0 | 1289.33 |
@AdvancedPaymentAllocation @ProgressiveLoanSchedule
Scenario: Verify AdvancedPaymentAllocation behaviour: loanScheduleProcessingType-horizontal, charge after maturity, in advanced repayment (future installment type: NEXT_INSTALLMENT)
When Global config "charge-accrual-date" value set to "submitted-date"
When Admin sets the business date to "01 September 2023"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 September 2023 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 September 2023" with "1000" amount and expected disbursement date on "01 September 2023"
When Admin successfully disburse the loan on "01 September 2023" with "1000" EUR transaction amount
Then Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 September 2023 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 15 | 16 September 2023 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
# Add charge after maturity
When Admin adds "LOAN_NSF_FEE" due date charge with "17 October 2023" due date and 20 EUR transaction amount
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 September 2023 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 2 | 15 | 16 September 2023 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 5 | 1 | 17 October 2023 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 0.0 | 0.0 | 0.0 | 1020.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
Then Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
| NSF fee | true | Specified due date | 17 October 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
# Make due date repayments
And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount
When Admin sets the business date to "02 September 2023"
When Admin runs inline COB job for Loan
When Admin sets the business date to "02 September 2023"
And Customer makes "AUTOPAY" repayment on "02 September 2023" with 250 EUR transaction amount
Then Loan Repayment schedule has 5 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 September 2023 | 01 September 2023 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 September 2023 | 02 September 2023 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 250.0 | 0.0 | 0.0 |
| 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 5 | 1 | 17 October 2023 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 500.0 | 250.0 | 0.0 | 520.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
| 01 September 2023 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 |
| 01 September 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 |
| 02 September 2023 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 500.0 |
Then Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
| NSF fee | true | Specified due date | 17 October 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
When Admin sets the business date to "03 September 2023"
When Admin runs inline COB job for Loan
#Make backdated repayment to trigger loan transaction reprocessing
And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount
When Admin sets the business date to "04 September 2023"
#Run COB to check there is no accounting meltdown and accrual is handled properly
When Admin runs inline COB job for Loan
@@ -60,18 +60,10 @@ public class LoanChargePaidBy extends AbstractPersistableCustom<Long> {
return this.loanTransaction;
}
public void setLoanTransaction(final LoanTransaction loanTransaction) {
this.loanTransaction = loanTransaction;
}
public LoanCharge getLoanCharge() {
return this.loanCharge;
}
public void setLoanCharge(final LoanCharge loanCharge) {
this.loanCharge = loanCharge;
}
public BigDecimal getAmount() {
return this.amount;
}
@@ -83,4 +75,8 @@ public class LoanChargePaidBy extends AbstractPersistableCustom<Long> {
public Integer getInstallmentNumber() {
return this.installmentNumber;
}
public void setInstallmentNumber(Integer installmentNumber) {
this.installmentNumber = installmentNumber;
}
}
@@ -245,10 +245,14 @@ public class LoanRepaymentScheduleProcessingWrapper {
public static boolean isInPeriod(LocalDate transactionDate, LoanRepaymentScheduleInstallment targetInstallment,
List<LoanRepaymentScheduleInstallment> installments) {
int firstPeriod = fetchFirstNormalInstallmentNumber(installments);
return (targetInstallment.getInstallmentNumber().equals(firstPeriod)
? !DateUtils.isBefore(transactionDate, targetInstallment.getFromDate())
: DateUtils.isAfter(transactionDate, targetInstallment.getFromDate()))
&& !DateUtils.isAfter(transactionDate, targetInstallment.getDueDate());
return isInPeriod(transactionDate, targetInstallment, targetInstallment.getInstallmentNumber().equals(firstPeriod));
}
private static boolean isInPeriod(LocalDate transactionDate, LoanRepaymentScheduleInstallment targetInstallment,
boolean isFirstPeriod) {
LocalDate fromDate = targetInstallment.getFromDate();
LocalDate dueDate = targetInstallment.getDueDate();
return isFirstPeriod ? DateUtils.occursOnDayFromAndUpToAndIncluding(fromDate, dueDate, transactionDate)
: DateUtils.occursOnDayFromExclusiveAndUpToAndIncluding(fromDate, dueDate, transactionDate);
}
}
@@ -18,14 +18,16 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import jakarta.validation.constraints.NotNull;
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.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.jetbrains.annotations.NotNull;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
/**
* A wrapper around loan schedule related data exposing needed behaviour by loan.
@@ -34,42 +36,61 @@ 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);
Loan loan = loanCharge.getLoan();
Money zero = Money.zero(currency);
Money totalInterest = zero;
Money totalPrincipal = zero;
for (final LoanRepaymentScheduleInstallment installment : repaymentPeriods) {
totalInterest = totalInterest.plus(installment.getInterestCharged(currency));
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
}
LoanChargePaidBy accrualBy = null;
if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { // TODO: why only if not interest bearing
LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, repaymentPeriods);
if (addedPeriod != null) {
addedPeriod.updateObligationsMet(currency, disbursementDate);
}
accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst()
.orElse(null);
}
LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods);
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
if (!period.isDownPayment()) {
boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber);
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();
if (period.isDownPayment()) {
continue;
}
boolean installmentChargeApplicable = !period.isRecalculatedInterestComponent();
boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber);
LocalDate dueDate = period.getDueDate();
final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, dueDate, loanCharge, currency, period,
totalPrincipal, totalInterest, installmentChargeApplicable, isFirstNonDownPaymentPeriod);
final Money feeChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, dueDate, loanCharge, currency,
installmentChargeApplicable, isFirstNonDownPaymentPeriod, feeCharge());
final Money feeChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, dueDate, loanCharge, currency,
installmentChargeApplicable, isFirstNonDownPaymentPeriod, feeCharge());
Predicate<LoanCharge> penaltyPredicate = LoanCharge::isPenaltyCharge;
final Money penaltyChargesDueForRepaymentPeriod = penaltyChargesDueWithin(startDate, dueDate, loanCharge, currency, period,
totalPrincipal, totalInterest, installmentChargeApplicable, isFirstNonDownPaymentPeriod);
final Money penaltyChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, dueDate, loanCharge, currency,
installmentChargeApplicable, isFirstNonDownPaymentPeriod, penaltyPredicate);
final Money penaltyChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, dueDate, loanCharge, currency,
installmentChargeApplicable, isFirstNonDownPaymentPeriod, penaltyPredicate);
period.addToChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod,
feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod,
penaltyChargesWrittenOffForRepaymentPeriod);
if (accrualBy != null && period.isAdditional()
&& loanChargeIsDue(startDate, dueDate, isFirstNonDownPaymentPeriod, loanCharge)) {
Money amount = Money.of(currency, accrualBy.getAmount());
boolean isFee = loanCharge.isFeeCharge();
period.updateAccrualPortion(period.getInterestAccrued(currency),
MathUtil.plus(period.getFeeAccrued(currency), (isFee ? amount : null)),
MathUtil.plus(period.getPenaltyAccrued(currency), (isFee ? null : amount)));
accrualBy.setInstallmentNumber(period.getInstallmentNumber());
}
startDate = dueDate;
}
}
@@ -198,17 +219,53 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
}
@NotNull
private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge,
private BigDecimal getBaseAmount(MonetaryCurrency currency, 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;
BigDecimal baseAmount = getBaseAmount(loanCharge, period.getPrincipal(currency).getAmount(),
period.getInterestCharged(currency).getAmount());
return MathUtil.add(amount, baseAmount);
}
@NotNull
private BigDecimal getBaseAmount(LoanCharge loanCharge, BigDecimal principal, BigDecimal interest) {
ChargeCalculationType calcType = loanCharge.getChargeCalculation();
if (calcType.isPercentageOfAmountAndInterest()) {
return MathUtil.add(principal, interest);
}
if (calcType.isPercentageOfInterest()) {
return interest;
}
return principal;
}
/**
* @return newly added period if there is any
*/
public LoanRepaymentScheduleInstallment addChargeOnlyRepaymentInstallmentIfRequired(@NotNull LoanCharge loanCharge,
List<LoanRepaymentScheduleInstallment> installments) {
if (installments == null) {
return null;
}
if (!loanCharge.isSpecifiedDueDate()) {
return null;
}
LocalDate chargeDueDate = loanCharge.getEffectiveDueDate();
LoanRepaymentScheduleInstallment latestInstallment = installments.stream().filter(i -> !i.isDownPayment())
.reduce((first, second) -> second).orElseThrow();
if (!DateUtils.isAfter(chargeDueDate, latestInstallment.getDueDate())) {
return null;
}
if (latestInstallment.isAdditional()) {
latestInstallment.updateDueDate(chargeDueDate);
return null;
} else {
Loan loan = loanCharge.getLoan();
final LoanRepaymentScheduleInstallment additionalInstallment = new LoanRepaymentScheduleInstallment(loan,
(loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestInstallment.getDueDate(), chargeDueDate, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
additionalInstallment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(additionalInstallment);
return additionalInstallment;
}
}
}
@@ -46,6 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
@@ -62,6 +63,8 @@ import org.springframework.util.CollectionUtils;
*/
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
@Override
public boolean accept(String s) {
return getCode().equalsIgnoreCase(s) || getName().equalsIgnoreCase(s);
@@ -510,7 +513,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
newLoanTransaction.getLoanTransactionRelations().add(
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
}
protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
@@ -887,33 +889,17 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> charges,
List<LoanRepaymentScheduleInstallment> installments) {
if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) {
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.stream().filter(i -> !i.isDownPayment())
.reduce((first, second) -> second).orElseThrow();
LocalDate installmentDueDate = null;
LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges);
if (latestCharge != null
&& DateUtils.isAfter(latestCharge.getEffectiveDueDate(), latestRepaymentScheduleInstalment.getDueDate())) {
installmentDueDate = latestCharge.getEffectiveDueDate();
}
if (installmentDueDate != null) {
if (latestRepaymentScheduleInstalment.isAdditional()) {
latestRepaymentScheduleInstalment.updateDueDate(installmentDueDate);
} else {
Loan loan = latestCharge.getLoan();
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(installments.size() + 1), latestRepaymentScheduleInstalment.getDueDate(), installmentDueDate, BigDecimal.ZERO,
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
installment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges);
if (latestCharge == null) {
return;
}
loanChargeProcessor.addChargeOnlyRepaymentInstallmentIfRequired(latestCharge, installments);
}
private LoanCharge getLatestLoanChargeWithSpecificDueDate(Set<LoanCharge> charges) {
protected LoanCharge getLatestLoanChargeWithSpecificDueDate(Set<LoanCharge> charges) {
if (charges == null) {
return null;
}
LoanCharge latestCharge = null;
List<LoanCharge> chargesWithSpecificDueDate = new ArrayList<>();
chargesWithSpecificDueDate.addAll(charges.stream().filter(charge -> charge.isSpecifiedDueDate()).toList());
@@ -132,7 +132,9 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
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,
Loan loan = mock(Loan.class);
when(loan.isInterestBearing()).thenReturn(false);
LoanCharge loanCharge = new LoanCharge(loan, 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;
}
@@ -150,5 +152,4 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest);
return period;
}
}
@@ -72,7 +72,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
@@ -95,8 +94,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
public final EMICalculator emiCalculator;
@Override
@@ -162,8 +159,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
installments.removeIf(LoanRepaymentScheduleInstallment::isReAged);
installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional);
addChargeOnlyRepaymentInstallmentIfRequired(charges, installments);
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetBalances();
currentInstallment.updateObligationsMet(currency, disbursementDate);
@@ -838,7 +838,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
if (!loan.isChargedOff()) {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge()
.equalsIgnoreCase("submitted-date");
.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE);
for (LoanRepaymentScheduleInstallment installment : installments) {
checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment);
}
@@ -1006,7 +1006,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCharge loanCharge) {
if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) {
final String errorMessage = "Charge and Loan must have the same currency.";
throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage);
@@ -1021,24 +1020,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
}
if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) {
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments()
.get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1);
if (DateUtils.isAfter(loanCharge.getDueDate(), latestRepaymentScheduleInstalment.getDueDate())) {
if (latestRepaymentScheduleInstalment.isAdditional()) {
latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate());
} else {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(),
loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
installment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
}
loan.addLoanCharge(loanCharge);
loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge);
// we want to apply charge transactions only for those loans charges that are applied when a loan is active and