FINERACT-2081: Payments after loan charge - COB accrual
This commit is contained in:
committed by
Adam Saghy
parent
889fc8d2cb
commit
e94ce7f1df
@@ -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
|
||||
+4
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+96
-39
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-25
@@ -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());
|
||||
|
||||
+3
-2
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-5
@@ -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);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user