Skip to content

Commit

Permalink
FINERACT-2117: Early and late payment - In advance payment strategy: …
Browse files Browse the repository at this point in the history
…Adjust last, unpaid period
  • Loading branch information
Jose Alberto Hernandez committed Aug 15, 2024
1 parent 2a445a5 commit 36bb078
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

public enum AdvancePaymentsAdjustmentType {

RESCHEDULE_NEXT_REPAYMENTS(1), REDUCE_NUMBER_OF_INSTALLMENTS(2), REDUCE_EMI_AMOUNT(3);
RESCHEDULE_NEXT_REPAYMENTS(1), REDUCE_NUMBER_OF_INSTALLMENTS(2), REDUCE_EMI_AMOUNT(3), ADJUST_LAST_UNPAID_PERIOD(4);

public final Integer value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,8 @@ private Money applyEarlyPaymentStrategy(final LoanApplicationTerms loanApplicati
// until this
// amount becomes zero
break;
case ADJUST_LAST_UNPAID_PERIOD:
break;
default:
break;
}
Expand Down Expand Up @@ -2687,6 +2689,8 @@ private Money fetchEarlyPaidAmount(final Money principalPortion, final Money pri
// until this
// amount becomes zero
break;
case ADJUST_LAST_UNPAID_PERIOD:
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* <li>RESCHEDULE_NEXT_REPAYMENTS</li>
* <li>REDUCE_NUMBER_OF_INSTALLMENTS</li>
* <li>REDUCE_EMI_AMOUNT</li>
* <li>ADJUST_LAST_UNPAID_PERIOD</li>
* </ul>
*/

Expand All @@ -35,7 +36,9 @@ public enum LoanRescheduleStrategyMethod {
INVALID(0, "loanRescheduleStrategyMethod.invalid"), //
RESCHEDULE_NEXT_REPAYMENTS(1, "loanRescheduleStrategyMethod.reschedule.next.repayments"), //
REDUCE_NUMBER_OF_INSTALLMENTS(2, "loanRescheduleStrategyMethod.reduce.number.of.installments"), //
REDUCE_EMI_AMOUNT(3, "loanRescheduleStrategyMethod.reduce.emi.amount");
REDUCE_EMI_AMOUNT(3, "loanRescheduleStrategyMethod.reduce.emi.amount"), //
ADJUST_LAST_UNPAID_PERIOD(4, "loanRescheduleStrategyMethod.adjust.last.unpaid.period"), //
;

private final Integer value;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ public List<EnumOptionData> retrieveRescheduleStrategyTypeOptions() {

return Arrays.asList(rescheduleStrategyType(LoanRescheduleStrategyMethod.REDUCE_EMI_AMOUNT),
rescheduleStrategyType(LoanRescheduleStrategyMethod.REDUCE_NUMBER_OF_INSTALLMENTS),
rescheduleStrategyType(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS));
rescheduleStrategyType(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS),
rescheduleStrategyType(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ public static EnumOptionData rescheduleStrategyType(final LoanRescheduleStrategy
case RESCHEDULE_NEXT_REPAYMENTS ->
new EnumOptionData(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS.getValue().longValue(),
LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS.getCode(), "Reschedule next repayments");
case ADJUST_LAST_UNPAID_PERIOD ->
new EnumOptionData(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue().longValue(),
LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getCode(), "Adjust last, unpaid period");
default -> new EnumOptionData(LoanRescheduleStrategyMethod.INVALID.getValue().longValue(),
LoanRescheduleStrategyMethod.INVALID.getCode(), "Invalid");
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
Expand Down Expand Up @@ -345,6 +346,22 @@ public void validateForCreate(final JsonCommand command) {
// Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);

String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.isOneOfEnumValues(LoanScheduleProcessingType.class);

if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategyCode)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.for.progressive.loan.schedule.handling",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
}

Long delinquencyBucketId = null;
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element)) {
delinquencyBucketId = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element);
Expand Down Expand Up @@ -816,22 +833,6 @@ public void validateForCreate(final JsonCommand command) {
validateLoanScheduleType(transactionProcessingStrategyCode, baseDataValidator, element);
}

String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.isOneOfEnumValues(LoanScheduleProcessingType.class);

if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategyCode)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.for.progressive.loan.schedule.handling",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
}

if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategyCode)
&& LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)) {
advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(advancedPaymentAllocationsJsonParser
Expand Down Expand Up @@ -1042,7 +1043,24 @@ private void validateInterestRecalculationParams(final JsonElement element, fina
final Integer rescheduleStrategyMethod = this.fromApiJsonHelper
.extractIntegerNamed(LoanProductConstants.rescheduleStrategyMethodParameterName, element, Locale.getDefault());
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).value(rescheduleStrategyMethod)
.notNull().inMinMaxRange(1, 3);
.notNull().inMinMaxRange(1, 4);
final LoanRescheduleStrategyMethod loanRescheduleStrategyMethod = LoanRescheduleStrategyMethod
.fromInt(rescheduleStrategyMethod);

final String loanScheduleType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
if (LoanScheduleType.CUMULATIVE.equals(LoanScheduleType.valueOf(loanScheduleType))
&& LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
"reschedule.strategy.method.not.supported.for.loan.schedule.type.cumulative",
"Adjust last, unpaid period is only supported for Progressive loan schedule type");
}

if (LoanScheduleType.PROGRESSIVE.equals(LoanScheduleType.valueOf(loanScheduleType))
&& !LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
"reschedule.strategy.method.not.supported.for.loan.schedule.type.progressive",
"Reschedule strategy type is not supported for Progressive loan schedule type");
}
}

