Skip to content

Commit

Permalink
SU-320 (#1298)
Browse files Browse the repository at this point in the history
* SU-320
  • Loading branch information
faheem205 authored Nov 20, 2024
1 parent e501b34 commit c33f270
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4219,9 +4219,17 @@ private Money calculateTotalOverpayment() {
.plus(scheduledRepayment.getPenaltyChargesWrittenOff(currency));
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
.plus(scheduledRepayment.getFeeChargesPaid(currency))
.plus(scheduledRepayment.getPenaltyChargesPaid(currency).plus(scheduledRepayment.getAdvancePrincipalAmount()))
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency))
.plus(scheduleWrittenOffValue);
if (scheduledRepayment.isLastInstallment(installments) && scheduledRepayment.isOverpaidInAdvance(currency)
&& scheduledRepayment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) {
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments
.plus(scheduledRepayment.getInterestWaived(currency));
// Do not add advance payment amount if installment was overpaid
continue;
} else {
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getAdvancePrincipalAmount());
}

cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
}
Expand All @@ -4239,7 +4247,18 @@ private Money calculateTotalOverpayment() {

// if total paid in transactions doesnt match repayment schedule then
// theres an overpayment.
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
Money overpaid = totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
if (overpaid.isZero()) {
Money totalPrincipalPaid = Money.zero(this.getCurrency());
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
totalPrincipalPaid = totalPrincipalPaid.add(
scheduledRepayment.getPrincipalCompleted(this.getCurrency()).plus(scheduledRepayment.getAdvancePrincipalAmount()));
}
if (totalPrincipalPaid.isGreaterThan(this.getPrincipal())) {
overpaid = totalPrincipalPaid.minus(this.getPrincipal());
}
}
return overpaid;
}

public Money calculateTotalRecoveredPayments() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,8 @@ public Money payInterestComponent(final LocalDate transactionDate, final Money t
this.originalInterestChargedAmount = this.interestCharged;
this.interestCharged = getInterestPaid(currency).plus(getInterestWaived(currency)).plus(getInterestWrittenOff(currency))
.plus(interestDue).getAmount();
} else {
this.originalInterestChargedAmount = BigDecimal.ZERO;
}

} else {
Expand Down Expand Up @@ -1261,7 +1263,7 @@ public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate
}
}

private void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency,
public void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency,
final Money amountPaidInRepaymentPeriod) {
if (isInAdvance(transactionDate)) {
this.totalPaidInAdvance = asMoney(this.totalPaidInAdvance, currency).plus(amountPaidInRepaymentPeriod).getAmount();
Expand Down Expand Up @@ -1330,6 +1332,7 @@ public void updateInterestCharged(final BigDecimal interestCharged) {
}

public void updateObligationMet(final Boolean obligationMet) {

this.obligationsMet = obligationMet;
}

Expand Down Expand Up @@ -1668,4 +1671,14 @@ public void setInterestRecalculatedOnDate(LocalDate interestRecalculatedOnDate)
this.interestRecalculatedOnDate = interestRecalculatedOnDate;
}

public boolean isLastInstallment(List<LoanRepaymentScheduleInstallment> installments) {
return this.installmentNumber.equals(installments.get(installments.size() - 1).getInstallmentNumber());
}

public boolean isOverpaidInAdvance(MonetaryCurrency currency) {
return this.getPrincipal(currency).isGreaterThanZero() && this.getInterestCharged(currency).isZero()
&& this.getFeeChargesCharged(currency).isZero() && this.getPenaltyChargesCharged(currency).isZero()
&& this.isRecalculatedInterestComponent();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
Expand All @@ -37,7 +38,12 @@ public Money calculateTotalPrincipalRepaid(final List<LoanRepaymentScheduleInsta
Money total = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
total = total.plus(installment.getPrincipalCompleted(currency));
total = total.plus(installment.getAdvancePrincipalAmount());
if (installment.isLastInstallment(repaymentScheduleInstallments) && installment.isOverpaidInAdvance(currency)
&& installment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) {
continue;
} else {
total = total.plus(installment.getAdvancePrincipalAmount());
}
}
return total;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1172,10 +1172,14 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
}
// For having similar logic we are populating installment list even when the future installment
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
// As per SU+ requirements, advance payment goes to outstanding balance so first immediate advance
// installment
// will always be seleted
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getFromDate())).toList();
.filter(e -> loanTransaction.isBefore(e.getFromDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getFromDate()))
Expand All @@ -1187,7 +1191,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
}

