diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index bd5c2f4a08b..f6e562a7eee 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -913,6 +913,14 @@ public CommandWrapperBuilder creditBalanceRefundWorkingCapitalLoanTransaction(fi return this; } + public CommandWrapperBuilder updatePeriodPaymentRateWorkingCapitalLoanApplication(final Long loanId) { + this.actionName = "UPDATERATE"; + this.entityName = "WORKINGCAPITALLOAN"; + this.entityId = loanId; + this.href = "/workingcapitalloans/" + loanId; + return this; + } + public CommandWrapperBuilder createClientIdentifier(final Long clientId) { this.actionName = ACTION_CREATE; this.entityName = ENTITY_CLIENTIDENTIFIER; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java index 89fbebfb829..073a100561c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/workingcapitalproduct/DefaultWorkingCapitalLoanProduct.java @@ -29,7 +29,8 @@ public enum DefaultWorkingCapitalLoanProduct implements WorkingCapitalLoanProduc WCLP_BREACH, // WCLP_BREACH_NEAR_BREACH, // WCLP_BREACH_DISALLOW_ATTRIBUTES_OVERRIDE, // - WCLP_BREACH_NEAR_BREACH_DISALLOW_ATTRIBUTES_OVERRIDE; // + WCLP_BREACH_NEAR_BREACH_DISALLOW_ATTRIBUTES_OVERRIDE, // + WCLP_PERIOD_PAYMENT_RATE; // @Override public String getName() { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java index d24582de4fc..0a2b1f80de6 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalLoanRequestFactory.java @@ -25,6 +25,7 @@ import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdDiscountRequest; +import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRateRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRequest; import org.apache.fineract.test.data.workingcapitalproduct.DefaultWorkingCapitalLoanProduct; import org.apache.fineract.test.data.workingcapitalproduct.WorkingCapitalLoanProductResolver; @@ -45,6 +46,7 @@ public class WorkingCapitalLoanRequestFactory { public static final BigDecimal DEFAULT_TOTAL_PAYMENT = new BigDecimal(100); public static final BigDecimal DEFAULT_PERIOD_PAYMENT_RATE = new BigDecimal(1); public static final BigDecimal DEFAULT_DISCOUNT_ZERO = BigDecimal.ZERO; + public static final BigDecimal DEFAULT_PAYMENT_RATE = new BigDecimal(15); public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); public static final String DATE_SUBMIT_STRING = FORMATTER.format(Utils.now().minusMonths(1L)); @@ -121,4 +123,10 @@ public PutWorkingCapitalLoansLoanIdDiscountRequest defaultWorkingCapitalLoanUpda .dateFormat(DATE_FORMAT)// .locale(DEFAULT_LOCALE);// } + + public PutWorkingCapitalLoansLoanIdRateRequest defaultWorkingCapitalLoanUpdateRateRequest() { + return new PutWorkingCapitalLoansLoanIdRateRequest() // + .periodPaymentRate(DEFAULT_PAYMENT_RATE) // + .locale(DEFAULT_LOCALE); // + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java index 0b1b6f60900..c8f2873e316 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java @@ -1117,4 +1117,8 @@ public static String nearBreachMustBeLowerThenBreachFailure() { public static String nearBreachIdNotFoundFailure(long nearBreachId) { return String.format("Working Capital Near Breach with id %s was not found.", nearBreachId); } + + public static String periodPaymentRateOnNonActiveLoanFailure() { + return "Period payment rate change is allowed only for active loans"; + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 9786ea777d2..b627c27b3f9 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -71,9 +71,11 @@ import org.apache.fineract.client.models.ProjectedAmortizationScheduleData; import org.apache.fineract.client.models.ProjectedAmortizationSchedulePaymentData; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdDiscountRequest; +import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRateRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdResponse; import org.apache.fineract.client.models.WorkingCapitalLoanCommandTemplateData; +import org.apache.fineract.client.models.WorkingCapitalLoanPeriodPaymentRateChangeData; import org.apache.fineract.test.data.LoanStatus; import org.apache.fineract.test.data.paymenttype.DefaultPaymentType; import org.apache.fineract.test.data.paymenttype.PaymentTypeResolver; @@ -322,6 +324,273 @@ public void creatingAWorkingCapitalLoanWithMissingMandatoryFieldsWillResultAnErr log.info("Verified working capital loan creation failed with missing mandatory fields"); } + @Then("Creating a working capital loan with near breachId {long} on {string} will result with error") + public void createLoanWithInvalidNearBreachId(final long nearBreachId, final String submittedOnDate) { + final Long breachId = createBreachAndGetId(); + final PostWorkingCapitalLoansRequest request = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate).breachId(breachId) + .nearBreachId(nearBreachId); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(exception.getDeveloperMessage()) + .contains(String.format("Working Capital Near Breach with id %s was not found.", nearBreachId)); + assertThat(exception.getStatus()).as("HTTP status").isEqualTo(404); + } + + @Then("Admin creates working capital loan with breach override allowed with breach override and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreach(final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final Long overrideBreachId = createBreachAndGetId(); + createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, null); + } + + @Then("Admin creates working capital loan with breach override allowed with breach and near breach override and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachOverride(final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final Long overrideBreachId = createBreachAndGetId(); + final Long overrideNearBreachId = createNearBreachAndGetId(); + createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, overrideNearBreachId); + } + + @Then("Admin creates working capital loan with {int} {string} breach override and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreachOverrideData(int breachFrequency, String breachFrequencyType, + final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); + createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, null); + } + + @Then("Admin creates working capital loan with {int} {string} breach and {int} {string} near breach override and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachOverrideData(int breachFrequency, String breachFrequencyType, + int nearBreachFrequency, String nearBreachFrequencyType, final DataTable table) { + final List> data = table.asLists(); + final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); + final Long overrideNearBreachId = createNearBreachOverrideAndGetId(nearBreachFrequency, nearBreachFrequencyType); + createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); + } + + @Then("Admin creates working capital loan with breach override allowed with {int} {string} breach and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreachhData(final DataTable table, int breachFrequency, String breachFrequencyType, + int nearBreachFrequency, String nearBreachFrequencyType) { + final List> data = table.asLists(); + final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); + final Long overrideNearBreachId = createNearBreachOverrideAndGetId(nearBreachFrequency, nearBreachFrequencyType); + createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); + } + + @Then("Admin creates working capital loan with breach override allowed with {int} {string} breach and {int} {string} near breach and the following data:") + public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachData(int breachFrequency, String breachFrequencyType, + int nearBreachFrequency, String nearBreachFrequencyType, final DataTable table) { + final List> data = table.asLists(); + final Long overrideBreachId = createBreachAndGetId(breachFrequency, breachFrequencyType); + final Long overrideNearBreachId = createNearBreachAndGetId(nearBreachFrequency, nearBreachFrequencyType); + createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); + } + + @Then("Admin creates working capital loan with breach from WCLP while override is allowed and the following data:") + public void createLoanWithBreachFromWCLPOverrideAllowedData(DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + + final String loanProduct = loanData.get(0); + final Long loanProductId = resolveLoanProductId(loanProduct); + final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); + testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachIdFromWCLP); + + createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, breachIdFromWCLP, null); + } + + @Then("Admin creates working capital loan with breach and near breach from WCLP while override is allowed and the following data:") + public void createLoanWithBreachNearBreachFromWCLPOverrideAllowedData(DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final String loanProduct = loanData.get(0); + final Long loanProductId = resolveLoanProductId(loanProduct); + + final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); + final Long nearBreachIdFromWCLP = getNearBreachIdFromWCLP(loanProductId); + testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachIdFromWCLP); + testContext().set(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID, nearBreachIdFromWCLP); + + createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, breachIdFromWCLP, nearBreachIdFromWCLP); + } + + @Then("Admin creates working capital loan with with breach and near breach on {string} date") + public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachData(String submittedOnDate) { + final Long breachId = createBreachAndGetId(); + final Long nearBreachId = createNearBreachAndGetId(); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) + .breachId(breachId).nearBreachId(nearBreachId); + createWorkingCapitalLoanAccount(loansRequest); + } + + @Then("Verify working capital loan account has been created with correct breach data") + public void checkCreateWCLoanAccountBreachData() { + final Long breachId = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID); + checkCreateWCLoanAccountBreachData(breachId); + } + + @Then("Verify working capital loan account has been created with correct breach data inherited from WCLP level") + public void checkCreateWCLoanAccountBreachDataFromWCLP() { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanResponse.getLoanId(); + + GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() + .retrieveWorkingCapitalLoanById(loanId); + + final Long loanProductId = loanProductResponse.getProduct().getId(); + final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); + + checkCreateWCLoanAccountBreachData(breachIdFromWCLP); + } + + @Then("Verify working capital loan account has been created with correct breach override data") + public void checkCreateWCLoanAccountBreachOverrideData() { + final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID_OVERRIDE); + checkCreateWCLoanAccountBreachData(breachIdFromWCLP); + } + + @Then("Verify working capital loan account has been created with correct breach and near breach data") + public void checkCreateWCLoanAccountBreachAndNearBreachData() { + final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID); + final Long nearBreachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID); + checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); + } + + @Then("Verify working capital loan account has been created with correct breach and near breach override data") + public void checkCreateWCLoanAccountBreachAndNearBreachOverrideData() { + final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID_OVERRIDE); + final Long nearBreachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID_OVERRIDE); + checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); + } + + @Then("Verify working capital loan account has been created with correct breach and near breach data inherited from WCLP level") + public void checkCreateWCLoanAccountBreachNearBreachDataFromWCLP() { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanResponse.getLoanId(); + + GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() + .retrieveWorkingCapitalLoanById(loanId); + + final Long loanProductId = loanProductResponse.getProduct().getId(); + final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); + final Long nearBreachIdFromWCLP = getNearBreachIdFromWCLP(loanProductId); + + checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); + } + + @Then("Verify working capital loan account has been created with none breach nor near breach data") + public void checkCreateWCLoanAccountNoneBreachNearBreachData() { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long loanId = loanResponse.getLoanId(); + + final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); + assertThat(loanAccountResponse.getBreach()).isNull(); + assertThat(loanAccountResponse.getNearBreach()).isNull(); + } + + @Then("Admin failed to create working capital loan while breach override disallowed with breach override and the following data:") + public void createLoanWithBreachOverrideDisallowedWithBreachFailure(final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final Long overrideBreachId = createBreachOverrideAndGetId(); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountWithBreachNearBreachRequest(loanData, + overrideBreachId, null); + String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @Then("Admin failed to create working capital loan while breach override disallowed with breach override and default following data:") + public void createLoanWithBreachOverrideDisallowedWithBreachDefaultFailure(final DataTable table) { + final List loanData = table.asLists().get(1); + final String loanProduct = loanData.get(0); + final String submittedOnDate = loanData.get(1); + + final Long overrideBreachId = createBreachOverrideAndGetId(); + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) + .breachId(overrideBreachId); + String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @Then("Admin failed to create working capital loan while breach override disallowed with breach and near breach override and the following data:") + public void createLoanWithBreachOverrideDisallowedWithBreachAndNearBreachFailure(final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final Long overrideBreachId = createBreachOverrideAndGetId(); + final Long overrideNearBreachId = createNearBreachOverrideAndGetId(); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountWithBreachNearBreachRequest(loanData, + overrideBreachId, overrideNearBreachId); + String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @Then("Admin failed to create working capital loan while breach override disallowed with breach and near breach override and default following data:") + public void createLoanWithBreachOverrideDisallowedWithBreachAndNearBreachDefaultFailure(final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final String loanProduct = loanData.get(0); + final String submittedOnDate = loanData.get(1); + + final Long overrideBreachId = createBreachOverrideAndGetId(); + final Long overrideNearBreachId = createNearBreachOverrideAndGetId(); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) + .breachId(overrideBreachId).nearBreachId(overrideNearBreachId); + + String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @Then("Admin failed to create WC loan account on {string} with breach {int} {string} frequency lower then near breach {int} {string} frequency") + public void createLoanWithBreachLowerThenNearBreachFailure(String submittedOnDate, int breachFrequency, String breachFrequencyType, + int nearBreachFrequency, String nearBreachFrequencyType) { + final Long breachId = createBreachAndGetId(breachFrequency, breachFrequencyType); + final Long nearBreachId = createNearBreachAndGetId(nearBreachFrequency, nearBreachFrequencyType); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) + .breachId(breachId).nearBreachId(nearBreachId); + String message = ErrorMessageHelper.nearBreachMustBeLowerThenBreachFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @Then("Admin failed to create WC loan account on {string} without breach, but with near breach") + public void createLoanWithoutBreachButWithNearBreachFailure(String submittedOnDate) { + final Long nearBreachId = createNearBreachAndGetId(); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) + .nearBreachId(nearBreachId); + String message = ErrorMessageHelper.nearBreachCannotEnableWithoutBreachFailure(); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + } + + @When("Admin failed to create Working Capital on {string} with period payment rate {string} value and outcomes with {} error message") + public void adminAddWorkingCapitalPeriodPaymentRateInvalidDataFailure(String submittedOnDate, final String periodPaymentRate, + final String errorMessage) { + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) + .periodPaymentRate(new BigDecimal(periodPaymentRate)); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, errorMessage); + } + + @When("Admin failed to create Working Capital with period payment rate {string} value and outcomes with {} error message with default following data:") + public void createWorkingCapitalWithPeriodPaymentRateInvalidDataFailure(final String periodPaymentRate, final String errorMessage, + final DataTable table) { + final List> data = table.asLists(); + final List loanData = data.get(1); + final String loanProduct = loanData.get(0); + final String submittedOnDate = loanData.get(1); + + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) + .periodPaymentRate(new BigDecimal(periodPaymentRate)); + verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, errorMessage); + } + @When("Admin modifies the working capital loan with the following data:") public void modifyWorkingCapitalLoan(final DataTable table) { final List> data = table.asLists(); @@ -466,6 +735,13 @@ public void modifyLoanWithInvalidNearBreachId(final long nearBreachId) { verifyModifyWorkingCapitalLoanAccountFailure(modifyRequest, 404, errorMessage); } + @Then("Admin failed to modify WC loan account with period payment rate {string} value and outcomes with {} error message") + public void modifyLoanWithInvalidPeriodPaymentRateFailure(String periodPaymentRate, String errorMessage) { + final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() // + .periodPaymentRate(new BigDecimal(periodPaymentRate));// + verifyModifyWorkingCapitalLoanAccountFailure(modifyRequest, 400, errorMessage); + } + @When("Admin deletes the working capital loan account") public void deleteWorkingCapitalLoanAccount() { deleteLoan(false); @@ -997,10 +1273,83 @@ public void updateDiscountWCLoanOverrideDisallowedByProductFailure(String discou updateDiscountFailedCheck(discountAmount, errorMessage); } - @And("Update discount with {string} amount on Working Capital loan account failed due to exceed discount amount") - public void updateDiscountWCLoanExceedDiscountAmountProductFailure(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountExceedCreatedDiscountFailure(); - updateDiscountFailedCheck(discountAmount, errorMessage); + @And("Update discount with {string} amount on Working Capital loan account failed due to exceed discount amount") + public void updateDiscountWCLoanExceedDiscountAmountProductFailure(String discountAmount) { + String errorMessage = ErrorMessageHelper.discountExceedCreatedDiscountFailure(); + updateDiscountFailedCheck(discountAmount, errorMessage); + } + + @When("Admin update Working Capital period payment rate with {string} value") + public void adminAddWorkingCapitalPeriodPaymentRate(String periodPaymentRate) { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanResponse.getLoanId(); + + PutWorkingCapitalLoansLoanIdRateRequest rateChangeRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanUpdateRateRequest().periodPaymentRate(new BigDecimal(periodPaymentRate)); + + final CommandProcessingResult rateChangeResponse = ok( + () -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateById(loanId, rateChangeRequest)); + final Long rateChangeId = rateChangeResponse.getResourceId(); + + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_RATE_CHANGE_ID, rateChangeId); + assertThat(rateChangeResponse.getChanges()).isNotNull(); + checkWorkingCapitalPeriodPaymentRate(loanId, periodPaymentRate); + } + + @When("Admin update Working Capital period payment rate with {string} value by externalId") + public void adminAddWorkingCapitalPeriodPaymentRateByExternalId(String periodPaymentRate) { + final Long loanId = getCreatedLoanId(); + final String externalId = retrieveLoanExternalId(loanId); + + PutWorkingCapitalLoansLoanIdRateRequest rateChangeRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanUpdateRateRequest().periodPaymentRate(new BigDecimal(periodPaymentRate)); + + final CommandProcessingResult rateChangeResponse = ok( + () -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateByExternalId(externalId, rateChangeRequest)); + final Long rateChangeId = rateChangeResponse.getResourceId(); + + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_RATE_CHANGE_ID, rateChangeId); + assertThat(rateChangeResponse.getChanges()).isNotNull(); + checkWorkingCapitalPeriodPaymentRate(loanId, periodPaymentRate); + } + + @When("Admin update Working Capital period payment rate failed with {string} value on non active loan") + public void adminAddWorkingCapitalPeriodPaymentRateNonActiveLoanFailure(final String periodPaymentRate) { + String errorMessage = ErrorMessageHelper.periodPaymentRateOnNonActiveLoanFailure(); + updatePeriodPaymentRateFailed(periodPaymentRate, errorMessage); + } + + @When("Admin update Working Capital period payment rate failed with {string} value with {} error message") + public void adminAddWorkingCapitalPeriodPaymentRateInvalidDataFailure(final String periodPaymentRate, final String errorMessage) { + updatePeriodPaymentRateFailed(periodPaymentRate, errorMessage); + } + + @When("Working Capital Loan Period Payment Rate changes history contains the following data:") + public void adminChecksWorkingCapitalPeriodPaymentRateChangesHistory(DataTable table) { + PostWorkingCapitalLoansResponse loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanCreateResponse.getLoanId(); + String resourceId = String.valueOf(loanId); + + List rateChangesResponse = ok( + () -> fineractClient.workingCapitalLoans().getWorkingCapitalLoanRateChangeHistoryById(loanId)); + + List> data = table.asLists(); + List header = table.row(0); + checkPeriodPaymentRateChangeHistory(data, rateChangesResponse, header, resourceId); + } + + @When("Working Capital Loan Period Payment Rate changes history by externalId contains the following data:") + public void adminChecksWorkingCapitalPeriodPaymentRateChangesHistoryByExternalId(DataTable table) { + final Long loanId = getCreatedLoanId(); + String resourceId = String.valueOf(loanId); + final String externalId = retrieveLoanExternalId(loanId); + + List rateChangesResponse = ok( + () -> fineractClient.workingCapitalLoans().getWorkingCapitalLoanRateChangeHistoryByExternalId(externalId)); + + List> data = table.asLists(); + List header = table.row(0); + checkPeriodPaymentRateChangeHistory(data, rateChangesResponse, header, resourceId); } // ==================================== @@ -1337,408 +1686,162 @@ private void submitLoanAndStore(final PostWorkingCapitalLoansRequest request) { () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response); testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_CREATE_RESPONSE, response); - log.info("Working Capital Loan created, loan ID: {}", response.getLoanId()); - } - - @Then("Working capital loan account has delinquencyGraceDays {int} and delinquencyStartType {string}") - public void verifyLoanGraceDays(int expectedGraceDays, String expectedStartType) { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - final Long loanId = loanResponse.getLoanId(); - - final GetWorkingCapitalLoansLoanIdResponse response = ok( - () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); - - assertThat(response.getDelinquencyGraceDays()).as("delinquencyGraceDays").isEqualTo(expectedGraceDays); - assertThat(response.getDelinquencyStartType()).as("delinquencyStartType").isNotNull(); - assertThat(response.getDelinquencyStartType().getCode()).as("delinquencyStartType code").isEqualTo(expectedStartType); - } - - @When("Admin modifies the working capital loan with grace days:") - public void modifyLoanWithGraceDays(final DataTable table) { - final Map row = table.asMaps().get(0); - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - final Long loanId = loanResponse.getLoanId(); - - final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() // - .delinquencyGraceDays( - Optional.ofNullable(row.get("delinquencyGraceDays")).filter(s -> !s.isEmpty()).map(Integer::valueOf).orElse(null)) // - .delinquencyStartType(row.get("delinquencyStartType")); - - final PutWorkingCapitalLoansLoanIdResponse response = ok( - () -> fineractClient.workingCapitalLoans().modifyWorkingCapitalLoanApplicationById(loanId, modifyRequest, "")); - testContext().set(TestContextKey.LOAN_MODIFY_RESPONSE, response); - } - - @When("Admin approves the working capital loan on {string}") - public void approveWorkingCapitalLoan(final String approvedOnDate) { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - final Long loanId = loanResponse.getLoanId(); - - final PostWorkingCapitalLoansLoanIdRequest approveRequest = new PostWorkingCapitalLoansLoanIdRequest() // - .approvedOnDate(approvedOnDate) // - .expectedDisbursementDate(approvedOnDate) // - .dateFormat(DATE_FORMAT) // - .locale(WorkingCapitalLoanRequestFactory.DEFAULT_LOCALE); - - ok(() -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "approve", approveRequest)); - log.info("Approved working capital loan {}", loanId); - } - - @Then("Creating a working capital loan with invalid delinquencyGraceDays {int} will result with status code {int}") - public void createLoanWithInvalidGraceDays(int graceDays, int expectedStatus) { - final Long clientId = extractClientId(); - final Long productId = testContext().get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_FOR_LOAN_TEST); - - final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) - .productId(productId) // - .delinquencyGraceDays(graceDays); - - final CallFailedRuntimeException exception = fail( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); - } - - @Then("Creating a working capital loan with invalid delinquencyStartType {string} will result with status code {int}") - public void createLoanWithInvalidStartType(String startType, int expectedStatus) { - final Long clientId = extractClientId(); - final Long productId = testContext().get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_FOR_LOAN_TEST); - - final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) - .productId(productId) // - .delinquencyStartType(startType); - - final CallFailedRuntimeException exception = fail( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); - } - - @Then("Creating a working capital loan with breachId {long} on {string} will result with status code {int}") - public void createLoanWithInvalidBreachId(final long breachId, final String submittedOnDate, final int expectedStatus) { - final Long clientId = extractClientId(); - final Long loanProductId = resolveLoanProductId(DefaultWorkingCapitalLoanProduct.WCLP.name()); - - final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) - .productId(loanProductId) // - .submittedOnDate(submittedOnDate) // - .expectedDisbursementDate(submittedOnDate) // - .principalAmount(new BigDecimal("100")) // - .totalPayment(new BigDecimal("100")) // - .periodPaymentRate(new BigDecimal("1")) // - .discount(BigDecimal.ZERO) // - .breachId(breachId); - - final CallFailedRuntimeException exception = fail( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); - } - - @Then("Creating a working capital loan with breach override allowed {string} on {string} will result with status code {int}") - public void createLoanWithBreachOverrideAllowed(final String breachOverrideAllowed, final String submittedOnDate, - final int expectedStatus) { - final Long clientId = extractClientId(); - final boolean overrideAllowed = Boolean.parseBoolean(breachOverrideAllowed); - - final Long productBreachId = createBreachAndGetId(); - final Long overrideBreachId = createBreachAndGetId(); - final Long productId = createWorkingCapitalProductForBreachOverride(overrideAllowed, productBreachId); - - final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) - .productId(productId) // - .submittedOnDate(submittedOnDate) // - .expectedDisbursementDate(submittedOnDate) // - .principalAmount(new BigDecimal("100")) // - .totalPayment(new BigDecimal("100")) // - .periodPaymentRate(new BigDecimal("1")) // - .discount(null) // - .breachId(overrideBreachId); - - if (expectedStatus == 200) { - final PostWorkingCapitalLoansResponse response = ok( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(response).isNotNull(); - assertThat(response.getLoanId()).isNotNull(); - return; - } - - final CallFailedRuntimeException exception = fail( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); - } - - @Then("Creating a working capital loan with near breachId {long} on {string} will result with error") - public void createLoanWithInvalidNearBreachId(final long nearBreachId, final String submittedOnDate) { - final Long breachId = createBreachAndGetId(); - final PostWorkingCapitalLoansRequest request = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate).breachId(breachId) - .nearBreachId(nearBreachId); - - final CallFailedRuntimeException exception = fail( - () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); - assertThat(exception.getDeveloperMessage()) - .contains(String.format("Working Capital Near Breach with id %s was not found.", nearBreachId)); - assertThat(exception.getStatus()).as("HTTP status").isEqualTo(404); - } - - @Then("Admin creates working capital loan with breach override allowed with breach override and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreach(final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final Long overrideBreachId = createBreachAndGetId(); - createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, null); - } - - @Then("Admin creates working capital loan with breach override allowed with breach and near breach override and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachOverride(final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final Long overrideBreachId = createBreachAndGetId(); - final Long overrideNearBreachId = createNearBreachAndGetId(); - createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, overrideNearBreachId); - } - - @Then("Admin creates working capital loan with {int} {string} breach override and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreachOverrideData(int breachFrequency, String breachFrequencyType, - final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); - createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, overrideBreachId, null); - } - - @Then("Admin creates working capital loan with {int} {string} breach and {int} {string} near breach override and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachOverrideData(int breachFrequency, String breachFrequencyType, - int nearBreachFrequency, String nearBreachFrequencyType, final DataTable table) { - final List> data = table.asLists(); - final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); - final Long overrideNearBreachId = createNearBreachOverrideAndGetId(nearBreachFrequency, nearBreachFrequencyType); - createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); - } - - @Then("Admin creates working capital loan with breach override allowed with {int} {string} breach and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreachhData(final DataTable table, int breachFrequency, String breachFrequencyType, - int nearBreachFrequency, String nearBreachFrequencyType) { - final List> data = table.asLists(); - final Long overrideBreachId = createBreachOverrideAndGetId(breachFrequency, breachFrequencyType); - final Long overrideNearBreachId = createNearBreachOverrideAndGetId(nearBreachFrequency, nearBreachFrequencyType); - createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); - } - - @Then("Admin creates working capital loan with breach override allowed with {int} {string} breach and {int} {string} near breach and the following data:") - public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachData(int breachFrequency, String breachFrequencyType, - int nearBreachFrequency, String nearBreachFrequencyType, final DataTable table) { - final List> data = table.asLists(); - final Long overrideBreachId = createBreachAndGetId(breachFrequency, breachFrequencyType); - final Long overrideNearBreachId = createNearBreachAndGetId(nearBreachFrequency, nearBreachFrequencyType); - createWorkingCapitalLoanAccountWithBreachNearBreachData(data.get(1), overrideBreachId, overrideNearBreachId); - } - - @Then("Admin creates working capital loan with breach from WCLP while override is allowed and the following data:") - public void createLoanWithBreachFromWCLPOverrideAllowedData(DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - - final String loanProduct = loanData.get(0); - final Long loanProductId = resolveLoanProductId(loanProduct); - final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); - testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachIdFromWCLP); - - createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, breachIdFromWCLP, null); - } - - @Then("Admin creates working capital loan with breach and near breach from WCLP while override is allowed and the following data:") - public void createLoanWithBreachNearBreachFromWCLPOverrideAllowedData(DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final String loanProduct = loanData.get(0); - final Long loanProductId = resolveLoanProductId(loanProduct); - - final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); - final Long nearBreachIdFromWCLP = getNearBreachIdFromWCLP(loanProductId); - testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachIdFromWCLP); - testContext().set(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID, nearBreachIdFromWCLP); - - createWorkingCapitalLoanAccountWithBreachNearBreachData(loanData, breachIdFromWCLP, nearBreachIdFromWCLP); - } - - @Then("Admin creates working capital loan with with breach on {string} date") - public void createLoanWithBreachOverrideAllowedWithBreachData(String submittedOnDate) { - final Long breachId = createBreachAndGetId(); - - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) - .breachId(breachId); - createWorkingCapitalLoanAccount(loansRequest); - } - - @Then("Admin creates working capital loan with with breach and near breach on {string} date") - public void createLoanWithBreachOverrideAllowedWithBreachAndNearBreachData(String submittedOnDate) { - final Long breachId = createBreachAndGetId(); - final Long nearBreachId = createNearBreachAndGetId(); - - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) - .breachId(breachId).nearBreachId(nearBreachId); - createWorkingCapitalLoanAccount(loansRequest); - } - - public void checkCreateWCLoanAccountBreachData(Long breachId) { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Long loanId = loanResponse.getLoanId(); - - final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); - assert loanAccountResponse.getBreach() != null; - assertThat(loanAccountResponse.getBreach().getId()).isEqualTo(breachId); - assertThat(loanAccountResponse.getNearBreach()).isNull(); + log.info("Working Capital Loan created, loan ID: {}", response.getLoanId()); } - public void checkCreateWCLoanAccountBreachNearBreachData(Long breachId, Long nearBreachId) { + @Then("Working capital loan account has delinquencyGraceDays {int} and delinquencyStartType {string}") + public void verifyLoanGraceDays(int expectedGraceDays, String expectedStartType) { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Long loanId = loanResponse.getLoanId(); + final Long loanId = loanResponse.getLoanId(); - final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); - assert loanAccountResponse.getBreach() != null; - assert loanAccountResponse.getNearBreach() != null; - assertThat(loanAccountResponse.getBreach().getId()).isEqualTo(breachId); - assertThat(loanAccountResponse.getNearBreach().getId()).isEqualTo(nearBreachId); - } + final GetWorkingCapitalLoansLoanIdResponse response = ok( + () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); - @Then("Verify working capital loan account has been created with correct breach data") - public void checkCreateWCLoanAccountBreachData() { - final Long breachId = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID); - checkCreateWCLoanAccountBreachData(breachId); + assertThat(response.getDelinquencyGraceDays()).as("delinquencyGraceDays").isEqualTo(expectedGraceDays); + assertThat(response.getDelinquencyStartType()).as("delinquencyStartType").isNotNull(); + assertThat(response.getDelinquencyStartType().getCode()).as("delinquencyStartType code").isEqualTo(expectedStartType); } - @Then("Verify working capital loan account has been created with correct breach data inherited from WCLP level") - public void checkCreateWCLoanAccountBreachDataFromWCLP() { + @When("Admin modifies the working capital loan with grace days:") + public void modifyLoanWithGraceDays(final DataTable table) { + final Map row = table.asMaps().get(0); final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); - - GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() - .retrieveWorkingCapitalLoanById(loanId); + final Long loanId = loanResponse.getLoanId(); - final Long loanProductId = loanProductResponse.getProduct().getId(); - final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); + final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() // + .delinquencyGraceDays( + Optional.ofNullable(row.get("delinquencyGraceDays")).filter(s -> !s.isEmpty()).map(Integer::valueOf).orElse(null)) // + .delinquencyStartType(row.get("delinquencyStartType")); - checkCreateWCLoanAccountBreachData(breachIdFromWCLP); + final PutWorkingCapitalLoansLoanIdResponse response = ok( + () -> fineractClient.workingCapitalLoans().modifyWorkingCapitalLoanApplicationById(loanId, modifyRequest, "")); + testContext().set(TestContextKey.LOAN_MODIFY_RESPONSE, response); } - @Then("Verify working capital loan account has been created with correct breach override data") - public void checkCreateWCLoanAccountBreachOverrideData() { - final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID_OVERRIDE); - checkCreateWCLoanAccountBreachData(breachIdFromWCLP); - } + @When("Admin approves the working capital loan on {string}") + public void approveWorkingCapitalLoan(final String approvedOnDate) { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final Long loanId = loanResponse.getLoanId(); - @Then("Verify working capital loan account has been created with correct breach and near breach data") - public void checkCreateWCLoanAccountBreachAndNearBreachData() { - final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID); - final Long nearBreachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID); - checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); - } + final PostWorkingCapitalLoansLoanIdRequest approveRequest = new PostWorkingCapitalLoansLoanIdRequest() // + .approvedOnDate(approvedOnDate) // + .expectedDisbursementDate(approvedOnDate) // + .dateFormat(DATE_FORMAT) // + .locale(WorkingCapitalLoanRequestFactory.DEFAULT_LOCALE); - @Then("Verify working capital loan account has been created with correct breach and near breach override data") - public void checkCreateWCLoanAccountBreachAndNearBreachOverrideData() { - final Long breachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_BREACH_ID_OVERRIDE); - final Long nearBreachIdFromWCLP = testContext().get(TestContextKey.WORKING_CAPITAL_NEAR_BREACH_ID_OVERRIDE); - checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); + ok(() -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "approve", approveRequest)); + log.info("Approved working capital loan {}", loanId); } - @Then("Verify working capital loan account has been created with correct breach and near breach data inherited from WCLP level") - public void checkCreateWCLoanAccountBreachNearBreachDataFromWCLP() { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); - - GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() - .retrieveWorkingCapitalLoanById(loanId); + @Then("Creating a working capital loan with invalid delinquencyGraceDays {int} will result with status code {int}") + public void createLoanWithInvalidGraceDays(int graceDays, int expectedStatus) { + final Long clientId = extractClientId(); + final Long productId = testContext().get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_FOR_LOAN_TEST); - final Long loanProductId = loanProductResponse.getProduct().getId(); - final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); - final Long nearBreachIdFromWCLP = getNearBreachIdFromWCLP(loanProductId); + final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) + .productId(productId) // + .delinquencyGraceDays(graceDays); - checkCreateWCLoanAccountBreachNearBreachData(breachIdFromWCLP, nearBreachIdFromWCLP); + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); } - @Then("Verify working capital loan account has been created with none breach nor near breach data") - public void checkCreateWCLoanAccountNoneBreachNearBreachData() { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Long loanId = loanResponse.getLoanId(); + @Then("Creating a working capital loan with invalid delinquencyStartType {string} will result with status code {int}") + public void createLoanWithInvalidStartType(String startType, int expectedStatus) { + final Long clientId = extractClientId(); + final Long productId = testContext().get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_FOR_LOAN_TEST); - final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); - assertThat(loanAccountResponse.getBreach()).isNull(); - assertThat(loanAccountResponse.getNearBreach()).isNull(); + final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) + .productId(productId) // + .delinquencyStartType(startType); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); } - @Then("Admin failed to create working capital loan while breach override disallowed with breach override and the following data:") - public void createLoanWithBreachOverrideDisallowedWithBreachFailure(final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final Long overrideBreachId = createBreachOverrideAndGetId(); + @Then("Creating a working capital loan with breachId {long} on {string} will result with status code {int}") + public void createLoanWithInvalidBreachId(final long breachId, final String submittedOnDate, final int expectedStatus) { + final Long clientId = extractClientId(); + final Long loanProductId = resolveLoanProductId(DefaultWorkingCapitalLoanProduct.WCLP.name()); - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountWithBreachNearBreachRequest(loanData, - overrideBreachId, null); - String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) + .productId(loanProductId) // + .submittedOnDate(submittedOnDate) // + .expectedDisbursementDate(submittedOnDate) // + .principalAmount(new BigDecimal("100")) // + .totalPayment(new BigDecimal("100")) // + .periodPaymentRate(new BigDecimal("1")) // + .discount(BigDecimal.ZERO) // + .breachId(breachId); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); } - @Then("Admin failed to create working capital loan while breach override disallowed with breach override and default following data:") - public void createLoanWithBreachOverrideDisallowedWithBreachDefaultFailure(final DataTable table) { - final List loanData = table.asLists().get(1); - final String loanProduct = loanData.get(0); - final String submittedOnDate = loanData.get(1); + @Then("Creating a working capital loan with breach override allowed {string} on {string} will result with status code {int}") + public void createLoanWithBreachOverrideAllowed(final String breachOverrideAllowed, final String submittedOnDate, + final int expectedStatus) { + final Long clientId = extractClientId(); + final boolean overrideAllowed = Boolean.parseBoolean(breachOverrideAllowed); - final Long overrideBreachId = createBreachOverrideAndGetId(); - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) + final Long productBreachId = createBreachAndGetId(); + final Long overrideBreachId = createBreachAndGetId(); + final Long productId = createWorkingCapitalProductForBreachOverride(overrideAllowed, productBreachId); + + final PostWorkingCapitalLoansRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoansRequest(clientId) + .productId(productId) // + .submittedOnDate(submittedOnDate) // + .expectedDisbursementDate(submittedOnDate) // + .principalAmount(new BigDecimal("100")) // + .totalPayment(new BigDecimal("100")) // + .periodPaymentRate(new BigDecimal("1")) // + .discount(null) // .breachId(overrideBreachId); - String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); - } - @Then("Admin failed to create working capital loan while breach override disallowed with breach and near breach override and the following data:") - public void createLoanWithBreachOverrideDisallowedWithBreachAndNearBreachFailure(final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final Long overrideBreachId = createBreachOverrideAndGetId(); - final Long overrideNearBreachId = createNearBreachOverrideAndGetId(); + if (expectedStatus == 200) { + final PostWorkingCapitalLoansResponse response = ok( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(response).isNotNull(); + assertThat(response.getLoanId()).isNotNull(); + return; + } - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountWithBreachNearBreachRequest(loanData, - overrideBreachId, overrideNearBreachId); - String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + assertThat(exception.getStatus()).as("HTTP status").isEqualTo(expectedStatus); } - @Then("Admin failed to create working capital loan while breach override disallowed with breach and near breach override and default following data:") - public void createLoanWithBreachOverrideDisallowedWithBreachAndNearBreachDefaultFailure(final DataTable table) { - final List> data = table.asLists(); - final List loanData = data.get(1); - final String loanProduct = loanData.get(0); - final String submittedOnDate = loanData.get(1); - - final Long overrideBreachId = createBreachOverrideAndGetId(); - final Long overrideNearBreachId = createNearBreachOverrideAndGetId(); - - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) - .breachId(overrideBreachId).nearBreachId(overrideNearBreachId); + @Then("Admin creates working capital loan with with breach on {string} date") + public void createLoanWithBreachOverrideAllowedWithBreachData(String submittedOnDate) { + final Long breachId = createBreachAndGetId(); - String message = ErrorMessageHelper.overrideDisallowedByProductFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) + .breachId(breachId); + createWorkingCapitalLoanAccount(loansRequest); } - @Then("Admin failed to create WC loan account on {string} with breach {int} {string} frequency lower then near breach {int} {string} frequency") - public void createLoanWithBreachLowerThenNearBreachFailure(String submittedOnDate, int breachFrequency, String breachFrequencyType, - int nearBreachFrequency, String nearBreachFrequencyType) { - final Long breachId = createBreachAndGetId(breachFrequency, breachFrequencyType); - final Long nearBreachId = createNearBreachAndGetId(nearBreachFrequency, nearBreachFrequencyType); + public void checkCreateWCLoanAccountBreachData(Long breachId) { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long loanId = loanResponse.getLoanId(); - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) - .breachId(breachId).nearBreachId(nearBreachId); - String message = ErrorMessageHelper.nearBreachMustBeLowerThenBreachFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); + assert loanAccountResponse.getBreach() != null; + assertThat(loanAccountResponse.getBreach().getId()).isEqualTo(breachId); + assertThat(loanAccountResponse.getNearBreach()).isNull(); } - @Then("Admin failed to create WC loan account on {string} without breach, but with near breach") - public void createLoanWithoutBreachButWithNearBreachFailure(String submittedOnDate) { - final Long nearBreachId = createNearBreachAndGetId(); + public void checkCreateWCLoanAccountBreachNearBreachData(Long breachId, Long nearBreachId) { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + Long loanId = loanResponse.getLoanId(); - final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(submittedOnDate) - .nearBreachId(nearBreachId); - String message = ErrorMessageHelper.nearBreachCannotEnableWithoutBreachFailure(); - verifyCreateWorkingCapitalLoanAccountFailure(loansRequest, 400, message); + final GetWorkingCapitalLoansLoanIdResponse loanAccountResponse = retrieveLoanDetails(loanId); + assert loanAccountResponse.getBreach() != null; + assert loanAccountResponse.getNearBreach() != null; + assertThat(loanAccountResponse.getBreach().getId()).isEqualTo(breachId); + assertThat(loanAccountResponse.getNearBreach().getId()).isEqualTo(nearBreachId); } public PostWorkingCapitalLoansRequest createWorkingCapitalLoanAccountDefaultRequest(String submittedOnDate) { @@ -2457,4 +2560,67 @@ private String resolveExternalIdSlot(final String slotName) { return resolved; } + public void updatePeriodPaymentRateFailed(String periodPaymentRate, String errorMessage) { + final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanResponse.getLoanId(); + + PutWorkingCapitalLoansLoanIdRateRequest rateChangeRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanUpdateRateRequest().periodPaymentRate(new BigDecimal(periodPaymentRate)); + + CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateById(loanId, rateChangeRequest)); + + assertThat(exception.getStatus()).as(errorMessage).isEqualTo(400); + assertThat(exception.getDeveloperMessage()).contains(errorMessage); + } + + public void checkWorkingCapitalPeriodPaymentRate(Long loanId, String periodPaymentRate) { + final GetWorkingCapitalLoansLoanIdResponse loanDetailsResponse = retrieveLoanDetails(loanId); + assert loanDetailsResponse.getPeriodPaymentRate() != null; + assertThat(loanDetailsResponse.getPeriodPaymentRate().compareTo(new BigDecimal(periodPaymentRate))).isZero(); + } + + public void checkPeriodPaymentRateChangeHistory(List> data, + List rateChanges, List header, String resourceId) { + checkPeriodPaymentRatesTabRows(data, rateChanges, header, resourceId); + assertThat(rateChanges.size()) + .as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(resourceId, rateChanges.size(), data.size() - 1)) + .isEqualTo(data.size() - 1); + } + + public void checkPeriodPaymentRatesTabRows(List> data, List rateChanges, + List header, String resourceId) { + for (int i = 1; i < data.size(); i++) { + List expectedValues = data.get(i); + String transactionDateExpected = expectedValues.get(0); + List> actualValuesList = rateChanges.stream()// + .filter(rate -> transactionDateExpected.equals(FORMATTER.format(rate.getEffectiveDate())))// + .map(rate -> fetchValuesOfRateChangesHistory(header, rate))// + .collect(Collectors.toList());// + boolean containsExpectedValues = actualValuesList.stream()// + .anyMatch(actualValues -> actualValues.equals(expectedValues));// + assertThat(containsExpectedValues) + .as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, i, actualValuesList, expectedValues)).isTrue(); + } + } + + private List fetchValuesOfRateChangesHistory(List header, + WorkingCapitalLoanPeriodPaymentRateChangeData rateChangeData) { + List actualValues = new ArrayList<>(); + for (String headerName : header) { + switch (headerName) { + case "Effective Date" -> actualValues + .add(rateChangeData.getEffectiveDate() == null ? null : FORMATTER.format(rateChangeData.getEffectiveDate())); + case "Previous Rate" -> actualValues.add(rateChangeData.getPreviousRate() == null ? null + : new Utils.DoubleFormatter(rateChangeData.getPreviousRate().doubleValue()).format()); + case "New Rate" -> actualValues.add(rateChangeData.getNewRate() == null ? null + : new Utils.DoubleFormatter(rateChangeData.getNewRate().doubleValue()).format()); + case "Reversed" -> + actualValues.add(rateChangeData.getReversed() == null ? null : String.valueOf(rateChangeData.getReversed())); + default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName)); + } + } + return actualValues; + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 9db44f7aea0..a51acea119a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -332,6 +332,7 @@ public abstract class TestContextKey { public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_BREACH_NEAR_BREACH = "workingCapitalLoanProductCreateResponseWCLPBreachNearBreach"; public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_BREACH_DISALLOW_OVERRIDES = "workingCapitalLoanProductCreateResponseWCLPBreachDisallowOverrides"; public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_BREACH_NEAR_BREACH_DISALLOW_OVERRIDES = "workingCapitalLoanProductCreateResponseWCLPBreachNearBreachDisallowOverrides"; + public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_PERIOD_PAYMENT_RATE = "workingCapitalLoanProductCreateResponseWCLPPeriodPaymentRate"; public static final String WC_LOAN_IDS = "wcLoanIds"; public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST_FOR_UPDATE_WCLP = "workingCapitalLoanProductCreateRequestForUpdateWCLP"; public static final String DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_FOR_UPDATE_WCLP = "workingCapitalLoanProductCreateResponseForUpdateWCLP"; @@ -359,4 +360,5 @@ public abstract class TestContextKey { public static final String WORKING_CAPITAL_NEAR_BREACH_ID_FOR_UPDATE = "workingCapitalNearBreachIdForUpdate"; public static final String WORKING_CAPITAL_NEAR_BREACH_CREATE_REQUEST_FOR_UPDATE = "workingCapitalNearBreachCreateRequestForUpdate"; public static final String WC_LOAN_ACTION_TEMPLATE_RESPONSE = "wcLoanActionTemplateResponse"; + public static final String WORKING_CAPITAL_LOAN_RATE_CHANGE_ID = "wcLoanRateChangeId"; } diff --git a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java index ff112960efa..11c736d1001 100644 --- a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java +++ b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/WorkingCapitalInitializerStep.java @@ -149,6 +149,19 @@ public void initialize() throws Exception { TestContext.INSTANCE.set( TestContextKey.DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_BREACH_NEAR_BREACH_DISALLOW_OVERRIDES, responseDefaultWCPLBreachNearBreachDisallowOverrides); + + final String workingCapitalProductPeriodPaymentRateDefaultName = DefaultWorkingCapitalLoanProduct.WCLP_PERIOD_PAYMENT_RATE + .getName(); + final PostWorkingCapitalLoanProductsRequest defaultWCPLPeriodPaymentRateRequest = workingCapitalRequestFactory + .defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() // + .minPeriodPaymentRate(new BigDecimal(1)) // + .maxPeriodPaymentRate(new BigDecimal(95)) // + .periodPaymentRate(new BigDecimal(10)) // + .name(workingCapitalProductPeriodPaymentRateDefaultName);// + final PostWorkingCapitalLoanProductsResponse responseDefaultWCPLPeriodPaymentRate = createWorkingCapitalLoanProductIdempotent( + defaultWCPLPeriodPaymentRateRequest); + TestContext.INSTANCE.set(TestContextKey.DEFAULT_WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE_WCLP_PERIOD_PAYMENT_RATE, + responseDefaultWCPLPeriodPaymentRate); } private PostWorkingCapitalLoanProductsResponse createWorkingCapitalLoanProductIdempotent( diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature index c845bd18471..69721a0d354 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature @@ -216,3 +216,468 @@ Feature: WorkingCapitalAmortizationSchedule | 198 | 2019-07-18 | 198 | 50.00 | 50.00 | | 40.48 | 99.84 | 0.16 | 0.00 | | | -0.16 | 1000.00 | | 199 | 2019-07-19 | 199 | 50.00 | 50.00 | | 40.43 | 49.95 | 0.11 | 0.00 | | | -0.11 | 1000.00 | | 200 | 2019-07-20 | 200 | 50.00 | 50.00 | | 40.39 | 0.00 | 0.05 | 0.00 | | | -0.05 | 1000.00 | + + @TestRailId:C78826 + Scenario: Generate and retrieve a projected amortization schedule with 200 payments with update period payment rate in a middle of loan lifecycle - UC2 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 18 | 0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + + When Admin generates a projected amortization schedule with originationFeeAmount 1000.0, netDisbursementAmount 9000.0, totalPaymentValue 1000.0, periodPaymentRate 18, npvDayCount 360, expectedDisbursementDate "2019-01-01" + And Admin retrieves the projected amortization schedule + Then The retrieved amortization schedule has the following summary fields: + | originationFeeAmount | netDisbursementAmount | totalPaymentValue | periodPaymentRate | npvDayCount | expectedPaymentAmount | loanTerm | + | 1000.00 | 9000.00 | 1000.00 | 18 | 360 | 50.00 | 200 | + And The retrieved amortization schedule has payments with the following details: + | paymentNo | date | paymentsLeft | expectedPaymentAmount | forecastPaymentAmount | discountFactor | npvValue | balance | expectedAmortizationAmount | netAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | incomeModification | deferredBalance | + | 0 | 2019-01-01 | 0 | -9000.00 | | 1 | -9000.00 | 9000.00 | | | | | | 1000.00 | + | 1 | 2019-01-02 | 1 | 50.00 | 50.00 | | 49.95 | 8959.61 | 9.61 | 0.00 | | | -9.61 | 1000.00 | + | 2 | 2019-01-03 | 2 | 50.00 | 50.00 | | 49.89 | 8919.18 | 9.57 | 0.00 | | | -9.57 | 1000.00 | + | 3 | 2019-01-04 | 3 | 50.00 | 50.00 | | 49.84 | 8878.70 | 9.52 | 0.00 | | | -9.52 | 1000.00 | + | 4 | 2019-01-05 | 4 | 50.00 | 50.00 | | 49.79 | 8838.18 | 9.48 | 0.00 | | | -9.48 | 1000.00 | + | 5 | 2019-01-06 | 5 | 50.00 | 50.00 | | 49.73 | 8797.62 | 9.44 | 0.00 | | | -9.44 | 1000.00 | + | 6 | 2019-01-07 | 6 | 50.00 | 50.00 | | 49.68 | 8757.01 | 9.39 | 0.00 | | | -9.39 | 1000.00 | + | 7 | 2019-01-08 | 7 | 50.00 | 50.00 | | 49.63 | 8716.36 | 9.35 | 0.00 | | | -9.35 | 1000.00 | + | 8 | 2019-01-09 | 8 | 50.00 | 50.00 | | 49.57 | 8675.67 | 9.31 | 0.00 | | | -9.31 | 1000.00 | + | 9 | 2019-01-10 | 9 | 50.00 | 50.00 | | 49.52 | 8634.94 | 9.26 | 0.00 | | | -9.26 | 1000.00 | + | 10 | 2019-01-11 | 10 | 50.00 | 50.00 | | 49.47 | 8594.16 | 9.22 | 0.00 | | | -9.22 | 1000.00 | + | 11 | 2019-01-12 | 11 | 50.00 | 50.00 | | 49.42 | 8553.33 | 9.18 | 0.00 | | | -9.18 | 1000.00 | + | 12 | 2019-01-13 | 12 | 50.00 | 50.00 | | 49.36 | 8512.47 | 9.13 | 0.00 | | | -9.13 | 1000.00 | + | 13 | 2019-01-14 | 13 | 50.00 | 50.00 | | 49.31 | 8471.56 | 9.09 | 0.00 | | | -9.09 | 1000.00 | + | 14 | 2019-01-15 | 14 | 50.00 | 50.00 | | 49.26 | 8430.60 | 9.05 | 0.00 | | | -9.05 | 1000.00 | + | 15 | 2019-01-16 | 15 | 50.00 | 50.00 | | 49.21 | 8389.61 | 9.00 | 0.00 | | | -9.00 | 1000.00 | + | 16 | 2019-01-17 | 16 | 50.00 | 50.00 | | 49.15 | 8348.56 | 8.96 | 0.00 | | | -8.96 | 1000.00 | + | 17 | 2019-01-18 | 17 | 50.00 | 50.00 | | 49.10 | 8307.48 | 8.91 | 0.00 | | | -8.91 | 1000.00 | + | 18 | 2019-01-19 | 18 | 50.00 | 50.00 | | 49.05 | 8266.35 | 8.87 | 0.00 | | | -8.87 | 1000.00 | + | 19 | 2019-01-20 | 19 | 50.00 | 50.00 | | 49.00 | 8225.18 | 8.83 | 0.00 | | | -8.83 | 1000.00 | + | 20 | 2019-01-21 | 20 | 50.00 | 50.00 | | 48.94 | 8183.96 | 8.78 | 0.00 | | | -8.78 | 1000.00 | + | 21 | 2019-01-22 | 21 | 50.00 | 50.00 | | 48.89 | 8142.70 | 8.74 | 0.00 | | | -8.74 | 1000.00 | + | 22 | 2019-01-23 | 22 | 50.00 | 50.00 | | 48.84 | 8101.39 | 8.69 | 0.00 | | | -8.69 | 1000.00 | + | 23 | 2019-01-24 | 23 | 50.00 | 50.00 | | 48.79 | 8060.04 | 8.65 | 0.00 | | | -8.65 | 1000.00 | + | 24 | 2019-01-25 | 24 | 50.00 | 50.00 | | 48.74 | 8018.65 | 8.61 | 0.00 | | | -8.61 | 1000.00 | + | 25 | 2019-01-26 | 25 | 50.00 | 50.00 | | 48.68 | 7977.21 | 8.56 | 0.00 | | | -8.56 | 1000.00 | + | 26 | 2019-01-27 | 26 | 50.00 | 50.00 | | 48.63 | 7935.73 | 8.52 | 0.00 | | | -8.52 | 1000.00 | + | 27 | 2019-01-28 | 27 | 50.00 | 50.00 | | 48.58 | 7894.21 | 8.47 | 0.00 | | | -8.47 | 1000.00 | + | 28 | 2019-01-29 | 28 | 50.00 | 50.00 | | 48.53 | 7852.63 | 8.43 | 0.00 | | | -8.43 | 1000.00 | + | 29 | 2019-01-30 | 29 | 50.00 | 50.00 | | 48.48 | 7811.02 | 8.39 | 0.00 | | | -8.39 | 1000.00 | + | 30 | 2019-01-31 | 30 | 50.00 | 50.00 | | 48.42 | 7769.36 | 8.34 | 0.00 | | | -8.34 | 1000.00 | + | 31 | 2019-02-01 | 31 | 50.00 | 50.00 | | 48.37 | 7727.66 | 8.30 | 0.00 | | | -8.30 | 1000.00 | + | 32 | 2019-02-02 | 32 | 50.00 | 50.00 | | 48.32 | 7685.91 | 8.25 | 0.00 | | | -8.25 | 1000.00 | + | 33 | 2019-02-03 | 33 | 50.00 | 50.00 | | 48.27 | 7644.12 | 8.21 | 0.00 | | | -8.21 | 1000.00 | + | 34 | 2019-02-04 | 34 | 50.00 | 50.00 | | 48.22 | 7602.28 | 8.16 | 0.00 | | | -8.16 | 1000.00 | + | 35 | 2019-02-05 | 35 | 50.00 | 50.00 | | 48.17 | 7560.40 | 8.12 | 0.00 | | | -8.12 | 1000.00 | + | 36 | 2019-02-06 | 36 | 50.00 | 50.00 | | 48.12 | 7518.47 | 8.07 | 0.00 | | | -8.07 | 1000.00 | + | 37 | 2019-02-07 | 37 | 50.00 | 50.00 | | 48.06 | 7476.50 | 8.03 | 0.00 | | | -8.03 | 1000.00 | + | 38 | 2019-02-08 | 38 | 50.00 | 50.00 | | 48.01 | 7434.48 | 7.98 | 0.00 | | | -7.98 | 1000.00 | + | 39 | 2019-02-09 | 39 | 50.00 | 50.00 | | 47.96 | 7392.42 | 7.94 | 0.00 | | | -7.94 | 1000.00 | + | 40 | 2019-02-10 | 40 | 50.00 | 50.00 | | 47.91 | 7350.31 | 7.89 | 0.00 | | | -7.89 | 1000.00 | + | 41 | 2019-02-11 | 41 | 50.00 | 50.00 | | 47.86 | 7308.16 | 7.85 | 0.00 | | | -7.85 | 1000.00 | + | 42 | 2019-02-12 | 42 | 50.00 | 50.00 | | 47.81 | 7265.97 | 7.80 | 0.00 | | | -7.80 | 1000.00 | + | 43 | 2019-02-13 | 43 | 50.00 | 50.00 | | 47.76 | 7223.72 | 7.76 | 0.00 | | | -7.76 | 1000.00 | + | 44 | 2019-02-14 | 44 | 50.00 | 50.00 | | 47.71 | 7181.44 | 7.71 | 0.00 | | | -7.71 | 1000.00 | + | 45 | 2019-02-15 | 45 | 50.00 | 50.00 | | 47.66 | 7139.11 | 7.67 | 0.00 | | | -7.67 | 1000.00 | + | 46 | 2019-02-16 | 46 | 50.00 | 50.00 | | 47.60 | 7096.73 | 7.62 | 0.00 | | | -7.62 | 1000.00 | + | 47 | 2019-02-17 | 47 | 50.00 | 50.00 | | 47.55 | 7054.31 | 7.58 | 0.00 | | | -7.58 | 1000.00 | + | 48 | 2019-02-18 | 48 | 50.00 | 50.00 | | 47.50 | 7011.84 | 7.53 | 0.00 | | | -7.53 | 1000.00 | + | 49 | 2019-02-19 | 49 | 50.00 | 50.00 | | 47.45 | 6969.33 | 7.49 | 0.00 | | | -7.49 | 1000.00 | + | 50 | 2019-02-20 | 50 | 50.00 | 50.00 | | 47.40 | 6926.77 | 7.44 | 0.00 | | | -7.44 | 1000.00 | + | 51 | 2019-02-21 | 51 | 50.00 | 50.00 | | 47.35 | 6884.17 | 7.40 | 0.00 | | | -7.40 | 1000.00 | + | 52 | 2019-02-22 | 52 | 50.00 | 50.00 | | 47.30 | 6841.52 | 7.35 | 0.00 | | | -7.35 | 1000.00 | + | 53 | 2019-02-23 | 53 | 50.00 | 50.00 | | 47.25 | 6798.82 | 7.31 | 0.00 | | | -7.31 | 1000.00 | + | 54 | 2019-02-24 | 54 | 50.00 | 50.00 | | 47.20 | 6756.08 | 7.26 | 0.00 | | | -7.26 | 1000.00 | + | 55 | 2019-02-25 | 55 | 50.00 | 50.00 | | 47.15 | 6713.30 | 7.21 | 0.00 | | | -7.21 | 1000.00 | + | 56 | 2019-02-26 | 56 | 50.00 | 50.00 | | 47.10 | 6670.47 | 7.17 | 0.00 | | | -7.17 | 1000.00 | + | 57 | 2019-02-27 | 57 | 50.00 | 50.00 | | 47.05 | 6627.59 | 7.12 | 0.00 | | | -7.12 | 1000.00 | + | 58 | 2019-02-28 | 58 | 50.00 | 50.00 | | 47.00 | 6584.67 | 7.08 | 0.00 | | | -7.08 | 1000.00 | + | 59 | 2019-03-01 | 59 | 50.00 | 50.00 | | 46.95 | 6541.70 | 7.03 | 0.00 | | | -7.03 | 1000.00 | + | 60 | 2019-03-02 | 60 | 50.00 | 50.00 | | 46.90 | 6498.68 | 6.99 | 0.00 | | | -6.99 | 1000.00 | + | 61 | 2019-03-03 | 61 | 50.00 | 50.00 | | 46.85 | 6455.62 | 6.94 | 0.00 | | | -6.94 | 1000.00 | + | 62 | 2019-03-04 | 62 | 50.00 | 50.00 | | 46.80 | 6412.51 | 6.89 | 0.00 | | | -6.89 | 1000.00 | + | 63 | 2019-03-05 | 63 | 50.00 | 50.00 | | 46.75 | 6369.36 | 6.85 | 0.00 | | | -6.85 | 1000.00 | + | 64 | 2019-03-06 | 64 | 50.00 | 50.00 | | 46.70 | 6326.16 | 6.80 | 0.00 | | | -6.80 | 1000.00 | + | 65 | 2019-03-07 | 65 | 50.00 | 50.00 | | 46.65 | 6282.92 | 6.76 | 0.00 | | | -6.76 | 1000.00 | + | 66 | 2019-03-08 | 66 | 50.00 | 50.00 | | 46.60 | 6239.63 | 6.71 | 0.00 | | | -6.71 | 1000.00 | + | 67 | 2019-03-09 | 67 | 50.00 | 50.00 | | 46.55 | 6196.29 | 6.66 | 0.00 | | | -6.66 | 1000.00 | + | 68 | 2019-03-10 | 68 | 50.00 | 50.00 | | 46.50 | 6152.91 | 6.62 | 0.00 | | | -6.62 | 1000.00 | + | 69 | 2019-03-11 | 69 | 50.00 | 50.00 | | 46.45 | 6109.48 | 6.57 | 0.00 | | | -6.57 | 1000.00 | + | 70 | 2019-03-12 | 70 | 50.00 | 50.00 | | 46.40 | 6066.00 | 6.52 | 0.00 | | | -6.52 | 1000.00 | + | 71 | 2019-03-13 | 71 | 50.00 | 50.00 | | 46.35 | 6022.48 | 6.48 | 0.00 | | | -6.48 | 1000.00 | + | 72 | 2019-03-14 | 72 | 50.00 | 50.00 | | 46.30 | 5978.91 | 6.43 | 0.00 | | | -6.43 | 1000.00 | + | 73 | 2019-03-15 | 73 | 50.00 | 50.00 | | 46.25 | 5935.29 | 6.38 | 0.00 | | | -6.38 | 1000.00 | + | 74 | 2019-03-16 | 74 | 50.00 | 50.00 | | 46.20 | 5891.63 | 6.34 | 0.00 | | | -6.34 | 1000.00 | + | 75 | 2019-03-17 | 75 | 50.00 | 50.00 | | 46.15 | 5847.92 | 6.29 | 0.00 | | | -6.29 | 1000.00 | + | 76 | 2019-03-18 | 76 | 50.00 | 50.00 | | 46.10 | 5804.17 | 6.24 | 0.00 | | | -6.24 | 1000.00 | + | 77 | 2019-03-19 | 77 | 50.00 | 50.00 | | 46.06 | 5760.36 | 6.20 | 0.00 | | | -6.20 | 1000.00 | + | 78 | 2019-03-20 | 78 | 50.00 | 50.00 | | 46.01 | 5716.52 | 6.15 | 0.00 | | | -6.15 | 1000.00 | + | 79 | 2019-03-21 | 79 | 50.00 | 50.00 | | 45.96 | 5672.62 | 6.10 | 0.00 | | | -6.10 | 1000.00 | + | 80 | 2019-03-22 | 80 | 50.00 | 50.00 | | 45.91 | 5628.68 | 6.06 | 0.00 | | | -6.06 | 1000.00 | + | 81 | 2019-03-23 | 81 | 50.00 | 50.00 | | 45.86 | 5584.69 | 6.01 | 0.00 | | | -6.01 | 1000.00 | + | 82 | 2019-03-24 | 82 | 50.00 | 50.00 | | 45.81 | 5540.65 | 5.96 | 0.00 | | | -5.96 | 1000.00 | + | 83 | 2019-03-25 | 83 | 50.00 | 50.00 | | 45.76 | 5496.57 | 5.92 | 0.00 | | | -5.92 | 1000.00 | + | 84 | 2019-03-26 | 84 | 50.00 | 50.00 | | 45.71 | 5452.44 | 5.87 | 0.00 | | | -5.87 | 1000.00 | + | 85 | 2019-03-27 | 85 | 50.00 | 50.00 | | 45.66 | 5408.26 | 5.82 | 0.00 | | | -5.82 | 1000.00 | + | 86 | 2019-03-28 | 86 | 50.00 | 50.00 | | 45.62 | 5364.03 | 5.78 | 0.00 | | | -5.78 | 1000.00 | + | 87 | 2019-03-29 | 87 | 50.00 | 50.00 | | 45.57 | 5319.76 | 5.73 | 0.00 | | | -5.73 | 1000.00 | + | 88 | 2019-03-30 | 88 | 50.00 | 50.00 | | 45.52 | 5275.44 | 5.68 | 0.00 | | | -5.68 | 1000.00 | + | 89 | 2019-03-31 | 89 | 50.00 | 50.00 | | 45.47 | 5231.08 | 5.63 | 0.00 | | | -5.63 | 1000.00 | + | 90 | 2019-04-01 | 90 | 50.00 | 50.00 | | 45.42 | 5186.66 | 5.59 | 0.00 | | | -5.59 | 1000.00 | + | 91 | 2019-04-02 | 91 | 50.00 | 50.00 | | 45.37 | 5142.20 | 5.54 | 0.00 | | | -5.54 | 1000.00 | + | 92 | 2019-04-03 | 92 | 50.00 | 50.00 | | 45.32 | 5097.69 | 5.49 | 0.00 | | | -5.49 | 1000.00 | + | 93 | 2019-04-04 | 93 | 50.00 | 50.00 | | 45.28 | 5053.13 | 5.44 | 0.00 | | | -5.44 | 1000.00 | + | 94 | 2019-04-05 | 94 | 50.00 | 50.00 | | 45.23 | 5008.53 | 5.40 | 0.00 | | | -5.40 | 1000.00 | + | 95 | 2019-04-06 | 95 | 50.00 | 50.00 | | 45.18 | 4963.88 | 5.35 | 0.00 | | | -5.35 | 1000.00 | + | 96 | 2019-04-07 | 96 | 50.00 | 50.00 | | 45.13 | 4919.18 | 5.30 | 0.00 | | | -5.30 | 1000.00 | + | 97 | 2019-04-08 | 97 | 50.00 | 50.00 | | 45.08 | 4874.43 | 5.25 | 0.00 | | | -5.25 | 1000.00 | + | 98 | 2019-04-09 | 98 | 50.00 | 50.00 | | 45.03 | 4829.64 | 5.20 | 0.00 | | | -5.20 | 1000.00 | + | 99 | 2019-04-10 | 99 | 50.00 | 50.00 | | 44.99 | 4784.79 | 5.16 | 0.00 | | | -5.16 | 1000.00 | + | 100 | 2019-04-11 | 100 | 50.00 | 50.00 | | 44.94 | 4739.90 | 5.11 | 0.00 | | | -5.11 | 1000.00 | + | 101 | 2019-04-12 | 101 | 50.00 | 50.00 | | 44.89 | 4694.96 | 5.06 | 0.00 | | | -5.06 | 1000.00 | + | 102 | 2019-04-13 | 102 | 50.00 | 50.00 | | 44.84 | 4649.98 | 5.01 | 0.00 | | | -5.01 | 1000.00 | + | 103 | 2019-04-14 | 103 | 50.00 | 50.00 | | 44.80 | 4604.94 | 4.97 | 0.00 | | | -4.97 | 1000.00 | + | 104 | 2019-04-15 | 104 | 50.00 | 50.00 | | 44.75 | 4559.86 | 4.92 | 0.00 | | | -4.92 | 1000.00 | + | 105 | 2019-04-16 | 105 | 50.00 | 50.00 | | 44.70 | 4514.73 | 4.87 | 0.00 | | | -4.87 | 1000.00 | + | 106 | 2019-04-17 | 106 | 50.00 | 50.00 | | 44.65 | 4469.55 | 4.82 | 0.00 | | | -4.82 | 1000.00 | + | 107 | 2019-04-18 | 107 | 50.00 | 50.00 | | 44.60 | 4424.32 | 4.77 | 0.00 | | | -4.77 | 1000.00 | + | 108 | 2019-04-19 | 108 | 50.00 | 50.00 | | 44.56 | 4379.05 | 4.72 | 0.00 | | | -4.72 | 1000.00 | + | 109 | 2019-04-20 | 109 | 50.00 | 50.00 | | 44.51 | 4333.72 | 4.68 | 0.00 | | | -4.68 | 1000.00 | + | 110 | 2019-04-21 | 110 | 50.00 | 50.00 | | 44.46 | 4288.35 | 4.63 | 0.00 | | | -4.63 | 1000.00 | + | 111 | 2019-04-22 | 111 | 50.00 | 50.00 | | 44.41 | 4242.93 | 4.58 | 0.00 | | | -4.58 | 1000.00 | + | 112 | 2019-04-23 | 112 | 50.00 | 50.00 | | 44.37 | 4197.46 | 4.53 | 0.00 | | | -4.53 | 1000.00 | + | 113 | 2019-04-24 | 113 | 50.00 | 50.00 | | 44.32 | 4151.94 | 4.48 | 0.00 | | | -4.48 | 1000.00 | + | 114 | 2019-04-25 | 114 | 50.00 | 50.00 | | 44.27 | 4106.38 | 4.43 | 0.00 | | | -4.43 | 1000.00 | + | 115 | 2019-04-26 | 115 | 50.00 | 50.00 | | 44.22 | 4060.76 | 4.38 | 0.00 | | | -4.38 | 1000.00 | + | 116 | 2019-04-27 | 116 | 50.00 | 50.00 | | 44.18 | 4015.10 | 4.34 | 0.00 | | | -4.34 | 1000.00 | + | 117 | 2019-04-28 | 117 | 50.00 | 50.00 | | 44.13 | 3969.38 | 4.29 | 0.00 | | | -4.29 | 1000.00 | + | 118 | 2019-04-29 | 118 | 50.00 | 50.00 | | 44.08 | 3923.62 | 4.24 | 0.00 | | | -4.24 | 1000.00 | + | 119 | 2019-04-30 | 119 | 50.00 | 50.00 | | 44.04 | 3877.81 | 4.19 | 0.00 | | | -4.19 | 1000.00 | + | 120 | 2019-05-01 | 120 | 50.00 | 50.00 | | 43.99 | 3831.95 | 4.14 | 0.00 | | | -4.14 | 1000.00 | + | 121 | 2019-05-02 | 121 | 50.00 | 50.00 | | 43.94 | 3786.04 | 4.09 | 0.00 | | | -4.09 | 1000.00 | + | 122 | 2019-05-03 | 122 | 50.00 | 50.00 | | 43.90 | 3740.09 | 4.04 | 0.00 | | | -4.04 | 1000.00 | + | 123 | 2019-05-04 | 123 | 50.00 | 50.00 | | 43.85 | 3694.08 | 3.99 | 0.00 | | | -3.99 | 1000.00 | + | 124 | 2019-05-05 | 124 | 50.00 | 50.00 | | 43.80 | 3648.03 | 3.94 | 0.00 | | | -3.94 | 1000.00 | + | 125 | 2019-05-06 | 125 | 50.00 | 50.00 | | 43.76 | 3601.92 | 3.90 | 0.00 | | | -3.90 | 1000.00 | + | 126 | 2019-05-07 | 126 | 50.00 | 50.00 | | 43.71 | 3555.77 | 3.85 | 0.00 | | | -3.85 | 1000.00 | + | 127 | 2019-05-08 | 127 | 50.00 | 50.00 | | 43.66 | 3509.56 | 3.80 | 0.00 | | | -3.80 | 1000.00 | + | 128 | 2019-05-09 | 128 | 50.00 | 50.00 | | 43.62 | 3463.31 | 3.75 | 0.00 | | | -3.75 | 1000.00 | + | 129 | 2019-05-10 | 129 | 50.00 | 50.00 | | 43.57 | 3417.01 | 3.70 | 0.00 | | | -3.70 | 1000.00 | + | 130 | 2019-05-11 | 130 | 50.00 | 50.00 | | 43.52 | 3370.66 | 3.65 | 0.00 | | | -3.65 | 1000.00 | + | 131 | 2019-05-12 | 131 | 50.00 | 50.00 | | 43.48 | 3324.26 | 3.60 | 0.00 | | | -3.60 | 1000.00 | + | 132 | 2019-05-13 | 132 | 50.00 | 50.00 | | 43.43 | 3277.81 | 3.55 | 0.00 | | | -3.55 | 1000.00 | + | 133 | 2019-05-14 | 133 | 50.00 | 50.00 | | 43.38 | 3231.31 | 3.50 | 0.00 | | | -3.50 | 1000.00 | + | 134 | 2019-05-15 | 134 | 50.00 | 50.00 | | 43.34 | 3184.76 | 3.45 | 0.00 | | | -3.45 | 1000.00 | + | 135 | 2019-05-16 | 135 | 50.00 | 50.00 | | 43.29 | 3138.16 | 3.40 | 0.00 | | | -3.40 | 1000.00 | + | 136 | 2019-05-17 | 136 | 50.00 | 50.00 | | 43.24 | 3091.51 | 3.35 | 0.00 | | | -3.35 | 1000.00 | + | 137 | 2019-05-18 | 137 | 50.00 | 50.00 | | 43.20 | 3044.81 | 3.30 | 0.00 | | | -3.30 | 1000.00 | + | 138 | 2019-05-19 | 138 | 50.00 | 50.00 | | 43.15 | 2998.06 | 3.25 | 0.00 | | | -3.25 | 1000.00 | + | 139 | 2019-05-20 | 139 | 50.00 | 50.00 | | 43.11 | 2951.26 | 3.20 | 0.00 | | | -3.20 | 1000.00 | + | 140 | 2019-05-21 | 140 | 50.00 | 50.00 | | 43.06 | 2904.42 | 3.15 | 0.00 | | | -3.15 | 1000.00 | + | 141 | 2019-05-22 | 141 | 50.00 | 50.00 | | 43.01 | 2857.52 | 3.10 | 0.00 | | | -3.10 | 1000.00 | + | 142 | 2019-05-23 | 142 | 50.00 | 50.00 | | 42.97 | 2810.57 | 3.05 | 0.00 | | | -3.05 | 1000.00 | + | 143 | 2019-05-24 | 143 | 50.00 | 50.00 | | 42.92 | 2763.57 | 3.00 | 0.00 | | | -3.00 | 1000.00 | + | 144 | 2019-05-25 | 144 | 50.00 | 50.00 | | 42.88 | 2716.52 | 2.95 | 0.00 | | | -2.95 | 1000.00 | + | 145 | 2019-05-26 | 145 | 50.00 | 50.00 | | 42.83 | 2669.42 | 2.90 | 0.00 | | | -2.90 | 1000.00 | + | 146 | 2019-05-27 | 146 | 50.00 | 50.00 | | 42.79 | 2622.27 | 2.85 | 0.00 | | | -2.85 | 1000.00 | + | 147 | 2019-05-28 | 147 | 50.00 | 50.00 | | 42.74 | 2575.07 | 2.80 | 0.00 | | | -2.80 | 1000.00 | + | 148 | 2019-05-29 | 148 | 50.00 | 50.00 | | 42.69 | 2527.82 | 2.75 | 0.00 | | | -2.75 | 1000.00 | + | 149 | 2019-05-30 | 149 | 50.00 | 50.00 | | 42.65 | 2480.52 | 2.70 | 0.00 | | | -2.70 | 1000.00 | + | 150 | 2019-05-31 | 150 | 50.00 | 50.00 | | 42.60 | 2433.17 | 2.65 | 0.00 | | | -2.65 | 1000.00 | + | 151 | 2019-06-01 | 151 | 50.00 | 50.00 | | 42.56 | 2385.77 | 2.60 | 0.00 | | | -2.60 | 1000.00 | + | 152 | 2019-06-02 | 152 | 50.00 | 50.00 | | 42.51 | 2338.31 | 2.55 | 0.00 | | | -2.55 | 1000.00 | + | 153 | 2019-06-03 | 153 | 50.00 | 50.00 | | 42.47 | 2290.81 | 2.50 | 0.00 | | | -2.50 | 1000.00 | + | 154 | 2019-06-04 | 154 | 50.00 | 50.00 | | 42.42 | 2243.26 | 2.45 | 0.00 | | | -2.45 | 1000.00 | + | 155 | 2019-06-05 | 155 | 50.00 | 50.00 | | 42.38 | 2195.65 | 2.40 | 0.00 | | | -2.40 | 1000.00 | + | 156 | 2019-06-06 | 156 | 50.00 | 50.00 | | 42.33 | 2148.00 | 2.34 | 0.00 | | | -2.34 | 1000.00 | + | 157 | 2019-06-07 | 157 | 50.00 | 50.00 | | 42.29 | 2100.29 | 2.29 | 0.00 | | | -2.29 | 1000.00 | + | 158 | 2019-06-08 | 158 | 50.00 | 50.00 | | 42.24 | 2052.53 | 2.24 | 0.00 | | | -2.24 | 1000.00 | + | 159 | 2019-06-09 | 159 | 50.00 | 50.00 | | 42.20 | 2004.73 | 2.19 | 0.00 | | | -2.19 | 1000.00 | + | 160 | 2019-06-10 | 160 | 50.00 | 50.00 | | 42.15 | 1956.87 | 2.14 | 0.00 | | | -2.14 | 1000.00 | + | 161 | 2019-06-11 | 161 | 50.00 | 50.00 | | 42.11 | 1908.96 | 2.09 | 0.00 | | | -2.09 | 1000.00 | + | 162 | 2019-06-12 | 162 | 50.00 | 50.00 | | 42.06 | 1860.99 | 2.04 | 0.00 | | | -2.04 | 1000.00 | + | 163 | 2019-06-13 | 163 | 50.00 | 50.00 | | 42.02 | 1812.98 | 1.99 | 0.00 | | | -1.99 | 1000.00 | + | 164 | 2019-06-14 | 164 | 50.00 | 50.00 | | 41.97 | 1764.92 | 1.94 | 0.00 | | | -1.94 | 1000.00 | + | 165 | 2019-06-15 | 165 | 50.00 | 50.00 | | 41.93 | 1716.80 | 1.88 | 0.00 | | | -1.88 | 1000.00 | + | 166 | 2019-06-16 | 166 | 50.00 | 50.00 | | 41.88 | 1668.64 | 1.83 | 0.00 | | | -1.83 | 1000.00 | + | 167 | 2019-06-17 | 167 | 50.00 | 50.00 | | 41.84 | 1620.42 | 1.78 | 0.00 | | | -1.78 | 1000.00 | + | 168 | 2019-06-18 | 168 | 50.00 | 50.00 | | 41.79 | 1572.15 | 1.73 | 0.00 | | | -1.73 | 1000.00 | + | 169 | 2019-06-19 | 169 | 50.00 | 50.00 | | 41.75 | 1523.83 | 1.68 | 0.00 | | | -1.68 | 1000.00 | + | 170 | 2019-06-20 | 170 | 50.00 | 50.00 | | 41.70 | 1475.45 | 1.63 | 0.00 | | | -1.63 | 1000.00 | + | 171 | 2019-06-21 | 171 | 50.00 | 50.00 | | 41.66 | 1427.03 | 1.58 | 0.00 | | | -1.58 | 1000.00 | + | 172 | 2019-06-22 | 172 | 50.00 | 50.00 | | 41.61 | 1378.55 | 1.52 | 0.00 | | | -1.52 | 1000.00 | + | 173 | 2019-06-23 | 173 | 50.00 | 50.00 | | 41.57 | 1330.02 | 1.47 | 0.00 | | | -1.47 | 1000.00 | + | 174 | 2019-06-24 | 174 | 50.00 | 50.00 | | 41.53 | 1281.45 | 1.42 | 0.00 | | | -1.42 | 1000.00 | + | 175 | 2019-06-25 | 175 | 50.00 | 50.00 | | 41.48 | 1232.81 | 1.37 | 0.00 | | | -1.37 | 1000.00 | + | 176 | 2019-06-26 | 176 | 50.00 | 50.00 | | 41.44 | 1184.13 | 1.32 | 0.00 | | | -1.32 | 1000.00 | + | 177 | 2019-06-27 | 177 | 50.00 | 50.00 | | 41.39 | 1135.39 | 1.26 | 0.00 | | | -1.26 | 1000.00 | + | 178 | 2019-06-28 | 178 | 50.00 | 50.00 | | 41.35 | 1086.61 | 1.21 | 0.00 | | | -1.21 | 1000.00 | + | 179 | 2019-06-29 | 179 | 50.00 | 50.00 | | 41.31 | 1037.77 | 1.16 | 0.00 | | | -1.16 | 1000.00 | + | 180 | 2019-06-30 | 180 | 50.00 | 50.00 | | 41.26 | 988.88 | 1.11 | 0.00 | | | -1.11 | 1000.00 | + | 181 | 2019-07-01 | 181 | 50.00 | 50.00 | | 41.22 | 939.93 | 1.06 | 0.00 | | | -1.06 | 1000.00 | + | 182 | 2019-07-02 | 182 | 50.00 | 50.00 | | 41.17 | 890.93 | 1.00 | 0.00 | | | -1.00 | 1000.00 | + | 183 | 2019-07-03 | 183 | 50.00 | 50.00 | | 41.13 | 841.89 | 0.95 | 0.00 | | | -0.95 | 1000.00 | + | 184 | 2019-07-04 | 184 | 50.00 | 50.00 | | 41.09 | 792.79 | 0.90 | 0.00 | | | -0.90 | 1000.00 | + | 185 | 2019-07-05 | 185 | 50.00 | 50.00 | | 41.04 | 743.63 | 0.85 | 0.00 | | | -0.85 | 1000.00 | + | 186 | 2019-07-06 | 186 | 50.00 | 50.00 | | 41.00 | 694.43 | 0.79 | 0.00 | | | -0.79 | 1000.00 | + | 187 | 2019-07-07 | 187 | 50.00 | 50.00 | | 40.95 | 645.17 | 0.74 | 0.00 | | | -0.74 | 1000.00 | + | 188 | 2019-07-08 | 188 | 50.00 | 50.00 | | 40.91 | 595.86 | 0.69 | 0.00 | | | -0.69 | 1000.00 | + | 189 | 2019-07-09 | 189 | 50.00 | 50.00 | | 40.87 | 546.49 | 0.64 | 0.00 | | | -0.64 | 1000.00 | + | 190 | 2019-07-10 | 190 | 50.00 | 50.00 | | 40.82 | 497.08 | 0.58 | 0.00 | | | -0.58 | 1000.00 | + | 191 | 2019-07-11 | 191 | 50.00 | 50.00 | | 40.78 | 447.61 | 0.53 | 0.00 | | | -0.53 | 1000.00 | + | 192 | 2019-07-12 | 192 | 50.00 | 50.00 | | 40.74 | 398.08 | 0.48 | 0.00 | | | -0.48 | 1000.00 | + | 193 | 2019-07-13 | 193 | 50.00 | 50.00 | | 40.69 | 348.51 | 0.43 | 0.00 | | | -0.43 | 1000.00 | + | 194 | 2019-07-14 | 194 | 50.00 | 50.00 | | 40.65 | 298.88 | 0.37 | 0.00 | | | -0.37 | 1000.00 | + | 195 | 2019-07-15 | 195 | 50.00 | 50.00 | | 40.61 | 249.20 | 0.32 | 0.00 | | | -0.32 | 1000.00 | + | 196 | 2019-07-16 | 196 | 50.00 | 50.00 | | 40.56 | 199.47 | 0.27 | 0.00 | | | -0.27 | 1000.00 | + | 197 | 2019-07-17 | 197 | 50.00 | 50.00 | | 40.52 | 149.68 | 0.21 | 0.00 | | | -0.21 | 1000.00 | + | 198 | 2019-07-18 | 198 | 50.00 | 50.00 | | 40.48 | 99.84 | 0.16 | 0.00 | | | -0.16 | 1000.00 | + | 199 | 2019-07-19 | 199 | 50.00 | 50.00 | | 40.43 | 49.95 | 0.11 | 0.00 | | | -0.11 | 1000.00 | + | 200 | 2019-07-20 | 200 | 50.00 | 50.00 | | 40.39 | 0.00 | 0.05 | 0.00 | | | -0.05 | 1000.00 | + + When Admin sets the business date to "25 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "17" value + And Admin retrieves the projected amortization schedule + Then The retrieved amortization schedule has the following summary fields: + | originationFeeAmount | netDisbursementAmount | totalPaymentValue | periodPaymentRate | npvDayCount | expectedPaymentAmount | loanTerm | + | 1000.00 | 9000.00 | 1000.00 | 18 | 360 | 50.00 | 200 | + And The retrieved amortization schedule has payments with the following details: + | paymentNo | date | paymentsLeft | expectedPaymentAmount | forecastPaymentAmount | discountFactor | npvValue | balance | expectedAmortizationAmount | netAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | incomeModification | deferredBalance | + | 0 | 2019-01-01 | 0 | -9000.00 | | | -9000.00 | 9000.00 | | | | | | 1000.00 | + | 1 | 2019-01-02 | 1 | 50.00 | 50.00 | | 49.95 | 8959.61 | 9.61 | 0.00 | | | -9.61 | 1000.00 | + | 2 | 2019-01-03 | 2 | 50.00 | 50.00 | | 49.89 | 8919.18 | 9.57 | 0.00 | | | -9.57 | 1000.00 | + | 3 | 2019-01-04 | 3 | 50.00 | 50.00 | | 49.84 | 8878.70 | 9.52 | 0.00 | | | -9.52 | 1000.00 | + | 4 | 2019-01-05 | 4 | 50.00 | 50.00 | | 49.79 | 8838.18 | 9.48 | 0.00 | | | -9.48 | 1000.00 | + | 5 | 2019-01-06 | 5 | 50.00 | 50.00 | | 49.73 | 8797.62 | 9.44 | 0.00 | | | -9.44 | 1000.00 | + | 6 | 2019-01-07 | 6 | 50.00 | 50.00 | | 49.68 | 8757.01 | 9.39 | 0.00 | | | -9.39 | 1000.00 | + | 7 | 2019-01-08 | 7 | 50.00 | 50.00 | | 49.63 | 8716.36 | 9.35 | 0.00 | | | -9.35 | 1000.00 | + | 8 | 2019-01-09 | 8 | 50.00 | 50.00 | | 49.57 | 8675.67 | 9.31 | 0.00 | | | -9.31 | 1000.00 | + | 9 | 2019-01-10 | 9 | 50.00 | 50.00 | | 49.52 | 8634.94 | 9.26 | 0.00 | | | -9.26 | 1000.00 | + | 10 | 2019-01-11 | 10 | 50.00 | 50.00 | | 49.47 | 8594.16 | 9.22 | 0.00 | | | -9.22 | 1000.00 | + | 11 | 2019-01-12 | 11 | 50.00 | 50.00 | | 49.42 | 8553.33 | 9.18 | 0.00 | | | -9.18 | 1000.00 | + | 12 | 2019-01-13 | 12 | 50.00 | 50.00 | | 49.36 | 8512.47 | 9.13 | 0.00 | | | -9.13 | 1000.00 | + | 13 | 2019-01-14 | 13 | 50.00 | 50.00 | | 49.31 | 8471.56 | 9.09 | 0.00 | | | -9.09 | 1000.00 | + | 14 | 2019-01-15 | 14 | 50.00 | 50.00 | | 49.26 | 8430.60 | 9.05 | 0.00 | | | -9.05 | 1000.00 | + | 15 | 2019-01-16 | 15 | 50.00 | 50.00 | | 49.21 | 8389.61 | 9.00 | 0.00 | | | -9.00 | 1000.00 | + | 16 | 2019-01-17 | 16 | 50.00 | 50.00 | | 49.15 | 8348.56 | 8.96 | 0.00 | | | -8.96 | 1000.00 | + | 17 | 2019-01-18 | 17 | 50.00 | 50.00 | | 49.10 | 8307.48 | 8.91 | 0.00 | | | -8.91 | 1000.00 | + | 18 | 2019-01-19 | 18 | 50.00 | 50.00 | | 49.05 | 8266.35 | 8.87 | 0.00 | | | -8.87 | 1000.00 | + | 19 | 2019-01-20 | 19 | 50.00 | 50.00 | | 49.00 | 8225.18 | 8.83 | 0.00 | | | -8.83 | 1000.00 | + | 20 | 2019-01-21 | 20 | 50.00 | 50.00 | | 48.94 | 8183.96 | 8.78 | 0.00 | | | -8.78 | 1000.00 | + | 21 | 2019-01-22 | 21 | 50.00 | 50.00 | | 48.89 | 8142.70 | 8.74 | 0.00 | | | -8.74 | 1000.00 | + | 22 | 2019-01-23 | 22 | 50.00 | 50.00 | | 48.84 | 8101.39 | 8.69 | 0.00 | | | -8.69 | 1000.00 | + | 23 | 2019-01-24 | 23 | 50.00 | 50.00 | | 48.79 | 8060.04 | 8.65 | 0.00 | | | -8.65 | 1000.00 | + | 24 | 2019-01-25 | 24 | 47.22 | 47.22 | | 44.87 | 7988.51 | 17.08 | 0.00 | | | -17.08 | 1000.00 | + | 25 | 2019-01-26 | 25 | 47.22 | 47.22 | | 44.77 | 7958.31 | 17.02 | 0.00 | | | -17.02 | 1000.00 | + | 26 | 2019-01-27 | 26 | 47.22 | 47.22 | | 44.68 | 7928.04 | 16.95 | 0.00 | | | -16.95 | 1000.00 | + | 27 | 2019-01-28 | 27 | 47.22 | 47.22 | | 44.58 | 7897.70 | 16.89 | 0.00 | | | -16.89 | 1000.00 | + | 28 | 2019-01-29 | 28 | 47.22 | 47.22 | | 44.49 | 7867.30 | 16.82 | 0.00 | | | -16.82 | 1000.00 | + | 29 | 2019-01-30 | 29 | 47.22 | 47.22 | | 44.39 | 7836.84 | 16.76 | 0.00 | | | -16.76 | 1000.00 | + | 30 | 2019-01-31 | 30 | 47.22 | 47.22 | | 44.30 | 7806.31 | 16.69 | 0.00 | | | -16.69 | 1000.00 | + | 31 | 2019-02-01 | 31 | 47.22 | 47.22 | | 44.21 | 7775.72 | 16.63 | 0.00 | | | -16.63 | 1000.00 | + | 32 | 2019-02-02 | 32 | 47.22 | 47.22 | | 44.11 | 7745.06 | 16.56 | 0.00 | | | -16.56 | 1000.00 | + | 33 | 2019-02-03 | 33 | 47.22 | 47.22 | | 44.02 | 7714.34 | 16.50 | 0.00 | | | -16.50 | 1000.00 | + | 34 | 2019-02-04 | 34 | 47.22 | 47.22 | | 43.92 | 7683.55 | 16.43 | 0.00 | | | -16.43 | 1000.00 | + | 35 | 2019-02-05 | 35 | 47.22 | 47.22 | | 43.83 | 7652.69 | 16.37 | 0.00 | | | -16.37 | 1000.00 | + | 36 | 2019-02-06 | 36 | 47.22 | 47.22 | | 43.74 | 7621.77 | 16.30 | 0.00 | | | -16.30 | 1000.00 | + | 37 | 2019-02-07 | 37 | 47.22 | 47.22 | | 43.65 | 7590.79 | 16.23 | 0.00 | | | -16.23 | 1000.00 | + | 38 | 2019-02-08 | 38 | 47.22 | 47.22 | | 43.55 | 7559.74 | 16.17 | 0.00 | | | -16.17 | 1000.00 | + | 39 | 2019-02-09 | 39 | 47.22 | 47.22 | | 43.46 | 7528.62 | 16.10 | 0.00 | | | -16.10 | 1000.00 | + | 40 | 2019-02-10 | 40 | 47.22 | 47.22 | | 43.37 | 7497.43 | 16.04 | 0.00 | | | -16.04 | 1000.00 | + | 41 | 2019-02-11 | 41 | 47.22 | 47.22 | | 43.28 | 7466.18 | 15.97 | 0.00 | | | -15.97 | 1000.00 | + | 42 | 2019-02-12 | 42 | 47.22 | 47.22 | | 43.18 | 7434.87 | 15.90 | 0.00 | | | -15.90 | 1000.00 | + | 43 | 2019-02-13 | 43 | 47.22 | 47.22 | | 43.09 | 7403.48 | 15.84 | 0.00 | | | -15.84 | 1000.00 | + | 44 | 2019-02-14 | 44 | 47.22 | 47.22 | | 43.00 | 7372.03 | 15.77 | 0.00 | | | -15.77 | 1000.00 | + | 45 | 2019-02-15 | 45 | 47.22 | 47.22 | | 42.91 | 7340.51 | 15.70 | 0.00 | | | -15.70 | 1000.00 | + | 46 | 2019-02-16 | 46 | 47.22 | 47.22 | | 42.82 | 7308.93 | 15.63 | 0.00 | | | -15.63 | 1000.00 | + | 47 | 2019-02-17 | 47 | 47.22 | 47.22 | | 42.73 | 7277.27 | 15.57 | 0.00 | | | -15.57 | 1000.00 | + | 48 | 2019-02-18 | 48 | 47.22 | 47.22 | | 42.64 | 7245.55 | 15.50 | 0.00 | | | -15.50 | 1000.00 | + | 49 | 2019-02-19 | 49 | 47.22 | 47.22 | | 42.54 | 7213.77 | 15.43 | 0.00 | | | -15.43 | 1000.00 | + | 50 | 2019-02-20 | 50 | 47.22 | 47.22 | | 42.45 | 7181.91 | 15.36 | 0.00 | | | -15.36 | 1000.00 | + | 51 | 2019-02-21 | 51 | 47.22 | 47.22 | | 42.36 | 7149.99 | 15.30 | 0.00 | | | -15.30 | 1000.00 | + | 52 | 2019-02-22 | 52 | 47.22 | 47.22 | | 42.27 | 7118.00 | 15.23 | 0.00 | | | -15.23 | 1000.00 | + | 53 | 2019-02-23 | 53 | 47.22 | 47.22 | | 42.18 | 7085.94 | 15.16 | 0.00 | | | -15.16 | 1000.00 | + | 54 | 2019-02-24 | 54 | 47.22 | 47.22 | | 42.09 | 7053.81 | 15.09 | 0.00 | | | -15.09 | 1000.00 | + | 55 | 2019-02-25 | 55 | 47.22 | 47.22 | | 42.01 | 7021.62 | 15.02 | 0.00 | | | -15.02 | 1000.00 | + | 56 | 2019-02-26 | 56 | 47.22 | 47.22 | | 41.92 | 6989.35 | 14.96 | 0.00 | | | -14.96 | 1000.00 | + | 57 | 2019-02-27 | 57 | 47.22 | 47.22 | | 41.83 | 6957.02 | 14.89 | 0.00 | | | -14.89 | 1000.00 | + | 58 | 2019-02-28 | 58 | 47.22 | 47.22 | | 41.74 | 6924.62 | 14.82 | 0.00 | | | -14.82 | 1000.00 | + | 59 | 2019-03-01 | 59 | 47.22 | 47.22 | | 41.65 | 6892.15 | 14.75 | 0.00 | | | -14.75 | 1000.00 | + | 60 | 2019-03-02 | 60 | 47.22 | 47.22 | | 41.56 | 6859.61 | 14.68 | 0.00 | | | -14.68 | 1000.00 | + | 61 | 2019-03-03 | 61 | 47.22 | 47.22 | | 41.47 | 6827.00 | 14.61 | 0.00 | | | -14.61 | 1000.00 | + | 62 | 2019-03-04 | 62 | 47.22 | 47.22 | | 41.38 | 6794.32 | 14.54 | 0.00 | | | -14.54 | 1000.00 | + | 63 | 2019-03-05 | 63 | 47.22 | 47.22 | | 41.30 | 6761.57 | 14.47 | 0.00 | | | -14.47 | 1000.00 | + | 64 | 2019-03-06 | 64 | 47.22 | 47.22 | | 41.21 | 6728.75 | 14.40 | 0.00 | | | -14.40 | 1000.00 | + | 65 | 2019-03-07 | 65 | 47.22 | 47.22 | | 41.12 | 6695.86 | 14.33 | 0.00 | | | -14.33 | 1000.00 | + | 66 | 2019-03-08 | 66 | 47.22 | 47.22 | | 41.03 | 6662.90 | 14.26 | 0.00 | | | -14.26 | 1000.00 | + | 67 | 2019-03-09 | 67 | 47.22 | 47.22 | | 40.95 | 6629.88 | 14.19 | 0.00 | | | -14.19 | 1000.00 | + | 68 | 2019-03-10 | 68 | 47.22 | 47.22 | | 40.86 | 6596.78 | 14.12 | 0.00 | | | -14.12 | 1000.00 | + | 69 | 2019-03-11 | 69 | 47.22 | 47.22 | | 40.77 | 6563.61 | 14.05 | 0.00 | | | -14.05 | 1000.00 | + | 70 | 2019-03-12 | 70 | 47.22 | 47.22 | | 40.69 | 6530.37 | 13.98 | 0.00 | | | -13.98 | 1000.00 | + | 71 | 2019-03-13 | 71 | 47.22 | 47.22 | | 40.60 | 6497.06 | 13.91 | 0.00 | | | -13.91 | 1000.00 | + | 72 | 2019-03-14 | 72 | 47.22 | 47.22 | | 40.51 | 6463.68 | 13.84 | 0.00 | | | -13.84 | 1000.00 | + | 73 | 2019-03-15 | 73 | 47.22 | 47.22 | | 40.43 | 6430.22 | 13.77 | 0.00 | | | -13.77 | 1000.00 | + | 74 | 2019-03-16 | 74 | 47.22 | 47.22 | | 40.34 | 6396.70 | 13.70 | 0.00 | | | -13.70 | 1000.00 | + | 75 | 2019-03-17 | 75 | 47.22 | 47.22 | | 40.26 | 6363.10 | 13.62 | 0.00 | | | -13.62 | 1000.00 | + | 76 | 2019-03-18 | 76 | 47.22 | 47.22 | | 40.17 | 6329.44 | 13.55 | 0.00 | | | -13.55 | 1000.00 | + | 77 | 2019-03-19 | 77 | 47.22 | 47.22 | | 40.08 | 6295.70 | 13.48 | 0.00 | | | -13.48 | 1000.00 | + | 78 | 2019-03-20 | 78 | 47.22 | 47.22 | | 40.00 | 6261.89 | 13.41 | 0.00 | | | -13.41 | 1000.00 | + | 79 | 2019-03-21 | 79 | 47.22 | 47.22 | | 39.91 | 6228.00 | 13.34 | 0.00 | | | -13.34 | 1000.00 | + | 80 | 2019-03-22 | 80 | 47.22 | 47.22 | | 39.83 | 6194.05 | 13.27 | 0.00 | | | -13.27 | 1000.00 | + | 81 | 2019-03-23 | 81 | 47.22 | 47.22 | | 39.74 | 6160.02 | 13.19 | 0.00 | | | -13.19 | 1000.00 | + | 82 | 2019-03-24 | 82 | 47.22 | 47.22 | | 39.66 | 6125.92 | 13.12 | 0.00 | | | -13.12 | 1000.00 | + | 83 | 2019-03-25 | 83 | 47.22 | 47.22 | | 39.58 | 6091.75 | 13.05 | 0.00 | | | -13.05 | 1000.00 | + | 84 | 2019-03-26 | 84 | 47.22 | 47.22 | | 39.49 | 6057.51 | 12.98 | 0.00 | | | -12.98 | 1000.00 | + | 85 | 2019-03-27 | 85 | 47.22 | 47.22 | | 39.41 | 6023.19 | 12.90 | 0.00 | | | -12.90 | 1000.00 | + | 86 | 2019-03-28 | 86 | 47.22 | 47.22 | | 39.32 | 5988.80 | 12.83 | 0.00 | | | -12.83 | 1000.00 | + | 87 | 2019-03-29 | 87 | 47.22 | 47.22 | | 39.24 | 5954.33 | 12.76 | 0.00 | | | -12.76 | 1000.00 | + | 88 | 2019-03-30 | 88 | 47.22 | 47.22 | | 39.16 | 5919.80 | 12.68 | 0.00 | | | -12.68 | 1000.00 | + | 89 | 2019-03-31 | 89 | 47.22 | 47.22 | | 39.07 | 5885.18 | 12.61 | 0.00 | | | -12.61 | 1000.00 | + | 90 | 2019-04-01 | 90 | 47.22 | 47.22 | | 38.99 | 5850.50 | 12.54 | 0.00 | | | -12.54 | 1000.00 | + | 91 | 2019-04-02 | 91 | 47.22 | 47.22 | | 38.91 | 5815.74 | 12.46 | 0.00 | | | -12.46 | 1000.00 | + | 92 | 2019-04-03 | 92 | 47.22 | 47.22 | | 38.83 | 5780.91 | 12.39 | 0.00 | | | -12.39 | 1000.00 | + | 93 | 2019-04-04 | 93 | 47.22 | 47.22 | | 38.74 | 5746.00 | 12.31 | 0.00 | | | -12.31 | 1000.00 | + | 94 | 2019-04-05 | 94 | 47.22 | 47.22 | | 38.66 | 5711.02 | 12.24 | 0.00 | | | -12.24 | 1000.00 | + | 95 | 2019-04-06 | 95 | 47.22 | 47.22 | | 38.58 | 5675.96 | 12.16 | 0.00 | | | -12.16 | 1000.00 | + | 96 | 2019-04-07 | 96 | 47.22 | 47.22 | | 38.50 | 5640.83 | 12.09 | 0.00 | | | -12.09 | 1000.00 | + | 97 | 2019-04-08 | 97 | 47.22 | 47.22 | | 38.41 | 5605.63 | 12.01 | 0.00 | | | -12.01 | 1000.00 | + | 98 | 2019-04-09 | 98 | 47.22 | 47.22 | | 38.33 | 5570.35 | 11.94 | 0.00 | | | -11.94 | 1000.00 | + | 99 | 2019-04-10 | 99 | 47.22 | 47.22 | | 38.25 | 5534.99 | 11.86 | 0.00 | | | -11.86 | 1000.00 | + | 100 | 2019-04-11 | 100 | 47.22 | 47.22 | | 38.17 | 5499.56 | 11.79 | 0.00 | | | -11.79 | 1000.00 | + | 101 | 2019-04-12 | 101 | 47.22 | 47.22 | | 38.09 | 5464.06 | 11.71 | 0.00 | | | -11.71 | 1000.00 | + | 102 | 2019-04-13 | 102 | 47.22 | 47.22 | | 38.01 | 5428.47 | 11.64 | 0.00 | | | -11.64 | 1000.00 | + | 103 | 2019-04-14 | 103 | 47.22 | 47.22 | | 37.93 | 5392.82 | 11.56 | 0.00 | | | -11.56 | 1000.00 | + | 104 | 2019-04-15 | 104 | 47.22 | 47.22 | | 37.85 | 5357.08 | 11.49 | 0.00 | | | -11.49 | 1000.00 | + | 105 | 2019-04-16 | 105 | 47.22 | 47.22 | | 37.77 | 5321.27 | 11.41 | 0.00 | | | -11.41 | 1000.00 | + | 106 | 2019-04-17 | 106 | 47.22 | 47.22 | | 37.69 | 5285.39 | 11.33 | 0.00 | | | -11.33 | 1000.00 | + | 107 | 2019-04-18 | 107 | 47.22 | 47.22 | | 37.61 | 5249.42 | 11.26 | 0.00 | | | -11.26 | 1000.00 | + | 108 | 2019-04-19 | 108 | 47.22 | 47.22 | | 37.53 | 5213.38 | 11.18 | 0.00 | | | -11.18 | 1000.00 | + | 109 | 2019-04-20 | 109 | 47.22 | 47.22 | | 37.45 | 5177.27 | 11.10 | 0.00 | | | -11.10 | 1000.00 | + | 110 | 2019-04-21 | 110 | 47.22 | 47.22 | | 37.37 | 5141.08 | 11.03 | 0.00 | | | -11.03 | 1000.00 | + | 111 | 2019-04-22 | 111 | 47.22 | 47.22 | | 37.29 | 5104.81 | 10.95 | 0.00 | | | -10.95 | 1000.00 | + | 112 | 2019-04-23 | 112 | 47.22 | 47.22 | | 37.21 | 5068.46 | 10.87 | 0.00 | | | -10.87 | 1000.00 | + | 113 | 2019-04-24 | 113 | 47.22 | 47.22 | | 37.13 | 5032.03 | 10.80 | 0.00 | | | -10.80 | 1000.00 | + | 114 | 2019-04-25 | 114 | 47.22 | 47.22 | | 37.05 | 4995.53 | 10.72 | 0.00 | | | -10.72 | 1000.00 | + | 115 | 2019-04-26 | 115 | 47.22 | 47.22 | | 36.97 | 4958.95 | 10.64 | 0.00 | | | -10.64 | 1000.00 | + | 116 | 2019-04-27 | 116 | 47.22 | 47.22 | | 36.89 | 4922.30 | 10.56 | 0.00 | | | -10.56 | 1000.00 | + | 117 | 2019-04-28 | 117 | 47.22 | 47.22 | | 36.81 | 4885.56 | 10.48 | 0.00 | | | -10.48 | 1000.00 | + | 118 | 2019-04-29 | 118 | 47.22 | 47.22 | | 36.74 | 4848.75 | 10.41 | 0.00 | | | -10.41 | 1000.00 | + | 119 | 2019-04-30 | 119 | 47.22 | 47.22 | | 36.66 | 4811.85 | 10.33 | 0.00 | | | -10.33 | 1000.00 | + | 120 | 2019-05-01 | 120 | 47.22 | 47.22 | | 36.58 | 4774.88 | 10.25 | 0.00 | | | -10.25 | 1000.00 | + | 121 | 2019-05-02 | 121 | 47.22 | 47.22 | | 36.50 | 4737.83 | 10.17 | 0.00 | | | -10.17 | 1000.00 | + | 122 | 2019-05-03 | 122 | 47.22 | 47.22 | | 36.42 | 4700.70 | 10.09 | 0.00 | | | -10.09 | 1000.00 | + | 123 | 2019-05-04 | 123 | 47.22 | 47.22 | | 36.35 | 4663.50 | 10.01 | 0.00 | | | -10.01 | 1000.00 | + | 124 | 2019-05-05 | 124 | 47.22 | 47.22 | | 36.27 | 4626.21 | 9.93 | 0.00 | | | -9.93 | 1000.00 | + | 125 | 2019-05-06 | 125 | 47.22 | 47.22 | | 36.19 | 4588.84 | 9.85 | 0.00 | | | -9.85 | 1000.00 | + | 126 | 2019-05-07 | 126 | 47.22 | 47.22 | | 36.12 | 4551.40 | 9.77 | 0.00 | | | -9.77 | 1000.00 | + | 127 | 2019-05-08 | 127 | 47.22 | 47.22 | | 36.04 | 4513.87 | 9.69 | 0.00 | | | -9.69 | 1000.00 | + | 128 | 2019-05-09 | 128 | 47.22 | 47.22 | | 35.96 | 4476.26 | 9.61 | 0.00 | | | -9.61 | 1000.00 | + | 129 | 2019-05-10 | 129 | 47.22 | 47.22 | | 35.89 | 4438.58 | 9.53 | 0.00 | | | -9.53 | 1000.00 | + | 130 | 2019-05-11 | 130 | 47.22 | 47.22 | | 35.81 | 4400.81 | 9.45 | 0.00 | | | -9.45 | 1000.00 | + | 131 | 2019-05-12 | 131 | 47.22 | 47.22 | | 35.73 | 4362.97 | 9.37 | 0.00 | | | -9.37 | 1000.00 | + | 132 | 2019-05-13 | 132 | 47.22 | 47.22 | | 35.66 | 4325.04 | 9.29 | 0.00 | | | -9.29 | 1000.00 | + | 133 | 2019-05-14 | 133 | 47.22 | 47.22 | | 35.58 | 4287.03 | 9.21 | 0.00 | | | -9.21 | 1000.00 | + | 134 | 2019-05-15 | 134 | 47.22 | 47.22 | | 35.51 | 4248.94 | 9.13 | 0.00 | | | -9.13 | 1000.00 | + | 135 | 2019-05-16 | 135 | 47.22 | 47.22 | | 35.43 | 4210.77 | 9.05 | 0.00 | | | -9.05 | 1000.00 | + | 136 | 2019-05-17 | 136 | 47.22 | 47.22 | | 35.36 | 4172.52 | 8.97 | 0.00 | | | -8.97 | 1000.00 | + | 137 | 2019-05-18 | 137 | 47.22 | 47.22 | | 35.28 | 4134.19 | 8.89 | 0.00 | | | -8.89 | 1000.00 | + | 138 | 2019-05-19 | 138 | 47.22 | 47.22 | | 35.21 | 4095.77 | 8.81 | 0.00 | | | -8.81 | 1000.00 | + | 139 | 2019-05-20 | 139 | 47.22 | 47.22 | | 35.13 | 4057.28 | 8.72 | 0.00 | | | -8.72 | 1000.00 | + | 140 | 2019-05-21 | 140 | 47.22 | 47.22 | | 35.06 | 4018.70 | 8.64 | 0.00 | | | -8.64 | 1000.00 | + | 141 | 2019-05-22 | 141 | 47.22 | 47.22 | | 34.98 | 3980.04 | 8.56 | 0.00 | | | -8.56 | 1000.00 | + | 142 | 2019-05-23 | 142 | 47.22 | 47.22 | | 34.91 | 3941.30 | 8.48 | 0.00 | | | -8.48 | 1000.00 | + | 143 | 2019-05-24 | 143 | 47.22 | 47.22 | | 34.83 | 3902.47 | 8.39 | 0.00 | | | -8.39 | 1000.00 | + | 144 | 2019-05-25 | 144 | 47.22 | 47.22 | | 34.76 | 3863.56 | 8.31 | 0.00 | | | -8.31 | 1000.00 | + | 145 | 2019-05-26 | 145 | 47.22 | 47.22 | | 34.68 | 3824.57 | 8.23 | 0.00 | | | -8.23 | 1000.00 | + | 146 | 2019-05-27 | 146 | 47.22 | 47.22 | | 34.61 | 3785.50 | 8.15 | 0.00 | | | -8.15 | 1000.00 | + | 147 | 2019-05-28 | 147 | 47.22 | 47.22 | | 34.54 | 3746.34 | 8.06 | 0.00 | | | -8.06 | 1000.00 | + | 148 | 2019-05-29 | 148 | 47.22 | 47.22 | | 34.46 | 3707.10 | 7.98 | 0.00 | | | -7.98 | 1000.00 | + | 149 | 2019-05-30 | 149 | 47.22 | 47.22 | | 34.39 | 3667.78 | 7.90 | 0.00 | | | -7.90 | 1000.00 | + | 150 | 2019-05-31 | 150 | 47.22 | 47.22 | | 34.32 | 3628.37 | 7.81 | 0.00 | | | -7.81 | 1000.00 | + | 151 | 2019-06-01 | 151 | 47.22 | 47.22 | | 34.24 | 3588.88 | 7.73 | 0.00 | | | -7.73 | 1000.00 | + | 152 | 2019-06-02 | 152 | 47.22 | 47.22 | | 34.17 | 3549.30 | 7.64 | 0.00 | | | -7.64 | 1000.00 | + | 153 | 2019-06-03 | 153 | 47.22 | 47.22 | | 34.10 | 3509.64 | 7.56 | 0.00 | | | -7.56 | 1000.00 | + | 154 | 2019-06-04 | 154 | 47.22 | 47.22 | | 34.03 | 3469.90 | 7.48 | 0.00 | | | -7.48 | 1000.00 | + | 155 | 2019-06-05 | 155 | 47.22 | 47.22 | | 33.95 | 3430.07 | 7.39 | 0.00 | | | -7.39 | 1000.00 | + | 156 | 2019-06-06 | 156 | 47.22 | 47.22 | | 33.88 | 3390.15 | 7.31 | 0.00 | | | -7.31 | 1000.00 | + | 157 | 2019-06-07 | 157 | 47.22 | 47.22 | | 33.81 | 3350.15 | 7.22 | 0.00 | | | -7.22 | 1000.00 | + | 158 | 2019-06-08 | 158 | 47.22 | 47.22 | | 33.74 | 3310.07 | 7.14 | 0.00 | | | -7.14 | 1000.00 | + | 159 | 2019-06-09 | 159 | 47.22 | 47.22 | | 33.67 | 3269.90 | 7.05 | 0.00 | | | -7.05 | 1000.00 | + | 160 | 2019-06-10 | 160 | 47.22 | 47.22 | | 33.60 | 3229.64 | 6.96 | 0.00 | | | -6.96 | 1000.00 | + | 161 | 2019-06-11 | 161 | 47.22 | 47.22 | | 33.52 | 3189.30 | 6.88 | 0.00 | | | -6.88 | 1000.00 | + | 162 | 2019-06-12 | 162 | 47.22 | 47.22 | | 33.45 | 3148.88 | 6.79 | 0.00 | | | -6.79 | 1000.00 | + | 163 | 2019-06-13 | 163 | 47.22 | 47.22 | | 33.38 | 3108.36 | 6.71 | 0.00 | | | -6.71 | 1000.00 | + | 164 | 2019-06-14 | 164 | 47.22 | 47.22 | | 33.31 | 3067.76 | 6.62 | 0.00 | | | -6.62 | 1000.00 | + | 165 | 2019-06-15 | 165 | 47.22 | 47.22 | | 33.24 | 3027.08 | 6.53 | 0.00 | | | -6.53 | 1000.00 | + | 166 | 2019-06-16 | 166 | 47.22 | 47.22 | | 33.17 | 2986.31 | 6.45 | 0.00 | | | -6.45 | 1000.00 | + | 167 | 2019-06-17 | 167 | 47.22 | 47.22 | | 33.10 | 2945.45 | 6.36 | 0.00 | | | -6.36 | 1000.00 | + | 168 | 2019-06-18 | 168 | 47.22 | 47.22 | | 33.03 | 2904.50 | 6.27 | 0.00 | | | -6.27 | 1000.00 | + | 169 | 2019-06-19 | 169 | 47.22 | 47.22 | | 32.96 | 2863.47 | 6.19 | 0.00 | | | -6.19 | 1000.00 | + | 170 | 2019-06-20 | 170 | 47.22 | 47.22 | | 32.89 | 2822.35 | 6.10 | 0.00 | | | -6.10 | 1000.00 | + | 171 | 2019-06-21 | 171 | 47.22 | 47.22 | | 32.82 | 2781.14 | 6.01 | 0.00 | | | -6.01 | 1000.00 | + | 172 | 2019-06-22 | 172 | 47.22 | 47.22 | | 32.75 | 2739.84 | 5.92 | 0.00 | | | -5.92 | 1000.00 | + | 173 | 2019-06-23 | 173 | 47.22 | 47.22 | | 32.68 | 2698.46 | 5.84 | 0.00 | | | -5.84 | 1000.00 | + | 174 | 2019-06-24 | 174 | 47.22 | 47.22 | | 32.61 | 2656.98 | 5.75 | 0.00 | | | -5.75 | 1000.00 | + | 175 | 2019-06-25 | 175 | 47.22 | 47.22 | | 32.54 | 2615.42 | 5.66 | 0.00 | | | -5.66 | 1000.00 | + | 176 | 2019-06-26 | 176 | 47.22 | 47.22 | | 32.47 | 2573.77 | 5.57 | 0.00 | | | -5.57 | 1000.00 | + | 177 | 2019-06-27 | 177 | 47.22 | 47.22 | | 32.40 | 2532.04 | 5.48 | 0.00 | | | -5.48 | 1000.00 | + | 178 | 2019-06-28 | 178 | 47.22 | 47.22 | | 32.33 | 2490.21 | 5.39 | 0.00 | | | -5.39 | 1000.00 | + | 179 | 2019-06-29 | 179 | 47.22 | 47.22 | | 32.26 | 2448.29 | 5.30 | 0.00 | | | -5.30 | 1000.00 | + | 180 | 2019-06-30 | 180 | 47.22 | 47.22 | | 32.20 | 2406.29 | 5.21 | 0.00 | | | -5.21 | 1000.00 | + | 181 | 2019-07-01 | 181 | 47.22 | 47.22 | | 32.13 | 2364.19 | 5.13 | 0.00 | | | -5.13 | 1000.00 | + | 182 | 2019-07-02 | 182 | 47.22 | 47.22 | | 32.06 | 2322.01 | 5.04 | 0.00 | | | -5.04 | 1000.00 | + | 183 | 2019-07-03 | 183 | 47.22 | 47.22 | | 31.99 | 2279.73 | 4.95 | 0.00 | | | -4.95 | 1000.00 | + | 184 | 2019-07-04 | 184 | 47.22 | 47.22 | | 31.92 | 2237.37 | 4.86 | 0.00 | | | -4.86 | 1000.00 | + | 185 | 2019-07-05 | 185 | 47.22 | 47.22 | | 31.86 | 2194.91 | 4.77 | 0.00 | | | -4.77 | 1000.00 | + | 186 | 2019-07-06 | 186 | 47.22 | 47.22 | | 31.79 | 2152.37 | 4.68 | 0.00 | | | -4.68 | 1000.00 | + | 187 | 2019-07-07 | 187 | 47.22 | 47.22 | | 31.72 | 2109.73 | 4.58 | 0.00 | | | -4.58 | 1000.00 | + | 188 | 2019-07-08 | 188 | 47.22 | 47.22 | | 31.65 | 2067.01 | 4.49 | 0.00 | | | -4.49 | 1000.00 | + | 189 | 2019-07-09 | 189 | 47.22 | 47.22 | | 31.59 | 2024.19 | 4.40 | 0.00 | | | -4.40 | 1000.00 | + | 190 | 2019-07-10 | 190 | 47.22 | 47.22 | | 31.52 | 1981.28 | 4.31 | 0.00 | | | -4.31 | 1000.00 | + | 191 | 2019-07-11 | 191 | 47.22 | 47.22 | | 31.45 | 1938.28 | 4.22 | 0.00 | | | -4.22 | 1000.00 | + | 192 | 2019-07-12 | 192 | 47.22 | 47.22 | | 31.38 | 1895.19 | 4.13 | 0.00 | | | -4.13 | 1000.00 | + | 193 | 2019-07-13 | 193 | 47.22 | 47.22 | | 31.32 | 1852.01 | 4.04 | 0.00 | | | -4.04 | 1000.00 | + | 194 | 2019-07-14 | 194 | 47.22 | 47.22 | | 31.25 | 1808.73 | 3.94 | 0.00 | | | -3.94 | 1000.00 | + | 195 | 2019-07-15 | 195 | 47.22 | 47.22 | | 31.18 | 1765.36 | 3.85 | 0.00 | | | -3.85 | 1000.00 | + | 196 | 2019-07-16 | 196 | 47.22 | 47.22 | | 31.12 | 1721.90 | 3.76 | 0.00 | | | -3.76 | 1000.00 | + | 197 | 2019-07-17 | 197 | 47.22 | 47.22 | | 31.05 | 1678.35 | 3.67 | 0.00 | | | -3.67 | 1000.00 | + | 198 | 2019-07-18 | 198 | 47.22 | 47.22 | | 30.99 | 1634.71 | 3.57 | 0.00 | | | -3.57 | 1000.00 | + | 199 | 2019-07-19 | 199 | 47.22 | 47.22 | | 30.92 | 1590.97 | 3.48 | 0.00 | | | -3.48 | 1000.00 | + | 200 | 2019-07-20 | 200 | 47.22 | 47.22 | | 30.85 | 1547.14 | 3.39 | 0.00 | | | -3.39 | 1000.00 | + | 201 | 2019-07-21 | 201 | 47.22 | 47.22 | | 30.79 | 1503.21 | 3.30 | 0.00 | | | -3.30 | 1000.00 | + | 202 | 2019-07-22 | 202 | 47.22 | 47.22 | | 30.72 | 1459.19 | 3.20 | 0.00 | | | -3.20 | 1000.00 | + | 203 | 2019-07-23 | 203 | 47.22 | 47.22 | | 30.66 | 1415.08 | 3.11 | 0.00 | | | -3.11 | 1000.00 | + | 204 | 2019-07-24 | 204 | 47.22 | 47.22 | | 30.59 | 1370.88 | 3.01 | 0.00 | | | -3.01 | 1000.00 | + | 205 | 2019-07-25 | 205 | 47.22 | 47.22 | | 30.53 | 1326.58 | 2.92 | 0.00 | | | -2.92 | 1000.00 | + | 206 | 2019-07-26 | 206 | 47.22 | 47.22 | | 30.46 | 1282.18 | 2.83 | 0.00 | | | -2.83 | 1000.00 | + | 207 | 2019-07-27 | 207 | 47.22 | 47.22 | | 30.40 | 1237.69 | 2.73 | 0.00 | | | -2.73 | 1000.00 | + | 208 | 2019-07-28 | 208 | 47.22 | 47.22 | | 30.33 | 1193.11 | 2.64 | 0.00 | | | -2.64 | 1000.00 | + | 209 | 2019-07-29 | 209 | 47.22 | 47.22 | | 30.27 | 1148.43 | 2.54 | 0.00 | | | -2.54 | 1000.00 | + | 210 | 2019-07-30 | 210 | 47.22 | 47.22 | | 30.20 | 1103.66 | 2.45 | 0.00 | | | -2.45 | 1000.00 | + | 211 | 2019-07-31 | 211 | 47.22 | 47.22 | | 30.14 | 1058.79 | 2.35 | 0.00 | | | -2.35 | 1000.00 | + | 212 | 2019-08-01 | 212 | 47.22 | 47.22 | | 30.08 | 1013.82 | 2.26 | 0.00 | | | -2.26 | 1000.00 | + | 213 | 2019-08-02 | 213 | 47.22 | 47.22 | | 30.01 | 968.76 | 2.16 | 0.00 | | | -2.16 | 1000.00 | + | 214 | 2019-08-03 | 214 | 47.22 | 47.22 | | 29.95 | 923.60 | 2.06 | 0.00 | | | -2.06 | 1000.00 | + | 215 | 2019-08-04 | 215 | 47.22 | 47.22 | | 29.89 | 878.35 | 1.97 | 0.00 | | | -1.97 | 1000.00 | + | 216 | 2019-08-05 | 216 | 47.22 | 47.22 | | 29.82 | 833.00 | 1.87 | 0.00 | | | -1.87 | 1000.00 | + | 217 | 2019-08-06 | 217 | 47.22 | 47.22 | | 29.76 | 787.56 | 1.77 | 0.00 | | | -1.77 | 1000.00 | + | 218 | 2019-08-07 | 218 | 47.22 | 47.22 | | 29.70 | 742.01 | 1.68 | 0.00 | | | -1.68 | 1000.00 | + | 219 | 2019-08-08 | 219 | 47.22 | 47.22 | | 29.63 | 696.38 | 1.58 | 0.00 | | | -1.58 | 1000.00 | + | 220 | 2019-08-09 | 220 | 47.22 | 47.22 | | 29.57 | 650.64 | 1.48 | 0.00 | | | -1.48 | 1000.00 | + | 221 | 2019-08-10 | 221 | 47.22 | 47.22 | | 29.51 | 604.80 | 1.39 | 0.00 | | | -1.39 | 1000.00 | + | 222 | 2019-08-11 | 222 | 47.22 | 47.22 | | 29.44 | 558.87 | 1.29 | 0.00 | | | -1.29 | 1000.00 | + | 223 | 2019-08-12 | 223 | 47.22 | 47.22 | | 29.38 | 512.84 | 1.19 | 0.00 | | | -1.19 | 1000.00 | + | 224 | 2019-08-13 | 224 | 47.22 | 47.22 | | 29.32 | 466.72 | 1.09 | 0.00 | | | -1.09 | 1000.00 | + | 225 | 2019-08-14 | 225 | 47.22 | 47.22 | | 29.26 | 420.49 | 0.99 | 0.00 | | | -0.99 | 1000.00 | + | 226 | 2019-08-15 | 226 | 47.22 | 47.22 | | 29.19 | 374.16 | 0.90 | 0.00 | | | -0.90 | 1000.00 | + | 227 | 2019-08-16 | 227 | 47.22 | 47.22 | | 29.13 | 327.74 | 0.80 | 0.00 | | | -0.80 | 1000.00 | + | 228 | 2019-08-17 | 228 | 47.22 | 47.22 | | 29.07 | 281.22 | 0.70 | 0.00 | | | -0.70 | 1000.00 | + | 229 | 2019-08-18 | 229 | 47.22 | 47.22 | | 29.01 | 234.60 | 0.60 | 0.00 | | | -0.60 | 1000.00 | + | 230 | 2019-08-19 | 230 | 47.22 | 47.22 | | 28.95 | 187.88 | 0.50 | 0.00 | | | -0.50 | 1000.00 | + | 231 | 2019-08-20 | 231 | 47.22 | 47.22 | | 28.89 | 141.06 | 0.40 | 0.00 | | | -0.40 | 1000.00 | + | 232 | 2019-08-21 | 232 | 47.22 | 47.22 | | 28.82 | 94.14 | 0.30 | 0.00 | | | -0.30 | 1000.00 | + | 233 | 2019-08-22 | 233 | 47.22 | 47.22 | | 28.76 | 47.12 | 0.20 | 0.00 | | | -0.20 | 1000.00 | + | 234 | 2019-08-23 | 234 | 47.22 | 47.22 | | 28.70 | 0.00 | 0.10 | 0.00 | | | -0.10 | 1000.00 | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature new file mode 100644 index 00000000000..9f69eed575d --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalPeriodPaymentRate.feature @@ -0,0 +1,284 @@ +@WorkingCapitalPeriodPaymentRateFeature +Feature: Working Capital Period Payment Rate + + @TestRailId:C78817 + Scenario: Verify Working Capital period payment rate added successfully on loan account - UC1 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +#--- update period payment rate ---# + When Admin sets the business date to "15 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "12.5" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 15 January 2026 | 1.0 | 12.5 | false | + When Admin sets the business date to "15 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 12.5 | 0.0 | + + @TestRailId:C78818 + Scenario: Verify Working Capital period payment rate added on first day of disbursement successfully on loan account - UC2 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +#--- update period payment rate ---# + And Admin update Working Capital period payment rate with "12.5" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | false | + When Admin sets the business date to "15 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 12.5 | 0.0 | + + @TestRailId:C78819 + Scenario: Verify Working Capital period payment rate added successfully a few times a day on loan account - UC3 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +#--- update period payment rate ---# + And Admin update Working Capital period payment rate with "12.5" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | false | +#--- update period payment rate ---# + And Admin update Working Capital period payment rate with "19.38" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | true | + | 01 January 2026 | 12.5 | 19.38 | false | + When Admin sets the business date to "15 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 19.38 | 0.0 | + + @TestRailId:C78820 + Scenario: Verify Working Capital period payment rate added successfully a few times on different dates on loan account - UC4 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +#--- update period payment rate ---# + And Admin update Working Capital period payment rate with "12.5" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | false | +#--- update period payment rate ---# + When Admin sets the business date to "15 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "19.38" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | true | + | 15 January 2026 | 12.5 | 19.38 | false | +#--- update period payment rate ---# + When Admin sets the business date to "25 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "18.09" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 01 January 2026 | 1.0 | 12.5 | true | + | 15 January 2026 | 12.5 | 19.38 | true | + | 25 February 2026 | 19.38 | 18.09 | false | + When Admin sets the business date to "15 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 18.09 | 0.0 | + + @TestRailId:C78821 + Scenario: Verify Working Capital period payment rate added successfully on different date on loan account - UC5 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 12.5 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 12.5 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 12.5 | 0.0 | +#--- update period payment rate ---# + When Admin sets the business date to "25 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "15" value + Then Working Capital Loan Period Payment Rate changes history contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 25 April 2026 | 12.5 | 15.0 | false | + When Admin sets the business date to "28 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | + + @TestRailId:C78822 + Scenario Outline: Verify update Working Capital period payment rate failed with outranged rate change value within loan product level defined range - UC6 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 01 January 2026 | 01 January 2026 | 100 | 100 | 15 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 15.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | +#--- update period payment rate with invalid value that is out of allowed min/max values defined on loan product level ---# + And Admin update Working Capital period payment rate failed with "" value with error message + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | + + Examples: + | rate_change_value | rate_change_error_message | + | 0.5 | Failed data validation due to: rate.below.product.minimum. | + | 99.5 | Failed data validation due to: rate.exceeds.product.maximum. | + + @TestRailId:C78823 + Scenario: Verify update Working Capital period payment rate update failed within non active loan - UC7 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 01 January 2026 | 01 January 2026 | 100 | 100 | 15 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 15.0 | 0.0 | + Then Admin update Working Capital period payment rate failed with "18" value on non active loan + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin update Working Capital period payment rate failed with "18" value on non active loan + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | + + @TestRailId:C78824 + Scenario Outline: Verify update Working Capital period payment rate failed with invalid rate change value - UC8 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 01 January 2026 | 01 January 2026 | 100 | 100 | 15 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 15.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | +#--- update period payment rate with invalid or already set up value ---# + And Admin update Working Capital period payment rate failed with "" value with error message + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 15.0 | 0.0 | + + Examples: + | rate_change_value | rate_change_error_message | + | 0 | The parameter `periodPaymentRate` must be greater than 0. | + | 15 | New period payment rate is the same as the current rate | + + @TestRailId:C78825 + Scenario: Verify Working Capital period payment rate added successfully by externalId on loan account - UC9 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 0.0 | + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | 0.0 | +#--- update period payment rate by externalId ---# + When Admin sets the business date to "15 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin update Working Capital period payment rate with "12.5" value by externalId + Then Working Capital Loan Period Payment Rate changes history by externalId contains the following data: + | Effective Date | Previous Rate | New Rate | Reversed | + | 15 January 2026 | 1.0 | 12.5 | false | + When Admin sets the business date to "15 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 12.5 | 0.0 | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature index 55edabbee86..45dfe375859 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature @@ -1505,4 +1505,62 @@ Feature: WorkingCapitalLoanAccount Examples: | near_breach_id | | 0 | - | 9223372036854775807 | \ No newline at end of file + | 9223372036854775807 | + + @TestRailId:C78813 + Scenario Outline: Period payment rate on create of WCL account failed with outranged from loan product level rate change value - UC1 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin failed to create Working Capital with period payment rate "" value and outcomes with error message with default following data: + | LoanProduct | submittedOnDate | + | WCLP_PERIOD_PAYMENT_RATE | 01 January 2026 | + + Examples: + | rate_change_value | rate_change_error_message | + | 0.5 | Failed data validation due to: must.be.greater.than.or.equal.to.min. | + | 99.5 | Failed data validation due to: must.be.less.than.or.equal.to.max. | + + @TestRailId:C78814 + Scenario Outline: Period payment rate on create of WCL account failed with invalid rate change value - UC2 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin failed to create Working Capital on "01 January 2026" with period payment rate "" value and outcomes with error message + + Examples: + | rate_change_value | rate_change_error_message | + | -1 | The parameter `periodPaymentRate` must be greater than or equal to 0. | + + @TestRailId:C78815 + Scenario Outline: Period payment rate on modify of WCL account failed with with outranged from loan product level rate change value - UC3 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 01 January 2026 | 01 January 2026 | 100 | 100 | 12.5 | 15 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP_PERIOD_PAYMENT_RATE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 12.5 | 15.0 | + Then Admin failed to modify WC loan account with period payment rate "" value and outcomes with "" error message + + Examples: + | rate_change_value | rate_change_error_message | + | 0.5 | Failed data validation due to: must.be.greater.than.or.equal.to.min. | + | 99.5 | Failed data validation due to: must.be.less.than.or.equal.to.max. | + + @TestRailId:C78816 + Scenario Outline: Period payment rate on modify of WCL account failed with invalid rate change value - UC4 + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 12.5 | 15 | + Then Working capital loan creation was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 12.5 | 15.0 | + Then Admin failed to modify WC loan account with period payment rate "" value and outcomes with "" error message + + Examples: + | rate_change_value | rate_change_error_message | + | -1 | The parameter `periodPaymentRate` must be greater than or equal to 0. | \ No newline at end of file diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java index 96a1f241324..b77ee978b7b 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java @@ -77,4 +77,7 @@ private WorkingCapitalLoanConstants() { public static final String WRITE_OFF_REASONS = "WriteOffReasons"; public static final String CHARGE_OFF_REASONS = "ChargeOffReasons"; + + // Period payment rate change parameters + public static final String periodPaymentRateParamName = "periodPaymentRate"; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java index 4bb58af0068..7e01beba520 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java @@ -54,10 +54,12 @@ import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanPeriodPaymentRateChangeData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyReadPlatformService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanPeriodPaymentRateChangeReadService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -74,6 +76,7 @@ public class WorkingCapitalLoanApiResource { private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService; + private final WorkingCapitalLoanPeriodPaymentRateChangeReadService rateChangeReadService; @GET @Path("template") @@ -346,4 +349,64 @@ private CommandProcessingResult updateDiscount(final Long loanId, final String l .updateDiscountWorkingCapitalLoanApplication(resolvedLoanId).build(); return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } + + @PUT + @Path("{loanId}/payment-rate") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "updateWorkingCapitalLoanRateById", summary = "Update period payment rate for an active Working Capital Loan", description = "Modifies the period payment rate and triggers schedule recalculation for the remaining term.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdRateRequest.class))) + public CommandProcessingResult updatePeriodPaymentRateById( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return updatePeriodPaymentRate(loanId, null, apiRequestBodyAsJson); + } + + @PUT + @Path("external-id/{loanExternalId}/payment-rate") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "updateWorkingCapitalLoanRateByExternalId", summary = "Update period payment rate for an active Working Capital Loan by external id", description = "Modifies the period payment rate and triggers schedule recalculation for the remaining term.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdRateRequest.class))) + public CommandProcessingResult updatePeriodPaymentRateByExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return updatePeriodPaymentRate(null, loanExternalId, apiRequestBodyAsJson); + } + + private CommandProcessingResult updatePeriodPaymentRate(final Long loanId, final String loanExternalIdStr, + final String apiRequestBodyAsJson) { + final Long resolvedLoanId = loanId != null ? loanId + : readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr)); + if (resolvedLoanId == null) { + throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr)); + } + final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson) + .updatePeriodPaymentRateWorkingCapitalLoanApplication(resolvedLoanId).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } + + @GET + @Path("{loanId}/rate-changes") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "getWorkingCapitalLoanRateChangeHistoryById", summary = "Retrieve rate change history for a Working Capital Loan", description = "Returns all rate change records for the loan, ordered by most recent first.") + public List getRateChangeHistoryById( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return this.rateChangeReadService.retrieveRateChangeHistory(loanId); + } + + @GET + @Path("external-id/{loanExternalId}/rate-changes") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "getWorkingCapitalLoanRateChangeHistoryByExternalId", summary = "Retrieve rate change history for a Working Capital Loan by external id", description = "Returns all rate change records for the loan, ordered by most recent first.") + public List getRateChangeHistoryByExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + final Long resolvedLoanId = readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalId)); + if (resolvedLoanId == null) { + throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalId)); + } + return this.rateChangeReadService.retrieveRateChangeHistory(resolvedLoanId); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java index ee9613a9df4..eaa3b9c5996 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java @@ -609,4 +609,19 @@ private GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse() {} public BigDecimal delinquentAmount; } + @Schema(description = "Request for updating period payment rate on an active Working Capital Loan") + public static final class PutWorkingCapitalLoansLoanIdRateRequest { + + private PutWorkingCapitalLoansLoanIdRateRequest() {} + + @Schema(example = "0.17", description = "New period payment rate") + public BigDecimal periodPaymentRate; + + @Schema(example = "Rate change note") + public String note; + + @Schema(example = "en_GB") + public String locale; + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java index c639f697608..c645d6f4ea7 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java @@ -54,4 +54,12 @@ public void applyPayment(@NonNull final ProjectedAmortizationScheduleModel model @NonNull final BigDecimal paymentAmount) { model.applyPayment(paymentDate, paymentAmount); } + + @Override + @NonNull + public ProjectedAmortizationScheduleModel applyRateChange(@NonNull final ProjectedAmortizationScheduleModel model, + @NonNull final BigDecimal newPeriodPaymentRate, @NonNull final LocalDate rateChangeDate) { + model.applyRateChange(newPeriodPaymentRate, rateChangeDate); + return model; + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java index f7e53452aad..9d0abcbb505 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java @@ -70,4 +70,20 @@ ProjectedAmortizationScheduleModel addDisbursement(@NonNull ProjectedAmortizatio * actual payment amount */ void applyPayment(@NonNull ProjectedAmortizationScheduleModel model, @NonNull LocalDate paymentDate, @NonNull BigDecimal paymentAmount); + + /** + * Applies a rate change to the model in-place. Adds a {@link ProjectedAmortizationScheduleModel.RateSegment} and + * rebuilds the payment list. + * + * @param model + * the model to mutate + * @param newPeriodPaymentRate + * the new period payment rate + * @param rateChangeDate + * effective date of the rate change + * @return the same model instance (mutated) + */ + @NonNull + ProjectedAmortizationScheduleModel applyRateChange(@NonNull ProjectedAmortizationScheduleModel model, + @NonNull BigDecimal newPeriodPaymentRate, @NonNull LocalDate rateChangeDate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java index d7980b39b4c..0f82064f4b6 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java @@ -24,6 +24,7 @@ import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +46,8 @@ *
  • {@link #generate} — create initial schedule (at loan creation)
  • *
  • {@link #regenerate} — recalculate with new amounts (at approval / disbursement)
  • *
  • {@link #applyPayment} — record payments by date; schedule rebuilds after each
  • + *
  • {@link #applyRateChange} — apply a mid-lifecycle rate change; adds a {@link RateSegment} and rebuilds the payment + * list in-place
  • * */ @Getter @@ -52,7 +55,7 @@ @Slf4j public final class ProjectedAmortizationScheduleModel { - private static final String MODEL_VERSION = "1"; + private static final String MODEL_VERSION = "2"; private final Money originationFeeAmount; private final Money netDisbursementAmount; @@ -79,6 +82,9 @@ public final class ProjectedAmortizationScheduleModel { @Getter(AccessLevel.NONE) private final List appliedPayments; + @Getter(AccessLevel.NONE) + private final List rateSegments; + @Getter(AccessLevel.NONE) private List payments; @@ -98,6 +104,7 @@ private ProjectedAmortizationScheduleModel(final Money originationFeeAmount, fin this.mc = mc; this.currency = currency; this.appliedPayments = new ArrayList<>(); + this.rateSegments = new ArrayList<>(); rebuildPayments(); } @@ -122,6 +129,7 @@ private ProjectedAmortizationScheduleModel(final MathContext mc, final MonetaryC this.mc = mc; this.currency = currency; this.appliedPayments = new ArrayList<>(); + this.rateSegments = new ArrayList<>(); this.payments = List.of(); } @@ -129,6 +137,21 @@ public List payments() { return payments; } + public List rateSegments() { + return rateSegments != null ? List.copyOf(rateSegments) : List.of(); + } + + public int effectiveTotalTerm() { + if (rateSegments == null || rateSegments.isEmpty()) { + return loanTerm; + } + final RateSegment last = rateSegments.getLast(); + // When startDayIndex > 0, the segment overlaps one day with the base schedule (the split day), + // so subtract 1. When startDayIndex == 0, there are no base days — no overlap. + final int overlap = last.startDayIndex() > 0 ? 1 : 0; + return last.startDayIndex() + last.segmentTerm() - overlap; + } + public static ProjectedAmortizationScheduleModel generate(final BigDecimal originationFeeAmount, final BigDecimal netDisbursementAmount, final BigDecimal totalPaymentValue, final BigDecimal periodPaymentRate, final int npvDayCount, final LocalDate expectedDisbursementDate, final MathContext mc, final MonetaryCurrency currency) { @@ -167,7 +190,7 @@ public static ProjectedAmortizationScheduleModel generate(final BigDecimal origi public LocalDate normalizePaymentDateForSchedule(final LocalDate paymentDate) { Objects.requireNonNull(paymentDate, "paymentDate"); final LocalDate firstInstallmentDate = expectedDisbursementDate.plusDays(1); - final LocalDate lastInstallmentDate = expectedDisbursementDate.plusDays(loanTerm); + final LocalDate lastInstallmentDate = expectedDisbursementDate.plusDays(effectiveTotalTerm()); if (paymentDate.isBefore(firstInstallmentDate) || paymentDate.equals(expectedDisbursementDate)) { return firstInstallmentDate; } @@ -197,9 +220,9 @@ public void applyPayment(final LocalDate paymentDate, final BigDecimal amount) { Objects.requireNonNull(amount, "amount"); final LocalDate scheduleDate = normalizePaymentDateForSchedule(paymentDate); final int index = resolvePaymentIndex(scheduleDate); - if (index < 0 || index >= loanTerm) { + if (index < 0 || index >= effectiveTotalTerm()) { throw new IllegalArgumentException("paymentDate " + paymentDate + " is outside the valid range [" - + expectedDisbursementDate.plusDays(1) + " .. " + expectedDisbursementDate.plusDays(loanTerm) + "]"); + + expectedDisbursementDate.plusDays(1) + " .. " + expectedDisbursementDate.plusDays(effectiveTotalTerm()) + "]"); } appliedPayments.add(new AppliedPayment(scheduleDate, amount)); rebuildPayments(); @@ -247,6 +270,99 @@ public void recalculateNetAmortizationAndDeferredBalanceFrom(final LocalDate rep this.payments = List.copyOf(adjusted); } + /** + * Applies a rate change at the given date. Adds a {@link RateSegment} covering the remaining term from the change + * date forward. The model is mutated in-place; the payment list is rebuilt. + * + *

    + * Any existing segments at or after the split point are removed first (supports undo/overwrite). + * + * @param newPeriodPaymentRate + * the new period payment rate + * @param rateChangeDate + * the date of the rate change (must be within model's date range) + */ + public void applyRateChange(final BigDecimal newPeriodPaymentRate, final LocalDate rateChangeDate) { + Objects.requireNonNull(newPeriodPaymentRate, "newPeriodPaymentRate"); + Objects.requireNonNull(rateChangeDate, "rateChangeDate"); + + final int rawSplitDayIndex = (int) ChronoUnit.DAYS.between(expectedDisbursementDate, rateChangeDate); + if (rawSplitDayIndex < 0) { + throw new IllegalArgumentException("rateChangeDate must not be before expectedDisbursementDate"); + } + + // When the rate change is past the base schedule's term, clamp the segment start + // to loanTerm. The loan is still active (borrower hasn't paid), so the remaining + // balance is netDisbursement - paymentsReceived. + final int splitDayIndex = Math.min(rawSplitDayIndex, loanTerm); + + // Remove existing segments at or after split (supports overwrite on second rate change) + // Guard against null rateSegments from V1 model deserialization + if (rateSegments == null) { + throw new IllegalStateException("Model not properly initialized; rateSegments is null"); + } + rateSegments.removeIf(s -> s.startDayIndex() >= splitDayIndex); + + // Collect actual payments received before the split + BigDecimal paymentsReceived = BigDecimal.ZERO; + for (final ProjectedPayment p : payments) { + if (p.paymentNo() <= 0 || p.paymentNo() > splitDayIndex) { + continue; + } + if (p.actualPaymentAmount() != null) { + paymentsReceived = paymentsReceived.add(p.actualPaymentAmount().getAmount(), mc); + } + } + + // Compute balance at split: if past term, use remaining principal; otherwise use base amortization + final BigDecimal balanceAtSplit; + if (rawSplitDayIndex >= loanTerm) { + balanceAtSplit = netDisbursementAmount.getAmount().subtract(paymentsReceived, mc); + } else if (splitDayIndex > 0) { + final BalancesAndAmortizations ba = computeBaseBalancesUpTo(splitDayIndex); + balanceAtSplit = ba.balances().get(splitDayIndex - 1); + } else { + balanceAtSplit = netDisbursementAmount.getAmount(); + } + + final BigDecimal origNet = netDisbursementAmount.getAmount(); + final BigDecimal origDiscount = originationFeeAmount.getAmount(); + final BigDecimal tpv = totalPaymentValue.getAmount(); + + final BigDecimal newNetDisb = balanceAtSplit; + final BigDecimal newDiscount = origDiscount.add(origNet, mc).subtract(balanceAtSplit, mc).subtract(paymentsReceived, mc); + final int scale = currency.getDigitsAfterDecimal(); + final BigDecimal newDailyPayment = tpv.multiply(newPeriodPaymentRate, mc).divide(BigDecimal.valueOf(npvDayCount), mc) + .setScale(scale, RoundingMode.HALF_UP); + final BigDecimal fractionalTotalDays = origNet.add(origDiscount, mc).subtract(paymentsReceived, mc).divide(newDailyPayment, mc) + .setScale(scale, RoundingMode.HALF_UP); + final int newTerm = fractionalTotalDays.intValue(); + + // When daily payment exceeds remaining gross (e.g., very short-term loan with high TPV), + // the fractional term rounds to 0. Use at least 1 period. + final int safeTerm = Math.max(newTerm, 1); + if (newNetDisb.signum() <= 0) { + throw new IllegalArgumentException("balance at split must be positive for rate change"); + } + + final BigDecimal newEir = TvmFunctions.rate(safeTerm, newDailyPayment.negate(), newNetDisb, mc); + + rateSegments.add(new RateSegment(splitDayIndex, newDailyPayment, safeTerm, newEir, newNetDisb, newDiscount)); + rateSegments.sort(Comparator.comparingInt(RateSegment::startDayIndex)); + + rebuildPayments(); + } + + /** + * Removes the last rate change segment and rebuilds the schedule. + */ + public void removeLastRateChange() { + if (rateSegments != null && !rateSegments.isEmpty()) { + rateSegments.removeLast(); + rebuildPayments(); + } + } + private void rebuildPayments() { final Map paymentsByDate = aggregatePaymentsByDate(); final List paymentList = buildPaymentList(paymentsByDate); @@ -262,8 +378,9 @@ private Map aggregatePaymentsByDate() { } private List buildPaymentList(final Map paymentsByDate) { - final List result = new ArrayList<>(loanTerm); - for (int i = 0; i < loanTerm; i++) { + final int totalTerm = effectiveTotalTerm(); + final List result = new ArrayList<>(totalTerm); + for (int i = 0; i < totalTerm; i++) { final LocalDate paymentDate = expectedDisbursementDate.plusDays(i + 1); result.add(paymentsByDate.get(paymentDate)); } @@ -284,18 +401,18 @@ private List buildPayments(final List payments, fi final BigDecimal totalNetAmortization = computeTotalNetAmortization(payments, runningExpected, appliedCount, tailNpv); final BigDecimal originationFee = originationFeeAmount.getAmount(); - final BigDecimal safeExpectedPayment = MathUtil.negativeToZero(expectedPaymentAmount.getAmount()); - final List result = new ArrayList<>(loanTerm + 2 + tailPayments.size()); + final List result = new ArrayList<>(effectiveTotalTerm() + 2 + tailPayments.size()); result.add(createDisbursementPayment(appliedCount)); BigDecimal cumulativeActualAmort = BigDecimal.ZERO; - for (int i = 0; i < loanTerm; i++) { + for (int i = 0; i < effectiveTotalTerm(); i++) { final int periodNo = i + 1; final boolean hasAppliedAmount = payments.get(i) != null; - final long count = (long) loanTerm + appliedCount - periodNo; + final long count = (long) effectiveTotalTerm() + appliedCount - periodNo; final long paymentsLeft = paymentsLeft(periodNo, appliedCount); - final BigDecimal safeDf = safeDiscountFactor(paymentsLeft); + final BigDecimal safeDf = safeDiscountFactor(paymentsLeft, periodNo); + final BigDecimal periodExpectedPayment = MathUtil.negativeToZero(expectedPaymentForDay(periodNo)); final BigDecimal safeRunningExpected = MathUtil.negativeToZero(runningExpected.get(i)); final BigDecimal npvSource = hasAppliedAmount ? payments.get(i) : safeRunningExpected; final BigDecimal npvValue = MathUtil.negativeToZero(npvSource.multiply(safeDf, mc)); @@ -319,7 +436,7 @@ private List buildPayments(final List payments, fi final BigDecimal deferredBalance = originationFee.subtract(cumulativeActualAmort, mc); final BigDecimal balance = ba.balances.get(i); result.add(new ProjectedPayment(periodNo, expectedDisbursementDate.plusDays(periodNo), count, paymentsLeft, - money(safeExpectedPayment), money(safeRunningExpected), safeDf, money(npvValue), money(balance), + money(periodExpectedPayment), money(safeRunningExpected), safeDf, money(npvValue), money(balance), money(safeExpectedAmort), money(netAmortization), hasAppliedAmount ? money(payments.get(i)) : null, actualAmortization != null ? money(actualAmortization) : null, money(incomeModification), money(deferredBalance))); } @@ -344,7 +461,7 @@ private static BigDecimal amountOrZero(final Money value) { private ProjectedPayment createDisbursementPayment(final int appliedCount) { final Money negDisbursement = netDisbursementAmount.negated(mc); - final long count = (long) loanTerm + appliedCount; + final long count = (long) effectiveTotalTerm() + appliedCount; return new ProjectedPayment(0, expectedDisbursementDate, count, 0L, negDisbursement, null, BigDecimal.ONE, negDisbursement, netDisbursementAmount, null, null, null, null, null, originationFeeAmount); } @@ -354,25 +471,33 @@ private ProjectedPayment createDisbursementPayment(final int appliedCount) { * {@code expectedAmort[i] = balance[i] + expectedPayment - balance[i-1]} */ private BalancesAndAmortizations computeBalancesAndAmortizations() { - final BigDecimal onePlusRate = BigDecimal.ONE.add(effectiveInterestRate, mc); - final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); - final List balances = new ArrayList<>(loanTerm); - final List expectedAmortizations = new ArrayList<>(loanTerm); + final int totalTerm = effectiveTotalTerm(); + final List balances = new ArrayList<>(totalTerm); + final List expectedAmortizations = new ArrayList<>(totalTerm); BigDecimal prevBalance = netDisbursementAmount.getAmount(); - for (int i = 0; i < loanTerm; i++) { - final BigDecimal balance = prevBalance.multiply(onePlusRate, mc).subtract(expectedPayment, mc); + for (int i = 0; i < totalTerm; i++) { + final int dayIndex = i + 1; + final RateSegment seg = segmentForDay(dayIndex); + // At segment boundary, reset balance to segment's net disbursement + if (seg != null && seg.startDayIndex() == dayIndex) { + prevBalance = seg.netDisbursementAtSplit(); + } + final BigDecimal eir = seg != null ? seg.effectiveInterestRate() : effectiveInterestRate; + final BigDecimal payment = seg != null ? seg.expectedPaymentAmount() : expectedPaymentAmount.getAmount(); + final BigDecimal onePlusRate = BigDecimal.ONE.add(eir, mc); + final BigDecimal balance = prevBalance.multiply(onePlusRate, mc).subtract(payment, mc); balances.add(balance); - expectedAmortizations.add(balance.add(expectedPayment, mc).subtract(prevBalance, mc)); + expectedAmortizations.add(balance.add(payment, mc).subtract(prevBalance, mc)); prevBalance = balance; } return new BalancesAndAmortizations(balances, expectedAmortizations); } private PaymentAnalysis analyzePayments(final List payments, final int appliedCount) { - final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); BigDecimal shortfall = BigDecimal.ZERO; BigDecimal excess = BigDecimal.ZERO; for (int i = 0; i < appliedCount; i++) { + final BigDecimal expectedPayment = expectedPaymentForDay(i + 1); final BigDecimal diff = payments.get(i).subtract(expectedPayment, mc); if (diff.signum() > 0) { excess = excess.add(diff, mc); @@ -386,10 +511,10 @@ private PaymentAnalysis analyzePayments(final List payments, final i /** Cursor-based: each payment consumes {@code actualPayment/expectedPayment} periods of expected amortization. */ private List computeActualAmortizations(final List expectedAmortizations, final List payments, final int appliedCount) { - final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); final List result = new ArrayList<>(appliedCount); BigDecimal cursor = BigDecimal.ZERO; for (int i = 0; i < appliedCount; i++) { + final BigDecimal expectedPayment = expectedPaymentForDay(i + 1); final BigDecimal periodsConsumed = payments.get(i).divide(expectedPayment, mc); result.add(consumeExpectedAmortization(expectedAmortizations, cursor, periodsConsumed)); cursor = cursor.add(periodsConsumed, mc); @@ -418,14 +543,13 @@ private BigDecimal consumeExpectedAmortization(final List expectedAm } private List computeRunningExpectedPayments(final BigDecimal excess) { - final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); - final List running = new ArrayList<>(loanTerm); - for (int i = 0; i < loanTerm; i++) { - running.add(expectedPayment); + final int totalTerm = effectiveTotalTerm(); + final List running = new ArrayList<>(totalTerm); + for (int i = 0; i < totalTerm; i++) { + running.add(expectedPaymentForDay(i + 1)); } - BigDecimal remainingExcess = excess; - for (int i = loanTerm - 1; i >= 0 && remainingExcess.signum() > 0; i--) { + for (int i = totalTerm - 1; i >= 0 && remainingExcess.signum() > 0; i--) { final BigDecimal reduction = remainingExcess.min(running.get(i)); running.set(i, running.get(i).subtract(reduction, mc)); remainingExcess = remainingExcess.subtract(reduction, mc); @@ -435,22 +559,21 @@ private List computeRunningExpectedPayments(final BigDecimal excess) private BigDecimal buildTailPeriodsAndComputeNpv(final List tailPayments, final BigDecimal shortfall, final int appliedCount) { - final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + final int totalTerm = effectiveTotalTerm(); BigDecimal tailNpv = BigDecimal.ZERO; BigDecimal remaining = shortfall; int tailIndex = 0; while (remaining.signum() > 0) { - final int periodNo = loanTerm + tailIndex + 1; + final int periodNo = totalTerm + tailIndex + 1; + final BigDecimal tailExpectedPayment = expectedPaymentForDay(totalTerm); // use last segment's payment final long dl = paymentsLeft(periodNo, appliedCount); - final BigDecimal df = safeDiscountFactor(dl); - final BigDecimal forecast = remaining.min(expectedPayment); + final BigDecimal df = safeDiscountFactor(dl, totalTerm); + final BigDecimal forecast = remaining.min(tailExpectedPayment); final BigDecimal npv = MathUtil.negativeToZero(forecast.multiply(df, mc)); - - final long count = (long) loanTerm + appliedCount - periodNo; + final long count = (long) totalTerm + appliedCount - periodNo; tailNpv = tailNpv.add(npv, mc); tailPayments.add(new ProjectedPayment(periodNo, expectedDisbursementDate.plusDays(periodNo), count, dl, null, money(forecast), df, money(npv), null, null, money(BigDecimal.ZERO), null, null, null, null)); - remaining = remaining.subtract(forecast, mc); tailIndex++; } @@ -460,17 +583,19 @@ private BigDecimal buildTailPeriodsAndComputeNpv(final List ta /** {@code totalNetAmortization = -netDisbursementAmount + sum(npvSource × DF) + tailNpv} */ private BigDecimal computeTotalNetAmortization(final List payments, final List runningExpected, final int appliedCount, final BigDecimal tailNpv) { + final int totalTerm = effectiveTotalTerm(); BigDecimal total = netDisbursementAmount.getAmount().negate(); - for (int i = 0; i < loanTerm; i++) { + for (int i = 0; i < totalTerm; i++) { final BigDecimal npvSource = payments.get(i) != null ? payments.get(i) : runningExpected.get(i); - final BigDecimal df = safeDiscountFactor(paymentsLeft(i + 1, appliedCount)); + final BigDecimal df = safeDiscountFactor(paymentsLeft(i + 1, appliedCount), i + 1); total = total.add(npvSource.multiply(df, mc), mc); } return total.add(tailNpv, mc); } - private BigDecimal safeDiscountFactor(final long paymentsLeft) { - final BigDecimal df = TvmFunctions.discountFactor(effectiveInterestRate, paymentsLeft, mc); + private BigDecimal safeDiscountFactor(final long paymentsLeft, final int dayIndex) { + final BigDecimal eir = eirForDay(dayIndex); + final BigDecimal df = TvmFunctions.discountFactor(eir, paymentsLeft, mc); return df.signum() <= 0 ? BigDecimal.ONE : df; } @@ -478,6 +603,46 @@ private long paymentsLeft(final int periodNumber, final int appliedCount) { return Math.max(0L, (long) periodNumber - appliedCount); } + private RateSegment segmentForDay(final int dayIndex) { + if (rateSegments == null || rateSegments.isEmpty()) { + return null; + } + RateSegment active = null; + for (final RateSegment seg : rateSegments) { + if (seg.startDayIndex() <= dayIndex) { + active = seg; + } else { + break; + } + } + return active; + } + + private BigDecimal eirForDay(final int dayIndex) { + final RateSegment seg = segmentForDay(dayIndex); + return seg != null ? seg.effectiveInterestRate() : effectiveInterestRate; + } + + private BigDecimal expectedPaymentForDay(final int dayIndex) { + final RateSegment seg = segmentForDay(dayIndex); + return seg != null ? seg.expectedPaymentAmount() : expectedPaymentAmount.getAmount(); + } + + private BalancesAndAmortizations computeBaseBalancesUpTo(final int upToDayIndex) { + final BigDecimal onePlusRate = BigDecimal.ONE.add(effectiveInterestRate, mc); + final BigDecimal basePayment = expectedPaymentAmount.getAmount(); + final List balances = new ArrayList<>(upToDayIndex); + final List expectedAmortizations = new ArrayList<>(upToDayIndex); + BigDecimal prevBalance = netDisbursementAmount.getAmount(); + for (int i = 0; i < upToDayIndex; i++) { + final BigDecimal balance = prevBalance.multiply(onePlusRate, mc).subtract(basePayment, mc); + balances.add(balance); + expectedAmortizations.add(balance.add(basePayment, mc).subtract(prevBalance, mc)); + prevBalance = balance; + } + return new BalancesAndAmortizations(balances, expectedAmortizations); + } + private Money money(final BigDecimal amount) { return Money.of(currency, amount, mc); } @@ -491,6 +656,10 @@ private record PaymentAnalysis(BigDecimal shortfall, BigDecimal excess) { public record AppliedPayment(LocalDate date, BigDecimal amount) { } + public record RateSegment(int startDayIndex, BigDecimal expectedPaymentAmount, int segmentTerm, BigDecimal effectiveInterestRate, + BigDecimal netDisbursementAtSplit, BigDecimal discountAtSplit) { + } + public static String getModelVersion() { return MODEL_VERSION; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java index 34d8a0e18bd..4522c3f6505 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java @@ -119,8 +119,14 @@ public static BigDecimal rate(final int nper, final BigDecimal pmt, final BigDec } /** - * Linear approximation for the initial Newton-Raphson guess: {@code r ≈ 2·(pmt·n + pv) / (pv·n)}. Falls back to - * {@value #DEFAULT_GUESS} if the estimate is non-positive. + * Linear approximation for the initial Newton-Raphson guess: {@code r ≈ 2·(pmt·n + pv) / (pv·n)}. + * + *

    + * When total payments exceed the present value (typical for loans with origination fees), the formula yields a + * negative estimate. Its absolute value is still a good approximation of the periodic rate — it equals + * {@code 2·interest / (pv·n)} — so we return {@code |estimate|} instead of a fixed default. This avoids + * catastrophic divergence in Newton-Raphson when {@code nper} is large (e.g., daily-payment loans with thousands of + * periods), where the old default of 0.01 caused {@code (1+0.01)^nper} to explode. */ private static BigDecimal estimateInitialGuess(final int nper, final BigDecimal pmt, final BigDecimal pv, final MathContext mc) { final BigDecimal n = BigDecimal.valueOf(nper); @@ -129,7 +135,10 @@ private static BigDecimal estimateInitialGuess(final int nper, final BigDecimal return DEFAULT_GUESS; } final BigDecimal estimate = pmt.multiply(n, mc).add(pv, mc).multiply(TWO, mc).divide(pvTimesN, mc); - return estimate.signum() > 0 ? estimate : DEFAULT_GUESS; + if (estimate.signum() == 0) { + return DEFAULT_GUESS; + } + return estimate.abs(); } /** diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java new file mode 100644 index 00000000000..1046c9f84c4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanPeriodPaymentRateChangeData.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +public record WorkingCapitalLoanPeriodPaymentRateChangeData(Long id, Long loanId, LocalDate effectiveDate, BigDecimal previousRate, + BigDecimal newRate, boolean reversed, LocalDate reversedOnDate, OffsetDateTime createdDate) { + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java new file mode 100644 index 00000000000..71957075f81 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanPeriodPaymentRateChange.java @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_wc_loan_period_payment_rate_change") +public class WorkingCapitalLoanPeriodPaymentRateChange extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wc_loan_id", nullable = false) + private WorkingCapitalLoan workingCapitalLoan; + + @Column(name = "effective_date", nullable = false) + private LocalDate effectiveDate; + + @Column(name = "previous_rate", scale = 6, precision = 19, nullable = false) + private BigDecimal previousRate; + + @Column(name = "new_rate", scale = 6, precision = 19, nullable = false) + private BigDecimal newRate; + + @Column(name = "is_reversed", nullable = false) + private boolean reversed; + + @Column(name = "reversed_on_date") + private LocalDate reversedOnDate; + + @Version + private int version; + + public static WorkingCapitalLoanPeriodPaymentRateChange create(final WorkingCapitalLoan loan, final LocalDate effectiveDate, + final BigDecimal previousRate, final BigDecimal newRate) { + final WorkingCapitalLoanPeriodPaymentRateChange change = new WorkingCapitalLoanPeriodPaymentRateChange(); + change.workingCapitalLoan = loan; + change.effectiveDate = effectiveDate; + change.previousRate = previousRate; + change.newRate = newRate; + change.reversed = false; + return change; + } + + public void reverse(final LocalDate reversalDate) { + this.reversed = true; + this.reversedOnDate = reversalDate; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UpdateRateWorkingCapitalLoanCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UpdateRateWorkingCapitalLoanCommandHandler.java new file mode 100644 index 00000000000..416bcaa4662 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UpdateRateWorkingCapitalLoanCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "WORKINGCAPITALLOAN", action = "UPDATERATE") +public class UpdateRateWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.updatePeriodPaymentRate(command.entityId(), command); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanPeriodPaymentRateChangeRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanPeriodPaymentRateChangeRepository.java new file mode 100644 index 00000000000..f1c89773d79 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanPeriodPaymentRateChangeRepository.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.repository; + +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodPaymentRateChange; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface WorkingCapitalLoanPeriodPaymentRateChangeRepository + extends JpaRepository { + + List findByWorkingCapitalLoanIdOrderByIdDesc(Long loanId); + + List findByWorkingCapitalLoanIdAndReversedFalse(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java index 5d436c1e7c5..f697dcf9541 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java @@ -44,6 +44,7 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.loanaccount.domain.ExpectedDisbursementDateValidator; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; @@ -91,6 +92,9 @@ public class WorkingCapitalLoanDataValidator { WorkingCapitalLoanConstants.paymentDetailsParamName, WorkingCapitalLoanConstants.externalIdParameterName)); private static final Set CREDIT_BALANCE_REFUND_SUPPORTED_PARAMETERS = new HashSet<>(REPAYMENT_SUPPORTED_PARAMETERS); + private static final Set UPDATE_RATE_SUPPORTED_PARAMETERS = new HashSet<>( + Arrays.asList("locale", WorkingCapitalLoanConstants.periodPaymentRateParamName, WorkingCapitalLoanConstants.noteParamName)); + private static final int NOTE_MAX_LENGTH = 1000; private static final int EXTERNAL_ID_MAX_LENGTH = 100; private static final int PAYMENT_DETAIL_STRING_MAX_LENGTH = 50; @@ -594,6 +598,58 @@ public void validateCreditBalanceRefund(final String json, final WorkingCapitalL throwExceptionIfValidationWarningsExist(dataValidationErrors); } + public void validateUpdatePeriodPaymentRate(final String json, final WorkingCapitalLoan loan) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + if (loan.getLoanStatus() != LoanStatus.ACTIVE) { + throw new PlatformApiDataValidationException("validation.msg.wc.loan.rate.change.not.allowed", + "Period payment rate change is allowed only for active loans", "loanStatus"); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_RATE_SUPPORTED_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(WorkingCapitalLoanConstants.RESOURCE_NAME); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final BigDecimal periodPaymentRate = this.fromApiJsonHelper + .extractBigDecimalNamed(WorkingCapitalLoanConstants.periodPaymentRateParamName, element, new HashSet<>()); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.periodPaymentRateParamName).value(periodPaymentRate).notNull() + .positiveAmount(); + + if (periodPaymentRate != null) { + final BigDecimal previousRate = loan.getLoanProductRelatedDetails().getPeriodPaymentRate(); + if (previousRate != null && previousRate.compareTo(periodPaymentRate) == 0) { + + throw new PlatformApiDataValidationException("validation.msg.wc.loan.rate.change.same.rate", + "New period payment rate is the same as the current rate", WorkingCapitalLoanConstants.periodPaymentRateParamName); + } + + if (loan.getLoanProduct() != null && loan.getLoanProduct().getMinMaxConstraints() != null) { + final BigDecimal minRate = loan.getLoanProduct().getMinMaxConstraints().getMinPeriodPaymentRate(); + final BigDecimal maxRate = loan.getLoanProduct().getMinMaxConstraints().getMaxPeriodPaymentRate(); + if (minRate != null && periodPaymentRate.compareTo(minRate) < 0) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.periodPaymentRateParamName) + .failWithCode("rate.below.product.minimum"); + } + if (maxRate != null && periodPaymentRate.compareTo(maxRate) > 0) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.periodPaymentRateParamName) + .failWithCode("rate.exceeds.product.maximum"); + } + } + } + + final String note = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.noteParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull() + .notExceedingLengthOf(NOTE_MAX_LENGTH); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java index b0448a73e73..460e7876129 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java @@ -51,11 +51,7 @@ public Optional readModel(final Long loanId, @Transactional public void writeModel(@NonNull final WorkingCapitalLoan loan, @NonNull final ProjectedAmortizationScheduleModel model) { final String jsonModel = parserService.toJson(model); - final ProjectedAmortizationLoanModel entity = repository.findByLoanId(loan.getId()).orElseGet(() -> { - final ProjectedAmortizationLoanModel newEntity = new ProjectedAmortizationLoanModel(); - newEntity.setLoan(loan); - return newEntity; - }); + final ProjectedAmortizationLoanModel entity = findOrCreateEntity(loan); entity.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); entity.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); entity.setJsonModel(jsonModel); @@ -63,4 +59,12 @@ public void writeModel(@NonNull final WorkingCapitalLoan loan, @NonNull final Pr repository.save(entity); } + private ProjectedAmortizationLoanModel findOrCreateEntity(final WorkingCapitalLoan loan) { + return repository.findByLoanId(loan.getId()).orElseGet(() -> { + final ProjectedAmortizationLoanModel newEntity = new ProjectedAmortizationLoanModel(); + newEntity.setLoan(loan); + return newEntity; + }); + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java index 645b71c6c92..3d76e9e11f7 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java @@ -35,4 +35,6 @@ public interface WorkingCapitalLoanAmortizationScheduleWriteService { void regenerateAmortizationScheduleOnUndoDisbursal(WorkingCapitalLoan loan); RepaymentAmortizationData applyRepayment(WorkingCapitalLoan loan, LocalDate transactionDate, BigDecimal repaymentAmount); + + void regenerateAmortizationScheduleOnRateChange(WorkingCapitalLoan loan, BigDecimal newRate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java index fba56f36acd..e2d6ec5b950 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java @@ -21,10 +21,13 @@ import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.Validate; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleCalculator; import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedPayment; import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleGenerateRequest; @@ -49,6 +52,8 @@ public class WorkingCapitalLoanAmortizationScheduleWriteServiceImpl implements W private final WorkingCapitalLoanRepository loanRepository; private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; + private final ProjectedAmortizationScheduleCalculator calculator; + private final ProjectedAmortizationScheduleModelParserService parserService; @Override public void generateAndSaveAmortizationSchedule(final Long loanId, final ProjectedAmortizationScheduleGenerateRequest request) { @@ -177,6 +182,43 @@ private BigDecimal sumRunningNpv(final ProjectedAmortizationScheduleModel model) return result; } + @Override + public void regenerateAmortizationScheduleOnRateChange(final WorkingCapitalLoan loan, final BigDecimal newRate) { + Validate.notNull(loan, "loan must not be null"); + Validate.notNull(newRate, "newRate must not be null"); + + final MathContext mc = MoneyHelper.getMathContext(); + final MonetaryCurrency currency = resolveCurrency(loan); + final ProjectedAmortizationScheduleModel model = scheduleRepositoryWrapper.readModel(loan.getId(), mc, currency) + .orElseThrow(() -> new IllegalStateException("Projected amortization schedule is not found for loan " + loan.getId())); + + final LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate(); + final LocalDate loanDisbursementDate = resolveLoanDisbursementDate(loan); + final int splitDayIndex = (int) ChronoUnit.DAYS.between(loanDisbursementDate, businessDate); + final LocalDate modelRateChangeDate = model.expectedDisbursementDate().plusDays(splitDayIndex); + + // Clear previous segments — the service auto-reverses previous rate changes, + // so each rate change starts fresh from the base schedule. + model.removeLastRateChange(); + + calculator.applyRateChange(model, newRate, modelRateChangeDate); + + scheduleRepositoryWrapper.writeModel(loan, model); + } + + private LocalDate resolveLoanDisbursementDate(final WorkingCapitalLoan loan) { + if (loan.getDisbursementDetails() != null && !loan.getDisbursementDetails().isEmpty()) { + final WorkingCapitalLoanDisbursementDetails detail = loan.getDisbursementDetails().getFirst(); + if (detail.getActualDisbursementDate() != null) { + return detail.getActualDisbursementDate(); + } + if (detail.getExpectedDisbursementDate() != null) { + return detail.getExpectedDisbursementDate(); + } + } + throw new IllegalStateException("Cannot determine disbursement date for loan " + loan.getId()); + } + private MonetaryCurrency resolveCurrency(final WorkingCapitalLoan loan) { if (loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getCurrency() != null) { return loan.getLoanProductRelatedDetails().getCurrency(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadService.java new file mode 100644 index 00000000000..bd7cec07918 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadService.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanPeriodPaymentRateChangeData; + +public interface WorkingCapitalLoanPeriodPaymentRateChangeReadService { + + List retrieveRateChangeHistory(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java new file mode 100644 index 00000000000..22ee040037f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanPeriodPaymentRateChangeData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodPaymentRateChange; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanPeriodPaymentRateChangeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class WorkingCapitalLoanPeriodPaymentRateChangeReadServiceImpl implements WorkingCapitalLoanPeriodPaymentRateChangeReadService { + + private final WorkingCapitalLoanPeriodPaymentRateChangeRepository repository; + + @Override + public List retrieveRateChangeHistory(final Long loanId) { + return repository.findByWorkingCapitalLoanIdOrderByIdDesc(loanId).stream().map(this::toData).toList(); + } + + private WorkingCapitalLoanPeriodPaymentRateChangeData toData(final WorkingCapitalLoanPeriodPaymentRateChange entity) { + return new WorkingCapitalLoanPeriodPaymentRateChangeData(entity.getId(), entity.getWorkingCapitalLoan().getId(), + entity.getEffectiveDate(), entity.getPreviousRate(), entity.getNewRate(), entity.isReversed(), entity.getReversedOnDate(), + entity.getCreatedDate().orElse(null)); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java index a63e9cc7d40..217506ce801 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java @@ -38,4 +38,6 @@ public interface WorkingCapitalLoanWritePlatformService { CommandProcessingResult makeRepayment(Long loanId, JsonCommand command); CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command); + + CommandProcessingResult updatePeriodPaymentRate(Long loanId, JsonCommand command); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 88d04977b07..d7035329626 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -58,11 +58,13 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanEvent; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanLifecycleStateMachine; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNote; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodPaymentRateChange; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanPeriodPaymentRateChangeRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionAllocationRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; @@ -93,6 +95,7 @@ public class WorkingCapitalLoanWritePlatformServiceImpl implements WorkingCapita private final InternalWorkingCapitalLoanPaymentService internalWorkingCapitalLoanPaymentService; private final CodeValueRepository codeValueRepository; private final BusinessEventNotifierService businessEventNotifierService; + private final WorkingCapitalLoanPeriodPaymentRateChangeRepository rateChangeRepository; @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { @@ -577,6 +580,57 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json .withClientId(loan.getClientId()).withLoanId(loanId).with(changes).build(); } + @Override + @Transactional + public CommandProcessingResult updatePeriodPaymentRate(final Long loanId, final JsonCommand command) { + final WorkingCapitalLoan loan = this.loanRepository.findById(loanId) + .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + this.validator.validateUpdatePeriodPaymentRate(command.json(), loan); + + final BigDecimal newRate = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.periodPaymentRateParamName, + command.parsedJson(), new HashSet<>()); + final BigDecimal previousRate = loan.getLoanProductRelatedDetails().getPeriodPaymentRate(); + + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + + final List activeChanges = this.rateChangeRepository + .findByWorkingCapitalLoanIdAndReversedFalse(loanId); + for (final WorkingCapitalLoanPeriodPaymentRateChange active : activeChanges) { + active.reverse(businessDate); + } + if (!activeChanges.isEmpty()) { + this.rateChangeRepository.saveAll(activeChanges); + } + + loan.getLoanProductRelatedDetails().setPeriodPaymentRate(newRate); + + final WorkingCapitalLoanPeriodPaymentRateChange rateChange = WorkingCapitalLoanPeriodPaymentRateChange.create(loan, businessDate, + previousRate, newRate); + this.rateChangeRepository.save(rateChange); + + try { + this.amortizationScheduleWriteService.regenerateAmortizationScheduleOnRateChange(loan, newRate); + } catch (IllegalStateException | IllegalArgumentException e) { + throw new PlatformApiDataValidationException("validation.msg.wc.loan.rate.change.calculation.failed", + "Rate change calculation failed: " + e.getMessage(), WorkingCapitalLoanConstants.periodPaymentRateParamName, e); + } + + final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); + createNote(noteText, loan); + this.loanRepository.saveAndFlush(loan); + + final Map changes = new LinkedHashMap<>(); + changes.put(WorkingCapitalLoanConstants.periodPaymentRateParamName, newRate); + changes.put("previousRate", previousRate); + if (StringUtils.isNotBlank(noteText)) { + changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); + } + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId) + .withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) + .withLoanId(loanId).with(changes).build(); + } + private PaymentDetail createAndPersistPaymentDetailFromCommand(final JsonCommand command, final Map changes) { final JsonElement paymentDetailsElement = command.jsonElement(WorkingCapitalLoanConstants.paymentDetailsParamName); if (paymentDetailsElement != null && paymentDetailsElement.isJsonNull()) { diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 0bd7d21f4d7..28a2ffd775a 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -55,4 +55,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0034_wc_loan_period_payment_rate_change.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0034_wc_loan_period_payment_rate_change.xml new file mode 100644 index 00000000000..a00d6d600cb --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0034_wc_loan_period_payment_rate_change.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATERATE_WORKINGCAPITALLOAN'; + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UNDORATECHANGE_WORKINGCAPITALLOAN'; + + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml index 18be331a22a..d54e0ec86bb 100644 --- a/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml +++ b/fineract-working-capital-loan/src/main/resources/jpa/static-weaving/module/fineract-working-capital-loan/persistence.xml @@ -170,6 +170,7 @@ org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation org.apache.fineract.portfolio.workingcapitalloannearbreach.domain.WorkingCapitalNearBreach + org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodPaymentRateChange org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProduct org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductConfigurableAttributes org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductPaymentAllocationRule diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java index e09a2cf63cb..d7db647ce6d 100644 --- a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java +++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java @@ -19,7 +19,11 @@ package org.apache.fineract.portfolio.workingcapitalloan.calc; 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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.MathContext; @@ -2261,6 +2265,86 @@ void testExcessPayment_term10_originationFee50_netDisbursement450_pay110() { 31.70); } + // ============================================================================================== + // applyRateChange tests — rate change mid-lifecycle with date gaps + // ============================================================================================== + + @Test + void testApplyRateChange_sameDayAsDisburse() { + final ProjectedAmortizationScheduleModel model = generateModel(); + model.applyRateChange(new BigDecimal("0.15"), EXPECTED_DISBURSEMENT_DATE); + + assertFalse(model.rateSegments().isEmpty()); + assertTrue(model.effectiveTotalTerm() > 0, "effective total term should be positive"); + } + + @Test + void testApplyRateChange_8daysAfterDisburse() { + final ProjectedAmortizationScheduleModel model = generateModel(); + final LocalDate rateChangeDate = EXPECTED_DISBURSEMENT_DATE.plusDays(8); + model.applyRateChange(new BigDecimal("0.15"), rateChangeDate); + + assertFalse(model.rateSegments().isEmpty()); + assertTrue(model.effectiveTotalTerm() > 0, "effective total term should be positive"); + } + + @Test + void testApplyRateChange_twiceWithDateGap() { + final ProjectedAmortizationScheduleModel model = generateModel(); + + model.applyRateChange(new BigDecimal("0.15"), EXPECTED_DISBURSEMENT_DATE); + assertNotNull(model.rateSegments()); + assertFalse(model.rateSegments().isEmpty()); + + final LocalDate secondChangeDate = EXPECTED_DISBURSEMENT_DATE.plusDays(8); + model.applyRateChange(new BigDecimal("0.11"), secondChangeDate); + + assertTrue(model.effectiveTotalTerm() > 0, "effective total term should be positive"); + } + + @Test + void testApplyRateChange_twiceWithDateGapAndPayment() { + final ProjectedAmortizationScheduleModel model = generateModel(); + + model.applyPayment(EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("500")); + + final LocalDate rateChangeDate = EXPECTED_DISBURSEMENT_DATE.plusDays(8); + model.applyRateChange(new BigDecimal("0.15"), rateChangeDate); + + assertTrue(model.effectiveTotalTerm() > 0, "effective total term should be positive"); + } + + @Test + void testApplyRateChange_nearEndOfTerm() { + final ProjectedAmortizationScheduleModel model = generateModel(); + final LocalDate rateChangeDate = EXPECTED_DISBURSEMENT_DATE.plusDays(195); + model.applyRateChange(new BigDecimal("0.15"), rateChangeDate); + + assertTrue(model.effectiveTotalTerm() > 0, "effective total term should be positive"); + } + + @Test + void testApplyRateChange_pastEndOfTerm() { + final ProjectedAmortizationScheduleModel model = generateModel(); + final int originalTerm = model.loanTerm(); + final LocalDate rateChangeDate = EXPECTED_DISBURSEMENT_DATE.plusDays(250); + + model.applyRateChange(new BigDecimal("0.15"), rateChangeDate); + + // Past-term rate change should succeed — segment starts clamped at loanTerm + assertFalse(model.rateSegments().isEmpty(), "should have a rate segment"); + assertEquals(originalTerm, model.rateSegments().getFirst().startDayIndex(), "segment should start at base loanTerm when past-term"); + assertTrue(model.effectiveTotalTerm() > originalTerm, "effective term should extend beyond base term"); + } + + @Test + void testApplyRateChange_beforeDisburseDate() { + final ProjectedAmortizationScheduleModel model = generateModel(); + assertThrows(IllegalArgumentException.class, () -> { + model.applyRateChange(new BigDecimal("0.15"), EXPECTED_DISBURSEMENT_DATE.minusDays(1)); + }); + } + private ProjectedAmortizationScheduleModel generateModel() { final ProjectedAmortizationScheduleModel model = calculator.generateModel(ORIGINATION_FEE, NET_DISBURSEMENT, TPV, RATE, DAY_COUNT, EXPECTED_DISBURSEMENT_DATE, MC, CURRENCY); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignWorkingCapitalLoanHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignWorkingCapitalLoanHelper.java new file mode 100644 index 00000000000..6eb70fe0ac6 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignWorkingCapitalLoanHelper.java @@ -0,0 +1,100 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client.feign.helpers; + +import static org.apache.fineract.client.feign.util.FeignCalls.ok; + +import java.util.List; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.CommandProcessingResult; +import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdResponse; +import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; +import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdDiscountRequest; +import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRateRequest; +import org.apache.fineract.client.models.WorkingCapitalLoanPeriodPaymentRateChangeData; + +public class FeignWorkingCapitalLoanHelper { + + private final FineractFeignClient fineractClient; + + public FeignWorkingCapitalLoanHelper(FineractFeignClient fineractClient) { + this.fineractClient = fineractClient; + } + + public Long submitApplication(PostWorkingCapitalLoansRequest request) { + PostWorkingCapitalLoansResponse response = ok( + () -> fineractClient.workingCapitalLoans().submitWorkingCapitalLoanApplication(request)); + return response.getResourceId(); + } + + public Long approve(Long loanId, PostWorkingCapitalLoansLoanIdRequest request) { + PostWorkingCapitalLoansLoanIdResponse result = ok( + () -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "approve", request)); + return result.getResourceId(); + } + + public Long disburse(Long loanId, PostWorkingCapitalLoansLoanIdRequest request) { + PostWorkingCapitalLoansLoanIdResponse result = ok( + () -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "disburse", request)); + return result.getResourceId(); + } + + public Long undoDisbursal(Long loanId, PostWorkingCapitalLoansLoanIdRequest request) { + PostWorkingCapitalLoansLoanIdResponse result = ok( + () -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "undodisbursal", request)); + return result.getResourceId(); + } + + public void undoApproval(Long loanId, PostWorkingCapitalLoansLoanIdRequest request) { + ok(() -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "undoapproval", request)); + } + + public void delete(Long loanId) { + ok(() -> fineractClient.workingCapitalLoans().deleteWorkingCapitalLoanApplication(loanId)); + } + + public GetWorkingCapitalLoansLoanIdResponse getLoanDetails(Long loanId) { + return ok(() -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + } + + public Long updateDiscount(Long loanId, PutWorkingCapitalLoansLoanIdDiscountRequest request) { + return ok(() -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanDiscountById(loanId, request)).getResourceId(); + } + + public CommandProcessingResult updateRate(Long loanId, PutWorkingCapitalLoansLoanIdRateRequest request) { + return ok(() -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateById(loanId, request)); + } + + public CallFailedRuntimeException updateRateExpectingError(Long loanId, PutWorkingCapitalLoansLoanIdRateRequest request) { + try { + ok(() -> fineractClient.workingCapitalLoans().updateWorkingCapitalLoanRateById(loanId, request)); + throw new AssertionError("Expected rate update to fail but it succeeded"); + } catch (final CallFailedRuntimeException e) { + return e; + } + } + + public List getRateChangeHistory(Long loanId) { + return ok(() -> fineractClient.workingCapitalLoans().getWorkingCapitalLoanRateChangeHistoryById(loanId)); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java new file mode 100644 index 00000000000..3535437a22e --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client.feign.modules; + +import java.math.BigDecimal; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; +import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRateRequest; + +public final class WorkingCapitalLoanRequestBuilders { + + private static final String LOCALE = "en"; + private static final String DATE_FORMAT = "dd MMMM yyyy"; + + private WorkingCapitalLoanRequestBuilders() {} + + public static PostWorkingCapitalLoansRequest submitApplication(Long clientId, Long productId, BigDecimal principal, + BigDecimal periodPaymentRate, String submittedOnDate, String expectedDisbursementDate) { + return new PostWorkingCapitalLoansRequest().clientId(clientId).productId(productId).principalAmount(principal) + .periodPaymentRate(periodPaymentRate).submittedOnDate(submittedOnDate).expectedDisbursementDate(expectedDisbursementDate) + .totalPayment(BigDecimal.valueOf(100000)).locale(LOCALE).dateFormat(DATE_FORMAT); + } + + public static PostWorkingCapitalLoansLoanIdRequest approve(String approvedOnDate, BigDecimal approvedAmount, + String expectedDisbursementDate) { + return new PostWorkingCapitalLoansLoanIdRequest().approvedOnDate(approvedOnDate).approvedLoanAmount(approvedAmount) + .expectedDisbursementDate(expectedDisbursementDate).locale(LOCALE).dateFormat(DATE_FORMAT); + } + + public static PostWorkingCapitalLoansLoanIdRequest disburse(String actualDisbursementDate, BigDecimal transactionAmount) { + return new PostWorkingCapitalLoansLoanIdRequest().actualDisbursementDate(actualDisbursementDate) + .transactionAmount(transactionAmount).locale(LOCALE).dateFormat(DATE_FORMAT); + } + + public static PostWorkingCapitalLoansLoanIdRequest undoDisbursal() { + return new PostWorkingCapitalLoansLoanIdRequest().locale(LOCALE).dateFormat(DATE_FORMAT); + } + + public static PostWorkingCapitalLoansLoanIdRequest emptyCommand() { + return new PostWorkingCapitalLoansLoanIdRequest().locale(LOCALE).dateFormat(DATE_FORMAT); + } + + public static PutWorkingCapitalLoansLoanIdRateRequest updateRate(BigDecimal newRate) { + return new PutWorkingCapitalLoansLoanIdRateRequest().periodPaymentRate(newRate).locale(LOCALE); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignWorkingCapitalLoanRateChangeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignWorkingCapitalLoanRateChangeTest.java new file mode 100644 index 00000000000..c503be6f2e6 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignWorkingCapitalLoanRateChangeTest.java @@ -0,0 +1,233 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client.feign.tests; + +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 java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse; +import org.apache.fineract.client.models.WorkingCapitalLoanPeriodPaymentRateChangeData; +import org.apache.fineract.integrationtests.client.FeignIntegrationTest; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignWorkingCapitalLoanHelper; +import org.apache.fineract.integrationtests.client.feign.modules.WorkingCapitalLoanRequestBuilders; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductTestBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class FeignWorkingCapitalLoanRateChangeTest extends FeignIntegrationTest { + + private FeignWorkingCapitalLoanHelper wcLoanHelper; + private FeignClientHelper clientHelper; + private WorkingCapitalLoanProductHelper productHelper; + private FeignBusinessDateHelper businessDateHelper; + + private Long clientId; + + private final List createdLoanIds = new ArrayList<>(); + private final List createdProductIds = new ArrayList<>(); + + @BeforeAll + void setupHelpers() { + wcLoanHelper = new FeignWorkingCapitalLoanHelper(fineractClient()); + clientHelper = new FeignClientHelper(fineractClient()); + productHelper = new WorkingCapitalLoanProductHelper(); + businessDateHelper = new FeignBusinessDateHelper(fineractClient()); + clientId = clientHelper.createClient(); + } + + @AfterAll + void cleanupEntities() { + for (final Long loanId : createdLoanIds) { + if (loanId == null) { + continue; + } + try { + wcLoanHelper.undoDisbursal(loanId, WorkingCapitalLoanRequestBuilders.undoDisbursal()); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup: loan may not be in disbursed state + } + try { + wcLoanHelper.undoApproval(loanId, WorkingCapitalLoanRequestBuilders.emptyCommand()); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup: loan may not be in approved state + } + try { + wcLoanHelper.delete(loanId); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup: loan may already be deleted or in non-deletable state + } + } + createdLoanIds.clear(); + createdProductIds.clear(); + } + + @Test + void testUpdateRateOnActiveLoan() { + Long loanId = createAndDisburseLoan(BigDecimal.valueOf(5000), BigDecimal.valueOf(18)); + + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(17))); + + GetWorkingCapitalLoansLoanIdResponse loan = wcLoanHelper.getLoanDetails(loanId); + assertNotNull(loan); + assertEquals(0, BigDecimal.valueOf(17).compareTo(loan.getPeriodPaymentRate())); + } + + @Test + void testRateChangeHistoryIsRecorded() { + Long loanId = createAndDisburseLoan(BigDecimal.valueOf(5000), BigDecimal.valueOf(18)); + + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(17))); + + List history = wcLoanHelper.getRateChangeHistory(loanId); + assertFalse(history.isEmpty(), "Rate change history should not be empty"); + + WorkingCapitalLoanPeriodPaymentRateChangeData change = history.getFirst(); + assertEquals(0, BigDecimal.valueOf(18).compareTo(change.getPreviousRate())); + assertEquals(0, BigDecimal.valueOf(17).compareTo(change.getNewRate())); + assertFalse(change.getReversed()); + } + + @Test + void testRateChangeNotAllowedOnNonActiveLoan() { + Long productId = createProduct(); + String today = Utils.dateFormatter.format(Utils.getLocalDateOfTenant()); + Long loanId = submitAndTrack(clientId, productId, BigDecimal.valueOf(5000), BigDecimal.valueOf(18), today); + + CallFailedRuntimeException exception = wcLoanHelper.updateRateExpectingError(loanId, + WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(17))); + assertTrue(exception.getStatus() >= 400, + "Rate change on non-active loan should fail with 4xx status, got: " + exception.getStatus()); + } + + @Test + void testMultipleRateChangesAutoReversesPrevious() { + Long loanId = createAndDisburseLoan(BigDecimal.valueOf(5000), BigDecimal.valueOf(18)); + + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(17))); + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(15))); + + List history = wcLoanHelper.getRateChangeHistory(loanId); + assertEquals(2, history.size(), "Should have 2 rate change records"); + + // Most recent (15%) should be active, previous (17%) should be auto-reversed + WorkingCapitalLoanPeriodPaymentRateChangeData latestChange = history.get(0); + WorkingCapitalLoanPeriodPaymentRateChangeData firstChange = history.get(1); + assertFalse(latestChange.getReversed(), "Latest rate change should be active"); + assertTrue(firstChange.getReversed(), "Previous rate change should be auto-reversed"); + + GetWorkingCapitalLoansLoanIdResponse loan = wcLoanHelper.getLoanDetails(loanId); + assertEquals(0, BigDecimal.valueOf(15).compareTo(loan.getPeriodPaymentRate())); + } + + @Test + void testMultipleRateChangesOnDifferentBusinessDates() { + businessDateHelper.runAt("2026-01-01", () -> { + Long clientForTest = clientHelper.createClient("01 January 2026"); + Long loanId = createAndDisburseLoanOnDate(clientForTest, BigDecimal.valueOf(50000), BigDecimal.valueOf(18), "01 January 2026"); + + // First rate change: 18 → 15 on Jan 1 + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(15))); + + // Advance business date by 8 days + businessDateHelper.updateBusinessDate("BUSINESS_DATE", "2026-01-09"); + + // Second rate change: 15 → 11 on Jan 9 + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(11))); + + GetWorkingCapitalLoansLoanIdResponse loan = wcLoanHelper.getLoanDetails(loanId); + assertEquals(0, BigDecimal.valueOf(11).compareTo(loan.getPeriodPaymentRate()), + "Rate should be updated to 11 after second rate change"); + + List history = wcLoanHelper.getRateChangeHistory(loanId); + assertEquals(2, history.size(), "Should have 2 rate change records"); + + // Latest (11%) should be active, first (15%) should be auto-reversed + assertFalse(history.get(0).getReversed(), "Latest rate change should be active"); + assertTrue(history.get(1).getReversed(), "Previous rate change should be auto-reversed"); + }); + } + + @Test + void testRateChangePastEndOfTermSucceeds() { + // Use a small principal with high rate to create a very short-term loan, + // then advance past term. The rate change should succeed — the segment starts + // at the base term end with the remaining principal as balance. + businessDateHelper.runAt("2026-01-01", () -> { + Long clientForTest = clientHelper.createClient("01 January 2026"); + Long loanId = createAndDisburseLoanOnDate(clientForTest, BigDecimal.valueOf(100), BigDecimal.valueOf(18), "01 January 2026"); + + // Advance past the loan term — rate change at day 5 is past the schedule end + businessDateHelper.updateBusinessDate("BUSINESS_DATE", "2026-01-06"); + + wcLoanHelper.updateRate(loanId, WorkingCapitalLoanRequestBuilders.updateRate(BigDecimal.valueOf(15))); + + GetWorkingCapitalLoansLoanIdResponse loan = wcLoanHelper.getLoanDetails(loanId); + assertEquals(0, BigDecimal.valueOf(15).compareTo(loan.getPeriodPaymentRate()), + "Rate should be updated to 15 after past-term rate change"); + }); + } + + private Long createAndDisburseLoanOnDate(Long clientIdParam, BigDecimal principal, BigDecimal rate, String date) { + Long productId = createProduct(); + Long loanId = submitAndTrack(clientIdParam, productId, principal, rate, date); + wcLoanHelper.approve(loanId, WorkingCapitalLoanRequestBuilders.approve(date, principal, date)); + wcLoanHelper.disburse(loanId, WorkingCapitalLoanRequestBuilders.disburse(date, principal)); + return loanId; + } + + private Long createAndDisburseLoan(BigDecimal principal, BigDecimal rate) { + Long productId = createProduct(); + String today = Utils.dateFormatter.format(Utils.getLocalDateOfTenant()); + Long loanId = submitAndTrack(clientId, productId, principal, rate, today); + + wcLoanHelper.approve(loanId, WorkingCapitalLoanRequestBuilders.approve(today, principal, today)); + wcLoanHelper.disburse(loanId, WorkingCapitalLoanRequestBuilders.disburse(today, principal)); + + return loanId; + } + + private Long submitAndTrack(Long clientIdParam, Long productId, BigDecimal principal, BigDecimal rate, String date) { + Long loanId = wcLoanHelper.submitApplication( + WorkingCapitalLoanRequestBuilders.submitApplication(clientIdParam, productId, principal, rate, date, date)); + createdLoanIds.add(loanId); + return loanId; + } + + private Long createProduct() { + String uniqueName = "WCL Rate " + Utils.uniqueRandomStringGenerator("", 8); + String uniqueShortName = Utils.uniqueRandomStringGenerator("", 4); + Long productId = productHelper + .createWorkingCapitalLoanProduct( + new WorkingCapitalLoanProductTestBuilder().withName(uniqueName).withShortName(uniqueShortName).build()) + .getResourceId(); + createdProductIds.add(productId); + return productId; + } +}