From 388fbaf3bfafa83b5a906a3f676947d56e1af7c4 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Wed, 20 May 2026 19:14:38 +0300 Subject: [PATCH] FINERACT-2455: Enable externalId (user-provided or auto-generated) for WC discount fee on both disburse and DISCOUNTFEE endpoints --- .../test/helper/ErrorMessageHelper.java | 24 - .../WorkingCapitalLoanAccountStepDef.java | 440 ++++++++++-------- .../fineract/test/support/TestContextKey.java | 3 + .../features/WorkingCapitalDiscount.feature | 375 ++++++++++++--- .../WorkingCapitalLoanConstants.java | 1 + .../WorkingCapitalLoanApiResourceSwagger.java | 2 + .../WorkingCapitalLoanDataValidator.java | 109 +++-- ...ngCapitalLoanWritePlatformServiceImpl.java | 7 +- 8 files changed, 645 insertions(+), 316 deletions(-) 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 a5e198cb4ab..52dd8595e98 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 @@ -1102,34 +1102,10 @@ public static String undoDisbursalDisallowedFailure(String status) { return String.format("Transition LOAN_DISBURSAL_UNDO is not allowed from status %s", status); } - public static String discountAmountExceedFailure() { - return "Failed data validation due to: amount.cannot.exceed.created.discount."; - } - - public static String discountAmountExceedApprovedFailure() { - return "Failed data validation due to: amount.cannot.exceed.approved.discount."; - } - - public static String discountAlreadySetBeforeDisburseFailure() { - return "Discount was already set before disbursement and cannot be added again"; - } - - public static String discountDiffDateFromDisburseFailure() { - return "Failed data validation due to: transaction.date.must.be.equal.disbursement.date."; - } - public static String overrideDisallowedByProductFailure() { return "Failed data validation due to: override.not.allowed.by.product."; } - public static String discountExceedCreatedDiscountFailure() { - return "Failed data validation due to: amount.cannot.exceed.created.discount."; - } - - public static String discountExceedProductDiscountFailure() { - return "Failed data validation due to: amount.cannot.exceed.product.discount."; - } - public static String nearBreachCannotEnableWithoutBreachFailure() { return "Failed data validation due to: cannot.enable.near.breach.without.breach."; } 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 744b71b20b7..b9384fc3fd3 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 @@ -141,7 +141,7 @@ public void createWorkingCapitalLoanUsingCreatedProduct(final DataTable table) { .get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE); final Long loanProductId = productResponse.getResourceId(); - final String submittedOnDate = rawData.get(0); + final String submittedOnDate = rawData.getFirst(); final String expectedDisbursementDate = rawData.get(1); final String principal = rawData.get(2); final String totalPayment = rawData.get(3); @@ -195,7 +195,7 @@ public void creatingWorkingCapitalLoanWithLpOverridablesDisabledWillResultAnErro final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final String submittedOnDate = loanData.get(1); final String expectedDisbursementDate = loanData.get(2); final String principal = loanData.get(3); @@ -240,7 +240,7 @@ public void creatingAWorkingCapitalLoanWithPrincipalAmountGreaterThanWorkingCapi final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long clientId = extractClientId(); final Long loanProductId = resolveLoanProductId(loanProduct); final PostWorkingCapitalLoansRequest loansRequest = buildCreateLoanRequest(clientId, loanProductId, loanData); @@ -261,7 +261,7 @@ public void creatingAWorkingCapitalLoanWithPrincipalAmountSmallerThanWorkingCapi final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long clientId = extractClientId(); final Long loanProductId = resolveLoanProductId(loanProduct); final PostWorkingCapitalLoansRequest loansRequest = buildCreateLoanRequest(clientId, loanProductId, loanData); @@ -281,7 +281,7 @@ public void creatingAWorkingCapitalLoanWithMissingMandatoryFieldsWillResultAnErr final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final String submittedOnDate = loanData.get(1); final String expectedDisbursementDate = loanData.get(2); final String principal = loanData.get(3); @@ -406,7 +406,7 @@ public void createLoanWithBreachFromWCLPOverrideAllowedData(DataTable table) { final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long loanProductId = resolveLoanProductId(loanProduct); final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); testContext().set(TestContextKey.WORKING_CAPITAL_BREACH_ID, breachIdFromWCLP); @@ -418,7 +418,7 @@ public void createLoanWithBreachFromWCLPOverrideAllowedData(DataTable table) { public void createLoanWithBreachNearBreachFromWCLPOverrideAllowedData(DataTable table) { final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long loanProductId = resolveLoanProductId(loanProduct); final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); @@ -448,11 +448,12 @@ public void checkCreateWCLoanAccountBreachData() { @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(); + final Long loanId = loanResponse.getLoanId(); GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() .retrieveWorkingCapitalLoanById(loanId); + Assertions.assertNotNull(loanProductResponse.getProduct()); final Long loanProductId = loanProductResponse.getProduct().getId(); final Long breachIdFromWCLP = getBreachIdFromWCLP(loanProductId); @@ -482,7 +483,7 @@ public void checkCreateWCLoanAccountBreachAndNearBreachOverrideData() { @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(); + final Long loanId = loanResponse.getLoanId(); GetWorkingCapitalLoansLoanIdResponse loanProductResponse = fineractClient.workingCapitalLoans() .retrieveWorkingCapitalLoanById(loanId); @@ -519,7 +520,7 @@ public void createLoanWithBreachOverrideDisallowedWithBreachFailure(final DataTa @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 loanProduct = loanData.getFirst(); final String submittedOnDate = loanData.get(1); final Long overrideBreachId = createBreachOverrideAndGetId(); @@ -546,7 +547,7 @@ public void createLoanWithBreachOverrideDisallowedWithBreachAndNearBreachFailure 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 loanProduct = loanData.getFirst(); final String submittedOnDate = loanData.get(1); final Long overrideBreachId = createBreachOverrideAndGetId(); @@ -594,7 +595,7 @@ public void createWorkingCapitalWithPeriodPaymentRateInvalidDataFailure(final St final DataTable table) { final List> data = table.asLists(); final List loanData = data.get(1); - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final String submittedOnDate = loanData.get(1); final PostWorkingCapitalLoansRequest loansRequest = createWorkingCapitalLoanAccountDefaultRequest(loanProduct, submittedOnDate) @@ -646,7 +647,7 @@ public void modifyWorkingCapitalLoanByExternalId(final DataTable table) { @Then("Changing submittedOnDate after expectedDisbursementDate results an error:") public void changingSubmittedOnDateAfterExpectedDisbursementDateResultsAnError(final DataTable table) { final List> data = table.asLists(); - final String submittedOnDate = data.get(1).get(0); + final String submittedOnDate = data.get(1).getFirst(); final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() .submittedOnDate(submittedOnDate); @@ -664,7 +665,7 @@ public void changingSubmittedOnDateAfterExpectedDisbursementDateResultsAnError(f @Then("Changing submittedOnDate after business date results an error:") public void changingSubmittedOnDateAfterBusinessDateResultsAnError(final DataTable table) { final List> data = table.asLists(); - final String submittedOnDate = data.get(1).get(0); + final String submittedOnDate = data.get(1).getFirst(); final String expectedDisbursementDate = data.get(1).get(1); final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() @@ -812,7 +813,7 @@ public void modifyingWithEmptyRequestResultsInAnError() { @Then("Modifying the working capital loan with future submittedOnDate results in an error:") public void modifyingWithFutureSubmittedOnDateResultsInAnError(final DataTable table) { final List> data = table.asLists(); - final String submittedOnDate = data.get(1).get(0); + final String submittedOnDate = data.get(1).getFirst(); final String expectedDisbursementDate = data.get(1).get(1); final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory.defaultModifyWorkingCapitalLoansRequest() @@ -840,7 +841,7 @@ public void adminAttemptsToModifyNonExistentWorkingCapitalLoan() { @Then("Modifying the working capital loan that is Disbursed in Active state results in an error") public void modifyingDisbursedWithActiveStateLoanResultsInAnError() { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); + final Long loanId = loanResponse.getLoanId(); final PutWorkingCapitalLoansLoanIdRequest modifyRequest = workingCapitalLoanRequestFactory .defaultModifyWorkingCapitalLoansRequest(); @@ -933,55 +934,20 @@ public void approvalOfWorkingCapitalLoanResultsAnError(final String approveDate, log.info("Verified working capital loan approval failed with expected error"); } - @When("Admin failed to approve the working capital loan on {string} with {string} amount and expected disbursement date on {string} with {string} exceeded product discount amount") - public void approveWorkingCapitalLoanWithExceededProductDiscountFailure(final String approveDate, final String approvedAmount, - final String expectedDisbursementDate, final String discountAmount) { - final PostWorkingCapitalLoansLoanIdRequest approveRequest = workingCapitalLoanRequestFactory - .defaultWorkingCapitalLoanApproveRequest()// - .approvedOnDate(approveDate)// - .approvedLoanAmount(new BigDecimal(approvedAmount))// - .discountAmount(new BigDecimal(discountAmount))// - .expectedDisbursementDate(expectedDisbursementDate);// - - final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans() - .stateTransitionWorkingCapitalLoanById(getCreatedLoanId(), "approve", approveRequest)); - - assertThat(exception.getStatus()).as(ErrorMessageHelper.discountExceedProductDiscountFailure()).isEqualTo(400); - assertThat(exception.getDeveloperMessage()).contains(ErrorMessageHelper.discountExceedProductDiscountFailure()); - } - - @When("Admin failed to approve the working capital loan on {string} with {string} amount and expected disbursement date on {string} with {string} exceeded discount amount") - public void approveWorkingCapitalLoanWithExceededDiscountFailure(final String approveDate, final String approvedAmount, - final String expectedDisbursementDate, final String discountAmount) { - final PostWorkingCapitalLoansLoanIdRequest approveRequest = workingCapitalLoanRequestFactory - .defaultWorkingCapitalLoanApproveRequest()// - .approvedOnDate(approveDate)// - .approvedLoanAmount(new BigDecimal(approvedAmount))// - .discountAmount(new BigDecimal(discountAmount))// - .expectedDisbursementDate(expectedDisbursementDate);// - - final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans() - .stateTransitionWorkingCapitalLoanById(getCreatedLoanId(), "approve", approveRequest)); - - assertThat(exception.getStatus()).as(ErrorMessageHelper.discountAmountExceedFailure()).isEqualTo(400); - assertThat(exception.getDeveloperMessage()).contains(ErrorMessageHelper.discountAmountExceedFailure()); - } - - @When("Admin failed to approve the working capital loan on {string} with {string} amount and expected disbursement date on {string} with {string} discount amount due to override disallowed by product") - public void approveWorkingCapitalLoanWithDiscountOverrideDisallowedFailure(final String approveDate, final String approvedAmount, - final String expectedDisbursementDate, final String discountAmount) { + @Then("Approving the working capital loan on {string} with {string} amount and expected disbursement date on {string} with {string} discount amount results an error with the following data:") + public void approveWorkingCapitalLoanWithDiscountResultsAnError(final String approveDate, final String approvedAmount, + final String expectedDisbursementDate, final String discountAmount, final DataTable table) { final PostWorkingCapitalLoansLoanIdRequest approveRequest = workingCapitalLoanRequestFactory .defaultWorkingCapitalLoanApproveRequest()// .approvedOnDate(approveDate)// .approvedLoanAmount(new BigDecimal(approvedAmount))// .discountAmount(new BigDecimal(discountAmount))// - .expectedDisbursementDate(expectedDisbursementDate);// + .expectedDisbursementDate(expectedDisbursementDate); final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans() .stateTransitionWorkingCapitalLoanById(getCreatedLoanId(), "approve", approveRequest)); - assertThat(exception.getStatus()).as(ErrorMessageHelper.overrideDisallowedByProductFailure()).isEqualTo(400); - assertThat(exception.getDeveloperMessage()).contains(ErrorMessageHelper.overrideDisallowedByProductFailure()); + verifyRepaymentErrorWithTable(exception, table); } @When("Admin rejects the working capital loan on {string}") @@ -1025,13 +991,14 @@ public void undoApprovalWorkingCapitalLoan(final DataTable table) { @Then("Working Capital loan status will be {string}") public void loanWCStatus(String statusExpected) { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); + final Long loanId = loanResponse.getLoanId(); String resourceId = String.valueOf(loanId); GetWorkingCapitalLoansLoanIdResponse loanDetailsResponse = ok( () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); testContext().set(TestContextKey.LOAN_RESPONSE, loanDetailsResponse); + Assertions.assertNotNull(loanDetailsResponse.getStatus()); Long loanStatusActualValue = loanDetailsResponse.getStatus().getId(); LoanStatus loanStatusExpected = LoanStatus.valueOf(statusExpected); @@ -1088,7 +1055,7 @@ public void disburseWCLoanWithClassification(final String actualDisbursementDate @Then("Verify Working Capital loan disbursement was successful on {string} with {string} EUR transaction amount") public void checkDisbursementData(String actualDisbursementDate, String transactionAmount) { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); + final Long loanId = loanResponse.getLoanId(); GetWorkingCapitalLoansLoanIdResponse loanDetailsResponse = ok( () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); @@ -1114,31 +1081,97 @@ public void disburseWCLoanWithDiscount(String actualDisbursementDate, String tra checkChangesExpectedStatus(TestContextKey.LOAN_DISBURSE_RESPONSE, ACTIVE); } - @When("Admin failed to disburse the working capital loan on {string} with {string} amount with {string} exceeded discount amount") - public void disburseWorkingCapitalLoanWithExceededDiscountFailure(String actualDisbursementDate, String transactionAmount, - String discountAmount) { - String errorMessage = ErrorMessageHelper.discountAmountExceedApprovedFailure(); - disburseWorkingCapitalLoanFailure(actualDisbursementDate, transactionAmount, discountAmount, errorMessage); + @When("Admin successfully disburse the Working Capital loan on {string} with {string} EUR transaction amount and {string} discount amount and a random discountExternalId") + public void disburseWCLoanWithDiscountAndRandomDiscountExternalId(final String actualDisbursementDate, final String transactionAmount, + final String discountAmount) { + final String randomDiscountExternalId = Utils.randomStringGenerator("TestDiscountExtId_", 10); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_DISBURSE_DISCOUNT_EXTERNAL_ID_USER_GENERATED, randomDiscountExternalId); + + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .transactionAmount(new BigDecimal(transactionAmount)) // + .discountAmount(new BigDecimal(discountAmount)) // + .discountExternalId(randomDiscountExternalId); + testContext().set(TestContextKey.LOAN_DISBURSE_REQUEST, disburseRequest); + + executeStateTransition("disburse", disburseRequest, TestContextKey.LOAN_DISBURSE_RESPONSE, false); + verifyStateTransitionSuccess(TestContextKey.LOAN_DISBURSE_RESPONSE, "disbursement"); + checkChangesExpectedStatus(TestContextKey.LOAN_DISBURSE_RESPONSE, ACTIVE); + } + + @Then("Initiating disbursement on {string} with {string} EUR transaction amount and {string} discount amount reusing the previously shared discountExternalId on Working Capital loan results an error with the following data:") + public void initiateDisbursementReusingSharedDiscountExternalIdResultsAnError(final String actualDisbursementDate, + final String transactionAmount, final String discountAmount, final DataTable table) { + final String sharedDiscountExternalId = testContext() + .get(TestContextKey.WORKING_CAPITAL_LOAN_DISBURSE_DISCOUNT_EXTERNAL_ID_USER_GENERATED); + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .transactionAmount(new BigDecimal(transactionAmount)) // + .discountAmount(new BigDecimal(discountAmount)) // + .discountExternalId(sharedDiscountExternalId); + executeDisburseAndExpectError(disburseRequest, table); + } + + @Then("Initiating disbursement on {string} with {string} EUR transaction amount and {string} discount amount and discountExternalId {string} on Working Capital loan results an error with the following data:") + public void initiateDisbursementWithExplicitDiscountExternalIdResultsAnError(final String actualDisbursementDate, + final String transactionAmount, final String discountAmount, final String discountExternalId, final DataTable table) { + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .transactionAmount(new BigDecimal(transactionAmount)) // + .discountAmount(new BigDecimal(discountAmount)) // + .discountExternalId(discountExternalId); + executeDisburseAndExpectError(disburseRequest, table); + } + + @Then("Initiating disbursement on {string} with {string} EUR transaction amount and discountExternalId {string} without discountAmount on Working Capital loan results an error with the following data:") + public void initiateDisbursementWithDiscountExternalIdAndNoDiscountAmountResultsAnError(final String actualDisbursementDate, + final String transactionAmount, final String discountExternalId, final DataTable table) { + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .transactionAmount(new BigDecimal(transactionAmount)) // + .discountExternalId(discountExternalId); + executeDisburseAndExpectError(disburseRequest, table); } - @When("Admin failed to disburse the working capital loan on {string} with {string} amount with {string} exceeded product discount amount") - public void disburseWorkingCapitalLoanWithExceededProductDiscountFailure(String actualDisbursementDate, String transactionAmount, - String discountAmount) { - String errorMessage = ErrorMessageHelper.discountExceedProductDiscountFailure(); - disburseWorkingCapitalLoanFailure(actualDisbursementDate, transactionAmount, discountAmount, errorMessage); + @Then("Initiating disbursement on {string} with {string} EUR transaction amount and {string} discount amount using {string} for both externalId and discountExternalId on Working Capital loan results an error with the following data:") + public void initiateDisbursementWithSameExternalIdForBothResultsAnError(final String actualDisbursementDate, + final String transactionAmount, final String discountAmount, final String sharedExternalId, final DataTable table) { + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .transactionAmount(new BigDecimal(transactionAmount)) // + .discountAmount(new BigDecimal(discountAmount)) // + .externalId(sharedExternalId) // + .discountExternalId(sharedExternalId); + executeDisburseAndExpectError(disburseRequest, table); + } + + @Then("Disbursing the working capital loan on {string} with {string} amount and {string} discount amount results an error with the following data:") + public void disburseWorkingCapitalLoanWithDiscountResultsAnError(final String actualDisbursementDate, final String transactionAmount, + final String discountAmount, final DataTable table) { + final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory + .defaultWorkingCapitalLoanDisburseRequest() // + .actualDisbursementDate(actualDisbursementDate) // + .discountAmount(new BigDecimal(discountAmount)) // + .transactionAmount(new BigDecimal(transactionAmount)); + executeDisburseAndExpectError(disburseRequest, table); } - @When("Admin failed to disburse the working capital loan on {string} with {string} amount with {string} discount amount due to override disallowed by product") - public void disburseWorkingCapitalLoanWithDiscountOverrideDisallowedFailure(final String actualDisbursementDate, - final String transactionAmount, final String discountAmount) { - String errorMessage = ErrorMessageHelper.overrideDisallowedByProductFailure(); - disburseWorkingCapitalLoanFailure(actualDisbursementDate, transactionAmount, discountAmount, errorMessage); + private void executeDisburseAndExpectError(final PostWorkingCapitalLoansLoanIdRequest disburseRequest, final DataTable table) { + final Long loanId = getCreatedLoanId(); + final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans() + .stateTransitionWorkingCapitalLoanById(loanId, disburseRequest, Map.of("command", "disburse"))); + verifyRepaymentErrorWithTable(exception, table); } @Then("Verify Working Capital loan disbursement was successful") public void checkDisbursementData() { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.getLoanId(); + final Long loanId = loanResponse.getLoanId(); GetWorkingCapitalLoansLoanIdResponse loanDetailsResponse = ok( () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); @@ -1269,70 +1302,125 @@ public void addDiscountFeeWCLoanDisbursement(String discountAmount) { final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() // .relatedResourceId(lastDisbursementResponse.getResourceId()).transactionAmount(new BigDecimal(discountAmount)); - executeStateTransition("DISCOUNTFEE", request, "DISCOUNT", false); + executeStateTransition("DISCOUNTFEE", request, TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE, false); + } + + @When("Admin adds Discount fee with {string} amount and a random externalId on Working Capital loan account for last disbursement") + public void addDiscountFeeWCLoanDisbursementWithRandomExternalId(final String discountAmount) { + final PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); + final String randomExternalId = Utils.randomStringGenerator("TestDiscountFeeExtId_", 10); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED, randomExternalId); + + final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() // + .relatedResourceId(lastDisbursementResponse.getResourceId()) // + .transactionAmount(new BigDecimal(discountAmount)) // + .externalId(randomExternalId); + + executeStateTransition("DISCOUNTFEE", request, TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE, false); } - @And("Add Discount fee with {string} amount on Working Capital loan account failed due to already added discount before disbursement") - public void addDiscountFeeWCLoanAlreadyAddedFailure(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountAlreadySetBeforeDisburseFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("Adding Discount fee with {string} amount reusing the previously shared externalId on Working Capital loan account for last disbursement results an error with the following data:") + public void addDiscountFeeReusingSharedExternalIdResultsAnError(final String discountAmount, final DataTable table) { + final Long loanId = getCreatedLoanId(); + final PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); + final String sharedExternalId = testContext().get(TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED); + final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() // + .relatedResourceId(lastDisbursementResponse.getResourceId()) // + .transactionAmount(new BigDecimal(discountAmount)) // + .externalId(sharedExternalId); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "DISCOUNTFEE", request)); + verifyRepaymentErrorWithTable(exception, table); } - @And("Add Discount fee with {string} amount on Working Capital loan account failed due to date diff from disbursement date") - public void addDiscountFeeWCLoanDiffFromDisburseDateFailure(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountDiffDateFromDisburseFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("Active Discount Fee transactions contain the user-generated externalId from DISCOUNTFEE") + public void activeDiscountFeeTransactionsContainUserGeneratedExternalIdFromDiscountFee() { + assertDiscountFeeContainsExpectedExternalId(TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED); } - @And("Add Discount fee with {string} amount on Working Capital loan account failed due to override disallowed by product") - public void addDiscountFeeWCLoanOverrideDisallowedByProductFailure(String discountAmount) { - String errorMessage = ErrorMessageHelper.overrideDisallowedByProductFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("Active Discount Fee transactions contain the user-generated discountExternalId from disburse") + public void activeDiscountFeeTransactionsContainUserGeneratedDiscountExternalIdFromDisburse() { + assertDiscountFeeContainsExpectedExternalId(TestContextKey.WORKING_CAPITAL_LOAN_DISBURSE_DISCOUNT_EXTERNAL_ID_USER_GENERATED); } - @And("Add Discount fee with {string} amount on Working Capital loan account failed due to exceed discount amount") - public void addDiscountFeeWCLoanExceedDiscountAmountProductFailure(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountExceedCreatedDiscountFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + private void assertDiscountFeeContainsExpectedExternalId(final String testContextKey) { + final String expectedExternalId = testContext().get(testContextKey); + assertThat(expectedExternalId) // + .as("Expected externalId must be set in test context under key `%s` by a prior setup step", testContextKey) // + .isNotBlank(); + final List activeDiscounts = fetchActiveDiscountFeeTransactions(); + assertActiveDiscountFeeTransactionsNotEmpty(activeDiscounts); + assertThat(activeDiscounts) // + .as("Active Discount Fee transactions must contain one with the expected externalId %s", expectedExternalId) // + .extracting(GetWorkingCapitalLoanTransactionIdResponse::getExternalId) // + .contains(expectedExternalId); } - @And("Working Capital Loan has transactions:") - public void workingCapitalLoanHasTransactions(final DataTable dataTable) throws InvocationTargetException, IllegalAccessException { - GetWorkingCapitalLoansLoanIdResponse getWorkingCapitalLoansLoanIdResponse = retrieveLoanDetails(getCreatedLoanId()); - List actualTransactions = getWorkingCapitalLoansLoanIdResponse.getTransactions(); - assertTable(GetWorkingCapitalLoanTransactionIdResponse.class, dataTable, actualTransactions); + @Then("All active Discount Fee transactions have an auto-generated externalId") + public void allActiveDiscountFeeTransactionsHaveAutoGeneratedExternalId() { + final List activeDiscounts = fetchActiveDiscountFeeTransactions(); + assertActiveDiscountFeeTransactionsNotEmpty(activeDiscounts); + assertThat(activeDiscounts) // + .as("Every active Discount Fee transaction must have a non-blank (auto-generated) externalId") // + .allSatisfy(txn -> assertThat(txn.getExternalId()).isNotBlank()); } - @Then("Admin successfully update discount with {string} amount on Working Capital loan account") - public void adminSuccessfullyUpdateDiscountWithAmountOnWorkingCapitalLoanAccount(String discountAmount) { - PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); - final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() // - .relatedResourceId(lastDisbursementResponse.getResourceId()).transactionAmount(new BigDecimal(discountAmount)); - executeStateTransition("DISCOUNTFEE", request, "DISCOUNT", false); + private List fetchActiveDiscountFeeTransactions() { + final GetWorkingCapitalLoansLoanIdResponse loanResponse = retrieveLoanDetails(getCreatedLoanId()); + final List transactions = loanResponse.getTransactions(); + if (transactions == null) { + return List.of(); + } + return transactions.stream() // + .filter(t -> t.getType() != null && "loanTransactionType.discountFee".equals(t.getType().getCode())) // + .filter(t -> !Boolean.TRUE.equals(t.getReversed())) // + .toList(); + } + + private void assertActiveDiscountFeeTransactionsNotEmpty(final List activeDiscounts) { + assertThat(activeDiscounts).as("At least one active Discount Fee transaction must exist").isNotEmpty(); } - @Then("Update discount with {string} amount on Working Capital loan account failed due to date diff from disbursement date") - public void updateDiscountWithAmountOnWorkingCapitalLoanAccountFailedDueToDateDiffFromDisbursementDate(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountDiffDateFromDisburseFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("Adding Discount fee with {string} amount on Working Capital loan account results an error with the following data:") + public void addingDiscountFeeWCLoanResultsAnError(final String discountAmount, final DataTable table) { + final Long loanId = getCreatedLoanId(); + final PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); + Assertions.assertNotNull(lastDisbursementResponse); + + final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() + .relatedResourceId(lastDisbursementResponse.getResourceId()) // + .transactionAmount(new BigDecimal(discountAmount)); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, "DISCOUNTFEE", request)); + verifyRepaymentErrorWithTable(exception, table); } - @Then("Add discount with {string} amount on Working Capital loan account failed due to already added discount before disbursement") - public void addDiscountWithAmountOnWorkingCapitalLoanAccountFailedDueToAlreadyAddedDiscountBeforeDisbursement(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountAlreadySetBeforeDisburseFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @And("Working Capital Loan has transactions:") + public void workingCapitalLoanHasTransactions(final DataTable dataTable) throws InvocationTargetException, IllegalAccessException { + final GetWorkingCapitalLoansLoanIdResponse loanResponse = retrieveLoanDetails(getCreatedLoanId()); + final List actualTransactions = loanResponse.getTransactions(); + assertTable(GetWorkingCapitalLoanTransactionIdResponse.class, dataTable, actualTransactions); } - @Then("Update discount with {string} amount on Working Capital loan account failed due to override disallowed by product") - public void updateDiscountWithAmountOnWorkingCapitalLoanAccountFailedDueToOverrideDisallowedByProduct(String discountAmount) { - String errorMessage = ErrorMessageHelper.overrideDisallowedByProductFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("In Working Capital Loan Transactions all transactions have non-blank external-id") + public void workingCapitalLoanTransactionsHaveNonBlankExternalId() { + final GetWorkingCapitalLoansLoanIdResponse loanResponse = retrieveLoanDetails(getCreatedLoanId()); + final List transactions = loanResponse.getTransactions(); + Assertions.assertNotNull(transactions, "WC loan transactions list must not be null"); + for (final GetWorkingCapitalLoanTransactionIdResponse txn : transactions) { + assertThat(txn.getExternalId()).as("WC transaction id=%s type=%s date=%s must have a non-blank externalId", txn.getId(), + txn.getType() == null ? null : txn.getType().getValue(), txn.getTransactionDate()).isNotBlank(); + } } - @Then("Update discount with {string} amount on Working Capital loan account failed due to exceed discount amount") - public void updateDiscountWithAmountOnWorkingCapitalLoanAccountFailedDueToExceedDiscountAmount(String discountAmount) { - String errorMessage = ErrorMessageHelper.discountExceedProductDiscountFailure(); - addDiscountFeeFailedCheck(discountAmount, errorMessage); + @Then("Admin successfully update discount with {string} amount on Working Capital loan account") + public void adminSuccessfullyUpdateDiscountWithAmountOnWorkingCapitalLoanAccount(String discountAmount) { + PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); + final PostWorkingCapitalLoansLoanIdRequest request = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDiscountFeeRequest() // + .relatedResourceId(lastDisbursementResponse.getResourceId()).transactionAmount(new BigDecimal(discountAmount)); + executeStateTransition("DISCOUNTFEE", request, TestContextKey.WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE, false); } @When("Admin update Working Capital period payment rate with {string} value") @@ -1414,7 +1502,7 @@ public void adminChecksWorkingCapitalPeriodPaymentRateChangesHistoryByExternalId // Loan Lifecycle Helpers private void createWorkingCapitalLoanAccount(final List loanData) { - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long clientId = extractClientId(); final Long loanProductId = resolveLoanProductId(loanProduct); final PostWorkingCapitalLoansRequest loansRequest = buildCreateLoanRequest(clientId, loanProductId, loanData); @@ -1490,24 +1578,6 @@ private void executeStateTransition(final String command, final PostWorkingCapit testContext().set(responseKey, response); } - public void addDiscountFeeFailedCheck(String discountAmount, String errorMessage) { - final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - Assertions.assertNotNull(loanResponse); - Assertions.assertNotNull(loanResponse.getLoanId()); - long loanId = loanResponse.getLoanId(); - PostWorkingCapitalLoansLoanIdResponse lastDisbursementResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE); - Assertions.assertNotNull(lastDisbursementResponse); - - PostWorkingCapitalLoansLoanIdRequest updateDiscountRequest = workingCapitalLoanRequestFactory - .defaultWorkingCapitalLoanDiscountFeeRequest().relatedResourceId(lastDisbursementResponse.getResourceId()) - .transactionAmount(new BigDecimal(discountAmount)); - - CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans().stateTransitionWorkingCapitalLoanById(loanId, - "DISCOUNTFEE", updateDiscountRequest)); - assertThat(exception.getStatus()).as(errorMessage).isEqualTo(400); - assertThat(exception.getDeveloperMessage()).contains(errorMessage); - } - // Data Extraction Helpers private Long getCreatedLoanId() { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); @@ -1530,7 +1600,7 @@ private Long resolveLoanProductId(final String loanProductName) { } private BigDecimal extractPrincipalFromModifyTable(final DataTable table) { - final Map data = table.asMaps().get(0); + final Map data = table.asMaps().getFirst(); return new BigDecimal(data.get("principalAmount")); } @@ -1615,7 +1685,7 @@ private PostWorkingCapitalLoansRequest buildCreateLoanBaseRequest(final Long cli } private PutWorkingCapitalLoansLoanIdRequest buildModifyLoanRequest(final List loanData) { - final String submittedOnDate = loanData.get(0); + final String submittedOnDate = loanData.getFirst(); final String expectedDisbursementDate = loanData.get(1); final String principal = loanData.get(2); final String totalPayment = loanData.get(3); @@ -1673,7 +1743,7 @@ private void verifyStateTransitionSuccess(final String responseKey, final String private void verifyErrorResponse(final CallFailedRuntimeException exception, final DataTable table) { final List> data = table.asLists(); - final String expectedHttpCode = data.get(1).get(0); + final String expectedHttpCode = data.get(1).getFirst(); final String expectedErrorMessage = data.get(1).get(1); assertThat(exception.getStatus()).as("HTTP status code should be " + expectedHttpCode) @@ -1703,19 +1773,6 @@ public void disburseWCLoanFailure(String actualDisbursementDate, String transact assertThat(exception.getDeveloperMessage()).contains(errorMessage); } - public void disburseWorkingCapitalLoanFailure(String actualDisbursementDate, String transactionAmount, String discountAmount, - String errorMessage) { - PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory.defaultWorkingCapitalLoanDisburseRequest() - .actualDisbursementDate(actualDisbursementDate)// - .discountAmount(new BigDecimal(discountAmount)).transactionAmount(new BigDecimal(transactionAmount)); - - final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoans() - .stateTransitionWorkingCapitalLoanById(getCreatedLoanId(), "disburse", disburseRequest)); - - assertThat(exception.getStatus()).as(errorMessage).isEqualTo(400); - assertThat(exception.getDeveloperMessage()).contains(errorMessage); - } - @When("Admin creates a Working Capital Loan Product with delinquencyGraceDays {int} and delinquencyStartType {string} for loan test") public void createProductWithGraceDaysForLoanTest(int graceDays, String startType) { final String name = "WCLP-GD-" + Utils.randomStringGenerator("", 8); @@ -1732,13 +1789,13 @@ public void createProductWithGraceDaysForLoanTest(int graceDays, String startTyp @When("Admin creates a working capital loan with the grace days product and the following data:") public void createLoanWithGraceDaysProduct(final DataTable table) { - final Map row = table.asMaps().get(0); + final Map row = table.asMaps().getFirst(); submitLoanAndStore(buildGraceDaysLoanRequest(row)); } @When("Admin creates a working capital loan with grace days override and the following data:") public void createLoanWithGraceDaysOverride(final DataTable table) { - final Map row = table.asMaps().get(0); + final Map row = table.asMaps().getFirst(); final PostWorkingCapitalLoansRequest request = buildGraceDaysLoanRequest(row) // .delinquencyGraceDays( Optional.ofNullable(row.get("delinquencyGraceDays")).filter(s -> !s.isEmpty()).map(Integer::valueOf).orElse(null)) // @@ -1782,7 +1839,7 @@ public void verifyLoanGraceDays(int expectedGraceDays, String 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 Map row = table.asMaps().getFirst(); final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); final Long loanId = loanResponse.getLoanId(); @@ -1950,7 +2007,7 @@ public PostWorkingCapitalLoansRequest createWorkingCapitalLoanAccountDefaultRequ public PostWorkingCapitalLoansRequest createWorkingCapitalLoanAccountWithBreachNearBreachRequest(final List loanData, Long breachId, Long nearBreachId) { - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long clientId = extractClientId(); final Long loanProductId = resolveLoanProductId(loanProduct); final PostWorkingCapitalLoansRequest loansBaseRequest = buildCreateLoanBaseRequest(clientId, loanProductId, loanData); @@ -1960,7 +2017,7 @@ public PostWorkingCapitalLoansRequest createWorkingCapitalLoanAccountWithBreachN } public PostWorkingCapitalLoansRequest createWorkingCapitalLoanAccountWithBaseRequest(final List loanData) { - final String loanProduct = loanData.get(0); + final String loanProduct = loanData.getFirst(); final Long clientId = extractClientId(); final Long loanProductId = resolveLoanProductId(loanProduct); return buildCreateLoanBaseRequest(clientId, loanProductId, loanData); @@ -2307,7 +2364,7 @@ private PostWorkingCapitalLoanTransactionsPaymentDetailRequest buildPaymentDetai private Map convertDataTableToMap(final DataTable table) { final List> rows = table.asLists(String.class); - final List headers = rows.get(0); + final List headers = rows.getFirst(); final List values = rows.get(1); final Map map = new HashMap<>(); @@ -2397,7 +2454,7 @@ public void initiateCreditBalanceRefundResultsAnErrorWithDetails(final String tr private void verifyRepaymentErrorWithTable(final CallFailedRuntimeException exception, final DataTable table) { final List> data = table.asLists(); - final String expectedHttpCode = data.get(1).get(0); + final String expectedHttpCode = data.get(1).getFirst(); final String expectedErrorMessage = data.get(1).get(1); log.debug("Checking for Http code: {} and error message: \"{}\"", expectedHttpCode, expectedErrorMessage); @@ -2497,7 +2554,7 @@ public void assertWorkingCapitalLoanBalancePrincipalOutstanding(final String exp public void assertWorkingCapitalLoanBalancePayloadFields(final DataTable table) { final List> rows = table.asLists(); for (int i = 1; i < rows.size(); i++) { - assertBalanceFieldEquals(rows.get(i).get(0), rows.get(i).get(1)); + assertBalanceFieldEquals(rows.get(i).getFirst(), rows.get(i).get(1)); } } @@ -2689,50 +2746,45 @@ private void assertTable(Class tClass, List header, List Assertions.assertEquals(Double.parseDouble(expected), bigDecimal.doubleValue(), message); + case LoanTransactionEnumData loanTransactionEnumData -> + Assertions.assertEquals(expected, loanTransactionEnumData.getValue(), message); + case LocalDate localDate -> Assertions.assertEquals(expected, FORMATTER.format(localDate), message); + default -> Assertions.assertEquals(expectedValues.get(iM), actual == null ? null : actual.toString(), message); } } } } @Then("Working Capital Loan Transactions tab has a {string} transaction with date {string} which has the following Journal entries:") - public void verifyWorkingCapitalLoanTransactionJournalEntries(String transactionType, String transactionDate, DataTable table) { - Long loanId = getCreatedLoanId(); - TransactionType resolvedTransactionType = resolveTransactionType(transactionType); - List transactionsMatch = findMatchingTransactions(loanId, resolvedTransactionType, - transactionDate, false); - - verifyJournalEntries(transactionsMatch, loanId, table); + public void verifyWorkingCapitalLoanTransactionJournalEntries(final String transactionType, final String transactionDate, + final DataTable table) { + verifyTransactionsJournalEntries(transactionType, transactionDate, false, null, table); } @Then("Working Capital Loan Transactions tab has {int} {string} transactions with date {string} which have the following Journal entries:") - public void verifyMultipleWorkingCapitalLoanTransactionsJournalEntries(int expectedCount, String transactionType, - String transactionDate, DataTable table) throws IOException { - Long loanId = getCreatedLoanId(); - TransactionType resolvedTransactionType = resolveTransactionType(transactionType); - List transactionsMatch = findMatchingTransactions(loanId, resolvedTransactionType, - transactionDate, false); - - assertThat(transactionsMatch.size()).as("The number of transactions does not match the expected count! Expected: " + expectedCount - + ", Actual: " + transactionsMatch.size()).isEqualTo(expectedCount); - - verifyJournalEntries(transactionsMatch, loanId, table); + public void verifyMultipleWorkingCapitalLoanTransactionsJournalEntries(final int expectedCount, final String transactionType, + final String transactionDate, final DataTable table) { + verifyTransactionsJournalEntries(transactionType, transactionDate, false, expectedCount, table); } @Then("Working Capital Loan Transactions tab has a reversed {string} transaction with date {string} which has the following Journal entries:") - public void verifyReversedWorkingCapitalLoanTransactionJournalEntries(String transactionType, String transactionDate, DataTable table) - throws IOException { - Long loanId = getCreatedLoanId(); - TransactionType resolvedTransactionType = resolveTransactionType(transactionType); - List transactionsMatch = findMatchingTransactions(loanId, resolvedTransactionType, - transactionDate, true); + public void verifyReversedWorkingCapitalLoanTransactionJournalEntries(final String transactionType, final String transactionDate, + final DataTable table) { + verifyTransactionsJournalEntries(transactionType, transactionDate, true, null, table); + } + + private void verifyTransactionsJournalEntries(final String transactionType, final String transactionDate, final boolean reversed, + final Integer expectedCount, final DataTable table) { + final Long loanId = getCreatedLoanId(); + final TransactionType resolvedTransactionType = resolveTransactionType(transactionType); + final List transactionsMatch = findMatchingTransactions(loanId, resolvedTransactionType, + transactionDate, reversed); + if (expectedCount != null) { + assertThat(transactionsMatch.size()).as("The number of transactions does not match the expected count! Expected: " + + expectedCount + ", Actual: " + transactionsMatch.size()).isEqualTo(expectedCount); + } verifyJournalEntries(transactionsMatch, loanId, table); } @@ -2817,7 +2869,7 @@ public void checkPeriodPaymentRatesTabRows(List> data, List header, String resourceId) { for (int i = 1; i < data.size(); i++) { List expectedValues = data.get(i); - String transactionDateExpected = expectedValues.get(0); + String transactionDateExpected = expectedValues.getFirst(); List> actualValuesList = rateChanges.stream()// .filter(rate -> transactionDateExpected.equals(FORMATTER.format(rate.getEffectiveDate())))// .map(rate -> fetchValuesOfRateChangesHistory(header, rate))// 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 9038510e9cf..7a499f50f70 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 @@ -368,5 +368,8 @@ public abstract class TestContextKey { public static final String WORKING_CAPITAL_LOAN_RATE_CHANGE_ID = "wcLoanRateChangeId"; public static final String WORKING_CAPITAL_CHARGE_ID = "workingCapitalChargeId"; public static final String WORKING_CAPITAL_CHARGE_TEMPLATE = "workingCapitalChargeTemplate"; + public static final String WORKING_CAPITAL_LOAN_DISBURSE_DISCOUNT_EXTERNAL_ID_USER_GENERATED = "workingCapitalLoanDisburseDiscountExternalIdUserGenerated"; + public static final String WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED = "workingCapitalLoanDiscountFeeExternalIdUserGenerated"; + public static final String WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE = "workingCapitalLoanDiscountFeeResponse"; public static final String LAST_SAVINGS_ACCOUNT_ID = "lastSavingsAccountId"; } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDiscount.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDiscount.feature index c5dd9937654..ac8ac992438 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDiscount.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDiscount.feature @@ -63,7 +63,9 @@ Feature: Working Capital Discount | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | # --- add discount after disbursement on diff from same disbursement date should outcome with an error --- # When Admin sets the business date to "08 January 2026" - Then Add Discount fee with "10" amount on Working Capital loan account failed due to date diff from disbursement date + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | transaction.date.must.be.equal.disbursement.date | And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | @@ -82,13 +84,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 15.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "18" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "18" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 15.0 | 14.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "15" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "15" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -99,7 +105,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 13.0 | 13.0 | 0.0 | 0.0 | false | - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -122,13 +130,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 15.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "18" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "18" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 15.0 | 14.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "15" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "15" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -139,7 +151,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 13.0 | 13.0 | 0.0 | 0.0 | false | - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -171,7 +185,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 14.0 | 14.0 | 0.0 | 0.0 | false | - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -203,7 +219,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 13.0 | 13.0 | 0.0 | 0.0 | false | - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -220,13 +238,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount due to override disallowed by product + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "15" discount amount due to override disallowed by product + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "15" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | 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 @@ -237,7 +259,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | # --- add discount after disbursement is forbidden due to overrides disallowed --- # - And Add Discount fee with "20" amount on Working Capital loan account failed due to override disallowed by product + And Adding Discount fee with "20" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -253,13 +277,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount due to override disallowed by product + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "20" discount amount due to override disallowed by product + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | 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 @@ -270,7 +298,9 @@ Feature: Working Capital Discount | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | # --- add discount after disbursement is forbidden due to overrides disallowed --- # - And Add Discount fee with "20" amount on Working Capital loan account failed due to override disallowed by product + And Adding Discount fee with "20" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -302,7 +332,9 @@ Feature: Working Capital Discount | 01 January 2026 | Disbursement | 100 | 100 | 0 | 0 | false | | 01 January 2026 | Discount Fee | 50 | 50 | 0 | 0 | false | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -322,13 +354,17 @@ Feature: Working Capital Discount When Admin modifies the working capital loan with the following data: | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | | | | | | | 55.0 | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 55.0 | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "60" exceeded product discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "50" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -340,7 +376,9 @@ Feature: Working Capital Discount | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 50.0 | 50.0 | 0 | 0 | false | # --- add discount after disbursement is forbidden as discount is already added and updated on disbursement --- # - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -357,13 +395,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded product discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "40" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 40.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "50" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "50" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "40" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -375,7 +417,9 @@ Feature: Working Capital Discount | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 40.0 | 40.0 | 0 | 0 | false | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -392,13 +436,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded product discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "60" exceeded product discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "45" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -410,7 +458,9 @@ Feature: Working Capital Discount | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | | 01 January 2026 | Discount Fee | 45.0 | 45.0 | 0 | 0 | false | # --- add discount after disbursement is forbidden as discount is already added and updated on disbursement --- # - Then Add Discount fee with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | @@ -546,7 +596,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 18.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: @@ -569,7 +621,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 18.0 | 17.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "19" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "16" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -587,7 +641,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 18.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | @TestRailId:C78856 Scenario: Discount on Working Capital Loan account added while create WCP, updated while modify/approve loan and check after updo approval - UC1.1 @@ -617,7 +673,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 19.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: @@ -646,7 +704,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 18.0 | 17.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "19" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "16" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -663,7 +723,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 18.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | @TestRailId:C78858 Scenario: Discount on Working Capital Loan account added while approve WCP and check after updo approval - UC3 @@ -709,7 +771,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 17.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "19" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "16" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -730,7 +794,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 19.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "20" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "19" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -759,7 +825,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 17.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "19" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "16" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -786,7 +854,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 20.0 | 20.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "21" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "21" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "18" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -865,7 +935,9 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 18.0 | 18.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "19" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "19" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "17" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -884,13 +956,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount due to override disallowed by product + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "20" discount amount due to override disallowed by product + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "20" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | 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 @@ -932,7 +1008,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | # --- add discount with exceed discount amount is forbidden as loan discount is null but It exceeds product discount value --- # - Then Update discount with "60" amount on Working Capital loan account failed due to exceed discount amount + Then Adding Discount fee with "60" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | # --- add discount after disbursement on the same disbursement date --- # Then Admin successfully update discount with "12" amount on Working Capital loan account And Working capital loan account has the correct data: @@ -961,13 +1039,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 48.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "40" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 48.0 | 40.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "50" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "50" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "40" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -975,7 +1057,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Active | 140.0 | 100.0 | 100.0 | 1.0 | 48.0 | 40.0 | 40.0 | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add discount with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | # --- undo working capital disbursal --- # Then Admin successfully undo Working Capital disbursal And Working capital loan account has the correct data: @@ -1023,13 +1107,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 30.0 | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.created.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "20" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 30.0 | 20.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "50" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "50" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "20" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -1037,7 +1125,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Active | 120.0 | 100.0 | 100.0 | 1.0 | 30.0 | 20.0 | 20.0 | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add discount with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | # --- undo working capital disbursal --- # Then Admin successfully undo Working Capital disbursal And Working capital loan account has the correct data: @@ -1060,13 +1150,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded product discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "40" discount amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 40.0 | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "50" exceeded discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "50" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.approved.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "40" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -1074,7 +1168,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Active | 140.0 | 100.0 | 100.0 | 1.0 | null | 40.0 | 40.0 | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add discount with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | # --- undo working capital disbursal --- # Then Admin successfully undo Working Capital disbursal And Working capital loan account has the correct data: @@ -1098,13 +1194,17 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to approve the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" exceeded product discount amount + Then Approving the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" with "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" Then Working capital loan approval was successful And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | - Then Admin failed to disburse the working capital loan on "01 January 2026" with "100" amount with "60" exceeded product discount amount + Then Disbursing the working capital loan on "01 January 2026" with "100" amount and "60" discount amount results an error with the following data: + | HTTP response code | Error message | + | 400 | amount.cannot.exceed.product.discount | Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "45" discount amount Then Working Capital loan status will be "ACTIVE" Then Verify Working Capital loan disbursement was successful @@ -1112,7 +1212,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT | 2026-01-01 | 2026-01-01 | Active | 145.0 | 100.0 | 100.0 | 1.0 | null | null | 45.0 | # --- add discount after disbursement is forbidden as discount is already added --- # - Then Add discount with "10" amount on Working Capital loan account failed due to already added discount before disbursement + Then Adding Discount fee with "10" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | Discount was already set before disbursement | # --- undo working capital disbursal --- # Then Admin successfully undo Working Capital disbursal And Working capital loan account has the correct data: @@ -1148,7 +1250,9 @@ Feature: Working Capital Discount | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP_DISCOUNT_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Active | 100.0 | 100.0 | 100.0 | 1.0 | null | null | null | # --- add discount after disbursement is forbidden due to overrides disallowed --- # - And Update discount with "20" amount on Working Capital loan account failed due to override disallowed by product + And Adding Discount fee with "20" amount on Working Capital loan account results an error with the following data: + | HTTP response code | Error message | + | 400 | override.not.allowed.by.product | # --- undo working capital disbursal --- # Then Admin successfully undo Working Capital disbursal And Working capital loan account has the correct data: @@ -1187,3 +1291,170 @@ Feature: Working Capital Discount And Working capital loan account has the correct data: | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount | | WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 14.0 | null | + + Scenario: Discount fee added via DISCOUNTFEE without externalId gets an auto-generated externalId + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + 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 + Then Admin adds Discount fee with "12" amount on Working Capital loan account for last disbursement + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | + | 01 January 2026 | Discount Fee | 12.0 | 12.0 | 0.0 | 0.0 | false | + Then In Working Capital Loan Transactions all transactions have non-blank external-id + Then All active Discount Fee transactions have an auto-generated externalId + + Scenario: Discount fee added after disbursement with user-generated externalId is persisted as-is + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + 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 + Then Admin adds Discount fee with "12" amount and a random externalId on Working Capital loan account for last disbursement + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | + | 01 January 2026 | Discount Fee | 12.0 | 12.0 | 0.0 | 0.0 | false | + Then In Working Capital Loan Transactions all transactions have non-blank external-id + Then Active Discount Fee transactions contain the user-generated externalId from DISCOUNTFEE + + Scenario: Discount provided during disbursement without externalId gets an auto-generated externalId + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | + | 01 January 2026 | Discount Fee | 13.0 | 13.0 | 0.0 | 0.0 | false | + Then In Working Capital Loan Transactions all transactions have non-blank external-id + Then All active Discount Fee transactions have an auto-generated externalId + + Scenario: Discount provided during disbursement with user-generated discountExternalId is persisted as-is + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount and a random discountExternalId + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 100.0 | 100.0 | 0.0 | 0.0 | false | + | 01 January 2026 | Discount Fee | 13.0 | 13.0 | 0.0 | 0.0 | false | + Then In Working Capital Loan Transactions all transactions have non-blank external-id + Then Active Discount Fee transactions contain the user-generated discountExternalId from disburse + + Scenario: Reusing the same discountExternalId across two working capital loans on disburse is rejected + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount and a random discountExternalId + Then Working Capital loan status will be "ACTIVE" + Then Verify Working Capital loan disbursement was successful + When 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Initiating disbursement on "01 January 2026" with "100" EUR transaction amount and "13" discount amount reusing the previously shared discountExternalId on Working Capital loan results an error with the following data: + | HTTP response code | Error message | + | 400 | already.exists | + + Scenario: Providing discountExternalId on disburse without discountAmount is rejected + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Initiating disbursement on "01 January 2026" with "100" EUR transaction amount and discountExternalId "wcl-disburse-discount-no-amount-001" without discountAmount on Working Capital loan results an error with the following data: + | HTTP response code | Error message | + | 400 | not.allowed.without.positive.discount | + + Scenario: Providing discountExternalId on disburse with zero discountAmount is rejected + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Initiating disbursement on "01 January 2026" with "100" EUR transaction amount and "0" discount amount and discountExternalId "wcl-disburse-discount-zero-amount-001" on Working Capital loan results an error with the following data: + | HTTP response code | Error message | + | 400 | not.allowed.without.positive.discount | + + Scenario: Providing the same externalId for both disburse and discountExternalId is rejected + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + Then Initiating disbursement on "01 January 2026" with "100" EUR transaction amount and "13" discount amount using "wcl-disburse-shared-ext-id-001" for both externalId and discountExternalId on Working Capital loan results an error with the following data: + | HTTP response code | Error message | + | 400 | must.differ.from.disbursement.external.id | + + Scenario: Reusing the same externalId on DISCOUNTFEE across two working capital loans is rejected + 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + 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 + Then Admin adds Discount fee with "12" amount and a random externalId on Working Capital loan account for last disbursement + When 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 | | + Then Working capital loan creation was successful + Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + 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 + Then Adding Discount fee with "12" amount reusing the previously shared externalId on Working Capital loan account for last disbursement results an error with the following data: + | HTTP response code | Error message | + | 400 | already.exists | 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 baf46ba2072..2b15637376f 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 @@ -56,6 +56,7 @@ private WorkingCapitalLoanConstants() { public static final String approvedLoanAmountParamName = "approvedLoanAmount"; public static final String expectedDisbursementDateParamName = "expectedDisbursementDate"; public static final String discountAmountParamName = "discountAmount"; + public static final String discountExternalIdParameterName = "discountExternalId"; public static final String noteParamName = "note"; public static final String rejectedOnDateParamName = "rejectedOnDate"; 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 b2530a50167..a8ff69abe1e 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 @@ -520,6 +520,8 @@ private PostWorkingCapitalLoansLoanIdRequest() {} public Long classificationId; @Schema(example = "ext-disburse-001", description = "External ID; optional for disburse") public String externalId; + @Schema(example = "ext-discount-001", description = "External ID for the discount fee transaction created during disburse; optional. Only accepted when discountAmount is greater than 0. When omitted and auto-generation is enabled, a UUID is generated.") + public String discountExternalId; @Schema(description = "Payment details (Account No, Cheque No, Routing Code, Receipt No, Bank code)") public PostWorkingCapitalLoansLoanIdDisbursementPaymentDetails paymentDetails; @Schema(description = "Related resource ID for transaction, e.g., related transaction ID") 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 643c63a10c6..bdb0ffef87d 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 @@ -73,11 +73,11 @@ public class WorkingCapitalLoanDataValidator { private static final Set UNDO_APPROVAL_SUPPORTED_PARAMETERS = new HashSet<>( Arrays.asList("locale", "dateFormat", WorkingCapitalLoanConstants.noteParamName)); - private static final Set DISBURSAL_SUPPORTED_PARAMETERS = new HashSet<>( - Arrays.asList("locale", "dateFormat", WorkingCapitalLoanConstants.actualDisbursementDateParamName, - WorkingCapitalLoanConstants.transactionAmountParamName, WorkingCapitalLoanConstants.discountAmountParamName, - WorkingCapitalLoanConstants.noteParamName, WorkingCapitalLoanConstants.paymentDetailsParamName, - WorkingCapitalLoanConstants.externalIdParameterName, WorkingCapitalLoanConstants.classificationIdParamName)); + private static final Set DISBURSAL_SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("locale", "dateFormat", + WorkingCapitalLoanConstants.actualDisbursementDateParamName, WorkingCapitalLoanConstants.transactionAmountParamName, + WorkingCapitalLoanConstants.discountAmountParamName, WorkingCapitalLoanConstants.noteParamName, + WorkingCapitalLoanConstants.paymentDetailsParamName, WorkingCapitalLoanConstants.externalIdParameterName, + WorkingCapitalLoanConstants.discountExternalIdParameterName, WorkingCapitalLoanConstants.classificationIdParamName)); private static final Set PAYMENT_DETAILS_SUPPORTED_PARAMETERS = new HashSet<>( Arrays.asList(WorkingCapitalLoanConstants.paymentTypeIdParamName, WorkingCapitalLoanConstants.accountNumberParamName, @@ -94,7 +94,7 @@ public class WorkingCapitalLoanDataValidator { Arrays.asList("locale", "dateFormat", WorkingCapitalLoanConstants.noteParamName, WorkingCapitalLoanConstants.transactionAmountParamName, WorkingCapitalLoanConstants.classificationIdParamName, WorkingCapitalLoanConstants.relatedResourceIdParamName, WorkingCapitalLoanConstants.paymentDetailsParamName, - WorkingCapitalLoanConstants.noteParamName, WorkingCapitalLoanConstants.transactionDateParamName)); + WorkingCapitalLoanConstants.transactionDateParamName, 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<>( @@ -119,6 +119,8 @@ public void validateDiscountTransaction(final WorkingCapitalLoan loan, final Str final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource(WorkingCapitalLoanConstants.RESOURCE_NAME); + final JsonElement element = this.fromApiJsonHelper.parse(json); + if (isDiscountOverrideAllowed(loan)) { baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName) .failWithCode("override.not.allowed.by.product"); @@ -149,6 +151,8 @@ public void validateDiscountTransaction(final WorkingCapitalLoan loan, final Str baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull() .notExceedingLengthOf(NOTE_MAX_LENGTH); + validateTransactionExternalId(baseDataValidator, element, WorkingCapitalLoanConstants.externalIdParameterName); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -202,7 +206,7 @@ public void validateApproval(final String json, final WorkingCapitalLoan loan) { .extractLocalDateNamed(WorkingCapitalLoanConstants.expectedDisbursementDateParamName, element); baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.expectedDisbursementDateParamName).value(expectedDisbursementDate) .notNull(); - if (expectedDisbursementDate != null && approvedOnDate != null && DateUtils.isBefore(expectedDisbursementDate, approvedOnDate)) { + if (expectedDisbursementDate != null && DateUtils.isBefore(expectedDisbursementDate, approvedOnDate)) { baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.expectedDisbursementDateParamName) .failWithCode("cannot.be.before.approval.date"); } @@ -381,18 +385,10 @@ public void validateDisbursement(final String json, final WorkingCapitalLoan loa baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull() .notExceedingLengthOf(NOTE_MAX_LENGTH); - if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.externalIdParameterName, element)) { - final String externalIdStr = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.externalIdParameterName, - element); - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).value(externalIdStr).ignoreIfNull() - .notExceedingLengthOf(EXTERNAL_ID_MAX_LENGTH); - if (externalIdStr != null && !externalIdStr.isBlank()) { - final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - if (!externalId.isEmpty() && this.transactionRepository.existsByExternalId(externalId)) { - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).failWithCode("already.exists"); - } - } - } + validateTransactionExternalId(baseDataValidator, element, WorkingCapitalLoanConstants.externalIdParameterName); + validateTransactionExternalId(baseDataValidator, element, WorkingCapitalLoanConstants.discountExternalIdParameterName); + validateDiscountExternalIdRequiresPositiveDiscount(baseDataValidator, element); + validateDisbursementAndDiscountExternalIdsDiffer(baseDataValidator, element); validatePaymentDetails(baseDataValidator, element); @@ -433,6 +429,55 @@ private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator } } + private void validateDiscountExternalIdRequiresPositiveDiscount(final DataValidatorBuilder baseDataValidator, + final JsonElement element) { + if (!this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.discountExternalIdParameterName, element)) { + return; + } + final String discountExternalIdStr = this.fromApiJsonHelper + .extractStringNamed(WorkingCapitalLoanConstants.discountExternalIdParameterName, element); + if (StringUtils.isBlank(discountExternalIdStr)) { + return; + } + final BigDecimal discountAmount = this.fromApiJsonHelper + .parameterHasValue(WorkingCapitalLoanConstants.discountAmountParamName, element) + ? this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName, element, + new HashSet<>()) + : null; + if (discountAmount == null || discountAmount.signum() == 0) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountExternalIdParameterName) + .failWithCode("not.allowed.without.positive.discount"); + } + } + + private void validateDisbursementAndDiscountExternalIdsDiffer(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + final String disbursementExternalId = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.externalIdParameterName, + element); + final String discountExternalId = this.fromApiJsonHelper + .extractStringNamed(WorkingCapitalLoanConstants.discountExternalIdParameterName, element); + if (StringUtils.isNotBlank(disbursementExternalId) && StringUtils.isNotBlank(discountExternalId) + && disbursementExternalId.equals(discountExternalId)) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountExternalIdParameterName) + .failWithCode("must.differ.from.disbursement.external.id"); + } + } + + private void validateTransactionExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element, + final String paramName) { + if (!this.fromApiJsonHelper.parameterHasValue(paramName, element)) { + return; + } + final String externalIdStr = this.fromApiJsonHelper.extractStringNamed(paramName, element); + baseDataValidator.reset().parameter(paramName).value(externalIdStr).ignoreIfNull().notExceedingLengthOf(EXTERNAL_ID_MAX_LENGTH); + if (externalIdStr == null || externalIdStr.isBlank()) { + return; + } + final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); + if (!externalId.isEmpty() && this.transactionRepository.existsByExternalId(externalId)) { + baseDataValidator.reset().parameter(paramName).failWithCode("already.exists"); + } + } + private JsonElement resolvePaymentDetailsElement(final JsonElement element) { if (element != null && element.isJsonObject()) { final JsonObject root = element.getAsJsonObject(); @@ -523,18 +568,7 @@ public void validateRepayment(final String json, final WorkingCapitalLoan loan, baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull() .notExceedingLengthOf(NOTE_MAX_LENGTH); - if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.externalIdParameterName, element)) { - final String externalIdStr = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.externalIdParameterName, - element); - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).value(externalIdStr).ignoreIfNull() - .notExceedingLengthOf(EXTERNAL_ID_MAX_LENGTH); - if (externalIdStr != null && !externalIdStr.isBlank()) { - final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - if (!externalId.isEmpty() && this.transactionRepository.existsByExternalId(externalId)) { - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).failWithCode("already.exists"); - } - } - } + validateTransactionExternalId(baseDataValidator, element, WorkingCapitalLoanConstants.externalIdParameterName); validatePaymentDetails(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); @@ -618,18 +652,7 @@ public void validateCreditBalanceRefund(final String json, final WorkingCapitalL baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull() .notExceedingLengthOf(NOTE_MAX_LENGTH); - if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.externalIdParameterName, element)) { - final String externalIdStr = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.externalIdParameterName, - element); - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).value(externalIdStr).ignoreIfNull() - .notExceedingLengthOf(EXTERNAL_ID_MAX_LENGTH); - if (externalIdStr != null && !externalIdStr.isBlank()) { - final ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - if (!externalId.isEmpty() && this.transactionRepository.existsByExternalId(externalId)) { - baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.externalIdParameterName).failWithCode("already.exists"); - } - } - } + validateTransactionExternalId(baseDataValidator, element, WorkingCapitalLoanConstants.externalIdParameterName); validatePaymentDetails(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); 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 3385e8f85f3..c1dbfd9a787 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 @@ -317,11 +317,12 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand ExternalId discountTxnExternalId = null; if (discount != null && discount.compareTo(BigDecimal.ZERO) > 0) { - WorkingCapitalLoanTransaction discountTransaction = createAndPersistDiscountFeeTransaction(loan, disbursementTransaction, null, - discount, actualDisbursementDate, null, null); + final ExternalId discountExternalId = externalIdFactory.createFromCommand(command, + WorkingCapitalLoanConstants.discountExternalIdParameterName); + final WorkingCapitalLoanTransaction discountTransaction = createAndPersistDiscountFeeTransaction(loan, disbursementTransaction, + discountExternalId, discount, actualDisbursementDate, null, null); discountTransactionId = discountTransaction.getId(); discountTxnExternalId = discountTransaction.getExternalId(); - changes.put(WorkingCapitalLoanConstants.discountAmountParamName, discount); } updateBalanceOnDisburse(loan, transactionAmount); amortizationScheduleWriteService.generateAndSaveAmortizationScheduleOnDisbursement(loan, transactionAmount, actualDisbursementDate);