From a012f11825b11090191f01489f9090d4dbe11de3 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 23 Jul 2024 13:00:13 +0200 Subject: [PATCH] FINERACT-2107: Interest refund configuration --- .../resources/jpa/investor/persistence.xml | 1 + .../portfolio/loanaccount/domain/Loan.java | 6 +- .../domain/LoanTransactionType.java | 7 ++ .../domain/LoanApplicationTerms.java | 18 +-- .../loanproduct/LoanProductConstants.java | 1 + .../api/LoanProductsApiResourceSwagger.java | 7 ++ .../loanproduct/data/LoanProductData.java | 24 +++- .../loanproduct/domain/LoanProduct.java | 16 ++- .../domain/LoanProductRelatedDetail.java | 13 ++- .../LoanSupportedInterestRefundTypes.java | 47 ++++++++ .../PaymentAllocationTransactionType.java | 3 +- ...ortedInterestRefundTypesListConverter.java | 41 +++++++ .../loanproduct/service/LoanEnumerations.java | 2 + .../module/loan/module-changelog-master.xml | 1 + .../1022_add_interest_refund_support.xml | 36 ++++++ .../jpa/progressiveloan/persistence.xml | 1 + .../service/LoanScheduleAssembler.java | 3 +- .../api/LoanProductsApiResource.java | 6 +- .../LoanProductDataValidator.java | 16 ++- .../LoanProductReadPlatformServiceImpl.java | 13 ++- ...WritePlatformServiceJpaRepositoryImpl.java | 12 ++ .../src/main/resources/jpa/persistence.xml | 1 + .../DefaultScheduledDateGeneratorTest.java | 4 +- ...ncedPaymentAllocationIntegrationTests.java | 106 +++++++++++++++--- .../common/loans/LoanProductTestBuilder.java | 10 ++ 25 files changed, 348 insertions(+), 47 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanSupportedInterestRefundTypes.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/SupportedInterestRefundTypesListConverter.java create mode 100644 fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1022_add_interest_refund_support.xml diff --git a/fineract-investor/src/main/resources/jpa/investor/persistence.xml b/fineract-investor/src/main/resources/jpa/investor/persistence.xml index b52d34a10..1d136770f 100644 --- a/fineract-investor/src/main/resources/jpa/investor/persistence.xml +++ b/fineract-investor/src/main/resources/jpa/investor/persistence.xml @@ -118,6 +118,7 @@ org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter + org.apache.fineract.portfolio.loanproduct.domain.SupportedInterestRefundTypesListConverter org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 4e232000e..f9e4eeea6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -4457,8 +4457,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(), scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment, - scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, getSubmittedOnDate(), - loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()); + scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, getSubmittedOnDate()); } public BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, BigDecimal annualNominalInterestRate, @@ -4706,8 +4705,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false, - false, this.fixedPrincipalPercentagePerInstallment, false, repaymentStartDateType, getSubmittedOnDate(), - loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()); + false, this.fixedPrincipalPercentagePerInstallment, false, repaymentStartDateType, getSubmittedOnDate()); } public void updateRescheduledByUser(AppUser rescheduledByUser) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index fada088d8..2af4b1d90 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -65,6 +65,7 @@ public enum LoanTransactionType { REAMORTIZE(30, "loanTransactionType.reAmortize"), // INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), // ACCRUAL_ACTIVITY(32, "loanTransactionType.accrualActivity"), // + INTEREST_REFUND(33, "loanTransactionType.interestRefund"), // ; private final Integer value; @@ -113,6 +114,7 @@ public enum LoanTransactionType { case 30 -> LoanTransactionType.REAMORTIZE; case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER; case 32 -> LoanTransactionType.ACCRUAL_ACTIVITY; + case 33 -> LoanTransactionType.INTEREST_REFUND; default -> LoanTransactionType.INVALID; }; } @@ -221,4 +223,9 @@ public enum LoanTransactionType { public boolean isAccrualActivity() { return this.equals(LoanTransactionType.ACCRUAL_ACTIVITY); } + + public boolean isInterestRefund() { + return this.equals(LoanTransactionType.INTEREST_REFUND); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 54cc4e570..ed0813cc5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -53,6 +53,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCom import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; 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.domain.RepaymentStartDateType; @@ -225,6 +226,7 @@ public final class LoanApplicationTerms { private final LoanScheduleType loanScheduleType; private final LoanScheduleProcessingType loanScheduleProcessingType; private final boolean enableAccrualActivityPosting; + private final List supportedInterestRefundTypes; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -253,7 +255,7 @@ public final class LoanApplicationTerms { final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -272,7 +274,7 @@ public final class LoanApplicationTerms { isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, - fixedLength, enableAccrualActivityPosting); + fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } @@ -293,8 +295,7 @@ public final class LoanApplicationTerms { final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans, - final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, - final boolean enableAccrualActivityPosting) { + final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -346,7 +347,8 @@ public final class LoanApplicationTerms { isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, - fixedLength, enableAccrualActivityPosting); + fixedLength, loanProductRelatedDetail.isEnableAccrualActivityPosting(), + loanProductRelatedDetail.getSupportedInterestRefundTypes()); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -375,7 +377,8 @@ public final class LoanApplicationTerms { final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, - final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, boolean enableAccrualActivityPosting) { + final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, boolean enableAccrualActivityPosting, + final List supportedInterestRefundTypes) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -473,6 +476,7 @@ public final class LoanApplicationTerms { this.loanScheduleType = loanScheduleType; this.loanScheduleProcessingType = loanScheduleProcessingType; this.fixedLength = fixedLength; + this.supportedInterestRefundTypes = supportedInterestRefundTypes; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1337,7 +1341,7 @@ public final class LoanApplicationTerms { this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.loanScheduleType, - this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting); + this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting, this.supportedInterestRefundTypes); } public Integer getLoanTermFrequency() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 29b6cbcf9..90308e5b3 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -165,5 +165,6 @@ public interface LoanProductConstants { String FIXED_LENGTH = "fixedLength"; String ENABLE_ACCRUAL_ACTIVITY_POSTING = "enableAccrualActivityPosting"; + String SUPPORTED_INTEREST_REFUND_TYPES = "supportedInterestRefundTypes"; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 8bfcbd1b5..cd04ef8e8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Set; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger.GetDelinquencyBucketsResponse; /** @@ -318,6 +319,7 @@ final class LoanProductsApiResourceSwagger { public boolean active; } + public List supportedInterestRefundTypes; } @Schema(description = "PostLoanProductsResponse") @@ -600,6 +602,7 @@ final class LoanProductsApiResourceSwagger { @Schema(example = "0") public Integer principalThresholdForLastInstalment; public GetLoanProductsResponse.GetLoanProductsRepaymentStartDateType repaymentStartDateType; + public List supportedInterestRefundTypes; } @Schema(description = "GetLoanProductsTemplateResponse") @@ -1063,6 +1066,8 @@ final class LoanProductsApiResourceSwagger { public List creditAllocationAllocationTypes; public List creditAllocationTransactionTypes; + public List supportedInterestRefundTypes; + public List supportedInterestRefundTypesOptions; } @Schema(description = "GetLoanProductsProductIdResponse") @@ -1336,6 +1341,7 @@ final class LoanProductsApiResourceSwagger { public EnumOptionData loanScheduleProcessingType; @Schema(example = "false") public Boolean enableAccrualActivityPosting; + public List supportedInterestRefundTypes; } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1627,6 +1633,7 @@ final class LoanProductsApiResourceSwagger { public boolean active; } + public List supportedInterestRefundTypes; } public static final class AdvancedPaymentData { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index f455b26da..f1c232985 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -33,6 +33,7 @@ import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.calendar.data.CalendarData; import org.apache.fineract.portfolio.charge.data.ChargeData; @@ -134,6 +135,7 @@ public class LoanProductData implements Serializable { private final boolean canDefineInstallmentAmount; private final Integer installmentAmountInMultiplesOf; private final EnumOptionData repaymentStartDateType; + private final List supportedInterestRefundTypes; // charges private final Collection charges; @@ -190,6 +192,7 @@ public class LoanProductData implements Serializable { private final List interestRecalculationFrequencyTypeOptions; private final List floatingRateOptions; private final List repaymentStartDateTypeOptions; + private final List supportedInterestRefundTypesOptions; private final Boolean multiDisburseLoan; private final Integer maxTrancheCount; @@ -325,6 +328,7 @@ public class LoanProductData implements Serializable { final EnumOptionData loanScheduleTypeOptions = null; final EnumOptionData loanScheduleProcessingTypeOptions = null; final boolean enableAccrualActivityPosting = false; + final List supportedInterestRefundTypes = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -345,7 +349,7 @@ public class LoanProductData implements Serializable { fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } @@ -446,6 +450,7 @@ public class LoanProductData implements Serializable { final EnumOptionData loanScheduleType = null; final EnumOptionData loanScheduleProcessingType = null; final boolean enableAccrualActivityPosting = false; + final List supportedInterestRefundTypes = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -466,7 +471,7 @@ public class LoanProductData implements Serializable { fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } @@ -574,6 +579,7 @@ public class LoanProductData implements Serializable { final EnumOptionData loanScheduleType = LoanScheduleType.CUMULATIVE.asEnumOptionData(); final EnumOptionData loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.asEnumOptionData(); final boolean enableAccrualActivityPosting = false; + final List supportedInterestRefundTypes = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -594,7 +600,7 @@ public class LoanProductData implements Serializable { fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } @@ -696,6 +702,7 @@ public class LoanProductData implements Serializable { final EnumOptionData loanScheduleType = null; final EnumOptionData loanScheduleProcessingType = null; final boolean enableAccrualActivityPosting = false; + final List supportedInterestRefundTypes = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -716,7 +723,7 @@ public class LoanProductData implements Serializable { fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -767,7 +774,7 @@ public class LoanProductData implements Serializable { final Collection paymentAllocation, final Collection creditAllocation, final EnumOptionData repaymentStartDateType, final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { this.id = id; this.name = name; this.shortName = shortName; @@ -904,6 +911,8 @@ public class LoanProductData implements Serializable { this.loanScheduleTypeOptions = null; this.loanScheduleProcessingTypeOptions = null; this.enableAccrualActivityPosting = enableAccrualActivityPosting; + this.supportedInterestRefundTypes = supportedInterestRefundTypes; + this.supportedInterestRefundTypesOptions = null; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -925,7 +934,8 @@ public class LoanProductData implements Serializable { final List advancedPaymentAllocationFutureInstallmentAllocationRules, final List advancedPaymentAllocationTypes, final List loanScheduleTypeOptions, final List loanScheduleProcessingTypeOptions, final List creditAllocationTransactionTypes, - final List creditAllocationAllocationTypes) { + final List creditAllocationAllocationTypes, + final List supportedInterestRefundTypesOptions) { this.id = productData.id; this.name = productData.name; @@ -1080,6 +1090,8 @@ public class LoanProductData implements Serializable { this.loanScheduleProcessingTypeOptions = loanScheduleProcessingTypeOptions; this.loanScheduleTypeOptions = loanScheduleTypeOptions; this.enableAccrualActivityPosting = productData.enableAccrualActivityPosting; + this.supportedInterestRefundTypesOptions = supportedInterestRefundTypesOptions; + this.supportedInterestRefundTypes = productData.supportedInterestRefundTypes; } private Collection nullIfEmpty(final Collection charges) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 4ccadd68c..9d1920e45 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -442,6 +442,15 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean enableAccrualActivityPosting = command .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING); + List supportedInterestRefundTypes = new ArrayList<>(); + if (command.parameterExists(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES)) { + JsonArray supportedTransactionsForInterestRefund = command + .arrayOfParameterNamed(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES); + supportedTransactionsForInterestRefund.iterator().forEachRemaining(value -> { + supportedInterestRefundTypes.add(LoanSupportedInterestRefundTypes.valueOf(value.getAsString())); + }); + } + return new LoanProduct(fund, loanTransactionProcessingStrategy, loanProductPaymentAllocationRules, loanProductCreditAllocationRules, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, @@ -461,7 +470,7 @@ public class LoanProduct extends AbstractPersistableCustom { allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, - enableAccrualActivityPosting); + enableAccrualActivityPosting, supportedInterestRefundTypes); } @@ -679,7 +688,7 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType, final boolean enableInstallmentLevelDelinquency, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -728,7 +737,8 @@ public class LoanProduct extends AbstractPersistableCustom { recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, - enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, + supportedInterestRefundTypes); this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index a9096d074..d6790db78 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -19,12 +19,14 @@ package org.apache.fineract.portfolio.loanproduct.domain; import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.Embeddable; import jakarta.persistence.Embedded; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import java.math.BigDecimal; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import lombok.Getter; import lombok.Setter; @@ -153,6 +155,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "enable_accrual_activity_posting", nullable = false) private boolean enableAccrualActivityPosting = false; + @Convert(converter = SupportedInterestRefundTypesListConverter.class) + @Column(name = "supported_interest_refund_types") + private List supportedInterestRefundTypes = List.of(); + public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -165,7 +171,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, @@ -173,7 +179,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, - loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting); + loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } protected LoanProductRelatedDetail() { @@ -192,7 +198,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting) { + final boolean enableAccrualActivityPosting, List supportedInterestRefundTypes) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -226,6 +232,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.loanScheduleType = loanScheduleType; this.loanScheduleProcessingType = loanScheduleProcessingType; this.enableAccrualActivityPosting = enableAccrualActivityPosting; + this.supportedInterestRefundTypes = supportedInterestRefundTypes; } private Integer defaultToNullIfZero(final Integer value) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanSupportedInterestRefundTypes.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanSupportedInterestRefundTypes.java new file mode 100644 index 000000000..565c862ee --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanSupportedInterestRefundTypes.java @@ -0,0 +1,47 @@ +/** + * 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.portfolio.loanproduct.domain; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + +@Getter +@RequiredArgsConstructor +public enum LoanSupportedInterestRefundTypes { + + MERCHANT_ISSUED_REFUND(LoanTransactionType.MERCHANT_ISSUED_REFUND, "loanRefundType.merchant_issued_refund", "Merchant issued refund"), // + PAYOUT_REFUND(LoanTransactionType.PAYOUT_REFUND, "loanRefundType.payout_refund", "Payout refund"), // + ; + + private final LoanTransactionType transactionType; + private final String code; + private final String humanReadableName; + + public static List getValuesAsStringEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new StringEnumOptionData(v.name(), v.getCode(), v.getHumanReadableName())).toList(); + } + + public StringEnumOptionData getValueAsStringEnumOptionData() { + return new StringEnumOptionData(name(), getCode(), getHumanReadableName()); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java index 7842dd7b4..b9d8b1172 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java @@ -41,7 +41,8 @@ public enum PaymentAllocationTransactionType { WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), // CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), // REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan"), // - INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver"); + INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver"), // + INTEREST_REFUND(LoanTransactionType.INTEREST_REFUND, "Interest refund"); private final LoanTransactionType loanTransactionType; private final String humanReadableName; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/SupportedInterestRefundTypesListConverter.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/SupportedInterestRefundTypesListConverter.java new file mode 100644 index 000000000..5476e7453 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/SupportedInterestRefundTypesListConverter.java @@ -0,0 +1,41 @@ +/** + * 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.portfolio.loanproduct.domain; + +// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; +import org.apache.fineract.infrastructure.core.data.GenericEnumListConverter; + +@Converter(autoApply = true) +public class SupportedInterestRefundTypesListConverter extends GenericEnumListConverter + implements AttributeConverter, String> { + + @Override + public boolean isUnique() { + return true; + } + + public SupportedInterestRefundTypesListConverter() { + super(LoanSupportedInterestRefundTypes.class); + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index 4901cccbb..434a219b1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -321,6 +321,8 @@ public final class LoanEnumerations { LoanTransactionType.REAMORTIZE.getCode(), "Re-amortize"); case ACCRUAL_ACTIVITY -> new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ACTIVITY.getValue().longValue(), LoanTransactionType.ACCRUAL_ACTIVITY.getCode(), "Accrual Activity"); + case INTEREST_REFUND -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_REFUND.getValue().longValue(), + LoanTransactionType.INTEREST_REFUND.getCode(), "Interest Refund"); }; } diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index 4fffd9b3e..e3b76c417 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -44,4 +44,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1022_add_interest_refund_support.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1022_add_interest_refund_support.xml new file mode 100644 index 000000000..6f7d4ecf2 --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1022_add_interest_refund_support.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/persistence.xml b/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/persistence.xml index 24ff3eead..ca8dabe36 100644 --- a/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/persistence.xml +++ b/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/persistence.xml @@ -114,6 +114,7 @@ org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter + org.apache.fineract.portfolio.loanproduct.domain.SupportedInterestRefundTypesListConverter org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index dbed762c6..2bca76f2d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -522,7 +522,8 @@ public class LoanScheduleAssembler { fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, - loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()); + loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting(), + loanProduct.getLoanProductRelatedDetail().getSupportedInterestRefundTypes()); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, 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 8be92659d..75d16770b 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 @@ -59,6 +59,7 @@ import org.apache.fineract.infrastructure.core.api.ApiParameterHelper; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; @@ -84,6 +85,7 @@ import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrat import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; @@ -421,6 +423,8 @@ public class LoanProductsApiResource { final List advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList(); final List creditAllocationTransactionTypes = CreditAllocationTransactionType.getValuesAsEnumOptionDataList(); final List creditAllocationAllocationTypes = AllocationType.getValuesAsEnumOptionDataList(); + final List supportedInterestRefundTypesOptions = LoanSupportedInterestRefundTypes + .getValuesAsStringEnumOptionDataList(); return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, @@ -432,7 +436,7 @@ public class LoanProductsApiResource { advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules, advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, - creditAllocationAllocationTypes); + creditAllocationAllocationTypes, supportedInterestRefundTypesOptions); } } 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 7be800757..a38400074 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 @@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCa 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.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.springframework.stereotype.Component; @@ -175,7 +176,7 @@ public final class LoanProductDataValidator { LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.LOAN_SCHEDULE_TYPE, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, - LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING)); + LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -841,6 +842,19 @@ public final class LoanProductDataValidator { validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategyCode) + && this.fromApiJsonHelper.parameterExists(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, element)) { + String[] supportedTransactionsForInterestRefund = this.fromApiJsonHelper + .extractArrayNamed(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, element); + Arrays.stream(supportedTransactionsForInterestRefund) + .forEach(value -> baseDataValidator.reset().parameter(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES).value(value) + .isOneOfEnumValues(LoanSupportedInterestRefundTypes.class)); + } else if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, element)) { + baseDataValidator.reset().parameter(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES).failWithCode( + "supported.only.for.progressive.loan.schedule.handling", + "Automatic interest refund functionality is only supported for Progressive loans"); + } + 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 d9d8c63a1..b63935129 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 @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicInteger; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingEnumerations; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -56,6 +57,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductParamType; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; import org.apache.fineract.portfolio.rate.data.RateData; import org.apache.fineract.portfolio.rate.service.RateReadService; @@ -276,7 +278,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, " + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " + "lvi.maximum_gap as maximumGap, dbuc.id as delinquencyBucketId, dbuc.name as delinquencyBucketName, " - + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization, lp.loan_schedule_type as loanScheduleType, lp.loan_schedule_processing_type as loanScheduleProcessingType " + + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization, lp.loan_schedule_type as loanScheduleType, lp.loan_schedule_processing_type as loanScheduleProcessingType, lp.supported_interest_refund_types as supportedInterestRefundTypes " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " + " left join m_product_loan_guarantee_details lpg on lpg.loan_product_id=lp.id " @@ -526,6 +528,13 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final String loanScheduleProcessingTypeStr = rs.getString("loanScheduleProcessingType"); final LoanScheduleProcessingType loanScheduleProcessingType = LoanScheduleProcessingType.valueOf(loanScheduleProcessingTypeStr); final boolean enableAccrualActivityPosting = rs.getBoolean("enableAccrualActivityPosting"); + final String supportedInterestRefundTypesString = rs.getString("supportedInterestRefundTypes"); + List supportedInterestRefundTypes = List.of(); + if (supportedInterestRefundTypesString != null && !supportedInterestRefundTypesString.isEmpty()) { + supportedInterestRefundTypes = Arrays.stream(supportedInterestRefundTypesString.split(",")) + .map(LoanSupportedInterestRefundTypes::valueOf) + .map(LoanSupportedInterestRefundTypes::getValueAsStringEnumOptionData).toList(); + } return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -548,7 +557,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), - fixedLength, enableAccrualActivityPosting); + fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 55266d185..593e47e1b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductCannotBeModifiedDueToNonClosedLoansException; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductDateException; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; @@ -122,6 +123,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro } final LoanProduct loanProduct = LoanProduct.assembleFromJson(fund, loanTransactionProcessingStrategyCode, charges, command, this.aprCalculator, floatingRate, rates, loanProductPaymentAllocationRules, loanProductCreditAllocationRules); + loanProduct.updateLoanProductInRelatedClasses(); loanProduct.setTransactionProcessingStrategyName( loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(loanTransactionProcessingStrategyCode).getName()); @@ -264,6 +266,16 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro } } + if (command.parameterExists(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES)) { + JsonArray supportedTransactionsForInterestRefund = command + .arrayOfParameterNamed(LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES); + List supportedInterestRefundTypes = new ArrayList<>(); + supportedTransactionsForInterestRefund.iterator().forEachRemaining(value -> { + supportedInterestRefundTypes.add(LoanSupportedInterestRefundTypes.valueOf(value.getAsString())); + }); + product.getLoanProductRelatedDetail().setSupportedInterestRefundTypes(supportedInterestRefundTypes); + } + if (!changes.isEmpty()) { product.validateLoanProductPreSave(); this.loanProductRepository.saveAndFlush(product); diff --git a/fineract-provider/src/main/resources/jpa/persistence.xml b/fineract-provider/src/main/resources/jpa/persistence.xml index f2eb0777d..4419151df 100644 --- a/fineract-provider/src/main/resources/jpa/persistence.xml +++ b/fineract-provider/src/main/resources/jpa/persistence.xml @@ -121,6 +121,7 @@ org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter + org.apache.fineract.portfolio.loanproduct.domain.SupportedInterestRefundTypesListConverter org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java index c87f77407..442e4cd33 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java @@ -95,7 +95,7 @@ public class DefaultScheduledDateGeneratorTest { Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE, - submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false); + submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null); // when List result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms, @@ -166,7 +166,7 @@ public class DefaultScheduledDateGeneratorTest { null, null, null, null, null, Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, - DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false); + DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null); } private HolidayDetailDTO createHolidayDTO() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java index fda6d2f07..a3303169a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.common.AccountingConstants; import org.apache.fineract.client.models.AdvancedPaymentData; @@ -94,7 +95,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); // when - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); @@ -117,7 +119,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { // given a loan with two allocations AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); Assertions.assertNotNull(loanProduct.getPaymentAllocation()); @@ -138,7 +141,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { // given a loan with one allocation AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER + .getLoanProductId(loanProductTestBuilder(customization -> customization.addAdvancedPaymentAllocation(defaultAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); Assertions.assertNotNull(loanProduct.getPaymentAllocation()); @@ -168,7 +172,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { // given a loan with two allocations AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); Assertions.assertNotNull(loanProduct.getPaymentAllocation()); @@ -188,7 +193,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { // given a loan with two allocations AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); Assertions.assertNotNull(loanProduct.getPaymentAllocation()); @@ -229,8 +235,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse); // when - List> loanProductError = validationErrorHelper.getLoanProductError(createLoanJSON(repaymentPaymentAllocation), - "errors"); + List> loanProductError = validationErrorHelper.getLoanProductError( + loanProductTestBuilder(customization -> customization.addAdvancedPaymentAllocation(repaymentPaymentAllocation)), "errors"); Assertions.assertEquals("Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided", loanProductError.get(0).get("defaultUserMessage")); } @@ -242,8 +248,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation(); // when - Integer loanProductId = LOAN_TRANSACTION_HELPER - .getLoanProductId(createLoanJSON(defaultAllocation, interestPaymentWaiverAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, interestPaymentWaiverAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); @@ -266,7 +272,8 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { // given a loan with one allocation AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation(); - Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation)); + Integer loanProductId = LOAN_TRANSACTION_HELPER + .getLoanProductId(loanProductTestBuilder(customization -> customization.addAdvancedPaymentAllocation(defaultAllocation))); Assertions.assertNotNull(loanProductId); GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); Assertions.assertNotNull(loanProduct.getPaymentAllocation()); @@ -291,14 +298,81 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests { Assertions.assertEquals(interestPaymentWaiverAllocation, second.get()); } - private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) { - return new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4").withRepaymentAfterEvery("1") - .withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") + @Test + public void testCreateAndReadProgressiveLoanProductWithInterestRefund() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // when + String loanProductRequest = loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation)); + + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductRequest); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + + // then + Assertions.assertNotNull(loanProduct.getSupportedInterestRefundTypes()); + Assertions.assertEquals(0, loanProduct.getSupportedInterestRefundTypes().size()); + + // when a new interest refund transaction was added + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), + new PutLoanProductsProductIdRequest().supportedInterestRefundTypes(List.of("MERCHANT_ISSUED_REFUND"))); + + loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + + // then + Assertions.assertNotNull(loanProduct.getSupportedInterestRefundTypes()); + Assertions.assertEquals(1, loanProduct.getSupportedInterestRefundTypes().size()); + Assertions.assertEquals("MERCHANT_ISSUED_REFUND", loanProduct.getSupportedInterestRefundTypes().get(0).getId()); + + // Set both of them at creation + String loanProductRequest2 = loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .withSupportedInterestRefundTypes("PAYOUT_REFUND", "MERCHANT_ISSUED_REFUND")); + Integer loanProductId2 = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductRequest2); + Assertions.assertNotNull(loanProductId2); + GetLoanProductsProductIdResponse loanProduct2 = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId2); + + // then + Assertions.assertNotNull(loanProduct2.getSupportedInterestRefundTypes()); + Assertions.assertEquals(2, loanProduct2.getSupportedInterestRefundTypes().size()); + Assertions.assertEquals("PAYOUT_REFUND", loanProduct2.getSupportedInterestRefundTypes().get(0).getId()); + Assertions.assertEquals("MERCHANT_ISSUED_REFUND", loanProduct2.getSupportedInterestRefundTypes().get(1).getId()); + + // Remove the previously configured transactions + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId2.longValue(), + new PutLoanProductsProductIdRequest().supportedInterestRefundTypes(List.of())); + loanProduct2 = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId2); + // then + Assertions.assertNotNull(loanProduct2.getSupportedInterestRefundTypes()); + Assertions.assertEquals(0, loanProduct2.getSupportedInterestRefundTypes().size()); + } + + @Test + public void testCreateCumulativeLoanProductWithInterestRefund() { + // given + // when + String loanProductRequest = loanProductTestBuilder(customization -> customization.withSupportedInterestRefundTypes("PAYOUT_REFUND") + .withLoanScheduleType(LoanScheduleType.CUMULATIVE).withRepaymentStrategy("mifos-standard-strategy")); + LoanTransactionHelper loanTransactionHelperBadRequest = new LoanTransactionHelper(REQUEST_SPEC, + new ResponseSpecBuilder().expectStatusCode(400).build()); + List> loanProductError = loanTransactionHelperBadRequest.getLoanProductError(loanProductRequest, "errors"); + Assertions.assertEquals( + "validation.msg.loanproduct.supportedInterestRefundTypes.supported.only.for.progressive.loan.schedule.handling", + loanProductError.get(0).get("userMessageGlobalisationCode")); + } + + private String loanProductTestBuilder(Consumer customization) { + LoanProductTestBuilder builder = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") .withAccountingRulePeriodicAccrual(new Account[] { ASSET_ACCOUNT, EXPENSE_ACCOUNT, INCOME_ACCOUNT, OVERPAYMENT_ACCOUNT }) .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() - .withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).addAdvancedPaymentAllocation(advancedPaymentData) - .withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL) - .build(); + .withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).withLoanScheduleType(LoanScheduleType.PROGRESSIVE) + .withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL); + customization.accept(builder); + return builder.build(); } private PutLoanProductsProductIdRequest updateLoanProductRequest(AdvancedPaymentData... advancedPaymentData) { 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 7fd2529f4..928988db8 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 @@ -160,6 +160,7 @@ public class LoanProductTestBuilder { private String loanScheduleType = LoanScheduleType.CUMULATIVE.name(); private String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name(); private FullAccountingConfig fullAccountingConfig; + private List supportedInterestRefundTypes = null; public String build() { final HashMap map = build(null, null); @@ -320,6 +321,10 @@ public class LoanProductTestBuilder { map.put("repaymentStartDateType", repaymentStartDateType); } + if (this.supportedInterestRefundTypes != null) { + map.put("supportedInterestRefundTypes", supportedInterestRefundTypes); + } + return map; } @@ -776,6 +781,11 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder withSupportedInterestRefundTypes(String... refundTypes) { + this.supportedInterestRefundTypes = List.of(refundTypes); + return this; + } + public String getTransactionProcessingStrategyCode() { return transactionProcessingStrategyCode; }