FINERACT-2421: Disbursement charge amount incorrect calculation

This commit is contained in:
Jose Alberto Hernandez
2026-02-05 10:41:19 +01:00
committed by Adam Saghy
parent 2620c61afa
commit 1d1876edfd
22 changed files with 136 additions and 116 deletions
@@ -962,7 +962,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
for (LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
if (loanDisbursementDetails.actualDisbursementDate() == null) {
totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.principal()));
totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.getPrincipal()));
}
}
@@ -175,8 +175,8 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
for (LoanRepaymentScheduleInstallment installment : installments) {
if (loanDisbursementDetails != null
&& !DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDateAsLocalDate(), installment.getDueDate())) {
outstanding = outstanding.plus(loanDisbursementDetails.principal());
principal = principal.plus(loanDisbursementDetails.principal());
outstanding = outstanding.plus(loanDisbursementDetails.getPrincipal());
principal = principal.plus(loanDisbursementDetails.getPrincipal());
if (disbursementItr.hasNext()) {
loanDisbursementDetails = disbursementItr.next();
} else {
@@ -97,7 +97,7 @@ public final class LoanApprovedAmountValidatorImpl implements LoanApprovedAmount
BigDecimal totalPrincipalOnLoan = loan.getSummary().getTotalPrincipal();
BigDecimal totalExpectedPrincipal = loan.getDisbursementDetails().stream().filter(t -> t.actualDisbursementDate() == null)
.map(LoanDisbursementDetails::principal).reduce(BigDecimal.ZERO, BigDecimal::add);
.map(LoanDisbursementDetails::getPrincipal).reduce(BigDecimal.ZERO, BigDecimal::add);
if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan.add(totalExpectedPrincipal))) {
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
.failWithCode("less.than.disbursed.principal.and.capitalized.income");
@@ -145,7 +145,7 @@ public final class LoanApprovedAmountValidatorImpl implements LoanApprovedAmount
}
BigDecimal expectedDisbursementAmount = loan.getDisbursementDetails().stream().filter(t -> t.actualDisbursementDate() == null)
.map(LoanDisbursementDetails::principal).reduce(BigDecimal.ZERO, BigDecimal::add);
.map(LoanDisbursementDetails::getPrincipal).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal maximumAvailableDisbursementThreshold = maximumThresholdForApprovedAmount
.subtract(loan.getSummary().getTotalPrincipal()).subtract(expectedDisbursementAmount);
@@ -174,7 +174,7 @@ public class LoanChargeAssembler {
} else {
if (disbursementDetail.expectedDisbursementDateAsLocalDate().equals(expectedDisbursementDate)) {
final LoanCharge loanCharge = createNewWithoutLoan(chargeDefinition,
disbursementDetail.principal(), amount, chargeTime, chargeCalculation,
disbursementDetail.getPrincipal(), amount, chargeTime, chargeCalculation,
disbursementDetail.expectedDisbursementDateAsLocalDate(), chargePaymentModeEnum,
numberOfRepayments, externalId);
loanCharges.add(loanCharge);
@@ -191,8 +191,8 @@ public class LoanChargeAssembler {
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null;
for (LoanDisbursementDetails disbursementDetail : disbursementDetails) {
if (ChargeTimeType.TRANCHE_DISBURSEMENT.getValue().equals(chargeDefinition.getChargeTimeType())) {
final LoanCharge loanCharge = createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(),
amount, chargeTime, chargeCalculation,
final LoanCharge loanCharge = createNewWithoutLoan(chargeDefinition,
disbursementDetail.getPrincipal(), amount, chargeTime, chargeCalculation,
disbursementDetail.expectedDisbursementDateAsLocalDate(), chargePaymentModeEnum,
numberOfRepayments, externalId);
loanCharges.add(loanCharge);
@@ -309,7 +309,7 @@ public class LoanChargeAssembler {
amountPercentageAppliedTo = BigDecimal.ZERO;
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), dueDate)) {
amountPercentageAppliedTo = amountPercentageAppliedTo.add(loanDisbursementDetails.principal());
amountPercentageAppliedTo = amountPercentageAppliedTo.add(loanDisbursementDetails.getPrincipal());
}
}
}
@@ -105,7 +105,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachin
import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -159,7 +158,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final ConfigurationDomainService configurationDomainService;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final ExternalIdFactory externalIdFactory;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final LoanChargeAssembler loanChargeAssembler;
@@ -216,7 +214,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
externalId = externalIdFactory.create();
}
LocalDate dueDate = disbursementDetail.expectedDisbursementDateAsLocalDate();
loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null,
loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.getPrincipal(), null, null,
null, dueDate, null, null, externalId);
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
@@ -138,7 +138,7 @@ public class LoanDisbursementService {
disburseAmount = disburseAmount.zero();
for (LoanDisbursementDetails disbursementDetails : details) {
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
disburseAmount = disburseAmount.plus(disbursementDetails.principal());
disburseAmount = disburseAmount.plus(disbursementDetails.getPrincipal());
}
}
} else {
@@ -162,7 +162,7 @@ public class LoanDisbursementService {
// First try to find a tranche that exactly matches the requested disbursement amount
for (LoanDisbursementDetails disbursementDetails : details) {
if (disbursementDetails.actualDisbursementDate() == null
&& disbursementDetails.principal().compareTo(principalDisbursed) == 0) {
&& disbursementDetails.getPrincipal().compareTo(principalDisbursed) == 0) {
selectedTranche = disbursementDetails;
break;
}
@@ -198,9 +198,9 @@ public class LoanDisbursementService {
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
if (disbursementDetails.actualDisbursementDate() != null) {
setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal());
setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.getPrincipal());
}
totalAmount = totalAmount.add(disbursementDetails.principal());
totalAmount = totalAmount.add(disbursementDetails.getPrincipal());
}
loan.getLoanRepaymentScheduleDetail().setPrincipal(setPrincipalAmount);
} else {
@@ -435,7 +435,7 @@ public class LoanDisbursementService {
LoanDisbursementDetails singleDetail = undisbursedDetails.iterator().next();
BigDecimal loanPrincipal = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
if (singleDetail.principal().compareTo(loanPrincipal) == 0) {
if (singleDetail.getPrincipal().compareTo(loanPrincipal) == 0) {
return false;
}
}
@@ -452,7 +452,8 @@ public class LoanDisbursementService {
return disbursementDetails.stream()
.sorted(Comparator.comparing(LoanDisbursementDetails::expectedDisbursementDate)
.thenComparing((LoanDisbursementDetails d1, LoanDisbursementDetails d2) -> d2.principal().compareTo(d1.principal()))
.thenComparing(
(LoanDisbursementDetails d1, LoanDisbursementDetails d2) -> d2.getPrincipal().compareTo(d1.getPrincipal()))
.thenComparing(LoanDisbursementDetails::getId))
.collect(Collectors.toList());
}
@@ -1666,10 +1666,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
}
public String schema() {
return "dd.id as id, dd.loan_id as loanId, dd.expected_disburse_date as expectedDisbursementdate, dd.disbursedon_date as actualDisbursementdate,dd.principal as principal,dd.net_disbursal_amount as netDisbursalAmount,sum(lc.amount) chargeAmount, lc.amount_waived_derived waivedAmount, "
+ sqlGenerator.groupConcat("lc.id") + " loanChargeId "
+ "from m_loan l inner join m_loan_disbursement_detail dd on dd.loan_id = l.id left join m_loan_tranche_disbursement_charge tdc on tdc.disbursement_detail_id=dd.id "
+ "left join m_loan_charge lc on lc.id=tdc.loan_charge_id and lc.is_active=true";
return "dd.id as id, dd.loan_id as loanId, dd.expected_disburse_date as expectedDisbursementdate, dd.disbursedon_date as actualDisbursementdate, "
+ "dd.principal as principal,dd.net_disbursal_amount as netDisbursalAmount, sum(lc.amount) as chargeAmount, coalesce(sum(cad.amount), 0) as disburseChargeAmount, "
+ "lc.amount_waived_derived waivedAmount, coalesce(" + sqlGenerator.groupConcat("lc.id") + ", "
+ sqlGenerator.groupConcat("cad.id") + ") loanChargeId "
+ "from m_loan l inner join m_loan_disbursement_detail dd on dd.loan_id = l.id "
+ "left join m_loan_tranche_disbursement_charge tdc on tdc.disbursement_detail_id = dd.id "
+ "left join m_loan_charge lc on lc.id = tdc.loan_charge_id and lc.is_active = true "
+ "left join (select lch.id, lch.loan_id, lch.amount from m_loan_charge lch inner join m_charge ch on ch.id = lch.charge_id and ch.charge_time_enum = 1) cad on cad.loan_id = l.id";
}
@Override
@@ -1686,8 +1690,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
if (chargeAmount != null && waivedAmount != null) {
chargeAmount = chargeAmount.subtract(waivedAmount);
}
BigDecimal disburseChargeAmount = rs.getBigDecimal("disburseChargeAmount");
return new DisbursementData(id, loanId, expectedDisbursementdate, actualDisbursementdate, principal, netDisbursalAmount,
loanChargeId, chargeAmount, waivedAmount);
loanChargeId, chargeAmount, waivedAmount).setDisburseChargeAmount(disburseChargeAmount);
}
}
@@ -365,7 +365,12 @@ public class LoanRepaymentScheduleService {
if (dataItem.isDisbursement()) {
// Process disbursement data
DisbursementData data = (DisbursementData) dataItem.getData();
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
if (periods.isEmpty()) {
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
} else {
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount.subtract(data.getDisburseChargeAmount()),
waivedChargeAmount);
}
} else {
// Process capitalized income data
LoanTransactionRepaymentPeriodData data = (LoanTransactionRepaymentPeriodData) dataItem.getData();
@@ -2209,7 +2209,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
if (disbursementDetails.actualDisbursementDate() != null) {
totalDisbursedAmount = totalDisbursedAmount.add(disbursementDetails.principal());
totalDisbursedAmount = totalDisbursedAmount.add(disbursementDetails.getPrincipal());
}
}
if (totalDisbursedAmount.compareTo(loan.getApprovedPrincipal()) == 0) {
@@ -314,11 +314,10 @@ public class LoanAccountConfiguration {
LoanWritePlatformService loanWritePlatformService, LoanUtilService loanUtilService,
LoanChargeReadPlatformService loanChargeReadPlatformService, LoanLifecycleStateMachine loanLifecycleStateMachine,
AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, FromJsonHelper fromApiJsonHelper,
ConfigurationDomainService configurationDomainService,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
ExternalIdFactory externalIdFactory, AccountTransferDetailRepository accountTransferDetailRepository,
LoanChargeAssembler loanChargeAssembler, PaymentDetailWritePlatformService paymentDetailWritePlatformService,
NoteRepository noteRepository, LoanAccrualsProcessingService loanAccrualsProcessingService,
ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
AccountTransferDetailRepository accountTransferDetailRepository, LoanChargeAssembler loanChargeAssembler,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository,
LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService, LoanChargeService loanChargeService,
@@ -327,10 +326,9 @@ public class LoanAccountConfiguration {
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService, loanChargeReadPlatformService,
loanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper, configurationDomainService,
loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory, accountTransferDetailRepository, loanChargeAssembler,
paymentDetailWritePlatformService, noteRepository, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService,
loanChargeService, loanJournalEntryPoster);
externalIdFactory, accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
loanAccrualsProcessingService, loanDownPaymentTransactionValidator, loanChargeValidator, loanScheduleService,
reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService, loanChargeService, loanJournalEntryPoster);
}
@Bean