Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ public void updateFeeChargeOutstanding(final BigDecimal totalFeeChargesOutstandi
this.totalFeeChargesOutstanding = totalFeeChargesOutstanding;
}

public void updateFeeChargesCharged(final BigDecimal totalFeeChargesCharged) {
this.totalFeeChargesCharged = totalFeeChargesCharged;
}

public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) {
this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -32,9 +33,12 @@
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -79,10 +83,19 @@ public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
// currently this is not implemented and opens up buggy edge cases
// we work this around only for cases when the loan is already closed or the requested date doesn't change
// the loan's state
if (txCount != afterRemovalTxCount || chargeCount != afterRemovalChargeCount) {
// Only regenerate and recalculate if transactions or charges were removed
// For closed loans with no changes, the summary is already correct
boolean needsRecalculation = txCount != afterRemovalTxCount || chargeCount != afterRemovalChargeCount;

if (needsRecalculation) {
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan, scheduleGeneratorDTO);
recalculateSummaryForInstallmentsUpToDate(loan, date);
} else if (!loan.isClosed()) {
// For non-closed loans, still recalculate to adjust for installment fees not yet due
recalculateSummaryForInstallmentsUpToDate(loan, date);
}
// For closed loans with no changes, skip recalculation - summary is already correct

LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan);

Expand All @@ -97,7 +110,71 @@ public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
}

private void removeAfterDateCharges(Loan loan, LocalDate date) {
loan.removeCharges(c -> DateUtils.isAfter(c.getEffectiveDueDate(), date));
// Don't remove installment fees based on effectiveDueDate since they span multiple installments
// For installment fees, effectiveDueDate returns the first UNPAID installment's due date,
// which would incorrectly remove the entire fee even if some installments are already paid/due
// The recalculateSummaryForInstallmentsUpToDate method handles installment fee adjustments separately
loan.removeCharges(c -> !c.isInstalmentFee() && DateUtils.isAfter(c.getEffectiveDueDate(), date));
}

private void recalculateSummaryForInstallmentsUpToDate(Loan loan, LocalDate date) {
var currency = loan.getCurrency();
var summary = loan.getSummary();

// Recalculate fee charged based only on the remaining charges (which excludes removed after-date charges)
// This is more reliable than trying to subtract from installment data which may include removed charges
Money feeChargedFromRemainingCharges = calculateTotalFeeChargedFromCharges(loan, date, currency);

// Include fees due at disbursement which are always included
Money adjustedFeeCharged = feeChargedFromRemainingCharges.plus(summary.getTotalFeeChargesDueAtDisbursement(currency));

// Only proceed with adjustment if the fee charged differs from summary
if (adjustedFeeCharged.getAmount().compareTo(summary.getTotalFeeChargesCharged()) == 0) {
return; // No adjustment needed
}

// Recalculate outstanding based on the adjusted charged amount
BigDecimal adjustedFeeOutstanding = adjustedFeeCharged.getAmount().subtract(summary.getTotalFeeChargesRepaid())
.subtract(summary.getTotalFeeChargesWaived()).subtract(summary.getTotalFeeChargesWrittenOff());

summary.updateFeeChargesCharged(adjustedFeeCharged.getAmount());
summary.updateFeeChargeOutstanding(adjustedFeeOutstanding);

// Recalculate total outstanding since fee outstanding changed
BigDecimal adjustedTotalOutstanding = summary.getTotalPrincipalOutstanding().add(summary.getTotalInterestOutstanding())
.add(adjustedFeeOutstanding).add(summary.getTotalPenaltyChargesOutstanding());
summary.updateTotalOutstanding(adjustedTotalOutstanding);
}

private Money calculateTotalFeeChargedFromCharges(Loan loan, LocalDate date, MonetaryCurrency currency) {
Money total = Money.zero(currency);
for (LoanCharge charge : loan.getCharges()) {
if (charge.isActive() && !charge.isPenaltyCharge() && !charge.isDueAtDisbursement()) {
// For installment fees, calculate the portion up to the date
if (charge.isInstalmentFee()) {
Money installmentTotal = calculateInstallmentFeeUpToDate(charge, date, currency);
total = total.plus(installmentTotal);
} else {
// For one-time charges, include only if due on or before the date
LocalDate chargeDueDate = charge.getEffectiveDueDate();
if (chargeDueDate != null && !DateUtils.isAfter(chargeDueDate, date)) {
total = total.plus(charge.getAmount(currency));
}
}
}
}
return total;
}

private Money calculateInstallmentFeeUpToDate(LoanCharge charge, LocalDate date, MonetaryCurrency currency) {
Money total = Money.zero(currency);
for (var installmentCharge : charge.installmentCharges()) {
var installment = installmentCharge.getInstallment();
if (installment != null && !DateUtils.isAfter(installment.getDueDate(), date)) {
total = total.plus(installmentCharge.getAmount(currency));
}
}
return total;
}

private void removeAfterDateTransactions(Loan loan, LocalDate date) {
Expand Down
Loading