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:
|
Then Loan Repayment schedule has the following data in Total row:
|
||||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
| 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 |
|
| 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;
|
return this.loanTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoanTransaction(final LoanTransaction loanTransaction) {
|
|
||||||
this.loanTransaction = loanTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoanCharge getLoanCharge() {
|
public LoanCharge getLoanCharge() {
|
||||||
return this.loanCharge;
|
return this.loanCharge;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoanCharge(final LoanCharge loanCharge) {
|
|
||||||
this.loanCharge = loanCharge;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal getAmount() {
|
public BigDecimal getAmount() {
|
||||||
return this.amount;
|
return this.amount;
|
||||||
}
|
}
|
||||||
@@ -83,4 +75,8 @@ public class LoanChargePaidBy extends AbstractPersistableCustom<Long> {
|
|||||||
public Integer getInstallmentNumber() {
|
public Integer getInstallmentNumber() {
|
||||||
return this.installmentNumber;
|
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,
|
public static boolean isInPeriod(LocalDate transactionDate, LoanRepaymentScheduleInstallment targetInstallment,
|
||||||
List<LoanRepaymentScheduleInstallment> installments) {
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
int firstPeriod = fetchFirstNormalInstallmentNumber(installments);
|
int firstPeriod = fetchFirstNormalInstallmentNumber(installments);
|
||||||
return (targetInstallment.getInstallmentNumber().equals(firstPeriod)
|
return isInPeriod(transactionDate, targetInstallment, targetInstallment.getInstallmentNumber().equals(firstPeriod));
|
||||||
? !DateUtils.isBefore(transactionDate, targetInstallment.getFromDate())
|
|
||||||
: DateUtils.isAfter(transactionDate, targetInstallment.getFromDate()))
|
|
||||||
&& !DateUtils.isAfter(transactionDate, targetInstallment.getDueDate());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
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.
|
* 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,
|
public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate,
|
||||||
final List<LoanRepaymentScheduleInstallment> repaymentPeriods, LoanCharge loanCharge) {
|
final List<LoanRepaymentScheduleInstallment> repaymentPeriods, LoanCharge loanCharge) {
|
||||||
|
Loan loan = loanCharge.getLoan();
|
||||||
Money totalInterest = Money.zero(currency);
|
Money zero = Money.zero(currency);
|
||||||
Money totalPrincipal = Money.zero(currency);
|
Money totalInterest = zero;
|
||||||
|
Money totalPrincipal = zero;
|
||||||
for (final LoanRepaymentScheduleInstallment installment : repaymentPeriods) {
|
for (final LoanRepaymentScheduleInstallment installment : repaymentPeriods) {
|
||||||
totalInterest = totalInterest.plus(installment.getInterestCharged(currency));
|
totalInterest = totalInterest.plus(installment.getInterestCharged(currency));
|
||||||
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(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;
|
LocalDate startDate = disbursementDate;
|
||||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods);
|
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods);
|
||||||
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
|
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {
|
||||||
|
if (period.isDownPayment()) {
|
||||||
if (!period.isDownPayment()) {
|
continue;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
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
|
@NotNull
|
||||||
private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge,
|
private BigDecimal getBaseAmount(MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge,
|
||||||
BigDecimal amount) {
|
BigDecimal amount) {
|
||||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
BigDecimal baseAmount = getBaseAmount(loanCharge, period.getPrincipal(currency).getAmount(),
|
||||||
amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount())
|
period.getInterestCharged(currency).getAmount());
|
||||||
.add(period.getInterestCharged(monetaryCurrency).getAmount());
|
return MathUtil.add(amount, baseAmount);
|
||||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
|
||||||
amount = amount.add(period.getInterestCharged(monetaryCurrency).getAmount());
|
|
||||||
} else {
|
|
||||||
amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount());
|
|
||||||
}
|
|
||||||
return amount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
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.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
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 abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
|
||||||
|
|
||||||
|
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean accept(String s) {
|
public boolean accept(String s) {
|
||||||
return getCode().equalsIgnoreCase(s) || getName().equalsIgnoreCase(s);
|
return getCode().equalsIgnoreCase(s) || getName().equalsIgnoreCase(s);
|
||||||
@@ -510,7 +513,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
newLoanTransaction.getLoanTransactionRelations().add(
|
newLoanTransaction.getLoanTransactionRelations().add(
|
||||||
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
|
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
|
||||||
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
|
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
|
protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
|
||||||
@@ -887,33 +889,17 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
|||||||
|
|
||||||
protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> charges,
|
protected void addChargeOnlyRepaymentInstallmentIfRequired(Set<LoanCharge> charges,
|
||||||
List<LoanRepaymentScheduleInstallment> installments) {
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) {
|
LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges);
|
||||||
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.stream().filter(i -> !i.isDownPayment())
|
if (latestCharge == null) {
|
||||||
.reduce((first, second) -> second).orElseThrow();
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
loanChargeProcessor.addChargeOnlyRepaymentInstallmentIfRequired(latestCharge, installments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanCharge getLatestLoanChargeWithSpecificDueDate(Set<LoanCharge> charges) {
|
protected LoanCharge getLatestLoanChargeWithSpecificDueDate(Set<LoanCharge> charges) {
|
||||||
|
if (charges == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
LoanCharge latestCharge = null;
|
LoanCharge latestCharge = null;
|
||||||
List<LoanCharge> chargesWithSpecificDueDate = new ArrayList<>();
|
List<LoanCharge> chargesWithSpecificDueDate = new ArrayList<>();
|
||||||
chargesWithSpecificDueDate.addAll(charges.stream().filter(charge -> charge.isSpecifiedDueDate()).toList());
|
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.getName()).thenReturn("charge a");
|
||||||
when(charge.getCurrencyCode()).thenReturn("UDS");
|
when(charge.getCurrencyCode()).thenReturn("UDS");
|
||||||
when(charge.isPenalty()).thenReturn(penalty);
|
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);
|
ChargeCalculationType.FLAT, LocalDate.of(2023, 01, 15), ChargePaymentMode.REGULAR, 1, null, null);
|
||||||
return loanCharge;
|
return loanCharge;
|
||||||
}
|
}
|
||||||
@@ -150,5 +152,4 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
|
|||||||
Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest);
|
Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest);
|
||||||
return period;
|
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.LoanTransactionRelation;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
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.reaging.LoanReAgeParameter;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
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 static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
|
||||||
|
|
||||||
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
|
||||||
|
|
||||||
public final EMICalculator emiCalculator;
|
public final EMICalculator emiCalculator;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -162,8 +159,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
installments.removeIf(LoanRepaymentScheduleInstallment::isReAged);
|
installments.removeIf(LoanRepaymentScheduleInstallment::isReAged);
|
||||||
installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional);
|
installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional);
|
||||||
|
|
||||||
addChargeOnlyRepaymentInstallmentIfRequired(charges, installments);
|
|
||||||
|
|
||||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||||
currentInstallment.resetBalances();
|
currentInstallment.resetBalances();
|
||||||
currentInstallment.updateObligationsMet(currency, disbursementDate);
|
currentInstallment.updateObligationsMet(currency, disbursementDate);
|
||||||
|
|||||||
+1
-1
@@ -838,7 +838,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
|||||||
if (!loan.isChargedOff()) {
|
if (!loan.isChargedOff()) {
|
||||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||||
boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge()
|
boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge()
|
||||||
.equalsIgnoreCase("submitted-date");
|
.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE);
|
||||||
for (LoanRepaymentScheduleInstallment installment : installments) {
|
for (LoanRepaymentScheduleInstallment installment : installments) {
|
||||||
checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment);
|
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) {
|
private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCharge loanCharge) {
|
||||||
|
|
||||||
if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) {
|
if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) {
|
||||||
final String errorMessage = "Charge and Loan must have the same currency.";
|
final String errorMessage = "Charge and Loan must have the same currency.";
|
||||||
throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage);
|
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);
|
loan.addLoanCharge(loanCharge);
|
||||||
|
|
||||||
loanCharge = this.loanChargeRepository.saveAndFlush(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
|
// 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