FINERACT-2181: Fix Consult Loan repayment schedule with disbursement charge using interest

This commit is contained in:
Jose Alberto Hernandez
2025-05-10 14:35:29 +02:00
committed by Arnold Gálovics
parent f2efad40a6
commit 9026fb9546
4 changed files with 80 additions and 6 deletions
@@ -201,8 +201,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.externalId = externalId;
}
private void populateDerivedFields(final BigDecimal amountPercentageAppliedTo, final BigDecimal chargeAmount,
Integer numberOfRepayments, BigDecimal loanCharge) {
public void populateDerivedFields(final BigDecimal amountPercentageAppliedTo, final BigDecimal chargeAmount, Integer numberOfRepayments,
BigDecimal loanCharge) {
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
case INVALID:
@@ -36,7 +36,7 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo
private final Integer periodNumber;
private final LocalDate disbursementDate;
private final Money principalDisbursed;
private final BigDecimal chargesDueAtTimeOfDisbursement;
private BigDecimal chargesDueAtTimeOfDisbursement;
private boolean isEMIFixedSpecificToInstallment = false;
public static LoanScheduleModelDisbursementPeriod disbursement(final LocalDate disbursementDate, final Money principalDisbursed,
@@ -105,7 +105,7 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo
@Override
public void addLoanCharges(@SuppressWarnings("unused") BigDecimal feeCharge, @SuppressWarnings("unused") BigDecimal penaltyCharge) {
return;
this.chargesDueAtTimeOfDisbursement = this.chargesDueAtTimeOfDisbursement.add(feeCharge);
}
@Override
@@ -35,6 +35,7 @@ import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -110,6 +111,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplica
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator;
@@ -719,7 +722,9 @@ public class LoanScheduleAssembler {
final List<Holiday> holidays, final WorkingDays workingDays, final JsonElement element,
List<LoanDisbursementDetails> disbursementDetails) {
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails);
Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails);
final Set<LoanCharge> nonCompoundingCharges = validateDisbursementPercentageCharges(loanCharges);
loanCharges.removeAll(nonCompoundingCharges);
final MathContext mc = MoneyHelper.getMathContext();
HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
@@ -742,7 +747,11 @@ public class LoanScheduleAssembler {
loanApplicationTerms.getInterestMethod());
}
return loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO);
LoanScheduleModel loanScheduleModel = loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO);
if (!nonCompoundingCharges.isEmpty()) {
updateDisbursementWithCharges(loanScheduleModel.getPeriods(), nonCompoundingCharges);
}
return loanScheduleModel;
}
public LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationTerms loanApplicationTerms, final Long officeId,
@@ -1542,4 +1551,31 @@ public class LoanScheduleAssembler {
return Pair.of(loan, actualChanges);
}
private Set<LoanCharge> validateDisbursementPercentageCharges(final Set<LoanCharge> loanCharges) {
Set<LoanCharge> interestCharges = new HashSet<>();
if (loanCharges != null) {
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDisbursementCharge() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
interestCharges.add(loanCharge);
}
}
}
return interestCharges;
}
private void updateDisbursementWithCharges(final Collection<LoanScheduleModelPeriod> periods,
final Set<LoanCharge> nonCompoundingCharges) {
final BigDecimal totalInterest = periods.stream().filter(p -> p.isRepaymentPeriod()).map(LoanScheduleModelPeriod::interestDue)
.reduce(BigDecimal.ZERO, BigDecimal::add);
for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) {
if (loanScheduleModelPeriod instanceof LoanScheduleModelDisbursementPeriod) {
for (final LoanCharge loanCharge : nonCompoundingCharges) {
loanCharge.populateDerivedFields(totalInterest, loanCharge.amountOrPercentage(), null, BigDecimal.ZERO);
loanScheduleModelPeriod.addLoanCharges(loanCharge.getAmountOutstanding(), BigDecimal.ZERO);
}
}
}
}
}
@@ -1047,6 +1047,44 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
}
@Test
public void testLoanCharges_DISBURSEMENT_WITH_INTEREST() {
Calendar fourMonthsfromNowCalendar = Calendar.getInstance(Utils.getTimeZoneOfTenant());
fourMonthsfromNowCalendar.add(Calendar.MONTH, -4);
if (fourMonthsfromNowCalendar.get(Calendar.DAY_OF_MONTH) > 27) {
fourMonthsfromNowCalendar.add(Calendar.DAY_OF_MONTH, 4);
}
String fourMonthsfromNow = Utils.convertDateToURLFormat(fourMonthsfromNowCalendar);
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);
final Integer loanProductID = createLoanProduct(false, NONE);
List<HashMap> charges = new ArrayList<>();
Integer disbursementFee = ChargesHelper.createCharges(REQUEST_SPEC, RESPONSE_SPEC,
ChargesHelper.getLoanDisbursementJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST, "5"));
addCharges(charges, disbursementFee, "5", null);
List<HashMap> collaterals = new ArrayList<>();
final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, charges, null, "1000",
LoanApplicationTestBuilder.DEFAULT_STRATEGY, fourMonthsfromNow, collaterals);
Assertions.assertNotNull(loanID);
LOAN_TRANSACTION_HELPER.approveLoan(fourMonthsfromNow, loanID);
String loanDetails = LOAN_TRANSACTION_HELPER.getLoanDetails(REQUEST_SPEC, RESPONSE_SPEC, loanID);
LOAN_TRANSACTION_HELPER.disburseLoanWithNetDisbursalAmount(fourMonthsfromNow, loanID,
JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
// check for disbursement fee: Principal 1,000 with 24% Annual Rate for 6 Months we have Total Interest of:
// 120.00
ArrayList<HashMap> loanSchedule = LOAN_TRANSACTION_HELPER.getLoanRepaymentSchedule(REQUEST_SPEC, RESPONSE_SPEC, loanID);
HashMap disbursementDetail = loanSchedule.get(0);
// Disbursement Fee: 5% of 120.00 = 6.00
validateNumberForEqual("6.00", String.valueOf(disbursementDetail.get("feeChargesDue")));
}
@Test
public void testLoanCharges_DISBURSEMENT_WITH_TRANCHES() {
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);