Skip to content

Commit

Permalink
FINERACT-1968: Enhanced charge handling
Browse files Browse the repository at this point in the history
  • Loading branch information
adamsaghy committed Nov 23, 2023
1 parent 48300cd commit 054903a
Show file tree
Hide file tree
Showing 11 changed files with 638 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -779,14 +779,16 @@ private void handleChargePaidTransaction(final LoanCharge charge, final LoanTran
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
for (final LoanRepaymentScheduleInstallment installment : installments) {
boolean isDue = installment.isFirstPeriod()
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
? charge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate())
: charge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate());
if (installmentNumber == null && isDue) {
if (installmentNumber == null && isFirstNormalInstallment) {
chargePaymentInstallments.add(installment);
break;
} else if (installmentNumber != null && installment.getInstallmentNumber().equals(installmentNumber)) {
} else if (installment.getInstallmentNumber().equals(installmentNumber)) {
chargePaymentInstallments.add(installment);
break;
}
Expand Down Expand Up @@ -1347,7 +1349,8 @@ private void applyPeriodicAccruals(final Collection<LoanTransaction> accruals) {
LocalDate transactionDateForRange = isBasedOnSubmittedOnDate
? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getDueDate()
: loanTransaction.getTransactionDate();
if (installment.isInPeriod(transactionDateForRange)) {
boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments);
if (isInPeriod) {
interest = interest.plus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency()));
penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
Expand Down Expand Up @@ -6641,13 +6644,18 @@ public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) {
Money paidFromFutureInstallments = Money.zero(currency);
Money fee = Money.zero(currency);
Money penalty = Money.zero(currency);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);

for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) {
interest = interest.plus(installment.getInterestOutstanding(currency));
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
fee = fee.plus(installment.getFeeChargesOutstanding(currency));
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) {
Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment);
Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment,
isFirstNormalInstallment);
if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) {
interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]);
} else {
Expand Down Expand Up @@ -6680,7 +6688,7 @@ public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) {
}

private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency,
final LoanRepaymentScheduleInstallment installment) {
final LoanRepaymentScheduleInstallment installment, boolean isFirstNormalInstallment) {
Money penaltyForCurrentPeriod = Money.zero(getCurrency());
Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency());
Money feeForCurrentPeriod = Money.zero(getCurrency());
Expand All @@ -6694,7 +6702,7 @@ private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate,
interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()).plus(installment.getInterestPaid(getCurrency()));
for (LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
boolean isDue = installment.isFirstPeriod()
boolean isDue = isFirstNormalInstallment
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), paymentDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate);
if (isDue) {
Expand Down Expand Up @@ -6734,7 +6742,10 @@ public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
Money[] balances = new Money[3];
final MonetaryCurrency currency = getCurrency();
balances[0] = balances[1] = balances[2] = Money.zero(currency);
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
if (DateUtils.isEqual(paymentDate, installment.getDueDate())) {
Money interest = installment.getInterestCharged(currency);
Money fee = installment.getFeeChargesCharged(currency);
Expand All @@ -6745,7 +6756,7 @@ public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
break;
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate())
&& DateUtils.isBefore(paymentDate, installment.getDueDate())) {
balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment);
balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment, isFirstNormalInstallment);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -960,15 +960,6 @@ public void markAsAdditional() {
this.additional = true;
}

public boolean isFirstPeriod() {
return (this.installmentNumber == 1);
}

public boolean isInPeriod(LocalDate date) {
return (isFirstPeriod() ? !DateUtils.isBefore(date, getFromDate()) : DateUtils.isAfter(date, getFromDate()))
&& !DateUtils.isAfter(date, getDueDate());
}

public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
return this.loanTransactionToRepaymentScheduleMappings;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
Expand All @@ -43,13 +44,14 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
}
LocalDate startDate = disbursementDate;
LoanRepaymentScheduleInstallment firstNormalPeriod = repaymentPeriods.stream()
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber))
.filter(repaymentPeriod -> !repaymentPeriod.isDownPayment()).findFirst().orElseThrow();
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {

if (!period.isDownPayment()) {

boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream()
.filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber())
.allMatch(LoanRepaymentScheduleInstallment::isDownPayment);
boolean isFirstNonDownPaymentPeriod = period.equals(firstNormalPeriod);

final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges,
currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(),
Expand Down Expand Up @@ -235,4 +237,18 @@ private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymen
return amount;
}

public static int fetchFirstNormalInstallmentNumber(List<LoanRepaymentScheduleInstallment> installments) {
return installments.stream().sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber))
.filter(repaymentPeriod -> !repaymentPeriod.isDownPayment()).findFirst().orElseThrow().getInstallmentNumber();
}

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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,11 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
}
LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods);
for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) {

if (!period.isDownPayment()) {

boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream()
.filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber())
.allMatch(LoanRepaymentScheduleInstallment::isDownPayment);
boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber);

final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency,
period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur
}
}
LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (final LoanRepaymentScheduleInstallment installment : installments) {
boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
for (final LoanCharge loanCharge : transferCharges) {
boolean isDue = installment.isFirstPeriod()
boolean isDue = isFirstPeriod
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
if (isDue) {
Expand Down Expand Up @@ -635,15 +637,15 @@ protected Set<LoanCharge> extractPenaltyCharges(final Set<LoanCharge> loanCharge
return penaltyCharges;
}

protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set<LoanCharge> charges,
final Integer installmentNumber) {

Money amountRemaining = feeCharges;
Money amountRemaining = chargeAmount;
while (amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency());
Money feeAmount = feeCharges.zero();
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency());
Money feeAmount = chargeAmount.zero();
if (loanTransaction.isChargePayment()) {
feeAmount = feeCharges;
feeAmount = chargeAmount;
}
if (unpaidCharge == null) {
break; // All are trache charges
Expand All @@ -669,6 +671,18 @@ protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction,

}

public interface ChargesPaidByFunction {

void accept(LoanTransaction loanTransaction, Money feeCharges, Set<LoanCharge> charges, Integer installmentNumber);
}

public ChargesPaidByFunction getChargesPaymentFunction(LoanRepaymentScheduleInstallment.PaymentAction action) {
return switch (action) {
case PAY -> this::updateChargesPaidAmountBy;
case UNPAY -> this::undoChargesPaidAmountBy;
};
}

protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, final MonetaryCurrency currency) {
LoanCharge earliestUnpaidCharge = null;
LoanCharge installemntCharge = null;
Expand Down Expand Up @@ -770,15 +784,15 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}

protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money chargeAmount, final Set<LoanCharge> charges,
final Integer installmentNumber) {

Money amountRemaining = feeCharges;
Money amountRemaining = chargeAmount;
while (amountRemaining.isGreaterThanZero()) {
final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency());
final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency());

if (paidCharge != null) {
Money feeAmount = feeCharges.zero();
Money feeAmount = chargeAmount.zero();

final Money amountDeductedTowardsCharge = paidCharge.undoPaidOrPartiallyAmountBy(amountRemaining, installmentNumber,
feeAmount);
Expand Down
Loading

0 comments on commit 054903a

Please sign in to comment.