From ef2c4467618bb4971013b87a0ea0c8fa17436039 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Thu, 10 Aug 2023 13:15:15 +0530 Subject: [PATCH] FINERACT-1958-Autopayment-downpayment --- .../interoperation/util/MathUtil.java | 10 + .../data/LoanTransactionEnumData.java | 6 +- .../portfolio/loanaccount/domain/Loan.java | 39 ++- .../loanaccount/domain/LoanTransaction.java | 12 +- .../domain/LoanTransactionType.java | 10 +- .../domain/LoanApplicationTerms.java | 17 +- .../loanproduct/LoanProductConstants.java | 1 + .../loanproduct/domain/LoanProduct.java | 17 +- .../domain/LoanProductRelatedDetail.java | 20 +- .../loanproduct/service/LoanEnumerations.java | 2 + .../module/loan/module-changelog-master.xml | 1 + ...o_repayment_down_payment_configuration.xml | 39 +++ .../api/LoansApiResourceSwagger.java | 2 + .../loanaccount/data/LoanAccountData.java | 10 +- .../service/LoanScheduleAssembler.java | 4 +- .../service/LoanReadPlatformServiceImpl.java | 6 +- ...WritePlatformServiceJpaRepositoryImpl.java | 3 +- .../api/LoanProductsApiResource.java | 4 +- .../api/LoanProductsApiResourceSwagger.java | 6 + .../loanproduct/data/LoanProductData.java | 17 +- .../LoanProductDataValidator.java | 23 +- .../LoanProductReadPlatformServiceImpl.java | 6 +- ...oductWithDownPaymentConfigurationTest.java | 293 +++++++++++++++++- .../common/loans/LoanProductTestBuilder.java | 7 +- 24 files changed, 503 insertions(+), 52 deletions(-) create mode 100644 fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1003_add_loan_product_auto_repayment_down_payment_configuration.xml diff --git a/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java b/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java index b3c822c64..f1550b26b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java +++ b/fineract-loan/src/main/java/org/apache/fineract/interoperation/util/MathUtil.java @@ -424,4 +424,14 @@ public final class MathUtil { public static Money min(Money first, Money second, Money third, boolean notNull) { return min(min(first, second, notNull), third, notNull); } + + public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage, final int precision) { + BigDecimal percentageOf = BigDecimal.ZERO; + if (isGreaterThanZero(value)) { + final MathContext mc = new MathContext(precision, MoneyHelper.getRoundingMode()); + final BigDecimal multiplicand = percentage.divide(BigDecimal.valueOf(100L), mc); + percentageOf = value.multiply(multiplicand, mc); + } + return percentageOf; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index 47ed70bf7..8d6db7a77 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -54,6 +54,7 @@ public class LoanTransactionEnumData { private final boolean chargeAdjustment; private final boolean chargeback; private final boolean chargeoff; + private final boolean downPayment; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -83,11 +84,12 @@ public class LoanTransactionEnumData { this.chargeback = Long.valueOf(25).equals(this.id); this.chargeAdjustment = Long.valueOf(26).equals(this.id); this.chargeoff = Long.valueOf(27).equals(this.id); + this.downPayment = Long.valueOf(28).equals(this.id); } public boolean isRepaymentType() { - if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() - || isChargeAdjustment()) { + if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isChargeAdjustment() + || isDownPayment()) { return true; } return false; 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 7d78c4a1a..facf6bb29 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 @@ -2954,6 +2954,42 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } + public void handleDownPayment(final BigDecimal disbursedAmount, final JsonCommand command, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + if (isAutoRepaymentForDownPaymentEnabled()) { + LocalDate disbursedOn = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); + BigDecimal disbursedAmountPercentageForDownPayment = this.loanRepaymentScheduleDetail + .getDisbursedAmountPercentageForDownPayment(); + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + Money downPaymentMoney = Money.of(getCurrency(), + MathUtil.percentageOf(disbursedAmount, disbursedAmountPercentageForDownPayment, 9)); + LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), downPaymentMoney, null, disbursedOn, + externalId); + /* + * Currently mapping down payment transaction as repayment transaction to loan repayment schedule for + * consistency. Need to replace below logic for creating new installment for down payment and mapping. + */ + + LoanEvent event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER; + validateRepaymentTypeAccountStatus(downPaymentTransaction, event); + HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); + validateRepaymentDateIsOnHoliday(downPaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), + holidayDetailDTO.getHolidays()); + validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(), + holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); + + handleRepaymentOrRecoveryOrWaiverTransaction(downPaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); + } + } + + private boolean isAutoRepaymentForDownPaymentEnabled() { + return this.loanRepaymentScheduleDetail.isEnableDownPayment() + && this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment(); + } + public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment, final List existingTransactionIds, final List existingReversedTransactionIds) { existingTransactionIds.addAll(findExistingTransactionIds()); @@ -3132,7 +3168,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) { if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund() - || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment()) { + || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() + || repaymentTransaction.isDownPayment()) { if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) { final List dataValidationErrors = new ArrayList<>(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 9a2116c56..c7b1afd7f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -180,6 +180,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { externalId); } + public static LoanTransaction downPayment(final Office office, final Money amount, final PaymentDetail paymentDetail, + final LocalDate paymentDate, final ExternalId externalId) { + return new LoanTransaction(null, office, LoanTransactionType.DOWN_PAYMENT, paymentDetail, amount.getAmount(), paymentDate, + externalId); + } + public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) { for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) { final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping @@ -558,7 +564,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { public boolean isRepaymentLikeType() { return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() - || isChargeAdjustment(); + || isChargeAdjustment() || isDownPayment(); } public boolean isTypeAllowedForChargeback() { @@ -573,6 +579,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return LoanTransactionType.MERCHANT_ISSUED_REFUND.equals(getTypeOf()) && isNotReversed(); } + public boolean isDownPayment() { + return LoanTransactionType.DOWN_PAYMENT.equals(getTypeOf()) && isNotReversed(); + } + public boolean isPayoutRefund() { return LoanTransactionType.PAYOUT_REFUND.equals(getTypeOf()) && isNotReversed(); } 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 5ceaf0278..ef5bd3f66 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 @@ -59,7 +59,8 @@ public enum LoanTransactionType { CHARGE_REFUND(24, "loanTransactionType.chargeRefund"), // CHARGEBACK(25, "loanTransactionType.chargeback"), // CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), // - CHARGE_OFF(27, "loanTransactionType.chargeOff"); + CHARGE_OFF(27, "loanTransactionType.chargeOff"), // + DOWN_PAYMENT(28, "loanTransactionType.downPayment"); private final Integer value; private final String code; @@ -102,6 +103,7 @@ public enum LoanTransactionType { case 25 -> LoanTransactionType.CHARGEBACK; case 26 -> LoanTransactionType.CHARGE_ADJUSTMENT; case 27 -> LoanTransactionType.CHARGE_OFF; + case 28 -> LoanTransactionType.DOWN_PAYMENT; default -> LoanTransactionType.INVALID; }; } @@ -143,7 +145,7 @@ public enum LoanTransactionType { } public boolean isRepaymentType() { - return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()); + return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment()); } public boolean isRecoveryRepayment() { @@ -189,4 +191,8 @@ public enum LoanTransactionType { public boolean isChargeOff() { return this.equals(LoanTransactionType.CHARGE_OFF); } + + public boolean isDownPayment() { + return this.equals(LoanTransactionType.DOWN_PAYMENT); + } } 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 0a7243e5c..b40103736 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 @@ -209,6 +209,7 @@ public final class LoanApplicationTerms { private LocalDate newScheduledDueDateStart; private boolean isDownPaymentEnabled; private BigDecimal disbursedAmountPercentageForDownPayment; + private boolean isAutoRepaymentForDownPaymentEnabled; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -233,7 +234,8 @@ public final class LoanApplicationTerms { boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans, - final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment) { + final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, + final Boolean isAutoRepaymentForDownPaymentEnabled) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -250,7 +252,8 @@ public final class LoanApplicationTerms { calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, - isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment); + isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, + isAutoRepaymentForDownPaymentEnabled); } @@ -298,8 +301,10 @@ public final class LoanApplicationTerms { final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization(); final boolean isDownPaymentEnabled = loanProductRelatedDetail.isEnableDownPayment(); BigDecimal disbursedAmountPercentageForDownPayment = null; + boolean isAutoRepaymentForDownPaymentEnabled = false; if (isDownPaymentEnabled) { disbursedAmountPercentageForDownPayment = loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(); + isAutoRepaymentForDownPaymentEnabled = loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment(); } return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, @@ -315,7 +320,8 @@ public final class LoanApplicationTerms { isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, - isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment); + isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, + isAutoRepaymentForDownPaymentEnabled); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -342,7 +348,7 @@ public final class LoanApplicationTerms { final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled, - final BigDecimal disbursedAmountPercentageForDownPayment) { + final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -421,6 +427,7 @@ public final class LoanApplicationTerms { this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans; this.isDownPaymentEnabled = isDownPaymentEnabled; this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; + this.isAutoRepaymentForDownPaymentEnabled = isAutoRepaymentForDownPaymentEnabled; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1265,7 +1272,7 @@ public final class LoanApplicationTerms { this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod, this.inArrearsTolerance.getAmount(), this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, - this.disbursedAmountPercentageForDownPayment); + this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled); } 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 dd3a67710..0d6176b3d 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 @@ -150,5 +150,6 @@ public interface LoanProductConstants { // down-payment related String ENABLE_DOWN_PAYMENT = "enableDownPayment"; String DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT = "disbursedAmountPercentageForDownPayment"; + String ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT = "enableAutoRepaymentForDownPayment"; } 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 178cd2abb..d0c7aabed 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 @@ -388,6 +388,8 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean enableDownPayment = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT); final BigDecimal disbursedAmountPercentageDownPayment = command .bigDecimalValueOfParameterNamed(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT); + final boolean enableAutoRepaymentForDownPayment = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT); return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, @@ -405,7 +407,7 @@ public class LoanProduct extends AbstractPersistableCustom { minimumGapBetweenInstallments, maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment, disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent, - overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment); + overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment); } @@ -617,7 +619,8 @@ public class LoanProduct extends AbstractPersistableCustom { final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean disallowExpectedDisbursements, final boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType, final Integer overAppliedNumber, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent, - final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment) { + final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, + final boolean enableAutoRepaymentForDownPayment) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; this.name = name.trim(); @@ -650,7 +653,8 @@ public class LoanProduct extends AbstractPersistableCustom { considerPartialPeriodInterest, repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), - isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment); + isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, + enableAutoRepaymentForDownPayment); this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); @@ -1235,6 +1239,13 @@ public class LoanProduct extends AbstractPersistableCustom { this.loanProductRelatedDetail.updateDisbursedAmountPercentageForDownPayment(newValue); } + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, + this.loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT); + actualChanges.put(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, newValue); + this.loanProductRelatedDetail.updateEnableAutoRepaymentForDownPayment(newValue); + } + return actualChanges; } 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 d16c51b03..9b06c20d1 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 @@ -135,6 +135,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "disbursed_amount_percentage_for_down_payment", scale = 6, precision = 9) private BigDecimal disbursedAmountPercentageForDownPayment; + @Column(name = "enable_auto_repayment_for_down_payment", nullable = false) + private boolean enableAutoRepaymentForDownPayment; + public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -144,14 +147,15 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, - final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment) { + final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, + final boolean enableAutoRepaymentForDownPayment) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, - isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment); + isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment); } protected LoanProductRelatedDetail() { @@ -167,7 +171,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, - final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment) { + final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, + final boolean enableAutoRepaymentForDownPayment) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -196,6 +201,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.isEqualAmortization = isEqualAmortization; this.enableDownPayment = enableDownPayment; this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; + this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; } private Integer defaultToNullIfZero(final Integer value) { @@ -701,4 +707,12 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public void updateDisbursedAmountPercentageForDownPayment(BigDecimal disbursedAmountPercentageForDownPayment) { this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; } + + public Boolean isEnableAutoRepaymentForDownPayment() { + return enableAutoRepaymentForDownPayment; + } + + public void updateEnableAutoRepaymentForDownPayment(boolean enableAutoRepaymentForDownPayment) { + this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; + } } 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 5b57ed8a4..735caf788 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 @@ -293,6 +293,8 @@ public final class LoanEnumerations { LoanTransactionType.CHARGE_ADJUSTMENT.getCode(), "Charge Adjustment"); case CHARGE_OFF -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_OFF.getValue().longValue(), LoanTransactionType.CHARGE_OFF.getCode(), "Charge-off"); + case DOWN_PAYMENT -> new LoanTransactionEnumData(LoanTransactionType.DOWN_PAYMENT.getValue().longValue(), + LoanTransactionType.DOWN_PAYMENT.getCode(), "Down Payment"); }; } 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 ca7acea64..abab920a6 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 @@ -25,4 +25,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1003_add_loan_product_auto_repayment_down_payment_configuration.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1003_add_loan_product_auto_repayment_down_payment_configuration.xml new file mode 100644 index 000000000..a1f9cad65 --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1003_add_loan_product_auto_repayment_down_payment_configuration.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index dafac9e54..d450850c4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -1078,6 +1078,8 @@ final class LoansApiResourceSwagger { public Boolean enableDownPayment; @Schema(example = "0.000000") public BigDecimal disbursedAmountPercentageForDownPayment; + @Schema(example = "false") + public Boolean enableAutoRepaymentForDownPayment; } @Schema(description = "GetLoansResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index be54f2672..575d17c65 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -258,6 +258,7 @@ public class LoanAccountData { private Boolean enableDownPayment; private BigDecimal disbursedAmountPercentageForDownPayment; + private Boolean enableAutoRepaymentForDownPayment; public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, @@ -660,7 +661,7 @@ public class LoanAccountData { final BigDecimal topupAmount, final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment, final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud, LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate, final boolean chargedOff, final boolean enableDownPayment, - final BigDecimal disbursedAmountPercentageForDownPayment) { + final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment) { final CollectionData delinquent = CollectionData.template(); @@ -701,8 +702,8 @@ public class LoanAccountData { .setFixedPrincipalPercentagePerInstallment(fixedPrincipalPercentagePerInstallment).setDelinquent(delinquent) .setDelinquencyRange(delinquencyRange).setDisallowExpectedDisbursements(disallowExpectedDisbursements).setFraud(fraud) .setLastClosedBusinessDate(lastClosedBusinessDate).setOverpaidOnDate(overpaidOnDate).setChargedOff(chargedOff) - .setEnableDownPayment(enableDownPayment) - .setDisbursedAmountPercentageForDownPayment(disbursedAmountPercentageForDownPayment); + .setEnableDownPayment(enableDownPayment).setDisbursedAmountPercentageForDownPayment(disbursedAmountPercentageForDownPayment) + .setEnableAutoRepaymentForDownPayment(enableAutoRepaymentForDownPayment); } /* @@ -788,7 +789,8 @@ public class LoanAccountData { .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setFraud(acc.fraud).setLastClosedBusinessDate(acc.getLastClosedBusinessDate()).setOverpaidOnDate(acc.overpaidOnDate) .setChargedOff(acc.chargedOff).setEnableDownPayment(acc.enableDownPayment) - .setDisbursedAmountPercentageForDownPayment(acc.disbursedAmountPercentageForDownPayment); + .setDisbursedAmountPercentageForDownPayment(acc.disbursedAmountPercentageForDownPayment) + .setEnableAutoRepaymentForDownPayment(acc.enableAutoRepaymentForDownPayment); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, 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 f533b028d..55384b5b7 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 @@ -452,9 +452,11 @@ public class LoanScheduleAssembler { final boolean isDownPaymentEnabled = loanProduct.getLoanProductRelatedDetail().isEnableDownPayment(); BigDecimal disbursedAmountPercentageForDownPayment = null; + boolean isAutoRepaymentForDownPaymentEnabled = false; if (isDownPaymentEnabled) { disbursedAmountPercentageForDownPayment = loanProduct.getLoanProductRelatedDetail() .getDisbursedAmountPercentageForDownPayment(); + isAutoRepaymentForDownPaymentEnabled = loanProduct.getLoanProductRelatedDetail().isEnableAutoRepaymentForDownPayment(); } return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, @@ -469,7 +471,7 @@ public class LoanScheduleAssembler { loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipMeetingOnFirstDay, detailDTO, allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, - disbursedAmountPercentageForDownPayment); + disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index a5e06b898..5e187f2a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -694,7 +694,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, " + " l.total_recovered_derived as totalRecovered, topuploan.account_no as closureLoanAccountNo, " + " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate, " - + " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, l.enable_down_payment as enableDownPayment, l.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, " + + " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, l.enable_down_payment as enableDownPayment, l.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, l.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment," + " cobu.username as chargedOffByUsername, cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname " + " from m_loan l" // + " join m_product_loan lp on lp.id = l.product_id" // @@ -1040,6 +1040,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final boolean enableDownPayment = rs.getBoolean("enableDownPayment"); final BigDecimal disbursedAmountPercentageForDownPayment = rs.getBigDecimal("disbursedAmountPercentageForDownPayment"); + final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment"); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, @@ -1056,7 +1057,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud, - lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment); + lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment, + enableAutoRepaymentForDownPayment); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 02d2a833b..ada7fb2f8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -437,7 +437,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf disbursementTransaction.updateLoan(loan); loan.addLoanTransaction(disbursementTransaction); } - if (loan.getRepaymentScheduleInstallments().isEmpty()) { /* * If no schedule, generate one (applicable to non-tranche multi-disbursal loans) @@ -454,8 +453,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } else { changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); } - loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); + loan.handleDownPayment(amountToDisburse.getAmount(), command, scheduleGeneratorDTO); } if (!changes.isEmpty()) { if (changedTransactionDetail != null) { 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 dc7a22a2f..7e427ff67 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 @@ -112,7 +112,7 @@ public class LoanProductsApiResource { LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.ENABLE_DOWN_PAYMENT, - LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT)); + LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT)); private static final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>( Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); @@ -147,7 +147,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,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment\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,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment\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 b31fd1648..2b2e2b04d 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 @@ -159,6 +159,8 @@ final class LoanProductsApiResourceSwagger { public Boolean enableDownPayment; @Schema(example = "5.5") public BigDecimal disbursedAmountPercentageForDownPayment; + @Schema(example = "false") + public Boolean enableAutoRepaymentForDownPayment; // Interest Recalculation @Schema(example = "false") @@ -1211,6 +1213,8 @@ final class LoanProductsApiResourceSwagger { public Boolean enableDownPayment; @Schema(example = "5.5") public BigDecimal disbursedAmountPercentageForDownPayment; + @Schema(example = "false") + public Boolean enableAutoRepaymentForDownPayment; } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1336,6 +1340,8 @@ final class LoanProductsApiResourceSwagger { public Boolean enableDownPayment; @Schema(example = "5.5") public BigDecimal disbursedAmountPercentageForDownPayment; + @Schema(example = "false") + public Boolean enableAutoRepaymentForDownPayment; // 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 3c41d3903..a82ed2d0f 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 @@ -195,6 +195,7 @@ public class LoanProductData implements Serializable { private final boolean enableDownPayment; private final BigDecimal disbursedAmountPercentageForDownPayment; + private final boolean enableAutoRepaymentForDownPayment; /** * Used when returning lookup information about loan product for dropdowns. @@ -286,6 +287,7 @@ public class LoanProductData implements Serializable { final Integer overDueDaysForRepaymentEvent = null; final boolean enableDownPayment = false; final BigDecimal disbursedAmountPercentageDownPayment = null; + final boolean enableAutoRepaymentForDownPayment = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -304,7 +306,7 @@ public class LoanProductData implements Serializable { maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, - overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment); + overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment); } @@ -396,6 +398,7 @@ public class LoanProductData implements Serializable { final Integer overDueDaysForRepaymentEvent = null; final boolean enableDownPayment = false; final BigDecimal disbursedAmountPercentageDownPayment = null; + final boolean enableAutoRepaymentForDownPayment = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -414,7 +417,7 @@ public class LoanProductData implements Serializable { maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, - overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment); + overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment); } @@ -513,6 +516,7 @@ public class LoanProductData implements Serializable { final Integer overDueDaysForRepaymentEvent = null; final boolean enableDownPayment = false; final BigDecimal disbursedAmountPercentageDownPayment = null; + final boolean enableAutoRepaymentForDownPayment = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -531,7 +535,7 @@ public class LoanProductData implements Serializable { maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, - overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment); + overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment); } @@ -624,6 +628,7 @@ public class LoanProductData implements Serializable { final Integer overDueDaysForRepaymentEvent = null; final boolean enableDownPayment = false; final BigDecimal disbursedAmountPercentageDownPayment = null; + final boolean enableAutoRepaymentForDownPayment = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -642,7 +647,7 @@ public class LoanProductData implements Serializable { maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, - overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment); + overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment); } @@ -690,7 +695,7 @@ public class LoanProductData implements Serializable { final BigDecimal fixedPrincipalPercentagePerInstallment, final Collection delinquencyBucketOptions, final DelinquencyBucketData delinquencyBucket, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment, - final BigDecimal disbursedAmountPercentageForDownPayment) { + final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment) { this.id = id; this.name = name; this.shortName = shortName; @@ -810,6 +815,7 @@ public class LoanProductData implements Serializable { this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent; this.enableDownPayment = enableDownPayment; this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; + this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -963,6 +969,7 @@ public class LoanProductData implements Serializable { this.overDueDaysForRepaymentEvent = productData.overDueDaysForRepaymentEvent; this.enableDownPayment = productData.enableDownPayment; this.disbursedAmountPercentageForDownPayment = productData.disbursedAmountPercentageForDownPayment; + this.enableAutoRepaymentForDownPayment = productData.enableAutoRepaymentForDownPayment; } private Collection nullIfEmpty(final Collection charges) { 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 25bbd701c..f0e2761e0 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 @@ -160,7 +160,8 @@ public final class LoanProductDataValidator { LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, - LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT)); + LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, + LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -750,11 +751,30 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_DOWN_PAYMENT).value(enableDownPayment).ignoreIfNull() .validateForBooleanValue(); validateDownPaymentPercentage(enableDownPayment, baseDataValidator, element); + validateAutoRepaymentForDownPayment(enableDownPayment, baseDataValidator, element); } throwExceptionIfValidationWarningsExist(dataValidationErrors); } + private void validateAutoRepaymentForDownPayment(Boolean enableDownPayment, DataValidatorBuilder baseDataValidator, + JsonElement element) { + if (enableDownPayment) { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, element)) { + final Boolean enableAutoRepaymentForDownPayment = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, element); + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT) + .value(enableAutoRepaymentForDownPayment).ignoreIfNull().validateForBooleanValue(); + } + } else { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, element)) { + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT).failWithCode( + "supported.only.for.enable.down.payment.true", + "Auto repayment for down-payment is supported only if enable down-payment is true"); + } + } + } + private void validateDownPaymentPercentage(Boolean enableDownPayment, DataValidatorBuilder baseDataValidator, JsonElement element) { if (enableDownPayment) { if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, element)) { @@ -1656,6 +1676,7 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_DOWN_PAYMENT).value(enableDownPayment).ignoreIfNull() .validateForBooleanValue(); validateDownPaymentPercentage(enableDownPayment, baseDataValidator, element); + validateAutoRepaymentForDownPayment(enableDownPayment, baseDataValidator, element); } 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 5d73aea85..e919cd7bf 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 @@ -212,7 +212,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, lp.enable_down_payment as enableDownPayment, lp.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, " + + "lp.due_days_for_repayment_event as dueDaysForRepaymentEvent, lp.overdue_days_for_repayment_event as overDueDaysForRepaymentEvent, lp.enable_down_payment as enableDownPayment, lp.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, lp.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment, " + "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, " @@ -346,6 +346,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Integer overDueDaysForRepaymentEvent = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "overDueDaysForRepaymentEvent"); final boolean enableDownPayment = rs.getBoolean("enableDownPayment"); final BigDecimal disbursedAmountPercentageForDownPayment = rs.getBigDecimal("disbursedAmountPercentageForDownPayment"); + final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment"); String status = ""; if (closeDate != null && closeDate.isBefore(DateUtils.getBusinessLocalDate())) { @@ -502,7 +503,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, - dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment); + dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment, + enableAutoRepaymentForDownPayment); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithDownPaymentConfigurationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithDownPaymentConfigurationTest.java index 99c4f36da..974645b86 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithDownPaymentConfigurationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithDownPaymentConfigurationTest.java @@ -19,7 +19,9 @@ package org.apache.fineract.integrationtests; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; @@ -27,6 +29,7 @@ import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; import java.util.UUID; @@ -37,9 +40,16 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.accounting.JournalEntry; +import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; @@ -53,6 +63,8 @@ public class LoanProductWithDownPaymentConfigurationTest { private RequestSpecification requestSpec; private LoanTransactionHelper loanTransactionHelper; private ClientHelper clientHelper; + private AccountHelper accountHelper; + private JournalEntryHelper journalEntryHelper; @BeforeEach public void setup() { @@ -62,6 +74,8 @@ public class LoanProductWithDownPaymentConfigurationTest { this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); } @Test @@ -74,16 +88,17 @@ public class LoanProductWithDownPaymentConfigurationTest { // down-payment configuration Boolean enableDownPayment = true; BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); - + Boolean enableAutoRepaymentForDownPayment = false; // Loan Product creation with down-payment configuration Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, - "25"); + "25", enableAutoRepaymentForDownPayment); final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); assertNotNull(getLoanProductsProductResponse); assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); } @Test @@ -122,7 +137,7 @@ public class LoanProductWithDownPaymentConfigurationTest { Boolean enableDownPayment = true; // Loan Product with enable down payment and with disbursed amount percentage as zero - final HashMap loanProductMap = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "0") + final HashMap loanProductMap = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "0", false) .build(null, delinquencyBucketId); ArrayList loanProductErrorData = (ArrayList) validationErrorHelper @@ -132,7 +147,7 @@ public class LoanProductWithDownPaymentConfigurationTest { loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); // Loan Product with enable down payment and with disbursed amount percentage as greater than 100 - final HashMap loanProductMap_1 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "101") + final HashMap loanProductMap_1 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "101", false) .build(null, delinquencyBucketId); loanProductErrorData = (ArrayList) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_1), @@ -143,7 +158,7 @@ public class LoanProductWithDownPaymentConfigurationTest { // Loan Product with enable down payment and with disbursed amount percentage precision greater than 6 final HashMap loanProductMap_2 = new LoanProductTestBuilder() - .withEnableDownPayment(enableDownPayment, "12.55555555").build(null, delinquencyBucketId); + .withEnableDownPayment(enableDownPayment, "12.55555555", false).build(null, delinquencyBucketId); loanProductErrorData = (ArrayList) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_2), CommonConstants.RESPONSE_ERROR); @@ -152,8 +167,8 @@ public class LoanProductWithDownPaymentConfigurationTest { loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); // Loan Product with disable down payment and with disbursed amount percentage - final HashMap loanProductMap_3 = new LoanProductTestBuilder().withEnableDownPayment(false, "12.5").build(null, - delinquencyBucketId); + final HashMap loanProductMap_3 = new LoanProductTestBuilder().withEnableDownPayment(false, "12.5", false) + .build(null, delinquencyBucketId); loanProductErrorData = (ArrayList) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_3), CommonConstants.RESPONSE_ERROR); @@ -162,7 +177,7 @@ public class LoanProductWithDownPaymentConfigurationTest { loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); // Loan Product with enable down payment and without disbursed amount percentage - final HashMap loanProductMap_4 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, null) + final HashMap loanProductMap_4 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, null, false) .build(null, delinquencyBucketId); loanProductErrorData = (ArrayList) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_4), @@ -171,6 +186,15 @@ public class LoanProductWithDownPaymentConfigurationTest { assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.required.for.enable.down.payment.true", loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); + // Loan Product with disable down payment and enable auto repayment for down payment + final HashMap loanProductMap_5 = new LoanProductTestBuilder().withEnableDownPayment(false, null, true).build(null, + delinquencyBucketId); + + loanProductErrorData = (ArrayList) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_5), + CommonConstants.RESPONSE_ERROR); + assertNotNull(loanProductErrorData); + assertEquals("validation.msg.loanproduct.enableAutoRepaymentForDownPayment.supported.only.for.enable.down.payment.true", + loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); } @Test @@ -186,20 +210,22 @@ public class LoanProductWithDownPaymentConfigurationTest { // down-payment configuration Boolean enableDownPayment = true; BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); // Loan Product creation with down-payment configuration Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, - "25"); + "25", enableAutoRepaymentForDownPayment); final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); assertNotNull(getLoanProductsProductResponse); assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); - final Integer loanId = createLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); // Retrieve Loan with loanId @@ -209,6 +235,7 @@ public class LoanProductWithDownPaymentConfigurationTest { assertNotNull(loanDetails); assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); } @@ -225,20 +252,22 @@ public class LoanProductWithDownPaymentConfigurationTest { // down-payment configuration Boolean enableDownPayment = true; BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(12.5); + Boolean enableAutoRepaymentForDownPayment = false; final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); // Loan Product creation with down-payment configuration Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, - "12.5"); + "12.5", enableAutoRepaymentForDownPayment); final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); assertNotNull(getLoanProductsProductResponse); assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); - final Integer loanId = createLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr); // Retrieve Loan with loanId @@ -248,6 +277,7 @@ public class LoanProductWithDownPaymentConfigurationTest { assertNotNull(loanDetails); assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); // Modify Loan Product to update enable down payment configuration PutLoanProductsProductIdResponse loanProductModifyResponse = updateLoanProduct(loanTransactionHelper, @@ -259,6 +289,7 @@ public class LoanProductWithDownPaymentConfigurationTest { assertNotNull(getLoanProductsProductResponse_1); assertEquals(enableDownPayment, getLoanProductsProductResponse_1.getEnableDownPayment()); assertEquals(0, getLoanProductsProductResponse_1.getDisbursedAmountPercentageForDownPayment().compareTo(BigDecimal.valueOf(25.0))); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse_1.getEnableAutoRepaymentForDownPayment()); // make repayment for loan final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, @@ -271,9 +302,239 @@ public class LoanProductWithDownPaymentConfigurationTest { assertNotNull(loanDetails); assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); } + @Test + public void loanApplicationWithLoanProductWithEnableDownPaymentAndEnableAutoRepaymentForDownPaymentTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 03, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Accounts oof periodic accrual + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Delinquency Bucket + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + // down-payment configuration + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithDownPaymentConfigurationAndAccrualAccounting( + loanTransactionHelper, delinquencyBucketId, enableDownPayment, "25", enableAutoRepaymentForDownPayment, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), + loanExternalIdStr); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + // first disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1000"); + + // verify down-payment transaction created + checkDownPaymentTransaction(disbursementDate, 250.0f, 0.0f, 0.0f, 0.0f, loanId); + + // verify journal entries for down-payment + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "03 March 2023", + new JournalEntry(250, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "03 March 2023", + new JournalEntry(250, JournalEntry.TransactionType.DEBIT)); + + // second disbursement + + disbursementDate = LocalDate.of(2023, 03, 5); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + loanTransactionHelper.disburseLoanWithTransactionAmount("05 March 2023", loanId, "200"); + checkDownPaymentTransaction(disbursementDate, 50.0f, 0.0f, 0.0f, 0.0f, loanId); + + // verify journal entries for down-payment + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "05 March 2023", + new JournalEntry(50, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "05 March 2023", + new JournalEntry(50, JournalEntry.TransactionType.DEBIT)); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + @Test + public void loanApplicationWithLoanProductWithEnableDownPaymentAndDisableAutoRepaymentForDownPaymentVerifyNoDownPaymentCreatedTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 03, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Accounts oof periodic accrual + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Delinquency Bucket + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + // down-payment configuration + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithDownPaymentConfigurationAndAccrualAccounting( + loanTransactionHelper, delinquencyBucketId, enableDownPayment, "25", enableAutoRepaymentForDownPayment, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), + loanExternalIdStr); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + // first disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1000"); + + // verify no down-payment transaction created + checkNoDownPaymentTransaction(loanId); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + private void checkNoDownPaymentTransaction(final Integer loanID) { + ArrayList transactions = (ArrayList) loanTransactionHelper.getLoanTransactions(this.requestSpec, + this.responseSpec, loanID); + boolean isTransactionFound = false; + for (int i = 0; i < transactions.size(); i++) { + HashMap transactionType = (HashMap) transactions.get(i).get("type"); + boolean isDownPaymentTransaction = (Boolean) transactionType.get("downPayment"); + + if (isDownPaymentTransaction) { + isTransactionFound = true; + break; + } + } + assertFalse(isTransactionFound, "Down Payment entries are posted"); + } + + private void checkDownPaymentTransaction(final LocalDate transactionDate, final Float principalPortion, final Float interestPortion, + final Float feePortion, final Float penaltyPortion, final Integer loanID) { + ArrayList transactions = (ArrayList) loanTransactionHelper.getLoanTransactions(this.requestSpec, + this.responseSpec, loanID); + boolean isTransactionFound = false; + for (int i = 0; i < transactions.size(); i++) { + HashMap transactionType = (HashMap) transactions.get(i).get("type"); + boolean isDownPaymentTransaction = (Boolean) transactionType.get("downPayment"); + + if (isDownPaymentTransaction) { + ArrayList downPaymentDateAsArray = (ArrayList) transactions.get(i).get("date"); + LocalDate downPaymentEntryDate = LocalDate.of(downPaymentDateAsArray.get(0), downPaymentDateAsArray.get(1), + downPaymentDateAsArray.get(2)); + + if (transactionDate.isEqual(downPaymentEntryDate)) { + isTransactionFound = true; + assertEquals(principalPortion, Float.valueOf(String.valueOf(transactions.get(i).get("principalPortion"))), + "Mismatch in transaction amounts"); + assertEquals(interestPortion, Float.valueOf(String.valueOf(transactions.get(i).get("interestPortion"))), + "Mismatch in transaction amounts"); + assertEquals(feePortion, Float.valueOf(String.valueOf(transactions.get(i).get("feeChargesPortion"))), + "Mismatch in transaction amounts"); + assertEquals(penaltyPortion, Float.valueOf(String.valueOf(transactions.get(i).get("penaltyChargesPortion"))), + "Mismatch in transaction amounts"); + break; + } + } + } + assertTrue(isTransactionFound, "No Down Payment entries are posted"); + } + + private Integer createLoanAccountMultipleRepaymentsDisbursement(final Integer clientID, final Long loanProductID, + final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("30") + .withLoanTermFrequencyAsDays().withNumberOfRepayments("1").withRepaymentEveryAfter("30").withRepaymentFrequencyTypeAsDays() + .withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("03 March 2023") + .withSubmittedOnDate("03 March 2023").withLoanType("individual").withExternalId(externalId) + .build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("03 March 2023", "1000", loanId, null); + return loanId; + } + + private GetLoanProductsProductIdResponse createLoanProductWithDownPaymentConfigurationAndAccrualAccounting( + LoanTransactionHelper loanTransactionHelper, Integer delinquencyBucketId, Boolean enableDownPayment, + String disbursedAmountPercentageForDownPayment, boolean enableAutoRepaymentForDownPayment, final Account... accounts) { + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance() + .withAccountingRulePeriodicAccrual(accounts).withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30") + .withDaysInYear("365").withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true) + .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) + .build(null); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(loanProductJSON); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + private PutLoanProductsProductIdResponse updateLoanProduct(LoanTransactionHelper loanTransactionHelper, Long id) { // down-payment configuration Boolean enableDownPayment = true; @@ -291,14 +552,16 @@ public class LoanProductWithDownPaymentConfigurationTest { } private Integer createLoanProductWithDownPaymentConfiguration(final LoanTransactionHelper loanTransactionHelper, - final Integer delinquencyBucketId, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment) { + final Integer delinquencyBucketId, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment, + Boolean enableAutoRepaymentForDownPayment) { final HashMap loanProductMap = new LoanProductTestBuilder() - .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment).build(null, delinquencyBucketId); + .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) + .build(null, delinquencyBucketId); final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); return loanProductId; } - private Integer createLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) { + private Integer createApproveAndDisburseLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) { String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") 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 2b6cbe2e4..cbcd35362 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 @@ -145,6 +145,7 @@ public class LoanProductTestBuilder { private Integer overDueDaysForRepaymentEvent = null; private boolean enableDownPayment = false; private String disbursedAmountPercentageForDownPayment = null; + private boolean enableAutoRepaymentForDownPayment = false; public String build(final String chargeId) { final HashMap map = build(chargeId, null); @@ -285,6 +286,9 @@ public class LoanProductTestBuilder { if (this.disbursedAmountPercentageForDownPayment != null) { map.put("disbursedAmountPercentageForDownPayment", disbursedAmountPercentageForDownPayment); } + if (enableAutoRepaymentForDownPayment) { + map.put("enableAutoRepaymentForDownPayment", enableAutoRepaymentForDownPayment); + } return map; } @@ -697,9 +701,10 @@ public class LoanProductTestBuilder { } public LoanProductTestBuilder withEnableDownPayment(final Boolean enableDownPayment, - final String disbursedAmountPercentageForDownPayment) { + final String disbursedAmountPercentageForDownPayment, final Boolean enableAutoRepaymentForDownPayment) { this.enableDownPayment = enableDownPayment; this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; + this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; return this; }