From 57a1cb2f8c96969f8960c85c8e203ebfefde60e1 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Tue, 25 Apr 2023 12:52:55 +0530 Subject: [PATCH] FINERACT-1761-Repayment-due-event-configuration-at-product-level --- .../CheckLoanRepaymentDueBusinessStep.java | 5 + ...CheckLoanRepaymentOverdueBusinessStep.java | 5 + .../loanproduct/LoanProductConstants.java | 4 + .../api/LoanProductsApiResource.java | 5 +- .../api/LoanProductsApiResourceSwagger.java | 12 ++ .../loanproduct/data/LoanProductData.java | 30 +++- .../loanproduct/domain/LoanProduct.java | 42 +++++- .../LoanProductDataValidator.java | 23 ++- .../LoanProductReadPlatformServiceImpl.java | 6 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...duct_add_repayment_overdue_days_config.xml | 37 +++++ ...CheckLoanRepaymentDueBusinessStepTest.java | 35 +++++ ...kLoanRepaymentOverdueBusinessStepTest.java | 38 +++++ ...ithRepaymentDueEventConfigurationTest.java | 135 ++++++++++++++++++ .../common/loans/LoanProductTestBuilder.java | 20 +++ .../common/loans/LoanTransactionHelper.java | 6 + 16 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0104_loan_product_add_repayment_overdue_days_config.xml create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithRepaymentDueEventConfigurationTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java index 474142ab5..153a064e4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java @@ -42,6 +42,11 @@ public class CheckLoanRepaymentDueBusinessStep implements LoanCOBBusinessStep { public Loan execute(Loan loan) { log.debug("start processing loan repayment due business step loan for loan with id [{}]", loan.getId()); Long numberOfDaysBeforeDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentDueDays(); + if (loan.getLoanProduct().getDueDaysForRepaymentEvent() != null) { + if (loan.getLoanProduct().getDueDaysForRepaymentEvent() > 0) { + numberOfDaysBeforeDueDateToRaiseEvent = loan.getLoanProduct().getDueDaysForRepaymentEvent().longValue(); + } + } final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); for (LoanRepaymentScheduleInstallment repaymentSchedule : loanRepaymentScheduleInstallments) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java index c370fef36..5c9de4a17 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java @@ -42,6 +42,11 @@ public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessSte public Loan execute(Loan loan) { log.debug("start processing loan repayment overdue business step for loan with Id [{}]", loan.getId()); Long numberOfDaysAfterDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentOverdueDays(); + if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() != null) { + if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() > 0) { + numberOfDaysAfterDueDateToRaiseEvent = loan.getLoanProduct().getOverDueDaysForRepaymentEvent().longValue(); + } + } final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); for (LoanRepaymentScheduleInstallment repaymentSchedule : loanRepaymentScheduleInstallments) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index e92f8d71c..ce041b03f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -143,4 +143,8 @@ public interface LoanProductConstants { String OVER_APPLIED_NUMBER = "overAppliedNumber"; String DELINQUENCY_BUCKET_PARAM_NAME = "delinquencyBucketId"; + // repayment events related + String DUE_DAYS_FOR_REPAYMENT_EVENT = "dueDaysForRepaymentEvent"; + String OVER_DUE_DAYS_FOR_REPAYMENT_EVENT = "overDueDaysForRepaymentEvent"; + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index babae83b8..2a5556601 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -106,7 +106,8 @@ public class LoanProductsApiResource { "isLinkedToFloatingInterestRates", "floatingRatesId", "interestRateDifferential", "minDifferentialLendingRate", "defaultDifferentialLendingRate", "maxDifferentialLendingRate", "isFloatingInterestRateCalculationAllowed", LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME, - LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName)); + LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, + LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT)); private static final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>( Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); @@ -141,7 +142,7 @@ public class LoanProductsApiResource { @Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n" + "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n" + "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n" - + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion\n\n" + + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent\n\n" + "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n" diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 99bfbdf08..1f2afab08 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -149,6 +149,10 @@ final class LoanProductsApiResourceSwagger { public Boolean holdGuaranteeFunds; @Schema(example = "1") public Long delinquencyBucketId; + @Schema(example = "3") + public Integer dueDaysForRepaymentEvent; + @Schema(example = "3") + public Integer overDueDaysForRepaymentEvent; // Interest Recalculation @Schema(example = "false") @@ -1191,6 +1195,10 @@ final class LoanProductsApiResourceSwagger { public GetDelinquencyBucketsResponse delinquencyBucket; @Schema(example = "true") public Boolean disallowExpectedDisbursements; + @Schema(example = "3") + public Integer dueDaysForRepaymentEvent; + @Schema(example = "3") + public Integer overDueDaysForRepaymentEvent; } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1308,6 +1316,10 @@ final class LoanProductsApiResourceSwagger { public Boolean holdGuaranteeFunds; @Schema(example = "1") public Long delinquencyBucketId; + @Schema(example = "3") + public Integer dueDaysForRepaymentEvent; + @Schema(example = "3") + public Integer overDueDaysForRepaymentEvent; // Interest Recalculation @Schema(example = "false") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index 91c52c59c..805200367 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -190,6 +190,9 @@ public class LoanProductData implements Serializable { private final Collection delinquencyBucketOptions; private final DelinquencyBucketData delinquencyBucket; + private final Integer dueDaysForRepaymentEvent; + private final Integer overDueDaysForRepaymentEvent; + /** * Used when returning lookup information about loan product for dropdowns. */ @@ -276,6 +279,8 @@ public class LoanProductData implements Serializable { final boolean isRatesEnabled = false; final Collection delinquencyBucketOptions = null; final DelinquencyBucketData delinquencyBucket = null; + final Integer dueDaysForRepaymentEvent = null; + final Integer overDueDaysForRepaymentEvent = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -293,7 +298,8 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, + overDueDaysForRepaymentEvent); } @@ -381,6 +387,8 @@ public class LoanProductData implements Serializable { final boolean isRatesEnabled = false; final Collection delinquencyBucketOptions = null; final DelinquencyBucketData delinquencyBucket = null; + final Integer dueDaysForRepaymentEvent = null; + final Integer overDueDaysForRepaymentEvent = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -398,7 +406,8 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, + overDueDaysForRepaymentEvent); } @@ -493,6 +502,8 @@ public class LoanProductData implements Serializable { final boolean isRatesEnabled = false; final Collection delinquencyBucketOptions = null; final DelinquencyBucketData delinquencyBucket = null; + final Integer dueDaysForRepaymentEvent = null; + final Integer overDueDaysForRepaymentEvent = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -510,7 +521,8 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, + overDueDaysForRepaymentEvent); } @@ -599,6 +611,8 @@ public class LoanProductData implements Serializable { final boolean isRatesEnabled = false; final Collection delinquencyBucketOptions = null; final DelinquencyBucketData delinquencyBucket = null; + final Integer dueDaysForRepaymentEvent = null; + final Integer overDueDaysForRepaymentEvent = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -616,7 +630,8 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, + overDueDaysForRepaymentEvent); } @@ -662,7 +677,8 @@ public class LoanProductData implements Serializable { final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization, Collection rateOptions, Collection rates, final boolean isRatesEnabled, final BigDecimal fixedPrincipalPercentagePerInstallment, final Collection delinquencyBucketOptions, - final DelinquencyBucketData delinquencyBucket) { + final DelinquencyBucketData delinquencyBucket, final Integer dueDaysForRepaymentEvent, + final Integer overDueDaysForRepaymentEvent) { this.id = id; this.name = name; this.shortName = shortName; @@ -778,6 +794,8 @@ public class LoanProductData implements Serializable { this.isEqualAmortization = isEqualAmortization; this.delinquencyBucketOptions = delinquencyBucketOptions; this.delinquencyBucket = delinquencyBucket; + this.dueDaysForRepaymentEvent = dueDaysForRepaymentEvent; + this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -927,6 +945,8 @@ public class LoanProductData implements Serializable { this.isRatesEnabled = isRatesEnabled; this.delinquencyBucketOptions = delinquencyBucketOptions; this.delinquencyBucket = productData.delinquencyBucket; + this.dueDaysForRepaymentEvent = productData.dueDaysForRepaymentEvent; + this.overDueDaysForRepaymentEvent = productData.overDueDaysForRepaymentEvent; } private Collection nullIfEmpty(final Collection charges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index a28c66d2f..4ba45d272 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -202,6 +202,12 @@ public class LoanProduct extends AbstractPersistableCustom { @JoinColumn(name = "delinquency_bucket_id") private DelinquencyBucket delinquencyBucket; + @Column(name = "due_days_for_repayment_event") + private Integer dueDaysForRepaymentEvent; + + @Column(name = "overdue_days_for_repayment_event") + private Integer overDueDaysForRepaymentEvent; + public static LoanProduct assembleFromJson(final Fund fund, final String loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate, final List productRates) { @@ -373,6 +379,10 @@ public class LoanProduct extends AbstractPersistableCustom { final Integer overAppliedNumber = command.integerValueOfParameterNamed(LoanProductConstants.OVER_APPLIED_NUMBER); + final Integer dueDaysForRepaymentEvent = command.integerValueOfParameterNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT); + final Integer overDueDaysForRepaymentEvent = command + .integerValueOfParameterNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT); + return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, repaymentEvery, @@ -388,7 +398,8 @@ public class LoanProduct extends AbstractPersistableCustom { defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGapBetweenInstallments, maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment, disallowExpectedDisbursements, - allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber); + allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent, + overDueDaysForRepaymentEvent); } @@ -599,7 +610,7 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean disallowExpectedDisbursements, final boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType, - final Integer overAppliedNumber) { + final Integer overAppliedNumber, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; this.name = name.trim(); @@ -681,6 +692,10 @@ public class LoanProduct extends AbstractPersistableCustom { if (rates != null) { this.rates = rates; } + + this.dueDaysForRepaymentEvent = dueDaysForRepaymentEvent; + this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent; + validateLoanProductPreSave(); } @@ -1184,6 +1199,21 @@ public class LoanProduct extends AbstractPersistableCustom { this.overAppliedNumber = newValue; } + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, this.dueDaysForRepaymentEvent)) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT); + actualChanges.put(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, newValue); + actualChanges.put("locale", localeAsInput); + this.dueDaysForRepaymentEvent = newValue; + } + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, + this.overDueDaysForRepaymentEvent)) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT); + actualChanges.put(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, newValue); + actualChanges.put("locale", localeAsInput); + this.overDueDaysForRepaymentEvent = newValue; + } + return actualChanges; } @@ -1571,4 +1601,12 @@ public class LoanProduct extends AbstractPersistableCustom { this.delinquencyBucket = delinquencyBucket; } + public Integer getDueDaysForRepaymentEvent() { + return this.dueDaysForRepaymentEvent; + } + + public Integer getOverDueDaysForRepaymentEvent() { + return this.overDueDaysForRepaymentEvent; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 8c44abb38..892d0e8e7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -158,7 +158,8 @@ public final class LoanProductDataValidator { LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME, LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, - LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME)); + LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, + LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -733,6 +734,16 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.CAN_USE_FOR_TOPUP).value(canUseForTopup).validateForBooleanValue(); } + final Integer dueDaysForRepaymentEvent = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, element); + baseDataValidator.reset().parameter(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT).value(dueDaysForRepaymentEvent) + .integerZeroOrGreater(); + + final Integer overDueDaysForRepaymentEvent = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, element); + baseDataValidator.reset().parameter(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT).value(overDueDaysForRepaymentEvent) + .integerZeroOrGreater(); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -1600,6 +1611,16 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.CAN_USE_FOR_TOPUP).value(canUseForTopup).validateForBooleanValue(); } + final Integer dueDaysForRepaymentEvent = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, element); + baseDataValidator.reset().parameter(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT).value(dueDaysForRepaymentEvent) + .integerZeroOrGreater(); + + final Integer overDueDaysForRepaymentEvent = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, element); + baseDataValidator.reset().parameter(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT).value(overDueDaysForRepaymentEvent) + .integerZeroOrGreater(); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 3f966dc67..7122139c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -216,6 +216,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.disallow_expected_disbursements as disallowExpectedDisbursements, lp.allow_approved_disbursed_amounts_over_applied as allowApprovedDisbursedAmountsOverApplied, lp.over_applied_calculation_type as overAppliedCalculationType, over_applied_number as overAppliedNumber, " + "lp.days_in_month_enum as daysInMonth, lp.days_in_year_enum as daysInYear, lp.interest_recalculation_enabled as isInterestRecalculationEnabled, " + "lp.can_define_fixed_emi_amount as canDefineInstallmentAmount, lp.instalment_amount_in_multiples_of as installmentAmountInMultiplesOf, " + + "lp.due_days_for_repayment_event as dueDaysForRepaymentEvent, lp.overdue_days_for_repayment_event as overDueDaysForRepaymentEvent," + "lpr.pre_close_interest_calculation_strategy as preCloseInterestCalculationStrategy, " + "lpr.id as lprId, lpr.product_id as productId, lpr.compound_type_enum as compoundType, lpr.reschedule_strategy_enum as rescheduleStrategy, " + "lpr.rest_frequency_type_enum as restFrequencyEnum, lpr.rest_frequency_interval as restFrequencyInterval, " @@ -345,6 +346,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final boolean useBorrowerCycle = rs.getBoolean("useBorrowerCycle"); final LocalDate startDate = JdbcSupport.getLocalDate(rs, "startDate"); final LocalDate closeDate = JdbcSupport.getLocalDate(rs, "closeDate"); + final Integer dueDaysForRepaymentEvent = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "dueDaysForRepaymentEvent"); + final Integer overDueDaysForRepaymentEvent = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "overDueDaysForRepaymentEvent"); String status = ""; if (closeDate != null && closeDate.isBefore(DateUtils.getBusinessLocalDate())) { status = "loanProduct.inActive"; @@ -499,7 +502,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, - isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); + isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, + dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 9c6ad412d..8b150c20d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -123,4 +123,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0104_loan_product_add_repayment_overdue_days_config.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0104_loan_product_add_repayment_overdue_days_config.xml new file mode 100644 index 000000000..845e83cb6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0104_loan_product_add_repayment_overdue_days_config.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStepTest.java index f21a1754d..0de449f9b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStepTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStepTest.java @@ -42,6 +42,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.L import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -75,10 +76,13 @@ public class CheckLoanRepaymentDueBusinessStepTest { when(configurationDomainService.retrieveRepaymentDueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().plusDays(1); Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); LoanRepaymentScheduleInstallment repaymentInstallment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); List loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(null); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); // when @@ -97,10 +101,13 @@ public class CheckLoanRepaymentDueBusinessStepTest { when(configurationDomainService.retrieveRepaymentDueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDateAfter5Days = DateUtils.getBusinessLocalDate().plusDays(5); Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); List loanRepaymentScheduleInstallments = Arrays .asList(new LoanRepaymentScheduleInstallment(loanForProcessing, 1, LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDateAfter5Days, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0))); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(null); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); // when @@ -110,4 +117,32 @@ public class CheckLoanRepaymentDueBusinessStepTest { assertEquals(processedLoan, loanForProcessing); } + + @Test + public void givenLoanWithInstallmentDueAfterConfiguredDaysInLoanProductWhenStepExecutionThenBusinessEventIsRaised() { + ArgumentCaptor loanRepaymentDueEvent = ArgumentCaptor.forClass(LoanRepaymentDueBusinessEvent.class); + // given + // Global config settings + when(configurationDomainService.retrieveRepaymentDueDays()).thenReturn(2L); + LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().plusDays(1); + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + LoanRepaymentScheduleInstallment repaymentInstallment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, + LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), + BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); + List loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + // Loan Product setting overrides global settings + when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(1); + when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); + + // when + Loan processedLoan = underTest.execute(loanForProcessing); + // then + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanRepaymentDueEvent.capture()); + LoanRepaymentScheduleInstallment loanPayloadForEvent = loanRepaymentDueEvent.getValue().get(); + assertEquals(repaymentInstallment, loanPayloadForEvent); + assertEquals(processedLoan, loanForProcessing); + + } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java index 216199b09..41f76b602 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java @@ -42,6 +42,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.L import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -76,10 +77,13 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().minusDays(1); Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); LoanRepaymentScheduleInstallment repaymentInstallment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); List loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); // when @@ -97,10 +101,13 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDateBefore5Days = DateUtils.getBusinessLocalDate().minusDays(5); Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); List loanRepaymentScheduleInstallments = Arrays .asList(new LoanRepaymentScheduleInstallment(loanForProcessing, 1, LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDateBefore5Days, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0))); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); // when Loan processedLoan = underTest.execute(loanForProcessing); @@ -116,6 +123,7 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().minusDays(1); Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); LoanRepaymentScheduleInstallment repaymentInstallmentPaidOff = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); @@ -123,6 +131,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { repaymentInstallmentPaidOff.updateObligationMet(true); List loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallmentPaidOff); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); // when @@ -132,4 +142,32 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { assertEquals(processedLoan, loanForProcessing); } + @Test + public void givenLoanWithInstallmentOverdueAfterConfiguredDaysInLoanProductWhenStepExecutionThenBusinessEventIsRaised() { + ArgumentCaptor loanRepaymentDueBusinessEventArgumentCaptor = ArgumentCaptor + .forClass(LoanRepaymentOverdueBusinessEvent.class); + // given + // global configuration + when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(2L); + LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().minusDays(1); + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + LoanRepaymentScheduleInstallment repaymentInstallment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, + LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), + BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); + List loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + // product configuration overrides global configuration + when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1); + when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); + + // when + Loan processedLoan = underTest.execute(loanForProcessing); + // then + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanRepaymentDueBusinessEventArgumentCaptor.capture()); + LoanRepaymentScheduleInstallment loanPayloadForEvent = loanRepaymentDueBusinessEventArgumentCaptor.getValue().get(); + assertEquals(repaymentInstallment, loanPayloadForEvent); + assertEquals(processedLoan, loanForProcessing); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithRepaymentDueEventConfigurationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithRepaymentDueEventConfigurationTest.java new file mode 100644 index 000000000..54b3c08ac --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithRepaymentDueEventConfigurationTest.java @@ -0,0 +1,135 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.HashMap; +import java.util.UUID; +import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; +import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class LoanProductWithRepaymentDueEventConfigurationTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private ClientHelper clientHelper; + private LoanTransactionHelper loanTransactionHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void loanProductCreationWithDueDaysConfigurationForRepaymentEventTest() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Delinquency Bucket + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + // event days configuration + Integer dueDaysForRepaymentEvent = 1; + Integer overDueDaysForRepaymentEvent = 2; + + // Client and Loan account creation + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + Integer loanProductId = createLoanProductWithDueDaysForRepaymentEvent(loanTransactionHelper, delinquencyBucketId, + dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertNotNull(getLoanProductsProductResponse.getDueDaysForRepaymentEvent()); + assertNotNull(getLoanProductsProductResponse.getOverDueDaysForRepaymentEvent()); + assertEquals(getLoanProductsProductResponse.getDueDaysForRepaymentEvent(), dueDaysForRepaymentEvent); + assertEquals(getLoanProductsProductResponse.getOverDueDaysForRepaymentEvent(), overDueDaysForRepaymentEvent); + } + + @Test + public void loanProductUpdateWithDueDaysConfigurationForRepaymentEventTest() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Delinquency Bucket + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + // Client and Loan account creation + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, + delinquencyBucketId); + assertNotNull(getLoanProductsProductResponse); + + // Modify Loan Product + PutLoanProductsProductIdResponse loanProductModifyResponse = updateLoanProduct(loanTransactionHelper, + getLoanProductsProductResponse.getId()); + assertNotNull(loanProductModifyResponse); + + } + + private PutLoanProductsProductIdResponse updateLoanProduct(LoanTransactionHelper loanTransactionHelper, Long id) { + // event days configuration + Integer dueDaysForRepaymentEvent = 1; + Integer overDueDaysForRepaymentEvent = 2; + final PutLoanProductsProductIdRequest requestModifyLoan = new PutLoanProductsProductIdRequest() + .dueDaysForRepaymentEvent(dueDaysForRepaymentEvent).overDueDaysForRepaymentEvent(overDueDaysForRepaymentEvent).locale("en"); + return loanTransactionHelper.updateLoanProduct(id, requestModifyLoan); + } + + private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId) { + final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + + private Integer createLoanProductWithDueDaysForRepaymentEvent(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId, Integer dueDaysForRepaymentEvent, Integer overDueDaysForRepaymentEvent) { + final HashMap loanProductMap = new LoanProductTestBuilder().withDueDaysForRepaymentEvent(dueDaysForRepaymentEvent) + .withOverDueDaysForRepaymentEvent(overDueDaysForRepaymentEvent).build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanProductId; + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 1c4a38e04..ee14dffda 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -138,6 +138,8 @@ public class LoanProductTestBuilder { private String installmentAmountInMultiplesOf; private boolean canDefineInstallmentAmount; private Integer delinquencyBucketId; + private Integer dueDaysForRepaymentEvent = null; + private Integer overDueDaysForRepaymentEvent = null; public String build(final String chargeId) { final HashMap map = build(chargeId, null); @@ -267,6 +269,13 @@ public class LoanProductTestBuilder { map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings); } + if (this.dueDaysForRepaymentEvent != null) { + map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent); + } + if (this.overDueDaysForRepaymentEvent != null) { + map.put("overDueDaysForRepaymentEvent", this.overDueDaysForRepaymentEvent); + } + return map; } @@ -656,4 +665,15 @@ public class LoanProductTestBuilder { this.feeAndPenaltyAssetAccount = account; return this; } + + public LoanProductTestBuilder withDueDaysForRepaymentEvent(final Integer dueDaysForRepaymentEvent) { + this.dueDaysForRepaymentEvent = dueDaysForRepaymentEvent; + return this; + } + + public LoanProductTestBuilder withOverDueDaysForRepaymentEvent(final Integer overDueDaysForRepaymentEvent) { + this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent; + return this; + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 2455ea4a9..0af276d95 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -70,6 +70,8 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PutChargeTransactionChangesRequest; import org.apache.fineract.client.models.PutChargeTransactionChangesResponse; +import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; +import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; import org.apache.fineract.client.models.PutLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PutLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.PutLoansLoanIdRequest; @@ -1844,4 +1846,8 @@ public class LoanTransactionHelper extends IntegrationTest { final String get = Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_URL, null); return new Gson().fromJson(get, new TypeToken>() {}.getType()); } + + public PutLoanProductsProductIdResponse updateLoanProduct(Long id, PutLoanProductsProductIdRequest requestModifyLoan) { + return ok(fineract().loanProducts.updateLoanProduct(id, requestModifyLoan)); + } }