FINERACT-2124: Fix final income accrual calculation with multiple fee charges

This commit is contained in:
mariiaKraievska
2025-08-05 13:46:22 +02:00
committed by Adam Saghy
parent 68f20c29e9
commit 0daf42ec33
6 changed files with 328 additions and 3 deletions
@@ -1541,7 +1541,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int numberOfInstallments = 0;
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (!installment.isRecalculatedInterestComponent()) {
if (!installment.isRecalculatedInterestComponent() && !installment.isAdditional() && !installment.isDownPayment()
&& !installment.isReAged()) {
numberOfInstallments++;
}
}
@@ -281,6 +281,19 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
""")
BigDecimal findChargeAccrualAmount(@Param("loanCharge") LoanCharge loanCharge);
@Query("""
SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL THEN lcpb.amount
WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT THEN -lcpb.amount
ELSE 0 END), 0)
FROM LoanChargePaidBy lcpb
JOIN lcpb.loanTransaction lt
WHERE lcpb.loanCharge = :loanCharge
AND lcpb.installmentNumber = :installmentNumber
AND lt.reversed = false
""")
BigDecimal findChargeAccrualAmountByInstallment(@Param("loanCharge") LoanCharge loanCharge,
@Param("installmentNumber") Integer installmentNumber);
@Query("""
SELECT COALESCE(SUM(lt.unrecognizedIncomePortion), 0)
FROM LoanChargePaidBy lcpb
@@ -725,6 +725,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final Integer installmentNumber) {
Money amountRemaining = chargeAmount;
final Set<LoanCharge> chargesThatCannotBeFullyPaidByOneInstallment = new HashSet<>();
while (amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, chargeAmount.getCurrency());
Money feeAmount = chargeAmount.zero();
@@ -734,6 +736,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (unpaidCharge == null) {
break; // All are trache charges
}
// If we've already determined this charge cannot be fully paid by one installment, skip it
if (chargesThatCannotBeFullyPaidByOneInstallment.contains(unpaidCharge)) {
charges.remove(unpaidCharge);
}
final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining, installmentNumber, feeAmount);
if (!amountPaidTowardsCharge.isZero()) {
Set<LoanChargePaidBy> chargesPaidBies = loanTransaction.getLoanChargesPaid();
@@ -750,6 +758,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
chargesPaidBies.add(loanChargePaidBy);
}
amountRemaining = amountRemaining.minus(amountPaidTowardsCharge);
} else {
chargesThatCannotBeFullyPaidByOneInstallment.add(unpaidCharge);
}
}
@@ -786,6 +796,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
}
if (earliestUnpaidCharge == null || (chargePerInstallment != null && DateUtils.isAfter(earliestUnpaidCharge.getDueLocalDate(),
chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
earliestUnpaidCharge = installemntCharge;
@@ -655,7 +655,8 @@ public class LoanChargeService {
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge) {
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
final List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional() && !i.isReAged()).toList();
for (final LoanRepaymentScheduleInstallment installment : installments) {
BigDecimal amount;
if (loanCharge.getChargeCalculation().isFlat()) {