int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);

boolean stopProcessingAdvanceInstallment = false;
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> {
Expand Down Expand Up @@ -1220,7 +1224,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
}
}
case IN_ADVANCE -> {
if (loanTransaction.doNotProcessAdvanceInstallments()) {
if (loanTransaction.doNotProcessAdvanceInstallments() || stopProcessingAdvanceInstallment) {
// This condition will only be true if loan processing type is VERTICAL.
// For vertical payments, Past Due and Due installments MUST be processed Horizontally
exit = true;
Expand Down Expand Up @@ -1252,18 +1256,49 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
Money zero = transactionAmountUnprocessed.zero();
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
if (transactionAmountUnprocessed.isGreaterThanZero()) {
loanTransaction.updateComponents(transactionAmountUnprocessed, Money.zero(currency),
Money.zero(currency), Money.zero(currency));
inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount()
.add(transactionAmountUnprocessed.getAmount()));
inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI());
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed,
zero, zero, zero);
if (inAdvanceInstallment.isLastInstallment(installments)
&& inAdvanceInstallment.isOverpaidInAdvance(currency) && transactionAmountUnprocessed
.isGreaterThan(inAdvanceInstallment.getPrincipal(currency))) {
// This MUST be true only in case of advance overpayment after repayment
// schedule is regenerated
// Process principal and move the remaining amount to overpaid

Money paidPrincipalComponent = inAdvanceInstallment.payPrincipalComponent(
loanTransaction.getTransactionDate(), transactionAmountUnprocessed, false,
loanTransaction);

inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount()
.add(transactionAmountUnprocessed.getAmount()));

balances.setAggregatedPrincipalPortion(
balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed));
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed,
zero, zero, zero);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPrincipalComponent);
stopProcessingAdvanceInstallment = true;

} else {
balances.setAggregatedPrincipalPortion(
balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed));
inAdvanceInstallment.checkIfRepaymentPeriodObligationsAreMet(
loanTransaction.getTransactionDate(), currency);

inAdvanceInstallment.trackAdvanceAndLateTotalsForRepaymentPeriod(
loanTransaction.getTransactionDate(), currency, transactionAmountUnprocessed);
inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount()
.add(transactionAmountUnprocessed.getAmount()));
inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI());
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed,
zero, zero, zero);

transactionAmountUnprocessed = Money.zero(currency);
}
}
}
transactionAmountUnprocessed = Money.zero(currency);
exit = true;
} else {
exit = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3070,6 +3070,27 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math
advancePayments.add(new RecalculationDetail(loanTransaction.getTransactionDate(), loanTransaction));
}
}
// Extra amount paid in advance
if (outstandingBalance.isLessThanZero()) {
// If overpaid in advance then add the last installment to keep track of advance payment amount
outstandingBalanceAsPerRest = outstandingBalance.zero();
outstandingBalance = outstandingBalance.zero();
BigDecimal remainingLoanPrincipal = principalToBeScheduled.getAmount();
for (LoanRepaymentScheduleInstallment inst : loan.getRepaymentScheduleInstallments()) {
if (inst.getInstallmentNumber().intValue() < installment.getInstallmentNumber()) {
remainingLoanPrincipal = remainingLoanPrincipal
.subtract(inst.getPrincipal(loanApplicationTerms.getCurrency()).getAmount()
.subtract(inst.getAdvancePrincipalAmount()));
}
}
installment.setPrincipal(remainingLoanPrincipal);
installment.setInterestCharged(BigDecimal.ZERO);
installment.setFeeChargesCharged(BigDecimal.ZERO);
installment.setPenaltyCharges(BigDecimal.ZERO);
installment.setRecalculatedInterestComponent(true);
installment.getInstallmentCharges().clear();
newRepaymentScheduleInstallments.add(installment);
}
////
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1525,8 +1525,10 @@ public LoanScheduleData extractData(@NotNull final ResultSet rs) throws SQLExcep
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalDue);
}
BigDecimal advancePrincipalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "advancePrincipalAmount");
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount);
outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount);
if (this.outstandingLoanPrincipalBalance.compareTo(BigDecimal.ZERO) > 0) {
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount);
outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount);
}
final boolean isDownPayment = rs.getBoolean("isDownPayment");

LoanSchedulePeriodData periodData;
Expand Down

0 comments on commit c33f270

Please sign in to comment.