From b64b4b710528b37680f3866feb64389fe7159901 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Tue, 19 Mar 2024 00:20:24 -0600 Subject: [PATCH] FINERACT-2065: Schedule handling for fixed length configuration --- .../domain/LoanApplicationTerms.java | 39 +++ .../domain/DefaultScheduledDateGenerator.java | 17 +- .../LoanScheduleModelRepaymentPeriod.java | 2 + ...ateLoanScheduleQueryFromApiJsonHelper.java | 3 +- ...WritePlatformServiceJpaRepositoryImpl.java | 5 +- .../starter/LoanAccountConfiguration.java | 4 +- .../LoanProductDataValidator.java | 65 +++- ...ntAllocationLoanRepaymentScheduleTest.java | 277 ++++++++++++++++++ ...FixedLengthLoanProductIntegrationTest.java | 53 ++-- .../integrationtests/common/Utils.java | 15 + .../common/loans/LoanTransactionHelper.java | 5 + 11 files changed, 453 insertions(+), 32 deletions(-) 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 32f855433..e9d38c438 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 @@ -1832,4 +1832,43 @@ public final class LoanApplicationTerms { public Money getDownPaymentAmount() { return downPaymentAmount; } + + public Integer getFixedLength() { + return fixedLength; + } + + public LocalDate calculateMaxDateForFixedLength() { + final LocalDate startDate = getRepaymentStartDate(); + LocalDate maxDateForFixedLength = null; + if (fixedLength == null) { + return maxDateForFixedLength; + } + switch (repaymentPeriodFrequencyType) { + case DAYS: + maxDateForFixedLength = startDate.plusDays(fixedLength); + break; + case WEEKS: + maxDateForFixedLength = startDate.plusWeeks(fixedLength); + break; + case MONTHS: + maxDateForFixedLength = startDate.plusMonths(fixedLength); + break; + case YEARS: + maxDateForFixedLength = startDate.plusYears(fixedLength); + break; + case INVALID: + break; + case WHOLE_TERM: + log.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM"); + break; + } + return maxDateForFixedLength; + } + + public LocalDate getRepaymentStartDate() { + final RepaymentStartDateType repaymentStartDateType = getRepaymentStartDateType(); + return RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType) ? getExpectedDisbursementDate() + : getSubmittedOnDate(); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 5108bff37..fbf889e7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -34,7 +34,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarHistory; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.springframework.stereotype.Component; @Component @@ -46,16 +45,16 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); - RepaymentStartDateType repaymentStartDateType = loanApplicationTerms.getRepaymentStartDateType(); - - LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType) - ? loanApplicationTerms.getExpectedDisbursementDate() - : loanApplicationTerms.getSubmittedOnDate(); + LocalDate lastRepaymentDate = loanApplicationTerms.getRepaymentStartDate(); boolean isFirstRepayment = true; for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment); isFirstRepayment = false; } + if (loanApplicationTerms.getFixedLength() != null + && loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(lastRepaymentDate) != 0) { + lastRepaymentDate = loanApplicationTerms.calculateMaxDateForFixedLength(); + } lastRepaymentDate = adjustRepaymentDate(lastRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); return lastRepaymentDate; } @@ -104,6 +103,12 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { } } + final LocalDate maxDateForFixedLength = loanApplicationTerms.calculateMaxDateForFixedLength(); + // Fixed Length validation + if (maxDateForFixedLength != null && DateUtils.isAfter(dueRepaymentPeriodDate, maxDateForFixedLength)) { + dueRepaymentPeriodDate = maxDateForFixedLength; + } + return dueRepaymentPeriodDate; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java index af13f3f3d..a3db61f20 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.HashSet; import java.util.Set; +import lombok.Getter; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; @@ -29,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP /** * Domain representation of a Loan Schedule Repayment Period (not used for persistence) */ +@Getter public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod { private final int periodNumber; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 8252e309d..affcf2284 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -70,7 +70,8 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME, - LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName)); + LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, + LoanProductConstants.FIXED_LENGTH)); private final FromJsonHelper fromApiJsonHelper; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 2759122b0..96ab3a928 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -196,6 +196,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final LoanRepository loanRepository; private final GSIMReadPlatformService gsimReadPlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final LoanProductDataValidator loanProductDataValidator; @Transactional @Override @@ -253,6 +254,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } final Loan newLoanApplication = this.loanAssembler.assembleFrom(command); + final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson()); + loanProductDataValidator.fixedLengthValidations(newLoanApplication.getTransactionProcessingStrategyCode(), loanApplicationTerms, + this.fromJsonHelper.parse(command.json()), baseDataValidator); checkForProductMixRestrictions(newLoanApplication); @@ -438,7 +442,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa CalendarEntityType.LOANS.getValue()); this.calendarInstanceRepository.save(calendarInstance); } else { - final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson()); final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType"); if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS && repaymentFrequencyNthDayType != null) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index a9cacb79d..501e98582 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -231,7 +231,7 @@ public class LoanAccountConfiguration { RateAssembler rateAssembler, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) { + LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanProductDataValidator loanProductDataValidator) { return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator, loanProductCommandFromApiJsonDeserializer, fromApiJsonDeserializer, loanRepositoryWrapper, noteRepository, calculationPlatformService, loanAssembler, clientRepository, loanProductRepository, loanChargeAssembler, @@ -241,7 +241,7 @@ public class LoanAccountConfiguration { configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService, globalConfigurationRepository, entityMappingRepository, fineractEntityRelationRepository, loanProductReadPlatformService, rateAssembler, glimAccountInfoWritePlatformService, - glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine); + glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanProductDataValidator); } @Bean 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 625c7de60..433fcf630 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 @@ -39,6 +39,7 @@ import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; @@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; @@ -399,6 +401,7 @@ public final class LoanProductDataValidator { } // interest rates + BigDecimal interestRatePerPeriod = null; if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element) && this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) { if (isEqualAmortization) { @@ -519,8 +522,7 @@ public final class LoanProductDataValidator { "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, - element); + interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element); baseDataValidator.reset().parameter(INTEREST_RATE_PER_PERIOD).value(interestRatePerPeriod).notNull().zeroOrPositiveAmount(); final String minInterestRatePerPeriodParameterName = MIN_INTEREST_RATE_PER_PERIOD; @@ -564,6 +566,10 @@ public final class LoanProductDataValidator { 4); } + // Fixed Length validation + fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element, + baseDataValidator); + // Guarantee Funds Boolean holdGuaranteeFunds = false; if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) { @@ -1238,13 +1244,15 @@ public final class LoanProductDataValidator { .integerGreaterThanZero(); } + Integer numberOfRepayments = loanProduct.getNumberOfRepayments(); if (this.fromApiJsonHelper.parameterExists(NUMBER_OF_REPAYMENTS, element)) { - final Integer numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element); + numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element); baseDataValidator.reset().parameter(NUMBER_OF_REPAYMENTS).value(numberOfRepayments).notNull().integerGreaterThanZero(); } + Integer repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery(); if (this.fromApiJsonHelper.parameterExists(REPAYMENT_EVERY, element)) { - final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(REPAYMENT_EVERY, element); + repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(REPAYMENT_EVERY, element); baseDataValidator.reset().parameter(REPAYMENT_EVERY).value(repaymentEvery).notNull().integerGreaterThanZero(); } @@ -1364,6 +1372,7 @@ public final class LoanProductDataValidator { } // interest rates + BigDecimal interestRatePerPeriod = null; boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate(); if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) { isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element); @@ -1531,7 +1540,7 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(maxInterestRatePerPeriodParameterName).value(maxInterestRatePerPeriod).ignoreIfNull() .zeroOrPositiveAmount(); - BigDecimal interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); + interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); if (this.fromApiJsonHelper.parameterExists(INTEREST_RATE_PER_PERIOD, element)) { interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element); } @@ -1546,6 +1555,10 @@ public final class LoanProductDataValidator { 4); } + // Fixed Length validation + fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element, + baseDataValidator); + // Guarantee Funds Boolean holdGuaranteeFunds = loanProduct.isHoldGuaranteeFundsEnabled(); if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) { @@ -2394,4 +2407,46 @@ public final class LoanProductDataValidator { } } + + public void fixedLengthValidations(final String transactionProcessingStrategyCode, final LoanApplicationTerms loanApplicationTerms, + final JsonElement element, final DataValidatorBuilder baseDataValidator) { + fixedLengthValidations(transactionProcessingStrategyCode, loanApplicationTerms.getAnnualNominalInterestRate(), + loanApplicationTerms.getNumberOfRepayments(), loanApplicationTerms.getRepaymentEvery(), element, baseDataValidator); + } + + public void fixedLengthValidations(final String transactionProcessingStrategyCode, final BigDecimal interestRatePerPeriod, + final Integer numberOfRepayments, final Integer repayEvery, final JsonElement element, + final DataValidatorBuilder baseDataValidator) { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.FIXED_LENGTH, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + final Integer fixedLength = this.fromApiJsonHelper.extractIntegerNamed(LoanProductConstants.FIXED_LENGTH, element, locale); + baseDataValidator.reset().parameter(LoanProductConstants.FIXED_LENGTH).value(fixedLength).ignoreIfNull() + .integerGreaterThanZero(); + + if (fixedLength != null) { + if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategyCode)) { + final String errorMsg = "Fixed Length configuration is only allowed with " + + AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + " stratefy"; + throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.advanced.payment.allocation", + errorMsg, AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + } + + if (interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0) { + final String errorMsg = "Fixed Length configuration is only allowed for zero interest products"; + throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.zero.interest", errorMsg, + interestRatePerPeriod); + } + + final Integer valueToCompare = ((numberOfRepayments - 1) * repayEvery) + 1; + if (fixedLength.compareTo(valueToCompare) < 0) { + final String errorMsg = "Wrong configuration between Number Of Repayments: " + numberOfRepayments + " * " + repayEvery + + " and Fixed Length: " + fixedLength + " values"; + throw new GeneralPlatformDomainRuleException("error.msg.number.repayments.and.fixed.length.configuration.not.valid", + errorMsg, numberOfRepayments, repayEvery, valueToCompare, fixedLength); + } + } + } + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 59bc1a45d..9cf4c6791 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -3542,6 +3542,283 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC126: Advanced payment allocation with Fixed Length for 40 days and Loan Term for 45 days (3 repayments every 15 + // days) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + @Test + public void uc126() { + runAt("22 November 2023", () -> { + final Integer fixedLength = 40; // 40 days + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(3).repaymentEvery(15).fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023", + 1000.0, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("22 November 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 12, 7), 33.0, 0.0, 33.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 22), 33.0, 0.0, 33.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 1, 01), 34.0, 0.0, 34.0, 0.0, 0.0); + assertEquals(loanDetails.getNumberOfRepayments(), 3); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + // UC127: Advanced payment allocation with Fixed Length for 5 weeks and Loan Term for 6 weeks (3 repayments every 2 + // weeks) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + @Test + public void uc127() { + runAt("22 November 2023", () -> { + final Integer fixedLength = 5; // 5 weeks + final Integer repaymentFrequencyType = 1; // week + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(3).repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType.longValue()) + .fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023", + 1000.0, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(6).loanTermFrequencyType(repaymentFrequencyType) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(2) + .repaymentFrequencyType(repaymentFrequencyType); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("22 November 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 12, 6), 33.0, 0.0, 33.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 20), 33.0, 0.0, 33.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 27), 34.0, 0.0, 34.0, 0.0, 0.0); + assertEquals(loanDetails.getNumberOfRepayments(), 3); + assertEquals( + Utils.getDifferenceInWeeks(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + // UC128: Advanced payment allocation with Fixed Length for 11 months and Loan Term for 12 months (6 repayments each + // 2 months) + // every 2 months) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + @Test + public void uc128() { + runAt("22 November 2023", () -> { + final Integer fixedLength = 11; // 11 months + final Integer repaymentFrequencyType = 2; // month + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(6).repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType.longValue()) + .fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023", + 1000.0, 4); + + applicationRequest = applicationRequest.numberOfRepayments(6).loanTermFrequency(12) + .loanTermFrequencyType(repaymentFrequencyType) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(2) + .repaymentFrequencyType(repaymentFrequencyType); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("22 November 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 22), 17.0, 0.0, 17.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 22), 17.0, 0.0, 17.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 5, 22), 17.0, 0.0, 17.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 7, 22), 17.0, 0.0, 17.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 9, 22), 17.0, 0.0, 17.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 10, 22), 15.0, 0.0, 15.0, 0.0, 0.0); + assertEquals(loanDetails.getNumberOfRepayments(), 6); + assertEquals( + Utils.getDifferenceInMonths(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + // UC129: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments + // every month) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve -> expect validation error + @Test + public void uc129() { + final String operationDate = "22 November 2023"; + runAt(operationDate, () -> { + final Integer fixedLength = 5; // 5 months + final Integer repaymentFrequencyType = 2; // month + + // Try to create a Loan Product using Fixed Length with a wrong Loan Term setup + LOG.info("Try to create a Loan Product using Fixed Length with a wrong Loan Term setup"); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanProductHelper + .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRatePerPeriod(0.0).numberOfRepayments(6).repaymentEvery(1) + .repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(fixedLength))); + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.number.repayments.and.fixed.length.configuration.not.valid")); + }); + } + + // UC130: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments + // every month) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve -> expect validation error - Fixed Length without Advanced Payment Allocation + @Test + public void uc130() { + final String operationDate = "22 November 2023"; + runAt(operationDate, () -> { + final Integer fixedLength = 5; // 5 months + final Integer repaymentFrequencyType = 2; // month + + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + + // Try to use Fixed Length without Advanced Payment Allocation as transaction processing strategy code + LOG.info("Try to use Fixed Length without Advanced Payment Allocation as transaction processing strategy code"); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId()) + .productId(loanProductResponse.getResourceId()).loanType("individual").locale("en") + .submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN) + .amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0) + .repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6) + .loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0)) + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength) + .transactionProcessingStrategyCode(FineractStyleLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE) + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()))); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage() + .contains("error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation")); + }); + } + + // UC131: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments + // every month) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve -> expect validation error - Interest Rate value higher than Zero + @Test + public void uc131() { + final String operationDate = "22 November 2023"; + runAt(operationDate, () -> { + final Integer fixedLength = 5; // 5 months + final Integer repaymentFrequencyType = 2; // month + + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + + // Try to use Fixed Length with an Interest Rate value higher than Zero + LOG.info("Try to use Fixed Length with an Interest Rate value higher than Zero"); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId()) + .productId(loanProductResponse.getResourceId()).loanType("individual").locale("en") + .submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN) + .amortizationType(1).interestRatePerPeriod(BigDecimal.ONE).interestCalculationPeriodType(1).interestType(0) + .repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6) + .loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0)) + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength) + .transactionProcessingStrategyCode( + AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()))); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.fixed.length.only.supported.for.zero.interest")); + }); + } + + // UC132: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (3 repayments + // every 2 months) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve -> expect validation error - Loan acount Fixed Length with a wrong Loan Term setup + @Test + public void uc132() { + final String operationDate = "22 November 2023"; + runAt(operationDate, () -> { + final Integer fixedLength = 5; // 5 months + final Integer repaymentFrequencyType = 2; // month + + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + + // Try to use Fixed Length with a wrong Loan Term setup + LOG.info("Try to use Fixed Length with a wrong Loan Term setup"); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId()) + .productId(loanProductResponse.getResourceId()).loanType("individual").locale("en") + .submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN) + .amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0) + .repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6) + .loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0)) + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength) + .transactionProcessingStrategyCode( + AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()))); + + assertEquals(403, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.number.repayments.and.fixed.length.configuration.not.valid")); + }); + } + private static List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { AtomicInteger integer = new AtomicInteger(1); return Arrays.stream(paymentAllocationTypes).map(pat -> { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java index a85931ed5..eaacc633a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java @@ -22,10 +22,16 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,25 +41,25 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe @Test public void testCreateReadUpdateReadLoanProductWithFixedLength() { - // create with 5 - PostLoanProductsRequest loanProductsRequest = fixedLengthLoanProduct(5); + // create with 4 + PostLoanProductsRequest loanProductsRequest = fixedLengthLoanProduct(4); PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductsRequest); Assertions.assertNotNull(loanProduct.getResourceId()); // read GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper .retrieveLoanProductById(loanProduct.getResourceId()); - Assertions.assertEquals(5, getLoanProductsProductIdResponse.getFixedLength()); + Assertions.assertEquals(4, getLoanProductsProductIdResponse.getFixedLength()); - // update to 6 - PutLoanProductsProductIdRequest updateRequest = new PutLoanProductsProductIdRequest().fixedLength(6).locale("en"); + // update to 5 + PutLoanProductsProductIdRequest updateRequest = new PutLoanProductsProductIdRequest().fixedLength(5).locale("en"); PutLoanProductsProductIdResponse putLoanProductsProductIdResponse = loanProductHelper .updateLoanProductById(loanProduct.getResourceId(), updateRequest); Assertions.assertNotNull(putLoanProductsProductIdResponse.getResourceId()); // read again getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProduct.getResourceId()); - Assertions.assertEquals(6, getLoanProductsProductIdResponse.getFixedLength()); + Assertions.assertEquals(5, getLoanProductsProductIdResponse.getFixedLength()); // update to null loanTransactionHelper.updateLoanProduct(putLoanProductsProductIdResponse.getResourceId(), """ @@ -73,12 +79,18 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe runAt("01 January 2023", () -> { Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); - PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(6)); + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(4)); Assertions.assertNotNull(loanProduct.getResourceId()); - Long loanId = applyAndApproveLoan(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 4); + applicationRequest = applicationRequest + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + Long loanId = loanResponse.getLoanId(); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(6, loanDetails.getFixedLength()); + Assertions.assertEquals(4, loanDetails.getFixedLength()); }); } @@ -87,22 +99,29 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe runAt("01 January 2023", () -> { Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); - PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(6)); + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(4)); Assertions.assertNotNull(loanProduct.getResourceId()); - Long loanId = applyAndApproveLoan(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 1, // - loanApplication -> loanApplication.fixedLength(5) // - ); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 4); + applicationRequest = applicationRequest.fixedLength(5).repaymentEvery(1).repaymentFrequencyType(2).loanTermFrequencyType(2) + .loanTermFrequency(4).transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + Long loanId = loanResponse.getLoanId(); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); Assertions.assertEquals(5, loanDetails.getFixedLength()); }); } private PostLoanProductsRequest fixedLengthLoanProduct(Integer fixedLength) { - return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)// - .repaymentEvery(1)// - .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// - .transactionProcessingStrategyCode("mifos-standard-strategy").fixedLength(fixedLength); + return createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() // + .numberOfRepayments(4).repaymentEvery(1) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) // + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + .transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) // + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()) // + .interestRatePerPeriod(0.0).fixedLength(fixedLength); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java index 222bc0d13..5c653d051 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java @@ -19,6 +19,9 @@ package org.apache.fineract.integrationtests.common; import static io.restassured.RestAssured.given; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.MONTHS; +import static java.time.temporal.ChronoUnit.WEEKS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.fail; @@ -520,4 +523,16 @@ public final class Utils { public static LocalDate getDateAsLocalDate(String dateAsString) { return LocalDate.parse(dateAsString, dateFormatter); } + + public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) { + return DAYS.between(localDateBefore, localDateAfter); + } + + public static long getDifferenceInWeeks(final LocalDate localDateBefore, final LocalDate localDateAfter) { + return WEEKS.between(localDateBefore, localDateAfter); + } + + public static long getDifferenceInMonths(final LocalDate localDateBefore, final LocalDate localDateAfter) { + return MONTHS.between(localDateBefore, localDateAfter); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 9f06c03f6..aef2ed245 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -343,6 +343,11 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanCharges.retrieveTemplate9(loanExternalId)); } + public HashMap applyLoan(final String payload, final ResponseSpecification responseSpec) { + final String postURLForLoan = "/fineract-provider/api/v1/loans?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoan, payload, null); + } + public List getRepaymentTemplate(final Integer loanId) { final String GET_REPAYMENTS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/template?command=repayment&" + Utils.TENANT_IDENTIFIER;