RecalculationFrequencyType frequencyType = null;
Expand Down Expand Up @@ -1301,6 +1319,14 @@ public void validateForUpdate(final JsonCommand command, final LoanProduct loanP
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
}

String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.isOneOfEnumValues(LoanScheduleProcessingType.class);
}

// grace validation
Integer graceOnPrincipalPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment();
if (this.fromApiJsonHelper.parameterExists(GRACE_ON_PRINCIPAL_PAYMENT, element)) {
Expand Down Expand Up @@ -1837,14 +1863,6 @@ public void validateForUpdate(final JsonCommand command, final LoanProduct loanP
validateLoanScheduleType(transactionProcessingStrategyCode, baseDataValidator, element);
}

String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.isOneOfEnumValues(LoanScheduleProcessingType.class);
}

List<LoanProductPaymentAllocationRule> allocationRules = loanProduct.getPaymentAllocationRules();
if (this.fromApiJsonHelper.parameterExists(ADVANCED_PAYMENT_ALLOCATIONS, element)
&& LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
+ "Specifies which amount portion should be added to principal for interest recalculation. \n"
+ "Example Values:0=NONE(Only on principal), 1=INTEREST(Principal+Interest), 2=FEE(Principal+Fee), 3=FEE And INTEREST (Principal+Fee+Interest)\n"
+ "rescheduleStrategyMethod\n" + "Specifies what action should perform on loan repayment schedule for advance payments. \n"
+ "Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount\n"
+ "Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period\n"
+ "recalculationCompoundingFrequencyType\n"
+ "Specifies effective date from which the compounding of interest or fee amounts will be considered in recalculation on late payment.\n"
+ "Example Values:1=Same as repayment period, 2=Daily, 3=Weekly, 4=Monthly\n" + "recalculationCompoundingFrequencyInterval\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15845,7 +15845,7 @@ <h3>Loan Products</h3>
</tr>
<tr>
<td class=fielddesc>Specifies what action should perform on loan repayment schedule for advance payments. <br>
<span>Example Values:</span>1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount</td>
<span>Example Values:</span>1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period</td>
</tr>
<tr class=alt>
<td>recalculationCompoundingFrequencyType</td>
Expand Down Expand Up @@ -51748,7 +51748,7 @@ <h3>Loan Products</h3>
</tr>
<tr>
<td class=fielddesc>Specifies what action should perform on loan repayment schedule for advance payments. <br>
<span>Example Values:</span>1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount</td>
<span>Example Values:</span>1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period</td>
</tr>
<tr class=alt>
<td>recalculationCompoundingFrequencyType</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5095,6 +5095,41 @@ public void uc147b() {
});
}

// uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid
// period
@Test
public void uc148a() {
runAt("23 March 2024", () -> {
final Integer rescheduleStrategyMethod = 4; // Adjust last, unpaid period
PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(
(double) 80.0, rescheduleStrategyMethod);

PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProduct);
assertNotNull(loanProductResponse);
});
}

// uc148b: Negative Test: Advanced payment allocation, with Interest Recalculation in Loan Product but try to use
// Reduce EMI amount
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Try to Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and use Reduce EMI
// amount
@Test
public void uc148b() {
runAt("23 March 2024", () -> {
final Integer rescheduleStrategyMethod = 3; // Reduce EMI amount
PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(
(double) 80.0, rescheduleStrategyMethod);

CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> loanProductHelper.createLoanProduct(loanProduct));

Assertions.assertTrue(callFailedRuntimeException.getMessage().contains("is not supported for Progressive loan schedule type"));
});
}

private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,12 @@ private String getFullAdminAuthKey() {
return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey();
}

// Loan product with proper accounting setup
protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() {
return createOnePeriod30DaysPeriodicAccrualProduct((double) 0);
}

// Loan product with proper accounting setup
protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProduct(double interestRatePerPeriod) {
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
.description("Loan Product Description")//
Expand All @@ -224,7 +228,7 @@ protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAcc
.maxNumberOfRepayments(30)//
.isLinkedToFloatingInterestRates(false)//
.minInterestRatePerPeriod((double) 0)//
.interestRatePerPeriod((double) 0)//
.interestRatePerPeriod(interestRatePerPeriod)//
.maxInterestRatePerPeriod((double) 100)//
.interestRateFrequencyType(2)//
.repaymentEvery(30)//
Expand Down Expand Up @@ -302,6 +306,21 @@ protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAcc
.addPaymentAllocationItem(defaultAllocation);
}

protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(
final double interestRatePerPeriod, final Integer rescheduleStrategyMethod) {
String futureInstallmentAllocationRule = "NEXT_INSTALLMENT";
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(futureInstallmentAllocationRule);

return createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod) //
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")//
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
.loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString()) //
.addPaymentAllocationItem(defaultAllocation).enableDownPayment(false) //
.isInterestRecalculationEnabled(true).interestRecalculationCompoundingMethod(0) //
.preClosureInterestCalculationStrategy(1).recalculationRestFrequencyType(1).allowPartialPeriodInterestCalcualtion(true) //
.rescheduleStrategyMethod(rescheduleStrategyMethod);
}

private List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
Expand Down

0 comments on commit 36bb078

Please sign in to comment.