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_NSF_FEE(6L), //
LOAN_DISBURSEMENT_PERCENTAGE_FEE(7L), // LOAN_DISBURSEMENT_PERCENTAGE_FEE(7L), //
LOAN_TRANCHE_DISBURSEMENT_PERCENTAGE_FEE(8L), // 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), // LOAN_PERCENTAGE_LATE_FEE_AMOUNT_PLUS_INTEREST(10L), //
CLIENT_TEST_CHARGE_FEE(11L), // CLIENT_TEST_CHARGE_FEE(11L), //
LOAN_DISBURSEMENT_CHARGE(12L), // LOAN_DISBURSEMENT_CHARGE(12L), //
CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_AMOUNT(13L), // 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; 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_APPROVED_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, //
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC, // 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_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 @Override
@@ -60,6 +60,8 @@ public class LoanRequestFactory {
public static final String DATE_FORMAT = "dd MMMM yyyy"; public static final String DATE_FORMAT = "dd MMMM yyyy";
public static final String DEFAULT_LOCALE = "en"; public static final String DEFAULT_LOCALE = "en";
public static final DefaultLoanProduct DEFAULT_LOAN_PRODUCT = DefaultLoanProduct.valueOf("LP1"); 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_PAYMENT_TRANSACTION_AMOUNT = 200.00;
public static final Double DEFAULT_UNDO_TRANSACTION_AMOUNT = 0.0; public static final Double DEFAULT_UNDO_TRANSACTION_AMOUNT = 0.0;
public static final Double DEFAULT_REPAYMENT_TRANSACTION_AMOUNT = 200.00; 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 String DEFAULT_REAGING_FREQUENCY_TYPE = "MONTHS";
public static final BigDecimal DEFAULT_INTEREST_RATE_PER_PERIOD = new BigDecimal(0); 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_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_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 Integer DEFAULT_AMORTIZATION_TYPE = 1;
public static final String DEFAULT_LOAN_TYPE = "individual"; public static final String DEFAULT_LOAN_TYPE = "individual";
public static final Integer DEFAULT_NUMBER_OF_REPAYMENTS = 1; public static final Integer DEFAULT_NUMBER_OF_REPAYMENTS = 1;
public static final Integer DEFAULT_NUMBER_OF_INSTALLMENTS = 5; public static final Integer DEFAULT_NUMBER_OF_INSTALLMENTS = 5;
public static final Integer DEFAULT_REPAYMENT_FREQUENCY = 30; 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_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 DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT);
public static final String DATE_SUBMIT_STRING = FORMATTER.format(Utils.now().minusMonths(1L)); public static final String DATE_SUBMIT_STRING = FORMATTER.format(Utils.now().minusMonths(1L));
@@ -118,6 +123,30 @@ public class LoanRequestFactory {
.maxOutstandingLoanBalance(new BigDecimal(10000)); .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) { public PutLoansLoanIdRequest modifySubmittedOnDateOnLoan(Long clientId, String newSubmittedOnDate) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
String dateDisburseStr = formatter.format(Utils.now()); String dateDisburseStr = formatter.format(Utils.now());
@@ -972,4 +972,12 @@ public final class ErrorMessageHelper {
public static String addInterestPauseForNotInactiveLoanFailure() { public static String addInterestPauseForNotInactiveLoanFailure() {
return "Operations on interest pauses are restricted to active loans."; 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_NSF_FEE = "NSF fee";
public static final String CHARGE_LOAN_DISBURSEMENT_PERCENT_FEE = "Disbursement percentage 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_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_CLIENT_FIXED_FEE = "Fixed fee for Client";
public static final String CHARGE_DISBURSEMENT_CHARGE = "Disbursement Charge"; 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_AMOUNT = "Tranche Disbursement Charge Amount";
public static final String CHARGE_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT = "Tranche Disbursement Charge Percent"; 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_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_PERCENTAGE = 5D;
public static final Double CHARGE_AMOUNT_DISBURSEMENT_PERCENTAGE = 1.5D; public static final Double CHARGE_AMOUNT_DISBURSEMENT_PERCENTAGE = 1.5D;
public static final Double CHARGE_AMOUNT_INSTALLMENT_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_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_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_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_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; 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, TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_PERCENT_CREATE_RESPONSE,
responseLoanTrancheDisbursePercent); responseLoanTrancheDisbursePercent);
// Loan - Installment % fee // Loan - Installment % fee Amount + Interest
ChargeRequest requestLoanInstallmentPercent = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN, CHARGE_LOAN_INSTALLMENT_PERCENT_FEE, final ChargeRequest requestLoanInstallmentPercentAmountPlusInterest = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
CHARGE_TIME_TYPE_INSTALLMENT, CHARGE_CALCULATION_TYPE_PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST, CHARGE_LOAN_INSTALLMENT_FEE_PERCENT_AMOUNT_PLUS_INTEREST, CHARGE_TIME_TYPE_INSTALLMENT,
CHARGE_AMOUNT_INSTALLMENT_PERCENTAGE, true, false); CHARGE_CALCULATION_TYPE_PERCENTAGE_LOAN_AMOUNT_PLUS_INTEREST, CHARGE_INSTALLMENT_FEE_AMOUNT_PERCENTAGE, true, false);
Response<PostChargesResponse> responseLoanInstallmentPercent = chargesApi.createCharge(requestLoanInstallmentPercent).execute(); final Response<PostChargesResponse> responseLoanInstallmentPercentAmountPlusInterest = chargesApi
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_INSTALLMENT_FEE_CREATE_RESPONSE, responseLoanInstallmentPercent); .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 // Loan - % late (overdue) fee amount+interest
ChargeRequest requestLoanPercentAmountPlusInterestLate = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN, ChargeRequest requestLoanPercentAmountPlusInterestLate = defaultChargesRequest(CHARGE_APPLIES_TO_LOAN,
@@ -175,6 +183,30 @@ public class ChargeGlobalInitializerStep implements FineractGlobalInitializerSte
.createCharge(requestTrancheDisbursementChargePercent).execute(); .createCharge(requestTrancheDisbursementChargePercent).execute();
TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT_CREATE_RESPONSE, TestContext.INSTANCE.set(TestContextKey.CHARGE_FOR_LOAN_TRANCHE_DISBURSEMENT_CHARGE_PERCENT_CREATE_RESPONSE,
responseTrancheDisbursementChargePercent); 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, public static ChargeRequest defaultChargesRequest(Enum<ChargeProductAppliesTo> appliesTo, String name, Integer chargeTimeType,
@@ -2982,6 +2982,171 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set( TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY,
responseLoanProductsRequestAdvInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity); 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, public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -90,7 +90,7 @@ public class LoanChargeStepDef extends AbstractStepDef {
Long chargeTypeId = chargeProductType.getValue(); Long chargeTypeId = chargeProductType.getValue();
if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue()) if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_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)); 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(); Long chargeTypeId = chargeProductType.getValue();
if (!chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue()) if (!chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
&& !chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_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)); 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); 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") @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 { public void addChargeDueDateOnChargedOff(String chargeType, String transactionDate, double transactionAmount) throws IOException {
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
@@ -284,7 +333,7 @@ public class LoanChargeStepDef extends AbstractStepDef {
Long chargeTypeId = chargeProductType.getValue(); Long chargeTypeId = chargeProductType.getValue();
if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue()) if (chargeTypeId.equals(ChargeProductType.LOAN_DISBURSEMENT_PERCENTAGE_FEE.getValue())
|| chargeTypeId.equals(ChargeProductType.LOAN_TRANCHE_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)); 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.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.IsCatchUpRunningDTO; 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.OldestCOBProcessedLoanDTO;
import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostAddAndDeleteDisbursementDetailRequest; import org.apache.fineract.client.models.PostAddAndDeleteDisbursementDetailRequest;
@@ -258,6 +259,20 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.createLoanEventCheck(response); 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}") @When("Admin crates a second default loan with date: {string}")
public void createSecondLoanWithDate(String date) throws IOException { public void createSecondLoanWithDate(String date) throws IOException {
Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE); Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
@@ -483,6 +498,12 @@ public class LoanStepDef extends AbstractStepDef {
createCustomizedLoan(data.get(1), false); 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:") @When("Admin creates a fully customized loan with emi and the following data:")
public void createFullyCustomizedLoanWithEmi(final DataTable table) throws IOException { public void createFullyCustomizedLoanWithEmi(final DataTable table) throws IOException {
final List<List<String>> data = table.asLists(); final List<List<String>> data = table.asLists();
@@ -3279,6 +3300,77 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.createLoanEventCheck(response); 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 { public void createFullyCustomizedLoanWithInterestRateFrequency(final List<String> loanData) throws IOException {
final String loanProduct = loanData.get(0); final String loanProduct = loanData.get(0);
final String submitDate = loanData.get(1); 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_CREATE_SECOND_LOAN_RESPONSE = "loanCreateSecondLoanResponse";
public static final String LOAN_MODIFY_RESPONSE = "loanModifyResponse"; public static final String LOAN_MODIFY_RESPONSE = "loanModifyResponse";
public static final String ADD_DUE_DATE_CHARGE_RESPONSE = "addDueDateChargeResponse"; 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_PROCESSING_FEE_RESPONSE = "addProcessingFeeResponse";
public static final String ADD_NSF_FEE_RESPONSE = "addNsfFeeResponse"; public static final String ADD_NSF_FEE_RESPONSE = "addNsfFeeResponse";
public static final String WAIVE_CHARGE_RESPONSE = "waiveChargeResponse"; 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_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_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_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_CLIENT_FIXED_FEE_CREATE_RESPONSE = "ChargeForClientFixedFeeCreateResponse";
public static final String CHARGE_FOR_LOAN_DISBURSEMENT_CHARGE_CREATE_RESPONSE = "ChargeForLoanDisbursementChargeCreateResponse"; public static final String CHARGE_FOR_LOAN_DISBURSEMENT_CHARGE_CREATE_RESPONSE = "ChargeForLoanDisbursementChargeCreateResponse";
public static final String LOAN_RESPONSE = "loanResponse"; 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 LOAN_BUY_DOWN_FEE_ADJUSTMENT_RESPONSE = "loanBuyDownFeeAdjustmentResponse";
public static final String MANUAL_JOURNAL_ENTRIES_REQUEST = "manualJournalEntriesRequest"; public static final String MANUAL_JOURNAL_ENTRIES_REQUEST = "manualJournalEntriesRequest";
public static final String MANUAL_JOURNAL_ENTRIES_RESPONSE = "manualJournalEntriesResponse"; 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; return repaymetsForAdjust;
} }
public int fetchNumberOfInstallmensAfterExceptions() { public int fetchNumberOfInstallmentsAfterExceptions() {
if (!this.repaymentScheduleInstallments.isEmpty()) { if (!this.repaymentScheduleInstallments.isEmpty()) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int numberOfInstallments = 0; 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) .chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap)
.installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build(); .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.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; 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.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -280,6 +281,8 @@ public final class LoanChargeApiJsonValidator {
case PERCENT_OF_INTEREST: case PERCENT_OF_INTEREST:
if (loanCharge.isSpecifiedDueDate()) { if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; 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; break;
@@ -438,6 +441,9 @@ public final class LoanChargeApiJsonValidator {
case PERCENT_OF_INTEREST: case PERCENT_OF_INTEREST:
if (chargeTime.isSpecifiedDueDate()) { if (chargeTime.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; 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; break;
@@ -22,7 +22,7 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -75,6 +75,14 @@ public class LoanChargeService {
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); 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) { public void recalculateLoanCharge(final Loan loan, final LoanCharge loanCharge, final int penaltyWaitPeriod) {
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
BigDecimal chargeAmt; BigDecimal chargeAmt;
@@ -94,12 +102,38 @@ public class LoanChargeService {
} }
if (loanCharge.isActive()) { if (loanCharge.isActive()) {
clearLoanInstallmentChargesBeforeRegeneration(loanCharge); clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt); totalChargeAmt);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); 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, public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) { final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
@@ -191,7 +225,8 @@ public class LoanChargeService {
} else { } else {
chargeAmt = loanCharge.amountOrPercentage(); 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 // NOTE: must add new loan charge to set of loan charges before
// reprocessing the repayment schedule. // reprocessing the repayment schedule.
@@ -275,7 +310,7 @@ public class LoanChargeService {
chargeAmt = loanCharge.amountOrPercentage(); chargeAmt = loanCharge.amountOrPercentage();
} }
if (charge != null) { if (charge != null) {
update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmentsAfterExceptions(),
totalChargeAmt); totalChargeAmt);
} }
} }
@@ -325,7 +360,7 @@ public class LoanChargeService {
case FLAT: case FLAT:
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
loanCharge.setAmount( loanCharge.setAmount(
newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions()))); newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions())));
} else { } else {
loanCharge.setAmount(newValue); loanCharge.setAmount(newValue);
} }
@@ -375,7 +410,7 @@ public class LoanChargeService {
loanCharge.setAmountPaid(null); loanCharge.setAmountPaid(null);
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
if (numberOfRepayments == null) { if (numberOfRepayments == null) {
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions(); numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
} }
loanCharge.setAmount(chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments))); loanCharge.setAmount(chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments)));
} else { } else {
@@ -574,40 +609,47 @@ public class LoanChargeService {
return calculatePerInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage()); return calculatePerInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage());
} }
private void updateInstallmentCharges(final LoanCharge loanCharge) { public void updateInstallmentCharges(final LoanCharge loanCharge) {
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge); final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge);
if (loanCharge.getLoanInstallmentCharge().isEmpty()) { if (loanCharge.getLoanInstallmentCharge().isEmpty()) {
loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments); loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments);
} else { } else {
int index = 0; final Map<Integer, LoanInstallmentCharge> newChargeMap = new HashMap<>();
final List<LoanInstallmentCharge> oldChargeInstallments = new ArrayList<>(); for (final LoanInstallmentCharge newCharge : newChargeInstallments) {
if (loanCharge.getLoanInstallmentCharge() != null && !loanCharge.getLoanInstallmentCharge().isEmpty()) { if (newCharge.getInstallment() != null && newCharge.getInstallment().getInstallmentNumber() != null) {
oldChargeInstallments.addAll(loanCharge.getLoanInstallmentCharge()); newChargeMap.put(newCharge.getInstallment().getInstallmentNumber(), newCharge);
}
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);
} }
} }
loanCharge.getLoanInstallmentCharge().removeAll(remove);
while (index < loanChargePerInstallmentArray.length) { final Collection<LoanInstallmentCharge> chargesToRemoveFromLoanCharge = new HashSet<>();
loanCharge.getLoanInstallmentCharge().add(loanChargePerInstallmentArray[index++]); 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()) { 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) { private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge) {
@@ -615,9 +657,6 @@ public class LoanChargeService {
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isRecalculatedInterestComponent()) {
continue;
}
BigDecimal amount; BigDecimal amount;
if (loanCharge.getChargeCalculation().isFlat()) { if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amountOrPercentage(); amount = loanCharge.amountOrPercentage();
@@ -633,6 +672,78 @@ public class LoanChargeService {
return loanChargePerInstallments; 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, private BigDecimal calculateOverdueAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge,
final int penaltyWaitPeriod) { final int penaltyWaitPeriod) {
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment(); LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
@@ -674,7 +785,7 @@ public class LoanChargeService {
case FLAT: case FLAT:
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
if (numberOfRepayments == null) { if (numberOfRepayments == null) {
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions(); numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmentsAfterExceptions();
} }
loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments))); loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments)));
} else { } 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) { private Money getTotalAllTrancheDisbursementAmount(final Loan loan) {
Money amount = Money.zero(loan.getCurrency()); Money amount = Money.zero(loan.getCurrency());
if (loan.isMultiDisburmentLoan()) { if (loan.isMultiDisburmentLoan()) {
@@ -42,6 +42,8 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "1") @Schema(example = "1")
public Long id; public Long id;
@Schema(example = "60.0")
public BigDecimal amount;
} }
@Schema(description = "LoanProductChargeToGLAccountMapper") @Schema(description = "LoanProductChargeToGLAccountMapper")
@@ -1421,7 +1423,7 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "false") @Schema(example = "false")
public Boolean canDefineInstallmentAmount; public Boolean canDefineInstallmentAmount;
@Schema(example = "[]") @Schema(example = "[]")
public List<Integer> charges; public List<LoanProductChargeData> charges;
public Set<GetLoanProductsPrincipalVariationsForBorrowerCycle> productsPrincipalVariationsForBorrowerCycle; public Set<GetLoanProductsPrincipalVariationsForBorrowerCycle> productsPrincipalVariationsForBorrowerCycle;
@Schema(example = "[]") @Schema(example = "[]")
public List<Integer> interestRateVariationsForBorrowerCycle; 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.LoanChargeOffBehaviour;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule; 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.LoanInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; 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.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; 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.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
@@ -125,17 +127,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final InterestRefundService interestRefundService; private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule; private final LoanScheduleComponent loanSchedule;
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final LoanChargeService loanChargeService;
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper, public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory, final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
final LoanScheduleComponent loanSchedule, final LoanTransactionRepository loanTransactionRepository, 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); super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator; this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper; this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService; this.interestRefundService = interestRefundService;
this.loanTransactionRepository = loanTransactionRepository; this.loanTransactionRepository = loanTransactionRepository;
this.loanSchedule = loanSchedule; this.loanSchedule = loanSchedule;
this.loanChargeService = loanChargeService;
} }
@Override @Override
@@ -372,6 +377,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(), createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
transactionCtx); transactionCtx);
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
recalculateInstallmentFeeCharges(loanTransaction);
}
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges // determine how much is outstanding total and breakdown for principal, interest and charges
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) { for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
@@ -1215,11 +1224,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging // 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); handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
} else { } else {
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); 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) { private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
@@ -1670,6 +1705,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(), createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
transactionCtx); transactionCtx);
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
recalculateInstallmentFeeCharges(loanTransaction);
}
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges // determine how much is outstanding total and breakdown for principal, interest and charges
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) { 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) { if (futureFee.compareTo(BigDecimal.ZERO) > 0 || futurePenalty.compareTo(BigDecimal.ZERO) > 0) {
final Optional<LocalDate> latestDueDate = loan.getCharges().stream() final Optional<LocalDate> latestDueDate = loan.getCharges().stream()
.filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid()).map(LoanCharge::getDueDate) .filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid() && loanCharge.getDueDate() != null)
.max(LocalDate::compareTo); .map(LoanCharge::getDueDate).max(LocalDate::compareTo);
if (latestDueDate.isPresent()) { if (latestDueDate.isPresent()) {
final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast(); final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast();
@@ -2526,11 +2565,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
@NotNull @NotNull
private Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment installment, private Set<LoanCharge> getLoanChargesOfInstallment(final Set<LoanCharge> charges, final LoanRepaymentScheduleInstallment installment,
int firstInstallmentNumber) { final int firstInstallmentNumber) {
boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber); final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
return charges.stream() 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()); .collect(Collectors.toSet());
} }
@@ -66,12 +66,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
return loanCharge.isPresent(); return loanCharge.isPresent();
} }
public boolean isInstallmentFee() {
return loanCharge.map(LoanCharge::isInstalmentFee).orElse(false);
}
private boolean isAccrualActivity() { private boolean isAccrualActivity() {
return isTransaction() && loanTransaction.get().isAccrualActivity(); return isTransaction() && loanTransaction.get().isAccrualActivity();
} }
private boolean isBackdatedCharge() { 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() { private LocalDate getEffectiveDate() {
@@ -80,6 +85,8 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
} else if (loanCharge.isPresent()) { } else if (loanCharge.isPresent()) {
if (isBackdatedCharge()) { if (isBackdatedCharge()) {
return loanCharge.get().getDueDate(); return loanCharge.get().getDueDate();
} else if (isInstallmentFee()) {
return loanCharge.get().getLoan().isDisbursed() ? loanCharge.get().getSubmittedOnDate() : loanCharge.get().getDueDate();
} else { } else {
return loanCharge.get().getSubmittedOnDate(); return loanCharge.get().getSubmittedOnDate();
} }
@@ -119,6 +126,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
public int compareTo(@NonNull ChangeOperation o) { public int compareTo(@NonNull ChangeOperation o) {
int datePortion = DateUtils.compareWithNullsLast(this.getEffectiveDate(), o.getEffectiveDate()); int datePortion = DateUtils.compareWithNullsLast(this.getEffectiveDate(), o.getEffectiveDate());
if (datePortion == 0) { 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(); final boolean isAccrual = isAccrualActivity();
if (isAccrual != o.isAccrualActivity()) { if (isAccrual != o.isAccrualActivity()) {
return isAccrual ? 1 : -1; 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.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; 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.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -121,7 +122,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class), underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class),
Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class), Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class),
Mockito.mock(LoanTransactionRepository.class), Mockito.mock(LoanChargeValidator.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.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -202,18 +202,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId");
final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); 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()) { if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) {
// validates whether any pending disbursements are available to // validates whether any pending disbursements are available to
// apply this charge // apply this charge
@@ -1079,11 +1067,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
chargeDefinition.getName()); chargeDefinition.getName());
} }
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) { } 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 List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final Set<LoanCharge> loanCharges = new HashSet<>(1); final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(loanCharge); 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.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; 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.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -139,8 +140,8 @@ public class LoanAccountAutoStarter {
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule, final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator, final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) { final LoanBalanceService loanBalanceService, @Lazy final LoanChargeService loanChargeService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService, 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.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>()); when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>());
when(loanChargeService.calculateAmountPercentageAppliedTo(any(Loan.class), any(LoanCharge.class))).thenReturn(BigDecimal.TEN); 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.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null);
when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO); when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO);
when(loanCharge.getDueLocalDate()).thenReturn(LocalDate.now(ZoneId.systemDefault())); 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.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest extends BaseLoanIntegrationTest { public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest extends BaseLoanIntegrationTest {
@@ -67,9 +68,7 @@ public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocat
ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC); ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC);
} }
/* @Disabled
* TODO: To be disabled when Installment Fee Charges are handled for Advanced Payment Allocation
*/
@Test @Test
public void addingLoanChargeTypeInstallmentFeeForAdvancedPaymentAllocationGivesErrorTest() { public void addingLoanChargeTypeInstallmentFeeForAdvancedPaymentAllocationGivesErrorTest() {
try { try {