Skip to content

Commit

Permalink
FINERACT-1971: added test scenarios to cover repayment schedule with …
Browse files Browse the repository at this point in the history
…large charge amounts
  • Loading branch information
Rustam Zeinalov committed Feb 26, 2025
1 parent ff21304 commit f93f6c0
Show file tree
Hide file tree
Showing 37 changed files with 2,063 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import org.apache.fineract.client.services.LoanReschedulingApi;
import org.apache.fineract.client.services.LoanTransactionsApi;
import org.apache.fineract.client.services.LoansApi;
import org.apache.fineract.client.services.LoansPointInTimeApi;
import org.apache.fineract.client.services.MakerCheckerOr4EyeFunctionalityApi;
import org.apache.fineract.client.services.MappingFinancialActivitiesToAccountsApi;
import org.apache.fineract.client.services.MixMappingApi;
Expand Down Expand Up @@ -229,6 +230,7 @@ public final class FineractClient {
public final LoanCollateralApi loanCollaterals;
public final LoanProductsApi loanProducts;
public final LoanReschedulingApi loanSchedules;
public final LoansPointInTimeApi loansPointInTimeApi;
public final LoansApi loans;
public final LoanDisbursementDetailsApi loanDisbursementDetails;
public final LoanTransactionsApi loanTransactions;
Expand Down Expand Up @@ -355,6 +357,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
loanCollaterals = retrofit.create(LoanCollateralApi.class);
loanProducts = retrofit.create(LoanProductsApi.class);
loanSchedules = retrofit.create(LoanReschedulingApi.class);
loansPointInTimeApi = retrofit.create(LoansPointInTimeApi.class);
loans = retrofit.create(LoansApi.class);
loanDisbursementDetails = retrofit.create(LoanDisbursementDetailsApi.class);
loanTransactions = retrofit.create(LoanTransactionsApi.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.regex.Pattern;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
Expand Down Expand Up @@ -616,6 +617,21 @@ public DataValidatorBuilder arrayNotEmpty() {
return this;
}

public DataValidatorBuilder listNotEmpty() {
if (this.value == null && this.ignoreNullValue) {
return this;
}

final List<Object> list = (List<Object>) this.value;
if (CollectionUtils.isEmpty(list)) {
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".cannot.be.empty";
String defaultEnglishMessage = "The parameter `" + this.parameter + "` cannot be empty. You must select at least one.";
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter);
this.dataValidationErrors.add(error);
}
return this;
}

public DataValidatorBuilder jsonArrayNotEmpty() {
if (this.value == null && this.ignoreNullValue) {
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.core.service;

import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
Expand All @@ -35,6 +37,11 @@ public static ExternalId produce(String value) {
return StringUtils.isBlank(value) ? ExternalId.empty() : new ExternalId(value);
}

public static List<ExternalId> produce(List<String> values) {
Objects.requireNonNull(values, "values must not be null");
return values.stream().map(ExternalIdFactory::produce).toList();
}

public ExternalId createFromCommand(JsonCommand command, final String externalIdKey) {
String externalIdStr = null;
if (command.parsedJson() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;

/**
* Immutable data object representing currency.
Expand Down Expand Up @@ -87,4 +89,12 @@ private String generateDisplayLabel() {

return builder.toString();
}

@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
public interface Mapper {

default CurrencyData map(MonetaryCurrency source) {
return source.toData();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE, //
LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY, //
LP2_ADV_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_NO_CALC_ON_PAST_DUE_EMI_360_30_MULTIDISBURSE, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_DOWNPAYMENT, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,26 @@ public static String randomNameGenerator(final String prefix, final int lenOfRan
public static LocalDate now() {
return LocalDate.now(Clock.systemUTC());
}

/**
* A record that formats a double value based on whether it's a whole number or not.
* <p>
* If the value is a whole number, the output will have one decimal place (e.g., 16.0). Otherwise, it will have two
* decimal places (e.g., 16.90), but if the second decimal place is zero, it will be removed (so 16.90 becomes
* 16.9).
*/
public record DoubleFormatter(double value) {

public String format() {
boolean isWholeNumber = (value % 1.0 == 0);

String result = isWholeNumber ? String.format("%.1f", value) : String.format("%.2f", value);

// For non-whole numbers, remove trailing '0' if it exists
if (!isWholeNumber && result.endsWith("0")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,33 @@ public void initialize() throws Exception {
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY,
responseLoanProductsRequestAdvCustomAccelerateMaturityChargeOffBehaviourLastInstallmentStrategyProgressiveLoanSchedule);

// LP2 with progressive loan schedule + horizontal + interest EMI + interestRecognitionOnDisbursementDate = true
// + 360/30 + accrual activity
String name76 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY.getName();
PostLoanProductsRequest loanProductsRequestLP2AdvancedPaymentInterestRecognitionOnDisbursementEmi36030AccrualActivity = loanProductsRequestFactory
.defaultLoanProductsRequestLP2Emi()//
.name(name76)//
.enableAccrualActivityPosting(true)//
.daysInYearType(DaysInYearType.DAYS360.value)//
.daysInMonthType(DaysInMonthType.DAYS30.value)//
.isInterestRecalculationEnabled(true)//
.preClosureInterestCalculationStrategy(1)//
.rescheduleStrategyMethod(4)//
.interestRecalculationCompoundingMethod(0)//
.recalculationRestFrequencyType(2)//
.recalculationRestFrequencyInterval(1)//
.interestRecognitionOnDisbursementDate(true)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvancedPaymentInterestInterestRecognitionOnDisbursementEmi36030AccrualActivity = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvancedPaymentInterestRecognitionOnDisbursementEmi36030AccrualActivity).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY,
responseLoanProductsRequestLP2AdvancedPaymentInterestInterestRecognitionOnDisbursementEmi36030AccrualActivity);
}

public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2003,10 +2003,11 @@ private List<List<String>> getActualValuesList(List<GetLoansLoanIdLoanChargeData
actualValues.add(t.getChargeTimeType().getValue() == null ? null : t.getChargeTimeType().getValue());
actualValues.add(t.getDueDate() == null ? null : FORMATTER.format(t.getDueDate()));
actualValues.add(t.getChargeCalculationType().getValue() == null ? null : t.getChargeCalculationType().getValue());
actualValues.add(t.getAmount() == null ? null : String.valueOf(t.getAmount()));
actualValues.add(t.getAmount() == null ? null : String.valueOf(new Utils.DoubleFormatter(t.getAmount()).format()));
actualValues.add(t.getAmountPaid() == null ? null : String.valueOf(t.getAmountPaid()));
actualValues.add(t.getAmountWaived() == null ? null : String.valueOf(t.getAmountWaived()));
actualValues.add(t.getAmountOutstanding() == null ? null : String.valueOf(t.getAmountOutstanding()));
actualValues.add(
t.getAmountOutstanding() == null ? null : String.valueOf(new Utils.DoubleFormatter(t.getAmountOutstanding()).format()));
return actualValues;
}).collect(Collectors.toList());
}
Expand Down Expand Up @@ -3181,6 +3182,7 @@ private List<String> fetchValuesOfTransaction(List<String> header, GetLoansLoanI

private List<String> fetchValuesOfRepaymentSchedule(List<String> header, GetLoansLoanIdRepaymentPeriod repaymentPeriod) {
List<String> actualValues = new ArrayList<>();

for (String headerName : header) {
switch (headerName) {
case "Nr" -> actualValues.add(repaymentPeriod.getPeriod() == null ? null : String.valueOf(repaymentPeriod.getPeriod()));
Expand All @@ -3196,12 +3198,12 @@ private List<String> fetchValuesOfRepaymentSchedule(List<String> header, GetLoan
actualValues.add(repaymentPeriod.getPrincipalDue() == null ? null : String.valueOf(repaymentPeriod.getPrincipalDue()));
case "Interest" ->
actualValues.add(repaymentPeriod.getInterestDue() == null ? null : String.valueOf(repaymentPeriod.getInterestDue()));
case "Fees" -> actualValues
.add(repaymentPeriod.getFeeChargesDue() == null ? null : String.valueOf(repaymentPeriod.getFeeChargesDue()));
case "Penalties" -> actualValues.add(
repaymentPeriod.getPenaltyChargesDue() == null ? null : String.valueOf(repaymentPeriod.getPenaltyChargesDue()));
case "Due" -> actualValues.add(
repaymentPeriod.getTotalDueForPeriod() == null ? null : String.valueOf(repaymentPeriod.getTotalDueForPeriod()));
case "Fees" -> actualValues.add(repaymentPeriod.getFeeChargesDue() == null ? null
: String.valueOf(new Utils.DoubleFormatter(repaymentPeriod.getFeeChargesDue()).format()));
case "Penalties" -> actualValues.add(repaymentPeriod.getPenaltyChargesDue() == null ? null
: String.valueOf(new Utils.DoubleFormatter(repaymentPeriod.getPenaltyChargesDue()).format()));
case "Due" -> actualValues.add(repaymentPeriod.getTotalDueForPeriod() == null ? null
: String.valueOf(new Utils.DoubleFormatter(repaymentPeriod.getTotalDueForPeriod()).format()));
case "Paid" -> actualValues.add(
repaymentPeriod.getTotalPaidForPeriod() == null ? null : String.valueOf(repaymentPeriod.getTotalPaidForPeriod()));
case "In advance" -> actualValues.add(repaymentPeriod.getTotalPaidInAdvanceForPeriod() == null ? null
Expand All @@ -3211,7 +3213,7 @@ private List<String> fetchValuesOfRepaymentSchedule(List<String> header, GetLoan
case "Waived" -> actualValues.add(repaymentPeriod.getTotalWaivedForPeriod() == null ? null
: String.valueOf(repaymentPeriod.getTotalWaivedForPeriod()));
case "Outstanding" -> actualValues.add(repaymentPeriod.getTotalOutstandingForPeriod() == null ? null
: String.valueOf(repaymentPeriod.getTotalOutstandingForPeriod()));
: String.valueOf(new Utils.DoubleFormatter(repaymentPeriod.getTotalOutstandingForPeriod()).format()));
default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviour";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030AccrualActivity";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR = "loanProductCreateResponseLP2AdvancedPaymentAccelerateMaturityChargeOffBehaviour";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestRecognitionDisbursementDailyEmi36030AccrualActivity";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalcEmi36030ChargebackInterestPenaltyFeePrincipal";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_FEE_PRINCIPAL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalcEmi36030ChargebackInterestFeePrincipal";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalcEmi36030ChargebackPrincipalInterestFee";
Expand Down
Loading

0 comments on commit f93f6c0

Please sign in to comment.