Repository: apache/fineract
Pull request #4546: FINERACT-2237: Fix GSIM account creation to generate single parent for multiple clients
Author: login: JebronLames32
Created: 2025-04-05T06:10:59Z | Merged: 2025-04-14T15:05:18Z

## Description

This fix corrects the logic for GSIM account creation to ensure that only a single GSIM parent account is created and the remaining accounts are listed as child accounts under the GSIM account when multiple clients are provided in the request payload (with only one client having `"isParentAccount": 1` set). The previous logic caused multiple GSIM parent accounts to be erroneously created, each associated with only a single client as the condition for checking if a parent account is to be created was wrong.


## Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

- [x] Write the commit message as per https://github.com/apache/fineract/#pull-requests

- [ ] Acknowledge that we will not review PRs that are not passing the build _("green")_ - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.

- [x] Create/update unit or integration tests for verifying the changes made.

- [x] Follow coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions.

- [ ] Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes

- [x] Submission is not a "code dump".  (Large changes can be made "in repository" via a branch.  Ask on the developer mailing list for guidance, if required.)

FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.



Diff:
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
index 2645cfccae5..c7fb7d249e6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
@@ -168,7 +168,9 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null
+                            && ("1".equals(command.stringValueOfParameterNamed("isParentAccount"))
+                                    || command.booleanObjectValueOfParameterNamed("isParentAccount"))) {
                         // empty table check
                         if (gsimRepository.count() != 0) {
                             // Parent-Not an empty table
@@ -193,7 +195,12 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
                     } else {
                         if (gsimRepository.count() != 0) {
                             // Child-Not an empty table check
-                            gsimAccount = gsimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId);
+                            if (applicationId.compareTo(BigDecimal.ZERO) == 0) {
+                                gsimAccount = gsimRepository.findOneByIsAcceptingChildAndApplicationIdAndGroupId(true, applicationId,
+                                        groupId);
+                            } else {
+                                gsimAccount = gsimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId);
+                            }
                             accountNumber = gsimAccount.getAccountNumber() + (gsimAccount.getChildAccountsCount() + 1);
                             account.updateAccountNo(accountNumber);
                             this.gsimWritePlatformService.incrementChildAccountCount(gsimAccount);
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/GSIMRepositoy.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/GSIMRepositoy.java
index 3000ada17a4..a62350eee6b 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/GSIMRepositoy.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/GSIMRepositoy.java
@@ -31,4 +31,7 @@ public interface GSIMRepositoy
     GroupSavingsIndividualMonitoring findOneByIsAcceptingChildAndApplicationId(boolean acceptingChild, BigDecimal applicationId);
 
     GroupSavingsIndividualMonitoring findOneByAccountNumber(String accountNumber);
+
+    GroupSavingsIndividualMonitoring findOneByIsAcceptingChildAndApplicationIdAndGroupId(boolean acceptingChild, BigDecimal applicationId,
+            Long groupId);
 }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
