From c1800d5fbf1a8bf6f6c337351fd823d0ea22bb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Mon, 18 May 2026 17:50:19 +0200 Subject: [PATCH 1/3] FINERACT-2455: Working Capital Loan Product - Add Due & In Advanced allocation rules --- .../src/docs/en/chapters/features/index.adoc | 1 + .../working-capital-payment-allocation.adoc | 44 +++++++++++++++++++ .../factory/WorkingCapitalRequestFactory.java | 22 ++++++---- .../test/helper/ErrorMessageHelper.java | 2 +- ...alAdvancedPaymentAllocationsValidator.java | 5 +-- .../WorkingCapitalPaymentAllocationType.java | 9 ++-- .../WorkingCapitalLoanProductTestBuilder.java | 3 +- 7 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 fineract-doc/src/docs/en/chapters/features/working-capital-payment-allocation.adoc diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc index ef659afed5b..b4050ee3914 100644 --- a/fineract-doc/src/docs/en/chapters/features/index.adoc +++ b/fineract-doc/src/docs/en/chapters/features/index.adoc @@ -18,6 +18,7 @@ include::delayed-schedule-captures.adoc[leveloffset=+1] include::loan-origination-details.adoc[leveloffset=+1] include::taxes-on-loan-charges.adoc[leveloffset=+1] include::charges-template-filters.adoc[leveloffset=+1] +include::working-capital-payment-allocation.adoc include::working-capital-amortization-schedule.adoc[leveloffset=+1] include::working-capital-discount-fee-txn.adoc[leveloffset=+1] include::working-capital-charges.adoc[leveloffset=+1] diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-payment-allocation.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-payment-allocation.adoc new file mode 100644 index 00000000000..f7e28019665 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-payment-allocation.adoc @@ -0,0 +1,44 @@ += Working Capital Payment Allocation + +== Concept +On legacy and merchant term loans (progressive loans), 3 timely modifier were defined: + +* PAST DUE +* DUE +* IN ADVANCE + +For (sales based) Working Capital type of loan products, PAST DUE is not applicable, but DUE and IN ADVANCE balances are providing enough flexibility and fine-grained configuration options to be widely accepted and used for business needs. + +* *DUE*: +** *Principal*: Projected principal payment +** *Fee* and *Penalty*: current or older charges (based on specific due date) +* *IN ADVANCE*: +** *Principal*: over projected principal payment +** *Fee* and *Penalty*: Future charges (based on specific due date) + +== Validation + +Similarly to the progressive loan, Working Capital Loans accept as payment allocation the following values. When creating payment allocation the following values should be included. + +* DUE PRINCIPAL +* DUE FEE +* DUE PENALTY +* IN ADVANCE PRINCIPAL +* IN ADVANCE FEE +* IN ADVANCE PENALTY + +During loan product creation at lest one "default" configuration are included and by transaction type there could be optional allocation provided. When payment allocation is provided all available options should be included. + +== Loan Product Creation & Update + +During loan product creation there is the paymentAllocation is a mandatory option therefore one allocation for "default" payment allocation should be provided. + +=== Template + +Template API for Loan Product related operations are providing the available payment allocation settings. + +== Loan Account Creation & Update + +During loan account creation there is the paymentAllocation is optional. When Payment Allocation is provided during loan account creation, it is used for the account. If Not provided, then the loan product's settings will be copied to the new loan account. This logic grants the loan account level payment allocation settings are not affected by later modifications of the loan product. + + diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java index 507ed9e2c1a..7a121da013e 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/WorkingCapitalRequestFactory.java @@ -71,9 +71,13 @@ public class WorkingCapitalRequestFactory { public static final String WCLP_DESCRIPTION = "Working Capital Loan Product"; public static final String DEFAULT_WC_BREACH_NAME = "Default Working Capital breach"; public static final String DEFAULT_WC_NEAR_BREACH_NAME = "Default Working Capital near breach"; - public static final String PENALTY = "PENALTY"; - public static final String FEE = "FEE"; - public static final String PRINCIPAL = "PRINCIPAL"; + public static final String DUE_PENALTY = "DUE_PENALTY"; + public static final String DUE_FEE = "DUE_FEE"; + public static final String DUE_PRINCIPAL = "DUE_PRINCIPAL"; + public static final String IN_ADVANCE_PENALTY = "IN_ADVANCE_PENALTY"; + public static final String IN_ADVANCE_FEE = "IN_ADVANCE_FEE"; + public static final String IN_ADVANCE_PRINCIPAL = "IN_ADVANCE_PRINCIPAL"; + public static final Integer DEFAULT_WC_BREACH_FREQUENCY = 2; public static final String DEFAULT_WC_BREACH_FREQUENCY_TYPE = WorkingCapitalBreachFrequencyType.MONTHS.getCode(); public static final String DEFAULT_WC_BREACH_AMOUNT_CALCULATION_TYPE = WorkingCapitalBreachCalculationType.PERCENTAGE.getCode(); @@ -148,7 +152,7 @@ public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductReq .accountingRule(AccountingRuleEnum.NONE)// .paymentAllocation(List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), - List.of(PENALTY, FEE, PRINCIPAL))));// + List.of(DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, IN_ADVANCE_PENALTY, IN_ADVANCE_FEE, IN_ADVANCE_PRINCIPAL))));// } public PostWorkingCapitalLoanProductsRequest defaultWorkingCapitalLoanProductAllowAttributesOverrideRequest() { @@ -217,31 +221,31 @@ public PutWorkingCapitalLoanProductsProductIdRequest defaultWorkingCapitalLoanPr .allowAttributeOverrides(allowAttributeOverrides)// .paymentAllocation(List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), // - List.of(FEE, PRINCIPAL, PENALTY))));// + List.of(DUE_FEE, DUE_PRINCIPAL, DUE_PENALTY, IN_ADVANCE_FEE, IN_ADVANCE_PRINCIPAL, IN_ADVANCE_PENALTY))));// } public List invalidNumberOfPaymentAllocationRulesForWorkingCapitalLoanProductCreateRequest() { return List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), // - List.of(FEE, PRINCIPAL, PENALTY, "INTEREST")));// + List.of(DUE_FEE, DUE_PRINCIPAL, DUE_PENALTY, "INTEREST")));// } public List invalidPaymentAllocationRulesForWorkingCapitalLoanProductCreateRequest() { return List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), // - List.of(FEE, PRINCIPAL, "INTEREST")));// + List.of(DUE_FEE, DUE_PRINCIPAL, "INTEREST", IN_ADVANCE_FEE, IN_ADVANCE_PRINCIPAL, IN_ADVANCE_PENALTY)));// } public List invalidNumberOfPaymentAllocationRulesForWorkingCapitalLoanProductUpdateRequest() { return List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), // - List.of(FEE, PRINCIPAL, PENALTY, "INTEREST")));// + List.of(DUE_FEE, DUE_PRINCIPAL, DUE_PENALTY, "INTEREST")));// } public List invalidPaymentAllocationRulesForWorkingCapitalLoanProductUpdateRequest() { return List.of(// createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), // - List.of(FEE, PRINCIPAL, "INTEREST")));// + List.of(DUE_FEE, DUE_PRINCIPAL, "INTEREST", IN_ADVANCE_FEE, IN_ADVANCE_PRINCIPAL, IN_ADVANCE_PENALTY)));// } public static PostPaymentAllocation createPaymentAllocation(String transactionType, List paymentAllocationRules) { 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 4650eb8088d..7bae9e00947 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 @@ -1037,7 +1037,7 @@ public static String fieldValueZeroValueFailure(String fieldName) { } public static String paymentAllocationRulesInvalidNumberFailure(int actualNumberOfPaymentAllocationRules) { - return String.format("Each provided payment allocation must contain exactly 3 allocation rules, but %d were provided", + return String.format("Each provided payment allocation must contain exactly 6 allocation rules, but %d were provided", actualNumberOfPaymentAllocationRules); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsValidator.java index b46600ac387..5f7f8dc0dc3 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalAdvancedPaymentAllocationsValidator.java @@ -53,10 +53,9 @@ public void validate(final List } public void validatePairOfOrderAndPaymentAllocationType(final List> rules) { - // WCL has 3 allocation types: PENALTY, FEE, PRINCIPAL (no INTEREST) - final int expectedCount = 3; + final int expectedCount = WorkingCapitalPaymentAllocationType.values().length; if (rules.size() != expectedCount) { - raiseValidationError("wc-payment-allocation-order.must.contain.3.entries", + raiseValidationError("wc-payment-allocation-order.must.contain." + expectedCount + ".entries", "Each provided payment allocation must contain exactly " + expectedCount + " allocation rules, but " + rules.size() + " were provided"); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java index 6c5cc318b34..ed42a32ffe4 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java @@ -29,9 +29,12 @@ @RequiredArgsConstructor public enum WorkingCapitalPaymentAllocationType implements ApiFacingEnum { - PENALTY("PENALTY", "Penalty"), // - FEE("FEE", "Fee"), // - PRINCIPAL("PRINCIPAL", "Principal"); // + DUE_PENALTY("DUE_PENALTY", "Due Penalty"), // + DUE_FEE("DUE_FEE", "Due Fee"), // + DUE_PRINCIPAL("DUE_PRINCIPAL", "Due Principal"), // + IN_ADVANCE_PENALTY("IN_ADVANCE_PENALTY", "In Advance Penalty"), // + IN_ADVANCE_FEE("IN_ADVANCE_FEE", "In Advance Fee"), // + IN_ADVANCE_PRINCIPAL("IN_ADVANCE_PRINCIPAL", "In Advance Principal"); // private final String code; private final String humanReadableName; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java index 5971069caf2..f87fc5cf505 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloanproduct/WorkingCapitalLoanProductTestBuilder.java @@ -48,7 +48,8 @@ public class WorkingCapitalLoanProductTestBuilder { private static final BigDecimal DEFAULT_PERIOD_PAYMENT_RATE = BigDecimal.valueOf(1.0); private static final Integer DEFAULT_PERIOD_PAYMENT_FREQUENCY = 30; private static final String DEFAULT_PERIOD_PAYMENT_FREQUENCY_TYPE = WorkingCapitalLoanPeriodFrequencyType.DAYS.name(); - private static final List DEFAULT_PAYMENT_ALLOCATION_TYPES = List.of("PENALTY", "FEE", "PRINCIPAL"); + private static final List DEFAULT_PAYMENT_ALLOCATION_TYPES = List.of("DUE_PENALTY", "DUE_FEE", "DUE_PRINCIPAL", + "IN_ADVANCE_PENALTY", "IN_ADVANCE_FEE", "IN_ADVANCE_PRINCIPAL"); private static final AccountingRuleEnum DEFAULT_ACCOUNTING_RULE = AccountingRuleEnum.NONE; private String name = DEFAULT_NAME; From 988f1eaefae0adcede3ac7678fc5d122253eaa70 Mon Sep 17 00:00:00 2001 From: Rustam Zeinalov Date: Tue, 19 May 2026 17:28:19 +0200 Subject: [PATCH 2/3] FINERACT-2455: added e2e tests for verifying Working Capital Loan Product - Add Due & In Advanced allocation rules --- .../test/helper/ErrorMessageHelper.java | 4 + .../stepdef/loan/WorkingCapitalStepDef.java | 90 +++++++++++++++++++ .../WorkingCapitalLoanProduct.feature | 76 ++++++++++++++++ .../WorkingCapitalLoanProductCRUDTest.java | 11 ++- ...rkingCapitalLoanProductValidationTest.java | 2 +- 5 files changed, 178 insertions(+), 5 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 7bae9e00947..397daca427f 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 @@ -1045,6 +1045,10 @@ public static String paymentAllocationRulesInvalidValueFailure() { return "One or more payment allocation types are invalid or not recognized"; } + public static String paymentAllocationRulesDuplicateFailure() { + return "The list of provided payment allocation rules must not contain any duplicates"; + } + public static String workingCapitalLoanProductIdentifiedDoesNotExistFailure(String identifierId) { return String.format("Working Capital Loan Product with identifier %s does not exist", identifierId); } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java index 345cd738ab0..56eb5ee4a7d 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.feign.FineractFeignClient; @@ -51,6 +52,7 @@ import org.apache.fineract.client.models.InternalWorkingCapitalLoanPaymentRequest; import org.apache.fineract.client.models.PaymentTypeToGLAccountMapper; import org.apache.fineract.client.models.PostAllowAttributeOverrides; +import org.apache.fineract.client.models.PostPaymentAllocation; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest.AccountingRuleEnum; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse; @@ -1854,6 +1856,94 @@ public void verifyTemplateAdvancedAccountingOptions() { WorkingCapitalLoanProductAdvancedAccountingTestHelper.assertTemplateHasOptions(template); } + @Then("Working Capital Loan Product template advancedPaymentAllocationTypes contains:") + public void verifyTemplateAdvancedPaymentAllocationTypes(final DataTable table) { + final GetWorkingCapitalLoanProductsTemplateResponse template = testContext() + .get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_TEMPLATE_RESPONSE); + assertThat(template.getAdvancedPaymentAllocationTypes()).isNotNull().isNotEmpty(); + final Map actualCodeToValue = template.getAdvancedPaymentAllocationTypes().stream() + .collect(Collectors.toMap(StringEnumOptionData::getCode, StringEnumOptionData::getValue)); + final SoftAssertions assertions = new SoftAssertions(); + assertions.assertThat(actualCodeToValue).hasSize(table.asLists().size()); + for (final List row : table.asLists()) { + final String code = row.get(0); + final String expectedValue = row.get(1); + assertions.assertThat(actualCodeToValue).as("template missing allocation type code %s", code).containsKey(code); + assertions.assertThat(actualCodeToValue.get(code)).as("human readable name for %s", code).isEqualTo(expectedValue); + } + assertions.assertAll(); + } + + @When("Admin creates a new Working Capital Loan Product with payment allocation order:") + public void createWorkingCapitalLoanProductWithPaymentAllocationOrder(final DataTable table) { + final List rules = table.asList(); + final String productName = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10); + final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory.defaultWorkingCapitalLoanProductRequest() // + .name(productName) // + .paymentAllocation(List.of(WorkingCapitalRequestFactory + .createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), rules))); + final PostWorkingCapitalLoanProductsResponse response = createWorkingCapitalLoanProduct(request); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE, response); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_REQUEST, request); + checkWorkingCapitalLoanProductCreate(); + } + + @When("Admin updates Working Capital Loan Product payment allocation order:") + public void updateWorkingCapitalLoanProductPaymentAllocationOrder(final DataTable table) { + final List rules = table.asList(); + final PostWorkingCapitalLoanProductsResponse createResponse = testContext() + .get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE); + final Long resourceId = createResponse.getResourceId(); + final PutWorkingCapitalLoanProductsProductIdRequest updateRequest = new PutWorkingCapitalLoanProductsProductIdRequest() // + .locale(LoanProductsRequestFactory.LOCALE_EN) // + .paymentAllocation(List.of(WorkingCapitalRequestFactory + .createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), rules))); + final PutWorkingCapitalLoanProductsProductIdResponse response = ok( + () -> workingCapitalApi().updateWorkingCapitalLoanProduct(resourceId, updateRequest, Map.of())); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_UPDATE_RESPONSE, response); + testContext().set(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_UPDATE_REQUEST, updateRequest); + } + + @Then("Working Capital Loan Product payment allocation order is:") + public void verifyWorkingCapitalLoanProductPaymentAllocationOrder(final DataTable table) { + final PostWorkingCapitalLoanProductsResponse createResponse = testContext() + .get(TestContextKey.WORKING_CAPITAL_LOAN_PRODUCT_CREATE_RESPONSE); + final Long resourceId = createResponse.getResourceId(); + final GetWorkingCapitalLoanProductsProductIdResponse product = workingCapitalApi().retrieveOneWorkingCapitalLoanProduct(resourceId, + Map.of()); + assertThat(product.getPaymentAllocation()).isNotNull().isNotEmpty(); + final GetPaymentAllocation defaultAllocation = product.getPaymentAllocation().stream() // + .filter(pa -> PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue().equals(pa.getTransactionType())) // + .findFirst() // + .orElseThrow(() -> new RuntimeException("No DEFAULT payment allocation found on product")); + final List> rows = table.asLists(); + final SoftAssertions assertions = new SoftAssertions(); + assertions.assertThat(defaultAllocation.getPaymentAllocationOrder()).hasSize(rows.size()); + for (final List row : rows) { + final String expectedRule = row.get(0); + final Integer expectedOrder = Integer.valueOf(row.get(1)); + final boolean match = defaultAllocation.getPaymentAllocationOrder().stream() // + .anyMatch(p -> expectedRule.equals(p.getPaymentAllocationRule()) && expectedOrder.equals(p.getOrder())); + assertions.assertThat(match).as("expected payment allocation rule %s at order %d", expectedRule, expectedOrder).isTrue(); + } + assertions.assertAll(); + } + + @Then("Admin failed to create a new Working Capital Loan Product with duplicate payment allocation rules") + public void createWorkingCapitalLoanProductWithDuplicatePaymentAllocationRulesFailed() { + final String productName = DefaultWorkingCapitalLoanProduct.WCLP.getName() + Utils.randomStringGenerator("_", 10); + final List duplicateRules = List.of(// + WorkingCapitalRequestFactory.DUE_PENALTY, WorkingCapitalRequestFactory.DUE_PENALTY, WorkingCapitalRequestFactory.DUE_FEE, + WorkingCapitalRequestFactory.DUE_PRINCIPAL, WorkingCapitalRequestFactory.IN_ADVANCE_FEE, + WorkingCapitalRequestFactory.IN_ADVANCE_PRINCIPAL); + final PostWorkingCapitalLoanProductsRequest request = workingCapitalRequestFactory.defaultWorkingCapitalLoanProductRequest() // + .name(productName) // + .paymentAllocation(List.of(WorkingCapitalRequestFactory + .createPaymentAllocation(PostPaymentAllocation.TransactionTypeEnum.DEFAULT.getValue(), duplicateRules))); + final String errorMessage = ErrorMessageHelper.paymentAllocationRulesDuplicateFailure(); + checkCreateWorkingCapitalLoanProductWithInvalidDataFailure(request, 400, errorMessage); + } + @Then("Working Capital Loan Product has advanced accounting mappings") public void verifyProductHasAdvancedAccountingMappings() { final GetWorkingCapitalLoanProductsProductIdResponse product = retrieveCreatedProduct(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanProduct.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanProduct.feature index 104da586e8e..1ff2bd9a92c 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanProduct.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanProduct.feature @@ -416,3 +416,79 @@ Feature: WorkingCapitalLoanProduct Examples: | wcp_field_name | wcp_invalid_field_value | wcp_error_message | | nearBreachId | "0" | "Working Capital Near Breach with id 0 was not found." | + + @TestRailId:C80962 + Scenario: Verify WC Loan Product template exposes the 6 advanced payment allocation types + When Admin retrieves the Working Capital Loan Product template + Then Working Capital Loan Product template advancedPaymentAllocationTypes contains: + | DUE_PENALTY | Due Penalty | + | DUE_FEE | Due Fee | + | DUE_PRINCIPAL | Due Principal | + | IN_ADVANCE_PENALTY | In Advance Penalty | + | IN_ADVANCE_FEE | In Advance Fee | + | IN_ADVANCE_PRINCIPAL | In Advance Principal | + + @TestRailId:C80963 + Scenario: Verify WC Loan Product create persists DUE-first then IN_ADVANCE payment allocation order + When Admin creates a new Working Capital Loan Product with payment allocation order: + | DUE_PENALTY | + | DUE_FEE | + | DUE_PRINCIPAL | + | IN_ADVANCE_PENALTY | + | IN_ADVANCE_FEE | + | IN_ADVANCE_PRINCIPAL | + Then Working Capital Loan Product payment allocation order is: + | DUE_PENALTY | 1 | + | DUE_FEE | 2 | + | DUE_PRINCIPAL | 3 | + | IN_ADVANCE_PENALTY | 4 | + | IN_ADVANCE_FEE | 5 | + | IN_ADVANCE_PRINCIPAL | 6 | + Then Admin deletes a Working Capital Loan Product + + @TestRailId:C80964 + Scenario: Verify WC Loan Product create persists PRINCIPAL-first interleaved payment allocation order + When Admin creates a new Working Capital Loan Product with payment allocation order: + | DUE_PRINCIPAL | + | IN_ADVANCE_PRINCIPAL | + | DUE_FEE | + | IN_ADVANCE_FEE | + | DUE_PENALTY | + | IN_ADVANCE_PENALTY | + Then Working Capital Loan Product payment allocation order is: + | DUE_PRINCIPAL | 1 | + | IN_ADVANCE_PRINCIPAL | 2 | + | DUE_FEE | 3 | + | IN_ADVANCE_FEE | 4 | + | DUE_PENALTY | 5 | + | IN_ADVANCE_PENALTY | 6 | + Then Admin deletes a Working Capital Loan Product + + @TestRailId:C80965 + Scenario: Verify WC Loan Product update changes the payment allocation order + When Admin creates a new Working Capital Loan Product with payment allocation order: + | DUE_PENALTY | + | DUE_FEE | + | DUE_PRINCIPAL | + | IN_ADVANCE_PENALTY | + | IN_ADVANCE_FEE | + | IN_ADVANCE_PRINCIPAL | + When Admin updates Working Capital Loan Product payment allocation order: + | IN_ADVANCE_PRINCIPAL | + | IN_ADVANCE_FEE | + | IN_ADVANCE_PENALTY | + | DUE_PRINCIPAL | + | DUE_FEE | + | DUE_PENALTY | + Then Working Capital Loan Product payment allocation order is: + | IN_ADVANCE_PRINCIPAL | 1 | + | IN_ADVANCE_FEE | 2 | + | IN_ADVANCE_PENALTY | 3 | + | DUE_PRINCIPAL | 4 | + | DUE_FEE | 5 | + | DUE_PENALTY | 6 | + Then Admin deletes a Working Capital Loan Product + + @TestRailId:C80966 + Scenario: Verify WC Loan Product create fails when payment allocation rules contain duplicates + Then Admin failed to create a new Working Capital Loan Product with duplicate payment allocation rules diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java index f4d16523c98..87d25ac376a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductCRUDTest.java @@ -177,12 +177,13 @@ public void testRetrieveTemplate() { assertFalse(response.getAdvancedPaymentAllocationTransactionTypes().isEmpty(), "Payment allocation transaction type options should not be empty"); // Verify payment allocation types contain expected values - final List expectedPaymentAllocationTypes = List.of("PENALTY", "FEE", "PRINCIPAL"); + final List expectedPaymentAllocationTypes = List.of("DUE_PENALTY", "DUE_FEE", "DUE_PRINCIPAL", "IN_ADVANCE_PENALTY", + "IN_ADVANCE_FEE", "IN_ADVANCE_PRINCIPAL"); final List actualPaymentAllocationTypes = response.getAdvancedPaymentAllocationTypes().stream() .map(StringEnumOptionData::getCode).toList(); assertTrue(actualPaymentAllocationTypes.containsAll(expectedPaymentAllocationTypes), "Payment allocation types should contain all expected types"); - assertEquals(3, actualPaymentAllocationTypes.size(), "Payment allocation types should have exactly 3 types"); + assertEquals(6, actualPaymentAllocationTypes.size(), "Payment allocation types should have exactly 6 types"); } @Test @@ -275,7 +276,8 @@ public void testCreateWorkingCapitalLoanProductWithAllFields() { // Given final String uniqueId = UUID.randomUUID().toString().substring(0, 8); final String externalId = UUID.randomUUID().toString(); - final List paymentAllocationTypes = List.of("PENALTY", "FEE", "PRINCIPAL"); + final List paymentAllocationTypes = List.of("DUE_PENALTY", "DUE_FEE", "DUE_PRINCIPAL", "IN_ADVANCE_PENALTY", + "IN_ADVANCE_FEE", "IN_ADVANCE_PRINCIPAL"); final HashMap allowAttributeOverrides = new HashMap<>(); allowAttributeOverrides.put("amortizationType", true); allowAttributeOverrides.put("interestType", false); @@ -340,7 +342,8 @@ public void testHappyPath_CreateAndRetrieve_VerifyAllFields() { final String shortName = Utils.uniqueRandomStringGenerator("", 4); final String externalId = UUID.randomUUID().toString(); final String description = "Comprehensive test product with all fields"; - final List paymentAllocationTypes = List.of("PENALTY", "FEE", "PRINCIPAL"); + final List paymentAllocationTypes = List.of("DUE_PENALTY", "DUE_FEE", "DUE_PRINCIPAL", "IN_ADVANCE_PENALTY", + "IN_ADVANCE_FEE", "IN_ADVANCE_PRINCIPAL"); // Get fund and delinquency bucket from template final GetWorkingCapitalLoanProductsTemplateResponse template = wclProductHelper.retrieveTemplate(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java index 76f2eaf920a..7be80b3b221 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanProductValidationTest.java @@ -569,7 +569,7 @@ public void testCreateWorkingCapitalLoanProductWithInvalidPaymentAllocationType( assertEquals(400, exception.getStatus()); assertNotNull(exception.getDeveloperMessage()); assertEquals( - "Validation errors: [id] Each provided payment allocation must contain exactly 3 allocation rules, but 1 were provided", + "Validation errors: [id] Each provided payment allocation must contain exactly 6 allocation rules, but 1 were provided", exception.getDeveloperMessage()); } From 5913227de104e3789d2e8927b5b928c06bc1ce2b Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Wed, 20 May 2026 21:40:33 +0100 Subject: [PATCH 3/3] FINERACT-2455: Add Due type and Allocation type flags --- .../WorkingCapitalPaymentAllocationType.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java index ed42a32ffe4..aac9aa414c3 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalPaymentAllocationType.java @@ -18,9 +18,17 @@ */ package org.apache.fineract.portfolio.workingcapitalloanproduct.domain; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; +import static org.apache.fineract.portfolio.loanproduct.domain.DueType.DUE; +import static org.apache.fineract.portfolio.loanproduct.domain.DueType.IN_ADVANCE; + import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; +import org.apache.fineract.portfolio.loanproduct.domain.DueType; /** * Payment allocation types for Working Capital Loan Product. Only PRINCIPAL, FEE, and PENALTY (no INTEREST). @@ -29,13 +37,15 @@ @RequiredArgsConstructor public enum WorkingCapitalPaymentAllocationType implements ApiFacingEnum { - DUE_PENALTY("DUE_PENALTY", "Due Penalty"), // - DUE_FEE("DUE_FEE", "Due Fee"), // - DUE_PRINCIPAL("DUE_PRINCIPAL", "Due Principal"), // - IN_ADVANCE_PENALTY("IN_ADVANCE_PENALTY", "In Advance Penalty"), // - IN_ADVANCE_FEE("IN_ADVANCE_FEE", "In Advance Fee"), // - IN_ADVANCE_PRINCIPAL("IN_ADVANCE_PRINCIPAL", "In Advance Principal"); // + DUE_PENALTY(DUE, PENALTY, "DUE_PENALTY", "Due Penalty"), // + DUE_FEE(DUE, FEE, "DUE_FEE", "Due Fee"), // + DUE_PRINCIPAL(DUE, PRINCIPAL, "DUE_PRINCIPAL", "Due Principal"), // + IN_ADVANCE_PENALTY(IN_ADVANCE, PENALTY, "IN_ADVANCE_PENALTY", "In Advance Penalty"), // + IN_ADVANCE_FEE(IN_ADVANCE, FEE, "IN_ADVANCE_FEE", "In Advance Fee"), // + IN_ADVANCE_PRINCIPAL(IN_ADVANCE, PRINCIPAL, "IN_ADVANCE_PRINCIPAL", "In Advance Principal"); // + private final DueType dueType; + private final AllocationType allocationType; private final String code; private final String humanReadableName;