FINERACT-1981: Progressive Loan schedule handling - Installment charges

This commit is contained in:
mariiaKraievska
2025-07-08 23:22:50 +02:00
committed by Adam Saghy
parent f76a65ed99
commit d165a3feda
22 changed files with 2484 additions and 89 deletions
@@ -28,12 +28,15 @@ public enum ChargeProductType {
LOAN_NSF_FEE(6L), //
LOAN_DISBURSEMENT_PERCENTAGE_FEE(7L), //
LOAN_TRANCHE_DISBURSEMENT_PERCENTAGE_FEE(8L), //
LOAN_INSTALLMENT_PERCENTAGE_FEE(9L), //
LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST(9L), //
LOAN_PERCENTAGE_LATE_FEE_AMOUNT_PLUS_INTEREST(10L), //
CLIENT_TEST_CHARGE_FEE(11L), //
LOAN_DISBURSEMENT_CHARGE(12L), //
CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_AMOUNT(13L), //
CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT(14L); //
CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT(14L), //
LOAN_INSTALLMENT_FEE_FLAT(15L), //
LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT(16L), //
LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST(17L); //
public final Long value;
@@ -138,6 +138,12 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, //
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC, //
LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, //
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_DAILY_INSTALLMENT_FEE_FLAT_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_INTEREST_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE, //
;
@Override
@@ -60,6 +60,8 @@ public class LoanRequestFactory {
public static final String DATE_FORMAT = "dd MMMM yyyy";
public static final String DEFAULT_LOCALE = "en";
public static final DefaultLoanProduct DEFAULT_LOAN_PRODUCT = DefaultLoanProduct.valueOf("LP1");
public static final DefaultLoanProduct DEFAULT_PROGRESSIVE_LOAN_PRODUCT = DefaultLoanProduct
.valueOf("LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL");
public static final Double DEFAULT_PAYMENT_TRANSACTION_AMOUNT = 200.00;
public static final Double DEFAULT_UNDO_TRANSACTION_AMOUNT = 0.0;
public static final Double DEFAULT_REPAYMENT_TRANSACTION_AMOUNT = 200.00;
@@ -78,13 +80,16 @@ public class LoanRequestFactory {
public static final String DEFAULT_REAGING_FREQUENCY_TYPE = "MONTHS";
public static final BigDecimal DEFAULT_INTEREST_RATE_PER_PERIOD = new BigDecimal(0);
public static final Integer DEFAULT_INTEREST_TYPE = InterestType.FLAT.value;
public static final Integer DEFAULT_PROGRESSIVE_INTEREST_TYPE = InterestType.DECLINING_BALANCE.value;
public static final Integer DEFAULT_INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT_PERIOD = InterestCalculationPeriodTime.SAME_AS_REPAYMENT_PERIOD.value;
public static final Integer DEFAULT_PROGRESSIVE_INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT_PERIOD = InterestCalculationPeriodTime.DAILY.value;
public static final Integer DEFAULT_AMORTIZATION_TYPE = 1;
public static final String DEFAULT_LOAN_TYPE = "individual";
public static final Integer DEFAULT_NUMBER_OF_REPAYMENTS = 1;
public static final Integer DEFAULT_NUMBER_OF_INSTALLMENTS = 5;
public static final Integer DEFAULT_REPAYMENT_FREQUENCY = 30;
public static final String DEFAULT_TRANSACTION_PROCESSING_STRATEGY_CODE = TransactionProcessingStrategyCode.PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER.value;
public static final String DEFAULT_PROGRESSIVE_TRANSACTION_PROCESSING_STRATEGY_CODE = TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION.value;
public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
public static final String DATE_SUBMIT_STRING = FORMATTER.format(Utils.now().minusMonths(1L));
@@ -118,6 +123,30 @@ public class LoanRequestFactory {
.maxOutstandingLoanBalance(new BigDecimal(10000));
}
public PostLoansRequest defaultProgressiveLoansRequest(final Long clientId) {
return new PostLoansRequest()//
.clientId(clientId)//
.productId(loanProductResolver.resolve(DEFAULT_PROGRESSIVE_LOAN_PRODUCT))//
.submittedOnDate(DATE_SUBMIT_STRING)//
.expectedDisbursementDate(DATE_SUBMIT_STRING)//
.principal(DEFAULT_PRINCIPAL)//
.locale(DEFAULT_LOCALE)//
.loanTermFrequency(DEFAULT_LOAN_TERM_FREQUENCY)//
.loanTermFrequencyType(DEFAULT_LOAN_TERM_FREQUENCY_TYPE)//
.loanType(DEFAULT_LOAN_TYPE)//
.numberOfRepayments(DEFAULT_NUMBER_OF_REPAYMENTS)//
.repaymentEvery(DEFAULT_REPAYMENT_FREQUENCY)//
.repaymentFrequencyType(DEFAULT_REPAYMENT_FREQUENCY_TYPE)//
.interestRatePerPeriod(DEFAULT_INTEREST_RATE_PER_PERIOD)//
.interestType(DEFAULT_PROGRESSIVE_INTEREST_TYPE)//
.interestCalculationPeriodType(DEFAULT_PROGRESSIVE_INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT_PERIOD)//
.amortizationType(DEFAULT_AMORTIZATION_TYPE)//
.transactionProcessingStrategyCode(DEFAULT_PROGRESSIVE_TRANSACTION_PROCESSING_STRATEGY_CODE)//
.dateFormat(DATE_FORMAT)//
.graceOnArrearsAgeing(3)//
.maxOutstandingLoanBalance(new BigDecimal(10000));
}
public PutLoansLoanIdRequest modifySubmittedOnDateOnLoan(Long clientId, String newSubmittedOnDate) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
String dateDisburseStr = formatter.format(Utils.now());
@@ -972,4 +972,12 @@ public final class ErrorMessageHelper {
public static String addInterestPauseForNotInactiveLoanFailure() {
return "Operations on interest pauses are restricted to active loans.";
}
public static String addInstallmentFeeInterestPercentageChargeFailure() {
return "Failed data validation due to: installment.loancharge.with.calculation.type.interest.not.allowed.";
}
public static String addInstallmentFeePrincipalPercentageChargeFailure() {
return "Failed data validation due to: installment.loancharge.with.calculation.type.principal.not.allowed.";
}
}
@@ -52,12 +52,17 @@ public class ChargeGlobalInitializerStep implements FineractGlobalInitializerSte
public static final String CHARGE_LOAN_NSF_FEE = "NSF fee";
public static final String CHARGE_LOAN_DISBURSEMENT_PERCENT_FEE = "Disbursement percentage fee";
public static final String CHARGE_LOAN_TRANCHE_DISBURSEMENT_PERCENT_FEE = "Tranche Disbursement percentage fee";
public static final String CHARGE_LOAN_INSTALLMENT_PERCENT_FEE = "Installment percentage fee";
public static final String CHARGE_LOAN_INSTALLMENT_FEE_FLAT = "Installment flat fee";
public static final String CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_AMOUNT = "Installment percentage amount fee";
public static final String CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_INTEREST = "Installment percentage interest fee";
public static final String CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_AMOUNT_PLUS_INTEREST = "Installment percentage amount + interest fee";
public static final String CHARGE_CLIENT_FIXED_FEE = "Fixed fee for Client";
public static final String CHARGE_DISBURSEMENT_CHARGE = "Disbursement Charge";
public static final String CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_AMOUNT = "Tranche Disbursement Charge Amount";
public static final String CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT = "Tranche Disbursement Charge Percent";
public static final Double CHARGE_AMOUNT_FLAT = 25D;
public static final Double CHARGE_INSTALLMENT_FEE_AMOUNT_FLAT = 10D;
public static final Double CHARGE_INSTALLMENT_FEE_AMOUNT_PERCENTAGE = 1D;
public static final Double CHARGE_AMOUNT_PERCENTAGE = 5D;
public static final Double CHARGE_AMOUNT_DISBURSEMENT_PERCENTAGE = 1.5D;
public static final Double CHARGE_AMOUNT_INSTALLMENT_PERCENTAGE = 1.5D;
@@ -69,6 +74,7 @@ public class ChargeGlobalInitializerStep implements FineractGlobalInitializerSte
public static final Integer CHARGE_TIME_TYPE_INSTALLMENT = ChargeTimeType.INSTALLMENT_FEE.value;
public static final Integer CHARGE_CALCULATION_TYPE_FLAT = ChargeCalculationType.FLAT.value;
public static final Integer CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT = ChargeCalculationType.PERCENTAGE_AMOUNT.value;
public static final Integer CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST = ChargeCalculationType.PERCENTAGE_INTEREST.value;
public static final Integer CHARGE_CALCULATION_TYPE_PERCENTAGE_DISBURSEMENT_AMOUNT = ChargeCalculationType.PERCENTAGE_DISBURSEMENT_AMOUNT.value;
public static final Integer CHARGE_CALCULATION_TYPE_PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST = ChargeCalculationType.PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST.value;
@@ -130,12 +136,14 @@ public class ChargeGlobalInitializerStep implements FineractGlobalInitializerSte
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_PERCENT_CREATE_RESPONSE,
responseLoanTrancheDisbursePercent);
// Loan - Installment % fee
ChargeRequest requestLoanInstallmentPercent = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN, CHARGE_LOAN_INSTALLMENT_PERCENT_FEE,
CHARGE_TIME_TYPE_INSTALLMENT, CHARGE_CALCULATION_TYPE_PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST,
CHARGE_AMOUNT_INSTALLMENT_PERCENTAGE, true, false);
Response<PostChargesResponse> responseLoanInstallmentPercent = chargesApi.createCharge(requestLoanInstallmentPercent).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_CREATE_RESPONSE, responseLoanInstallmentPercent);
// Loan - Installment % fee Amount + Interest
final ChargeRequest requestLoanInstallmentPercentAmountPlusInterest = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_AMOUNT_PLUS_INTEREST, CHARGE_TIME_TYPE_INSTALLMENT,
CHARGE_CALCULATION_TYPE_PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST, CHARGE_INSTALLMENT_FEE_AMOUNT_PERCENTAGE, true, false);
final Response<PostChargesResponse> responseLoanInstallmentPercentAmountPlusInterest = chargesApi
.createCharge(requestLoanInstallmentPercentAmountPlusInterest).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE,
responseLoanInstallmentPercentAmountPlusInterest);
// Loan - % late (overdue) fee amount+interest
ChargeRequest requestLoanPercentAmountPlusInterestLate = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
@@ -175,6 +183,30 @@ public class ChargeGlobalInitializerStep implements FineractGlobalInitializerSte
.createCharge(requestTrancheDisbursementChargePercent).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT_CREATE_RESPONSE,
responseTrancheDisbursementChargePercent);
// Loan - Installment flat fee
final ChargeRequest requestLoanInstallmentFlat = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN, CHARGE_LOAN_INSTALLMENT_FEE_FLAT,
CHARGE_TIME_TYPE_INSTALLMENT, CHARGE_CALCULATION_TYPE_FLAT, CHARGE_INSTALLMENT_FEE_AMOUNT_FLAT, true, false);
final Response<PostChargesResponse> responseLoanInstallmentFlat = chargesApi.createCharge(requestLoanInstallmentFlat).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_FLAT_CREATE_RESPONSE, responseLoanInstallmentFlat);
// Loan - Installment % fee Amount
final ChargeRequest requestLoanInstallmentPercentAmount = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_AMOUNT, CHARGE_TIME_TYPE_INSTALLMENT, CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT,
CHARGE_INSTALLMENT_FEE_AMOUNT_PERCENTAGE, true, false);
final Response<PostChargesResponse> responseLoanInstallmentPercentAmount = chargesApi
.createCharge(requestLoanInstallmentPercentAmount).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_CREATE_RESPONSE,
responseLoanInstallmentPercentAmount);
// Loan - Installment % fee Interest
final ChargeRequest requestLoanInstallmentPercentInterest = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_INTEREST, CHARGE_TIME_TYPE_INSTALLMENT, CHARGE_CALCULATION_TYPE_PERCENTAGE_INTEREST,
CHARGE_AMOUNT_PERCENTAGE, true, false);
final Response<PostChargesResponse> responseLoanInstallmentPercentInterest = chargesApi
.createCharge(requestLoanInstallmentPercentInterest).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST_CREATE_RESPONSE,
responseLoanInstallmentPercentInterest);
}
public static ChargeRequest defaultChargesRequest(Enum<ChargeProductAppliesTo> appliesTo, String name, Integer chargeTimeType,
@@ -2982,6 +2982,171 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY,
responseLoanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity);
// LP2 with progressive loan schedule + horizontal + interest recalculation
// charges - Installment Fee Flat
// (LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_DAILY_INSTALLMENT_FEE_FLAT_CHARGES)
final String name116 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_DAILY_INSTALLMENT_FEE_FLAT_CHARGES.getName();
final List<LoanProductChargeData> chargesInstallmentFeeFlat = new ArrayList<>();
chargesInstallmentFeeFlat.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_FLAT.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInterestRecalcDailyInstallmentFeeFlatCharges = loanProductsRequestFactory
.defaultLoanProductsRequestLP2InterestDailyRecalculation()//
.name(name116)//
.charges(chargesInstallmentFeeFlat)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInterestRecalcDailyInstallmentFeeFlatCharges = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInterestRecalcDailyInstallmentFeeFlatCharges).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_DAILY_INSTALLMENT_FEE_FLAT_CHARGES,
responseLoanProductsRequestLP2AdvPaymentInterestRecalcDailyInstallmentFeeFlatCharges);
// LP2 with progressive loan schedule + horizontal
// charges - Installment Fee Percentage Amount
// (LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_CHARGES)
final String name117 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_CHARGES.getName();
final List<LoanProductChargeData> chargesInstallmentFeePercentAmount = new ArrayList<>();
chargesInstallmentFeePercentAmount
.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountCharges = loanProductsRequestFactory
.defaultLoanProductsRequestLP2()//
.name(name117)//
.charges(chargesInstallmentFeePercentAmount)//
.enableDownPayment(false)//
.enableAutoRepaymentForDownPayment(null)//
.disbursedAmountPercentageForDownPayment(null)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
.loanScheduleType("PROGRESSIVE") //
.loanScheduleProcessingType("HORIZONTAL")//
.interestRateFrequencyType(3)//
.maxInterestRatePerPeriod(10.0)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountCharges = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountCharges).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_CHARGES,
responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountCharges);
// LP2 with progressive loan schedule + horizontal
// charges - Installment Fee Percentage Interest
// (LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_INTEREST_CHARGES)
final String name118 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_INTEREST_CHARGES.getName();
final List<LoanProductChargeData> chargesInstallmentFeePercentInterest = new ArrayList<>();
chargesInstallmentFeePercentInterest
.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInstallmentFeePercentInterestCharges = loanProductsRequestFactory
.defaultLoanProductsRequestLP2()//
.name(name118)//
.charges(chargesInstallmentFeePercentInterest)//
.enableDownPayment(false)//
.enableAutoRepaymentForDownPayment(null)//
.disbursedAmountPercentageForDownPayment(null)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
.loanScheduleType("PROGRESSIVE") //
.loanScheduleProcessingType("HORIZONTAL")//
.interestRateFrequencyType(3)//
.maxInterestRatePerPeriod(10.0)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentInterestCharges = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInstallmentFeePercentInterestCharges).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_INTEREST_CHARGES,
responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentInterestCharges);
// LP2 with progressive loan schedule + horizontal
// charges - Installment Fee Percentage Amount + Interest
// (LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES)
final String name119 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES.getName();
final List<LoanProductChargeData> chargesInstallmentFeePercentAmountPlusInterest = new ArrayList<>();
chargesInstallmentFeePercentAmountPlusInterest
.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountPlusInterestCharges = loanProductsRequestFactory
.defaultLoanProductsRequestLP2()//
.name(name119)//
.charges(chargesInstallmentFeePercentAmountPlusInterest)//
.enableDownPayment(false)//
.enableAutoRepaymentForDownPayment(null)//
.disbursedAmountPercentageForDownPayment(null)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
.loanScheduleType("PROGRESSIVE") //
.loanScheduleProcessingType("HORIZONTAL")//
.interestRateFrequencyType(3)//
.maxInterestRatePerPeriod(10.0)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountPlusInterestCharges = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountPlusInterestCharges).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES,
responseLoanProductsRequestLP2AdvPaymentInstallmentFeePercentAmountPlusInterestCharges);
// LP2 with progressive loan schedule + horizontal
// charges - Installment Fee All
// (LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES)
final String name120 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES.getName();
final List<LoanProductChargeData> chargesInstallmentFeeAll = new ArrayList<>();
chargesInstallmentFeeAll.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_FLAT.value));
chargesInstallmentFeeAll.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT.value));
chargesInstallmentFeeAll.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST.value));
chargesInstallmentFeeAll
.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInstallmentFeeAllCharges = loanProductsRequestFactory
.defaultLoanProductsRequestLP2()//
.name(name120)//
.charges(chargesInstallmentFeeAll)//
.enableDownPayment(false)//
.enableAutoRepaymentForDownPayment(null)//
.disbursedAmountPercentageForDownPayment(null)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
.loanScheduleType("PROGRESSIVE") //
.loanScheduleProcessingType("HORIZONTAL")//
.interestRateFrequencyType(3)//
.maxInterestRatePerPeriod(10.0)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInstallmentFeeAllCharges = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInstallmentFeeAllCharges).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES,
responseLoanProductsRequestLP2AdvPaymentInstallmentFeeAllCharges);
// LP2 with progressive loan schedule + horizontal + multidisbursal
// charges - Installment Fee Flat + Interest %
// (LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE)
final String name121 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE.getName();
final List<LoanProductChargeData> chargesInstallmentFeeFlatPlusInterest = new ArrayList<>();
chargesInstallmentFeeFlatPlusInterest.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_FLAT.value));
chargesInstallmentFeeFlatPlusInterest
.add(new LoanProductChargeData().id(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST.value));
final PostLoanProductsRequest loanProductsRequestLP2AdvPaymentInstallmentFeeFlatPlusInterestChargesMultiDisburse = loanProductsRequestLP2AdvPaymentInstallmentFeePercentInterestCharges//
.name(name121)//
.charges(chargesInstallmentFeeFlatPlusInterest)//
.shortName(Utils.randomNameGenerator(SHORT_NAME_PREFIX_EMI, 3))//
.disallowExpectedDisbursements(true)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0);//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvPaymentInstallmentFeeFlatPlusInterestChargesMultiDisburse = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvPaymentInstallmentFeeFlatPlusInterestChargesMultiDisburse).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE,
responseLoanProductsRequestLP2AdvPaymentInstallmentFeeFlatPlusInterestChargesMultiDisburse);
}
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -90,7 +90,7 @@ public class LoanChargeStepDef extends AbstractStepDef {
Long chargeTypeId = chargeProductType.getValue();
if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_PERCENTAGE_FEE.getValue())) {
|| chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.getValue())) {
throw new IllegalStateException(String.format("The requested %s charge is NOT due date type, cannot be used here", chargeType));
}
@@ -115,7 +115,8 @@ public class LoanChargeStepDef extends AbstractStepDef {
Long chargeTypeId = chargeProductType.getValue();
if (!chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_DISBURSEMENT_PERCENTAGE_FEE.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_PERCENTAGE_FEE.getValue())) {
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.getValue())) {
throw new IllegalStateException(String.format("The requested %s charge is due date type, cannot be used here", chargeType));
}
@@ -128,6 +129,54 @@ public class LoanChargeStepDef extends AbstractStepDef {
testContext().set(TestContextKey.ADD_DUE_DATE_CHARGE_RESPONSE, loanChargeResponse);
}
@When("Admin adds {string} installment charge with {double} amount")
public void addInstallmentFeeCharge(final String chargeType, final double amount) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assert loanResponse.body() != null;
final long loanId = loanResponse.body().getLoanId();
final ChargeProductType chargeProductType = ChargeProductType.valueOf(chargeType);
final Long chargeTypeId = chargeProductType.getValue();
if (!chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_FLAT.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.getValue())) {
throw new IllegalStateException(
String.format("The requested %s charge is not installment fee type, cannot be used here", chargeType));
}
final PostLoansLoanIdChargesRequest loanIdChargesRequest = LoanChargeRequestFactory.defaultLoanChargeRequest()
.chargeId(chargeTypeId).amount(amount);
final Response<PostLoansLoanIdChargesResponse> loanChargeResponse = loanChargesApi
.executeLoanCharge(loanId, loanIdChargesRequest, "").execute();
ErrorHelper.checkSuccessfulApiCall(loanChargeResponse);
testContext().set(TestContextKey.ADD_INSTALLMENT_FEE_CHARGE_RESPONSE, loanChargeResponse);
}
@Then("Admin fails to add {string} installment charge with {double} amount because of wrong charge calculation type")
public void addInstallmentFeeChargeFails(final String chargeType, final double amount) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assert loanResponse.body() != null;
final long loanId = loanResponse.body().getLoanId();
final ChargeProductType chargeProductType = ChargeProductType.valueOf(chargeType);
final Long chargeTypeId = chargeProductType.getValue();
final PostLoansLoanIdChargesRequest loanIdChargesRequest = LoanChargeRequestFactory.defaultLoanChargeRequest()
.chargeId(chargeTypeId).amount(amount);
final Response<PostLoansLoanIdChargesResponse> loanChargeResponse = loanChargesApi
.executeLoanCharge(loanId, loanIdChargesRequest, "").execute();
testContext().set(TestContextKey.ADD_INSTALLMENT_FEE_CHARGE_RESPONSE, loanChargeResponse);
final ErrorResponse errorDetails = ErrorResponse.from(loanChargeResponse);
assertThat(errorDetails.getHttpStatusCode()).isEqualTo(400);
assertThat(errorDetails.getSingleError().getDeveloperMessage())
.isEqualTo(chargeProductType.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST)
? ErrorMessageHelper.addInstallmentFeeInterestPercentageChargeFailure()
: ErrorMessageHelper.addInstallmentFeePrincipalPercentageChargeFailure());
}
@Then("Admin is not able to add {string} due date charge with {string} due date and {double} EUR transaction amount because the of charged-off account")
public void addChargeDueDateOnChargedOff(String chargeType, String transactionDate, double transactionAmount) throws IOException {
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
@@ -284,7 +333,7 @@ public class LoanChargeStepDef extends AbstractStepDef {
Long chargeTypeId = chargeProductType.getValue();
if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_PERCENTAGE_FEE.getValue())) {
|| chargeTypeId.equals(ChargeProductType.LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST.getValue())) {
throw new IllegalStateException(String.format("The requested %s charge is NOT due date type, cannot be used here", chargeType));
}
@@ -82,6 +82,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.IsCatchUpRunningDTO;
import org.apache.fineract.client.models.LoanProductChargeData;
import org.apache.fineract.client.models.OldestCOBProcessedLoanDTO;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostAddAndDeleteDisbursementDetailRequest;
@@ -258,6 +259,20 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.createLoanEventCheck(response);
}
@When("Admin creates a new default Progressive Loan with date: {string}")
public void createProgressiveLoanWithDate(final String date) throws IOException {
final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
final Long clientId = clientResponse.body().getClientId();
final PostLoansRequest loansRequest = loanRequestFactory.defaultProgressiveLoansRequest(clientId).submittedOnDate(date)
.expectedDisbursementDate(date);
final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
ErrorHelper.checkSuccessfulApiCall(response);
eventCheckHelper.createLoanEventCheck(response);
}
@When("Admin crates a second default loan with date: {string}")
public void createSecondLoanWithDate(String date) throws IOException {
Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
@@ -483,6 +498,12 @@ public class LoanStepDef extends AbstractStepDef {
createCustomizedLoan(data.get(1), false);
}
@When("Admin creates a fully customized loan with loan product`s charges and following data:")
public void createFullyCustomizedLoanWithProductCharges(final DataTable table) throws IOException {
final List<List<String>> data = table.asLists();
createCustomizedLoanWithProductCharges(data.get(1));
}
@When("Admin creates a fully customized loan with emi and the following data:")
public void createFullyCustomizedLoanWithEmi(final DataTable table) throws IOException {
final List<List<String>> data = table.asLists();
@@ -3279,6 +3300,77 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.createLoanEventCheck(response);
}
private void createCustomizedLoanWithProductCharges(final List<String> loanData) throws IOException {
final String loanProduct = loanData.get(0);
final String submitDate = loanData.get(1);
final String principal = loanData.get(2);
final BigDecimal interestRate = new BigDecimal(loanData.get(3));
final String interestTypeStr = loanData.get(4);
final String interestCalculationPeriodStr = loanData.get(5);
final String amortizationTypeStr = loanData.get(6);
final Integer loanTermFrequency = Integer.valueOf(loanData.get(7));
final String loanTermFrequencyType = loanData.get(8);
final Integer repaymentFrequency = Integer.valueOf(loanData.get(9));
final String repaymentFrequencyTypeStr = loanData.get(10);
final Integer numberOfRepayments = Integer.valueOf(loanData.get(11));
final Integer graceOnPrincipalPayment = Integer.valueOf(loanData.get(12));
final Integer graceOnInterestPayment = Integer.valueOf(loanData.get(13));
final Integer graceOnInterestCharged = Integer.valueOf(loanData.get(14));
final String transactionProcessingStrategyCode = loanData.get(15);
final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
final Long clientId = clientResponse.body().getClientId();
final DefaultLoanProduct product = DefaultLoanProduct.valueOf(loanProduct);
final Long loanProductId = loanProductResolver.resolve(product);
final Response<GetLoanProductsProductIdResponse> loanProductDetails = loanProductsApi.retrieveLoanProductDetails(loanProductId)
.execute();
final List<PostLoansRequestChargeData> loanCharges = new ArrayList<>();
assert loanProductDetails.body() != null;
if (loanProductDetails.body().getCharges() != null) {
for (final LoanProductChargeData chargeData : loanProductDetails.body().getCharges()) {
loanCharges.add(new PostLoansRequestChargeData().chargeId(chargeData.getId()).amount(chargeData.getAmount()));
}
}
final LoanTermFrequencyType termFrequencyType = LoanTermFrequencyType.valueOf(loanTermFrequencyType);
final Integer loanTermFrequencyTypeValue = termFrequencyType.getValue();
final RepaymentFrequencyType repaymentFrequencyType = RepaymentFrequencyType.valueOf(repaymentFrequencyTypeStr);
final Integer repaymentFrequencyTypeValue = repaymentFrequencyType.getValue();
final InterestType interestType = InterestType.valueOf(interestTypeStr);
final Integer interestTypeValue = interestType.getValue();
final InterestCalculationPeriodTime interestCalculationPeriod = InterestCalculationPeriodTime.valueOf(interestCalculationPeriodStr);
final Integer interestCalculationPeriodValue = interestCalculationPeriod.getValue();
final AmortizationType amortizationType = AmortizationType.valueOf(amortizationTypeStr);
final Integer amortizationTypeValue = amortizationType.getValue();
final TransactionProcessingStrategyCode processingStrategyCode = TransactionProcessingStrategyCode
.valueOf(transactionProcessingStrategyCode);
final String transactionProcessingStrategyCodeValue = processingStrategyCode.getValue();
final PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId).productId(loanProductId)
.principal(new BigDecimal(principal)).interestRatePerPeriod(interestRate).interestType(interestTypeValue)
.interestCalculationPeriodType(interestCalculationPeriodValue).amortizationType(amortizationTypeValue)
.loanTermFrequency(loanTermFrequency).loanTermFrequencyType(loanTermFrequencyTypeValue)
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentFrequency)
.repaymentFrequencyType(repaymentFrequencyTypeValue).submittedOnDate(submitDate).expectedDisbursementDate(submitDate)
.graceOnPrincipalPayment(graceOnPrincipalPayment).graceOnInterestPayment(graceOnInterestPayment)
.graceOnInterestPayment(graceOnInterestCharged).transactionProcessingStrategyCode(transactionProcessingStrategyCodeValue)
.charges(loanCharges);
final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
ErrorHelper.checkSuccessfulApiCall(response);
eventCheckHelper.createLoanEventCheck(response);
}
public void createFullyCustomizedLoanWithInterestRateFrequency(final List<String> loanData) throws IOException {
final String loanProduct = loanData.get(0);
final String submitDate = loanData.get(1);
@@ -30,6 +30,7 @@ public abstract class TestContextKey {
public static final String LOAN_CREATE_SECOND_LOAN_RESPONSE = "loanCreateSecondLoanResponse";
public static final String LOAN_MODIFY_RESPONSE = "loanModifyResponse";
public static final String ADD_DUE_DATE_CHARGE_RESPONSE = "addDueDateChargeResponse";
public static final String ADD_INSTALLMENT_FEE_CHARGE_RESPONSE = "addInstallmentFeeChargeResponse";
public static final String ADD_PROCESSING_FEE_RESPONSE = "addProcessingFeeResponse";
public static final String ADD_NSF_FEE_RESPONSE = "addNsfFeeResponse";
public static final String WAIVE_CHARGE_RESPONSE = "waiveChargeResponse";
@@ -178,7 +179,10 @@ public abstract class TestContextKey {
public static final String CHARGE_FOR_LOAN_DISBURSEMENET_FEE_CREATE_RESPONSE = "ChargeForLoanDisbursementCreateResponse";
public static final String CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_PERCENT_CREATE_RESPONSE = "ChargeForLoanTrancheDisbursementPercentCreateResponse";
public static final String CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT_CREATE_RESPONSE = "ChargeForLoanTrancheDisbursementChargePercentCreateResponse";
public static final String CHARGE_FOR_LOAN_INSTALLMENT_FEE_CREATE_RESPONSE = "ChargeForLoanInstallmentCreateResponse";
public static final String CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanInstallmentFeePercentageAmountPlusInterestCreateResponse";
public static final String CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_AMOUNT_CREATE_RESPONSE = "ChargeForLoanInstallmentFeePercentageAmountCreateResponse";
public static final String CHARGE_FOR_LOAN_INSTALLMENT_FEE_PERCENTAGE_INTEREST_CREATE_RESPONSE = "ChargeForLoanInstallmentFeePercentageInterestCreateResponse";
public static final String CHARGE_FOR_LOAN_INSTALLMENT_FEE_FLAT_CREATE_RESPONSE = "ChargeForLoanInstallmentFeeFlatCreateResponse";
public static final String CHARGE_FOR_CLIENT_FIXED_FEE_CREATE_RESPONSE = "ChargeForClientFixedFeeCreateResponse";
public static final String CHARGE_FOR_LOAN_DISBURSEMENT_CHARGE_CREATE_RESPONSE = "ChargeForLoanDisbursementChargeCreateResponse";
public static final String LOAN_RESPONSE = "loanResponse";
@@ -235,4 +239,10 @@ public abstract class TestContextKey {
public static final String LOAN_BUY_DOWN_FEE_ADJUSTMENT_RESPONSE = "loanBuyDownFeeAdjustmentResponse";
public static final String MANUAL_JOURNAL_ENTRIES_REQUEST = "manualJournalEntriesRequest";
public static final String MANUAL_JOURNAL_ENTRIES_RESPONSE = "manualJournalEntriesResponse";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_DAILY_INSTALLMENT_FEE_FLAT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationInstallmentFeeFlatCharges";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeePercentageAmountCharges";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_INTEREST_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeePercentageInterestCharges";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAmountPlusPercentageInterestCharges";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAllCharges";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeFlatPlusPercentageInterestChargesMultidisbursal";
}
File diff suppressed because it is too large Load Diff
@@ -1517,7 +1517,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return repaymetsForAdjust;
}
public int fetchNumberOfInstallmensAfterExceptions() {
public int fetchNumberOfInstallmentsAfterExceptions() {
if (!this.repaymentScheduleInstallments.isEmpty()) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int numberOfInstallments = 0;
@@ -721,4 +721,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap)
.installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build();
}
public boolean hasInstallmentFor(final LoanRepaymentScheduleInstallment installment) {
return this.getInstallmentLoanCharge(installment.getInstallmentNumber()) != null;
}
}
@@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundExceptio
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.springframework.stereotype.Component;
@@ -280,6 +281,8 @@ public final class LoanChargeApiJsonValidator {
case PERCENT_OF_INTEREST:
if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
} else if (loanCharge.isInstalmentFee() && loanCharge.getLoan().isProgressiveSchedule()) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
@@ -438,6 +441,9 @@ public final class LoanChargeApiJsonValidator {
case PERCENT_OF_INTEREST:
if (chargeTime.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
} else if (chargeTime.isInstalmentFee()
&& loanProduct.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
@@ -22,7 +22,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -75,6 +75,14 @@ public class LoanChargeService {
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
}
public void recalculateParticularChargesAfterTransactionOccurs(final Loan loan, final List<LoanCharge> loanCharges,
final LocalDate transactionDate) {
for (final LoanCharge loanCharge : loanCharges) {
recalculateLoanCharge(loan, loanCharge, 0, transactionDate);
}
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
}
public void recalculateLoanCharge(final Loan loan, final LoanCharge loanCharge, final int penaltyWaitPeriod) {
BigDecimal amount = BigDecimal.ZERO;
BigDecimal chargeAmt;
@@ -94,12 +102,38 @@ public class LoanChargeService {
}
if (loanCharge.isActive()) {
clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
}
}
public void recalculateLoanCharge(final Loan loan, final LoanCharge loanCharge, final int penaltyWaitPeriod,
final LocalDate transactionDate) {
BigDecimal amount = BigDecimal.ZERO;
BigDecimal chargeAmt;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
if (loanCharge.isOverdueInstallmentCharge()) {
amount = calculateOverdueAmountPercentageAppliedTo(loan, loanCharge, penaltyWaitPeriod);
} else {
amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
}
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
if (loanCharge.isActive()) {
clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt, transactionDate);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
}
}
public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
@@ -191,7 +225,8 @@ public class LoanChargeService {
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt);
// NOTE: must add new loan charge to set of loan charges before
// reprocessing the repayment schedule.
@@ -275,7 +310,7 @@ public class LoanChargeService {
chargeAmt = loanCharge.amountOrPercentage();
}
if (charge != null) {
update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt);
}
}
@@ -325,7 +360,7 @@ public class LoanChargeService {
case FLAT:
if (loanCharge.isInstalmentFee()) {
loanCharge.setAmount(
newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions())));
newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions())));
} else {
loanCharge.setAmount(newValue);
}
@@ -375,7 +410,7 @@ public class LoanChargeService {
loanCharge.setAmountPaid(null);
if (loanCharge.isInstalmentFee()) {
if (numberOfRepayments == null) {
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions();
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
}
loanCharge.setAmount(chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments)));
} else {
@@ -574,40 +609,47 @@ public class LoanChargeService {
return calculatePerInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage());
}
private void updateInstallmentCharges(final LoanCharge loanCharge) {
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
public void updateInstallmentCharges(final LoanCharge loanCharge) {
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge);
if (loanCharge.getLoanInstallmentCharge().isEmpty()) {
loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments);
} else {
int index = 0;
final List<LoanInstallmentCharge> oldChargeInstallments = new ArrayList<>();
if (loanCharge.getLoanInstallmentCharge() != null && !loanCharge.getLoanInstallmentCharge().isEmpty()) {
oldChargeInstallments.addAll(loanCharge.getLoanInstallmentCharge());
}
Collections.sort(oldChargeInstallments);
final LoanInstallmentCharge[] loanChargePerInstallmentArray = newChargeInstallments
.toArray(new LoanInstallmentCharge[newChargeInstallments.size()]);
for (final LoanInstallmentCharge chargePerInstallment : oldChargeInstallments) {
if (index == loanChargePerInstallmentArray.length) {
remove.add(chargePerInstallment);
chargePerInstallment.getInstallment().getInstallmentCharges().remove(chargePerInstallment);
} else {
LoanInstallmentCharge newLoanInstallmentCharge = loanChargePerInstallmentArray[index++];
newLoanInstallmentCharge.getInstallment().getInstallmentCharges().remove(newLoanInstallmentCharge);
chargePerInstallment.copyFrom(newLoanInstallmentCharge);
final Map<Integer, LoanInstallmentCharge> newChargeMap = new HashMap<>();
for (final LoanInstallmentCharge newCharge : newChargeInstallments) {
if (newCharge.getInstallment() != null && newCharge.getInstallment().getInstallmentNumber() != null) {
newChargeMap.put(newCharge.getInstallment().getInstallmentNumber(), newCharge);
}
}
loanCharge.getLoanInstallmentCharge().removeAll(remove);
while (index < loanChargePerInstallmentArray.length) {
loanCharge.getLoanInstallmentCharge().add(loanChargePerInstallmentArray[index++]);
final Collection<LoanInstallmentCharge> chargesToRemoveFromLoanCharge = new HashSet<>();
final Collection<LoanInstallmentCharge> chargesToAddIntoLoanCharge = new HashSet<>();
for (final LoanInstallmentCharge oldCharge : loanCharge.getLoanInstallmentCharge()) {
final Integer oldInstallmentNumber = oldCharge.getInstallment().getInstallmentNumber();
if (newChargeMap.containsKey(oldInstallmentNumber)) {
chargesToRemoveFromLoanCharge.add(oldCharge);
oldCharge.getInstallment().getInstallmentCharges().remove(oldCharge);
chargesToAddIntoLoanCharge.add(newChargeMap.get(oldInstallmentNumber));
newChargeMap.remove(oldInstallmentNumber);
} else {
chargesToRemoveFromLoanCharge.add(oldCharge);
oldCharge.getInstallment().getInstallmentCharges().remove(oldCharge);
}
}
chargesToAddIntoLoanCharge.addAll(newChargeMap.values());
loanCharge.getLoanInstallmentCharge().removeAll(chargesToRemoveFromLoanCharge);
loanCharge.getLoanInstallmentCharge().addAll(chargesToAddIntoLoanCharge);
}
Money amount = Money.zero(loanCharge.getLoan().getCurrency());
Money totalAmount = Money.zero(loanCharge.getLoan().getCurrency());
for (LoanInstallmentCharge charge : loanCharge.getLoanInstallmentCharge()) {
amount = amount.plus(charge.getAmount());
totalAmount = totalAmount.plus(charge.getAmount());
}
loanCharge.setAmount(amount.getAmount());
loanCharge.setAmount(totalAmount.getAmount());
}
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge) {
@@ -615,9 +657,6 @@ public class LoanChargeService {
if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isRecalculatedInterestComponent()) {
continue;
}
BigDecimal amount;
if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amountOrPercentage();
@@ -633,6 +672,78 @@ public class LoanChargeService {
return loanChargePerInstallments;
}
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge,
final LocalDate transactionDate) {
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
if (loanCharge.isInstalmentFee()) {
final List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> i != null && i.isNotFullyPaidOff() && i.getDueDate() != null && !i.getDueDate().isBefore(transactionDate))
.toList();
for (final LoanRepaymentScheduleInstallment installment : installments) {
BigDecimal amount;
if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amountOrPercentage();
} else {
amount = calculateInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage(),
installment).getAmount();
}
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
installment.getInstallmentCharges().add(loanInstallmentCharge);
loanChargePerInstallments.add(loanInstallmentCharge);
}
}
return loanChargePerInstallments;
}
public void updateInstallmentCharges(final LoanCharge loanCharge, final LocalDate transactionDate) {
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge,
transactionDate);
if (loanCharge.getLoanInstallmentCharge().isEmpty()) {
loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments);
} else {
final List<LoanInstallmentCharge> oldLoanInstallmentCharges = loanCharge
.getLoanInstallmentCharge().stream().filter(i -> i != null && !i.isPaid() && i.getInstallment() != null
&& i.getInstallment().getDueDate() != null && !i.getInstallment().getDueDate().isBefore(transactionDate))
.toList();
final Map<Integer, LoanInstallmentCharge> newChargeMap = new HashMap<>();
for (final LoanInstallmentCharge newCharge : newChargeInstallments) {
if (newCharge.getInstallment() != null && newCharge.getInstallment().getInstallmentNumber() != null) {
newChargeMap.put(newCharge.getInstallment().getInstallmentNumber(), newCharge);
}
}
final Collection<LoanInstallmentCharge> chargesToRemoveFromLoanCharge = new HashSet<>();
final Collection<LoanInstallmentCharge> chargesToAddIntoLoanCharge = new HashSet<>();
for (final LoanInstallmentCharge oldCharge : oldLoanInstallmentCharges) {
final Integer oldInstallmentNumber = oldCharge.getInstallment().getInstallmentNumber();
if (newChargeMap.containsKey(oldInstallmentNumber)) {
chargesToRemoveFromLoanCharge.add(oldCharge);
oldCharge.getInstallment().getInstallmentCharges().remove(oldCharge);
chargesToAddIntoLoanCharge.add(newChargeMap.get(oldInstallmentNumber));
newChargeMap.remove(oldInstallmentNumber);
} else {
chargesToRemoveFromLoanCharge.add(oldCharge);
oldCharge.getInstallment().getInstallmentCharges().remove(oldCharge);
}
}
chargesToAddIntoLoanCharge.addAll(newChargeMap.values());
loanCharge.getLoanInstallmentCharge().removeAll(chargesToRemoveFromLoanCharge);
loanCharge.getLoanInstallmentCharge().addAll(chargesToAddIntoLoanCharge);
}
Money totalAmount = Money.zero(loanCharge.getLoan().getCurrency());
for (LoanInstallmentCharge charge : loanCharge.getLoanInstallmentCharge()) {
totalAmount = totalAmount.plus(charge.getAmount());
}
loanCharge.setAmount(totalAmount.getAmount());
}
private BigDecimal calculateOverdueAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge,
final int penaltyWaitPeriod) {
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
@@ -674,7 +785,7 @@ public class LoanChargeService {
case FLAT:
if (loanCharge.isInstalmentFee()) {
if (numberOfRepayments == null) {
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions();
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
}
loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments)));
} else {
@@ -701,6 +812,46 @@ public class LoanChargeService {
}
}
private void update(final LoanCharge loanCharge, final BigDecimal amount, final LocalDate dueDate, final BigDecimal loanPrincipal,
Integer numberOfRepayments, BigDecimal loanChargeAmount, final LocalDate transactionDate) {
if (dueDate != null) {
loanCharge.setDueDate(dueDate);
}
if (amount != null) {
switch (loanCharge.getChargeCalculation()) {
case INVALID:
break;
case FLAT:
if (loanCharge.isInstalmentFee()) {
if (numberOfRepayments == null) {
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
}
loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments)));
} else {
loanCharge.setAmount(amount);
}
break;
case PERCENT_OF_AMOUNT:
case PERCENT_OF_AMOUNT_AND_INTEREST:
case PERCENT_OF_INTEREST:
case PERCENT_OF_DISBURSEMENT_AMOUNT:
loanCharge.setPercentage(amount);
loanCharge.setAmountPercentageAppliedTo(loanPrincipal);
if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) {
loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo());
}
loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount));
break;
}
loanCharge.setAmountOrPercentage(amount);
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
updateInstallmentCharges(loanCharge, transactionDate);
}
}
}
private Money getTotalAllTrancheDisbursementAmount(final Loan loan) {
Money amount = Money.zero(loan.getCurrency());
if (loan.isMultiDisburmentLoan()) {
@@ -42,6 +42,8 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "1")
public Long id;
@Schema(example = "60.0")
public BigDecimal amount;
}
@Schema(description = "LoanProductChargeToGLAccountMapper")
@@ -1421,7 +1423,7 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "false")
public Boolean canDefineInstallmentAmount;
@Schema(example = "[]")
public List<Integer> charges;
public List<LoanProductChargeData> charges;
public Set<GetLoanProductsPrincipalVariationsForBorrowerCycle> productsPrincipalVariationsForBorrowerCycle;
@Schema(example = "[]")
public List<Integer> interestRateVariationsForBorrowerCycle;
@@ -76,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -97,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
@@ -125,17 +127,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanChargeService loanChargeService;
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
final LoanScheduleComponent loanSchedule, final LoanTransactionRepository loanTransactionRepository,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
final LoanChargeService loanChargeService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService;
this.loanTransactionRepository = loanTransactionRepository;
this.loanSchedule = loanSchedule;
this.loanChargeService = loanChargeService;
}
@Override
@@ -372,6 +377,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
transactionCtx);
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
recalculateInstallmentFeeCharges(loanTransaction);
}
loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
@@ -1215,11 +1224,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
if (disbursementTransaction.getLoan().isInterestBearing()) {
final Loan loan = disbursementTransaction.getLoan();
if (loan.isInterestBearing()) {
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
} else {
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
}
if (!disbursementTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
recalculateInstallmentFeeCharges(disbursementTransaction);
}
}
private void recalculateInstallmentFeeCharges(final LoanTransaction loanTransaction) {
final Loan loan = loanTransaction.getLoan();
final List<LoanCharge> loanInstallmentFeeCharges = loan.getActiveCharges().stream()
.filter(c -> c.isInstalmentFee() && c.getSubmittedOnDate().isBefore(loanTransaction.getTransactionDate())).toList();
loanChargeService.recalculateParticularChargesAfterTransactionOccurs(loan, loanInstallmentFeeCharges,
loanTransaction.getTransactionDate());
if (!loanInstallmentFeeCharges.isEmpty()) {
final List<LoanRepaymentScheduleInstallment> installmentsToUpdate = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> i.isNotFullyPaidOff() && !i.isAdditional()).toList();
for (LoanRepaymentScheduleInstallment installment : installmentsToUpdate) {
if (installment.isDownPayment()) {
continue;
}
final BigDecimal newFee = installment.getInstallmentCharges().stream().map(LoanInstallmentCharge::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (newFee.compareTo(BigDecimal.ZERO) != 0) {
installment.setFeeChargesCharged(newFee);
}
}
}
}
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
@@ -1670,6 +1705,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
transactionCtx);
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
recalculateInstallmentFeeCharges(loanTransaction);
}
loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
@@ -1765,8 +1804,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (futureFee.compareTo(BigDecimal.ZERO) > 0 || futurePenalty.compareTo(BigDecimal.ZERO) > 0) {
final Optional<LocalDate> latestDueDate = loan.getCharges().stream()
.filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid()).map(LoanCharge::getDueDate)
.max(LocalDate::compareTo);
.filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid() && loanCharge.getDueDate() != null)
.map(LoanCharge::getDueDate).max(LocalDate::compareTo);
if (latestDueDate.isPresent()) {
final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast();
@@ -2526,11 +2565,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
@NotNull
private Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment installment,
int firstInstallmentNumber) {
boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
private Set<LoanCharge> getLoanChargesOfInstallment(final Set<LoanCharge> charges, final LoanRepaymentScheduleInstallment installment,
final int firstInstallmentNumber) {
final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
return charges.stream()
.filter(loanCharge -> loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
.filter(loanCharge -> (loanCharge.isInstalmentFee() && loanCharge.hasInstallmentFor(installment))
|| loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
.collect(Collectors.toSet());
}
@@ -66,12 +66,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
return loanCharge.isPresent();
}
public boolean isInstallmentFee() {
return loanCharge.map(LoanCharge::isInstalmentFee).orElse(false);
}
private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
return isCharge() && loanCharge.isPresent() && loanCharge.get().getDueDate() != null
&& DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
}
private LocalDate getEffectiveDate() {
@@ -80,6 +85,8 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
} else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
return loanCharge.get().getDueDate();
} else if (isInstallmentFee()) {
return loanCharge.get().getLoan().isDisbursed() ? loanCharge.get().getSubmittedOnDate() : loanCharge.get().getDueDate();
} else {
return loanCharge.get().getSubmittedOnDate();
}
@@ -119,6 +126,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
public int compareTo(@NonNull ChangeOperation o) {
int datePortion = DateUtils.compareWithNullsLast(this.getEffectiveDate(), o.getEffectiveDate());
if (datePortion == 0) {
boolean thisIsDisb = this.isTransaction() && this.getLoanTransaction().isPresent()
&& this.getLoanTransaction().get().isDisbursement();
boolean otherIsDisb = o.isTransaction() && o.getLoanTransaction().isPresent() && o.getLoanTransaction().get().isDisbursement();
if (thisIsDisb && o.isCharge() && o.isInstallmentFee()) {
return -1;
}
if (this.isCharge() && this.isInstallmentFee() && otherIsDisb) {
return 1;
}
final boolean isAccrual = isAccrualActivity();
if (isAccrual != o.isAccrualActivity()) {
return isAccrual ? 1 : -1;
@@ -75,6 +75,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -121,7 +122,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class),
Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class),
Mockito.mock(LoanTransactionRepository.class), Mockito.mock(LoanChargeValidator.class),
Mockito.mock(LoanBalanceService.class));
Mockito.mock(LoanBalanceService.class), Mockito.mock(LoanChargeService.class));
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -202,18 +202,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId");
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId);
/*
* TODO: remove this check once handling for Installment fee charges is implemented for Advanced Payment
* strategy
*/
if (ChargeTimeType.fromInt(chargeDefinition.getChargeTimeType()).isInstalmentFee()
&& AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(loan.transactionProcessingStrategy())) {
final String errorMessageInstallmentChargeNotSupported = "Charge with identifier " + chargeDefinition.getId()
+ " cannot be applied: Installment fee charges are not supported for Advanced payment allocation strategy";
throw new ChargeCannotBeAppliedToException("loan", errorMessageInstallmentChargeNotSupported, chargeDefinition.getId());
}
if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) {
// validates whether any pending disbursements are available to
// apply this charge
@@ -1079,11 +1067,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
chargeDefinition.getName());
}
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loanCharge.isInstalmentFee() && loan.getStatus().isActive()) {
final String defaultUserMessage = "installment charge addition not allowed after disbursement";
throw new LoanChargeCannotBeAddedException("loanCharge", "installment.charge", defaultUserMessage, null,
chargeDefinition.getName());
}
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(loanCharge);
@@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -139,8 +140,8 @@ public class LoanAccountAutoStarter {
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
final LoanBalanceService loanBalanceService, @Lazy final LoanChargeService loanChargeService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService,
externalIdFactory, loanSchedule, loanTransactionRepository, loanChargeValidator, loanBalanceService);
externalIdFactory, loanSchedule, loanTransactionRepository, loanChargeValidator, loanBalanceService, loanChargeService);
}
}
@@ -199,7 +199,7 @@ class LoanChargeWritePlatformServiceImplTest {
when(loan.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>());
when(loanChargeService.calculateAmountPercentageAppliedTo(any(Loan.class), any(LoanCharge.class))).thenReturn(BigDecimal.TEN);
when(loan.fetchNumberOfInstallmensAfterExceptions()).thenReturn(5);
when(loan.fetchNumberOfInstallmentsAfterExceptions()).thenReturn(5);
when(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null);
when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO);
when(loanCharge.getDueLocalDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
@@ -46,6 +46,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest extends BaseLoanIntegrationTest {
@@ -67,9 +68,7 @@ public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocat
ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC);
}
/*
* TODO: To be disabled when Installment Fee Charges are handled for Advanced Payment Allocation
*/
@Disabled
@Test
public void addingLoanChargeTypeInstallmentFeeForAdvancedPaymentAllocationGivesErrorTest() {
try {