index adee98f18fe..0d62177e5b9 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
@@ -179,6 +179,57 @@ public void testSavingsAccount_CLOSE_APPLICATION() {
 
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testGsimSavingsAccount_WithTwoClients_ChildCountTwo() {
+
+        // Initialize the helper for savings account operations
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+
+        // Create two clients: one designated as the parent and one as the child
+        final Integer parentClientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(parentClientID);
+
+        final Integer childClientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(childClientID);
+
+        // Create a group and associate both clients with it
+        Integer groupID = GroupHelper.createGroup(this.requestSpec, this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+
+        // obtain the latest application ID of the gsim accounts
+        // BigDecimal applicationId = GroupHelper.getLastApplicationIdOfGsimSavingAccount(this.requestSpec,
+        // this.responseSpec, groupID).add(BigDecimal.ONE);
+
+        groupID = GroupHelper.associateClient(this.requestSpec, this.responseSpec, groupID.toString(), parentClientID.toString());
+        Assertions.assertNotNull(groupID);
+        groupID = GroupHelper.associateClient(this.requestSpec, this.responseSpec, groupID.toString(), childClientID.toString());
+        Assertions.assertNotNull(groupID);
+
+        // Create a savings product necessary for the GSIM application
+        final String minBalanceForInterestCalculation = null;
+        final String minRequiredBalance = null;
+        final String enforceMinRequiredBalance = "false";
+        final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, MINIMUM_OPENING_BALANCE,
+                minBalanceForInterestCalculation, minRequiredBalance, enforceMinRequiredBalance);
+        Assertions.assertNotNull(savingsProductID);
+
+        // Prepare the client array with two entries:
+        // one for the parent client (isParent = true) and one for the child client (isParent = false)
+        List<Map<String, Object>> clientArray = new ArrayList<>();
+        clientArray.add(clientArray(parentClientID, groupID, savingsProductID, "08 January 2013", true));
+        clientArray.add(clientArray(childClientID, groupID, savingsProductID, "08 January 2013", false));
+
+        // Apply for a GSIM savings account with both clients under the same application
+        final Integer gsimID = this.savingsAccountHelper.applyForGsimApplication(clientArray);
+        Assertions.assertNotNull(gsimID);
+
+        // get child account count
+        final Integer childAccountCount = GroupHelper.getChildAccountCount(this.requestSpec, this.responseSpec, groupID);
+        assertEquals(childAccountCount, 2);
+
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     public void testSavingsAccount_DELETE_APPLICATION() {
@@ -254,7 +305,7 @@ public void testGsimSavingsAccount_REJECT_APPLICATION() {
         Assertions.assertNotNull(savingsProductID);
 
         List<Map<String, Object>> clientArray = new ArrayList<>();
-        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013"));
+        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013", true));
 
         final Integer gsimID = this.savingsAccountHelper.applyForGsimApplication(clientArray);
 
@@ -306,7 +357,7 @@ public void testGsimSavingsAccount_DEPOSIT_APPLICATION() {
         Assertions.assertNotNull(savingsProductID);
 
         List<Map<String, Object>> clientArray = new ArrayList<>();
-        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013"));
+        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013", true));
         LOG.info("client Array : {} ", clientArray);
 
         final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID, ACCOUNT_TYPE_GROUP);
@@ -365,7 +416,7 @@ public void testGsimSavingsAccount_CLOSE_APPLICATION() {
         Assertions.assertNotNull(savingsProductID);
 
         List<Map<String, Object>> clientArray = new ArrayList<>();
-        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013"));
+        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013", true));
 
         final Integer gsimID = this.savingsAccountHelper.applyForGsimApplication(clientArray);
 
@@ -414,7 +465,7 @@ public void testGsimSavingsAccount_UPDATE_APPLICATION() {
         Assertions.assertNotNull(savingsProductID);
 
         List<Map<String, Object>> clientArray = new ArrayList<>();
-        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013"));
+        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013", true));
 
         final Integer gsimID = this.savingsAccountHelper.applyForGsimApplication(clientArray);
 
@@ -448,7 +499,7 @@ public void getGsimAccount() {
         Assertions.assertNotNull(savingsProductID);
 
         List<Map<String, Object>> clientArray = new ArrayList<>();
-        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013"));
+        clientArray.add(clientArray(clientID, groupID, savingsProductID, "08 January 2013", true));
         final Integer gsimID = this.savingsAccountHelper.applyForGsimApplication(clientArray);
 
         final List<String> retrievedGsimId = GroupHelper.verifyRetrieveGsimAccounts(this.requestSpec, this.responseSpec, groupID);
@@ -797,7 +848,7 @@ public static Integer createSavingsProduct(final RequestSpecification requestSpe
     }
 
     private Map<String, Object> clientArray(final Integer clientId, final Integer groupId, final Integer productId,
-            final String submittedOnDate) {
+            final String submittedOnDate, final boolean isParent) {
         Map<String, Object> map = new HashMap<>();
         map.put("clientId", clientId);
         map.put("groupId", groupId);
@@ -805,7 +856,7 @@ private Map<String, Object> clientArray(final Integer clientId, final Integer gr
         map.put("submittedOnDate", submittedOnDate);
         map.put("dateFormat", CommonConstants.DATE_FORMAT);
         map.put("locale", "en");
-        map.put("isParentAccount", "1");
+        map.put("isParentAccount", isParent);
         map.put("isGSIM", "true");
         return map;
     }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GroupHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GroupHelper.java
index eb6e9d11952..b6cabe39563 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GroupHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GroupHelper.java
@@ -407,6 +407,17 @@ public static List<String> verifyRetrieveGsimAccounts(final RequestSpecification
         return list;
     }
 
+    public static Integer getChildAccountCount(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
+            final Integer groupID) {
+        List<Object> list;
+        LOG.info("------------------------------GET CHILD ACCOUNT COUNT------------------------------------\n");
+        final String GROUP_URL = "/fineract-provider/api/v1/groups/" + groupID + "/gsimaccounts?" + Utils.TENANT_IDENTIFIER;
+        list = Utils.performServerGet(requestSpec, responseSpec, GROUP_URL, "childGSIMAccounts");
+
+        return ((ArrayList) list.get(0)).size();
+
+    }
+
     public static String randomNameGenerator(final String prefix, final int lenOfRandomSuffix) {
         return Utils.uniqueRandomStringGenerator(prefix, lenOfRandomSuffix);
     }


Reviews:
user: login: adamsaghy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:41:57Z
user: login: JebronLames32
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:20:57Z
user: login: JebronLames32
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:43:27Z
user: login: adamsaghy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:58:41Z
user: login: JebronLames32
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:34:08Z

Review comments:
diff_hunk: @@ -168,7 +168,7 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount")) {
path: fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
user: login: adamsaghy
body: How does this fix anything?
created_at: 2025-04-07T14:41:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +168,7 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount")) {
path: fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
user: login: JebronLames32
body: It is a boolean value that is being returned by `command.booleanObjectValueOfParameterNamed("isParentAccount")`. It will always evaluate to `true` when compared to not be null. 
created_at: 2025-04-10T20:20:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +168,7 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount")) {
path: fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
user: login: JebronLames32
body: I do realise that I should make a better change here. Making the changes to also consider the case where `isParentAccount` is evaluating to null
created_at: 2025-04-10T20:43:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +168,9 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null
path: fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
user: login: adamsaghy
body: Is this boolean or string now? I am confused...
created_at: 2025-04-14T13:58:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -168,7 +168,9 @@ public CommandProcessingResult submitApplication(final JsonCommand command) {
 
                     Group group = this.groupRepositoryWrapper.findOneWithNotFoundDetection(groupId);
 
-                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
+                    if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null
path: fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java
user: login: JebronLames32
body: 
![Screenshot From 2025-04-14 19-42-35](https://github.com/user-attachments/assets/b3562440-bc26-4664-9727-dd7f34831058)

Originally documented as “1”
In the legacy documentation, isParentAccount is being indicated using 1 to represent true (and possibly 0 or omission for false). However, the current Fineract implementation (i.e., `command.booleanObjectValueOfParameterNamed("isParentAccount"))` uses a Boolean for logic.

I added (`"1".equals(command.stringValueOfParameterNamed("isParentAccount"))`) to handle the documentation scenario where consumers send the value as "1" instead of "true".
Meanwhile, `command.booleanObjectValueOfParameterNamed("isParentAccount")` properly interprets JSON booleans (true/false).

I felt we could accommodate both forms of input:

- Boolean: true/false (which booleanObjectValueOfParameterNamed understands), and
- Legacy string: "1" for true.

The intended usage going forward could be to be a boolean field in the API.
The code allows for backward compatibility by also accepting the string "1" as an equivalent to true.
created_at: 2025-04-14T14:34:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: JebronLames32
created_at: 2025-04-05T06:19:01Z
body: The current unit tests for GSIM account at `GroupSavingsIntegrationTest.java` are marked deprecated and also do not cover the scenario where **multiple clients need to be part of a GSIM account.** 
Can someone please assist me in creating Unit tests compliant with [FINERACT-2165](https://issues.apache.org/jira/browse/FINERACT-2165)
author_association: CONTRIBUTOR
user: login: JebronLames32
created_at: 2025-04-11T09:59:52Z
body: - The API works when `isParentAccount` is set to true for one client and **omitted** (or set to **`null`**) for the others in the `clientArray`
- The API fails (makes it a parent account) when `isParentAccount` is explicitly set to **`false`** for the clients. Instead of accepting `false`, the API seems to expect the parameter to be omitted.
- I’m fixing this by ensuring the API accepts false as a valid value for isParentAccount. This avoids relying on omission (null) of this vlaue for accounts that are not the parent account for the GSIM
author_association: CONTRIBUTOR