This is an automated email from the ASF dual-hosted git repository.

victorromero pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 05662e64d5 FINERACT-2473: Add available balance to group accounts 
endpoint (#5468)
05662e64d5 is described below

commit 05662e64d59815e7c904dfc682964993c7db41b9
Author: Ralph Hopman <[email protected]>
AuthorDate: Sat Feb 14 23:15:34 2026 +0100

    FINERACT-2473: Add available balance to group accounts endpoint (#5468)
---
 .../data/SavingsAccountSummaryData.java            |  36 ++-
 ...etailsReadPlatformServiceJpaRepositoryImpl.java |  17 +-
 .../group/api/GroupsApiResourceSwagger.java        |   8 +
 .../GroupSavingsIntegrationTest.java               | 260 +++++++++++++++++++++
 4 files changed, 311 insertions(+), 10 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/accountdetails/data/SavingsAccountSummaryData.java
 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/accountdetails/data/SavingsAccountSummaryData.java
index 3cc739b879..009e634eb2 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/accountdetails/data/SavingsAccountSummaryData.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/accountdetails/data/SavingsAccountSummaryData.java
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.accountdetails.data;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import lombok.Getter;
 import org.apache.fineract.infrastructure.core.data.EnumOptionData;
 import org.apache.fineract.organisation.monetary.data.CurrencyData;
 import 
org.apache.fineract.portfolio.savings.data.SavingsAccountApplicationTimelineData;
@@ -29,6 +30,7 @@ import 
org.apache.fineract.portfolio.savings.data.SavingsAccountSubStatusEnumDat
 /**
  * Immutable data object for savings accounts.
  */
+@Getter
 @SuppressWarnings("unused")
 public class SavingsAccountSummaryData {
 
@@ -40,7 +42,27 @@ public class SavingsAccountSummaryData {
     private final String shortProductName;
     private final SavingsAccountStatusEnumData status;
     private final CurrencyData currency;
+    /**
+     * The total balance of the savings account.
+     */
     private final BigDecimal accountBalance;
+    /**
+     * Funds held as collateral for loan guarantees. When a savings account is 
used as a guarantor for a loan, the
+     * guarantee amount is held here and unavailable for withdrawal until the 
loan is repaid or the guarantee is
+     * released. Sourced from the database column on_hold_funds_derived.
+     */
+    private final BigDecimal onHoldFunds;
+    /**
+     * User-initiated holds explicitly placed on the account, including lien 
holds. These are manual holds placed
+     * through hold/release transactions to restrict withdrawals for specific 
purposes. Sourced from the database column
+     * total_savings_amount_on_hold.
+     */
+    private final BigDecimal savingsAmountOnHold;
+    /**
+     * The actual available balance that can be withdrawn, accounting for all 
holds. Calculated as: accountBalance -
+     * onHoldFunds (guarantor holds) - savingsAmountOnHold (user/lien holds)
+     */
+    private final BigDecimal availableBalance;
     // differentiate Individual, JLG or Group account
     private final EnumOptionData accountType;
     private final SavingsAccountApplicationTimelineData timeline;
@@ -52,7 +74,8 @@ public class SavingsAccountSummaryData {
 
     public SavingsAccountSummaryData(final Long id, final String accountNo, 
final String externalId, final Long productId,
             final String productName, final String shortProductName, final 
SavingsAccountStatusEnumData status, final CurrencyData currency,
-            final BigDecimal accountBalance, final EnumOptionData accountType, 
final SavingsAccountApplicationTimelineData timeline,
+            final BigDecimal accountBalance, final BigDecimal onHoldFunds, 
final BigDecimal savingsAmountOnHold,
+            final BigDecimal availableBalance, final EnumOptionData 
accountType, final SavingsAccountApplicationTimelineData timeline,
             final EnumOptionData depositType, final 
SavingsAccountSubStatusEnumData subStatus, final LocalDate 
lastActiveTransactionDate) {
         this.id = id;
         this.accountNo = accountNo;
@@ -63,18 +86,13 @@ public class SavingsAccountSummaryData {
         this.status = status;
         this.currency = currency;
         this.accountBalance = accountBalance;
+        this.onHoldFunds = onHoldFunds;
+        this.savingsAmountOnHold = savingsAmountOnHold;
+        this.availableBalance = availableBalance;
         this.accountType = accountType;
         this.timeline = timeline;
         this.depositType = depositType;
         this.subStatus = subStatus;
         this.lastActiveTransactionDate = lastActiveTransactionDate;
     }
-
-    public String getAccountNo() {
-        return accountNo;
-    }
-
-    public Long getId() {
-        return id;
-    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
index 7c1f50873e..9f6600d4f5 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
@@ -311,6 +311,8 @@ public class 
AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
             accountsSummary.append("sa.id as id, sa.account_no as accountNo, 
sa.external_id as externalId, sa.status_enum as statusEnum, ");
             accountsSummary.append("sa.account_type_enum as accountType, ");
             accountsSummary.append("sa.account_balance_derived as 
accountBalance, ");
+            accountsSummary.append("sa.on_hold_funds_derived as onHoldFunds, 
");
+            accountsSummary.append("sa.total_savings_amount_on_hold as 
onHoldAmount, ");
 
             accountsSummary.append("sa.submittedon_date as submittedOnDate,");
             accountsSummary.append("sbu.username as submittedByUsername,");
@@ -377,6 +379,18 @@ public class 
AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
             final String shortProductName = rs.getString("shortProductName");
             final Integer statusId = JdbcSupport.getInteger(rs, "statusEnum");
             final BigDecimal accountBalance = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accountBalance");
+            final BigDecimal onHoldFunds = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldFunds");
+            final BigDecimal onHoldAmount = 
JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "onHoldAmount");
+
+            BigDecimal availableBalance = accountBalance;
+            if (availableBalance != null && onHoldFunds != null) {
+                availableBalance = availableBalance.subtract(onHoldFunds);
+            }
+
+            if (availableBalance != null && onHoldAmount != null) {
+                availableBalance = availableBalance.subtract(onHoldAmount);
+            }
+
             final SavingsAccountStatusEnumData status = 
SavingsEnumerations.status(statusId);
             final Integer accountType = JdbcSupport.getInteger(rs, 
"accountType");
             final EnumOptionData accountTypeData = 
AccountEnumerations.loanType(accountType);
@@ -433,7 +447,8 @@ public class 
AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
                     activatedByLastname, closedOnDate, closedByUsername, 
closedByFirstname, closedByLastname);
 
             return new SavingsAccountSummaryData(id, accountNo, externalId, 
productId, productName, shortProductName, status, currency,
-                    accountBalance, accountTypeData, timeline, 
depositTypeData, subStatus, lastActiveTransactionDate);
+                    accountBalance, onHoldFunds, onHoldAmount, 
availableBalance, accountTypeData, timeline, depositTypeData, subStatus,
+                    lastActiveTransactionDate);
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResourceSwagger.java
index 8f27280b30..a7805647e5 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/api/GroupsApiResourceSwagger.java
@@ -415,6 +415,14 @@ final class GroupsApiResourceSwagger {
             public String productName;
             public GetGroupsGroupIdAccountsSavingStatus status;
             public GetGroupsGroupIdAccountsSavingCurrency currency;
+            @Schema(example = "5000.00")
+            public java.math.BigDecimal accountBalance;
+            @Schema(example = "300.00")
+            public java.math.BigDecimal onHoldFunds;
+            @Schema(example = "200.00")
+            public java.math.BigDecimal savingsAmountOnHold;
+            @Schema(example = "4500.00")
+            public java.math.BigDecimal availableBalance;
             public GetGroupsGroupIdAccountsSavingAccountType accountType;
         }
 
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 69043adffa..f1f609a51e 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
@@ -26,6 +26,7 @@ import io.restassured.http.ContentType;
 import io.restassured.specification.RequestSpecification;
 import io.restassured.specification.ResponseSpecification;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
@@ -45,6 +46,7 @@ import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
 import 
org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
 import 
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
@@ -72,6 +74,7 @@ public class GroupSavingsIntegrationTest {
     public static final String MINIMUM_OPENING_BALANCE = "1000.0";
     public static final String PRINCIPAL = "5000";
     public static final String GUARANTEE_AMOUNT = "500";
+    public static final String HOLD_AMOUNT = "300";
     public static final String ACCOUNT_TYPE_GROUP = "GROUP";
 
     private ResponseSpecification responseSpec;
@@ -1021,4 +1024,261 @@ public class GroupSavingsIntegrationTest {
                 "Should find at least one on-hold transaction with 
savingsClientName populated (group name)");
     }
 
+    @Test
+    public void testGroupAccountAvailableBalance() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create a client
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, 
this.responseSpec);
+        ClientHelper.verifyClientCreatedOnServer(this.requestSpec, 
this.responseSpec, clientID);
+        Assertions.assertNotNull(clientID);
+
+        // Create a group and associate the client
+        Integer groupID = GroupHelper.createGroup(this.requestSpec, 
this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+
+        // Create a savings product
+        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);
+
+        // Apply for and activate a group savings account
+        final Integer savingsId = 
this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID, 
ACCOUNT_TYPE_GROUP);
+        Assertions.assertNotNull(savingsId);
+
+        HashMap savingsStatusHashMap = 
this.savingsAccountHelper.approveSavings(savingsId);
+        SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
+
+        savingsStatusHashMap = 
this.savingsAccountHelper.activateSavings(savingsId);
+        SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
+
+        // Make a deposit to create a balance
+        Integer depositTransactionId = (Integer) 
this.savingsAccountHelper.depositToSavingsAccount(savingsId, DEPOSIT_AMOUNT,
+                SavingsAccountHelper.TRANSACTION_DATE, 
CommonConstants.RESPONSE_RESOURCE_ID);
+        Assertions.assertNotNull(depositTransactionId);
+
+        // Get the account summary to verify balance
+        // Note: Account has minimum opening balance (1000) + deposit (2000) = 
3000 total
+        HashMap summary = 
this.savingsAccountHelper.getSavingsSummary(savingsId);
+        Float expectedBalance = Float.parseFloat(MINIMUM_OPENING_BALANCE) + 
Float.parseFloat(DEPOSIT_AMOUNT);
+        assertEquals(expectedBalance, summary.get("accountBalance"), 
"Verifying Deposit Balance");
+
+        // Retrieve group accounts endpoint
+        final String GROUP_ACCOUNTS_URL = "/fineract-provider/api/v1/groups/" 
+ groupID + "/accounts?" + Utils.TENANT_IDENTIFIER;
+        HashMap groupAccountsResponse = 
Utils.performServerGet(this.requestSpec, this.responseSpec, GROUP_ACCOUNTS_URL, 
"");
+        Assertions.assertNotNull(groupAccountsResponse);
+
+        // Verify savingsAccounts array exists and has our account
+        ArrayList<HashMap> savingsAccounts = (ArrayList<HashMap>) 
groupAccountsResponse.get("savingsAccounts");
+        Assertions.assertNotNull(savingsAccounts, "savingsAccounts array 
should be present");
+        Assertions.assertTrue(savingsAccounts.size() > 0, "savingsAccounts 
should contain at least one account");
+
+        // Find our savings account in the response
+        HashMap account = null;
+        for (HashMap acc : savingsAccounts) {
+            if (acc.get("id").equals(savingsId)) {
+                account = acc;
+                break;
+            }
+        }
+        Assertions.assertNotNull(account, "Savings account should be in the 
response");
+
+        // Verify accountBalance and availableBalance fields are present
+        Assertions.assertNotNull(account.get("accountBalance"), 
"accountBalance field should be present");
+        Assertions.assertNotNull(account.get("availableBalance"), 
"availableBalance field should be present");
+
+        // Parse accountBalance
+        BigDecimal accountBalance = new 
BigDecimal(account.get("accountBalance").toString());
+
+        // Parse hold fields (may be null if no holds exist)
+        BigDecimal onHoldFunds = account.get("onHoldFunds") != null ? new 
BigDecimal(account.get("onHoldFunds").toString())
+                : BigDecimal.ZERO;
+        BigDecimal savingsAmountOnHold = account.get("savingsAmountOnHold") != 
null
+                ? new BigDecimal(account.get("savingsAmountOnHold").toString())
+                : BigDecimal.ZERO;
+
+        // Parse availableBalance
+        BigDecimal availableBalance = new 
BigDecimal(account.get("availableBalance").toString());
+
+        // Verify accountBalance matches expected total (minimum opening 
balance + deposit)
+        assertEquals(0, 
expectedBalance.compareTo(Float.parseFloat(accountBalance.toString())),
+                "accountBalance should equal minimum opening balance plus 
deposited amount");
+
+        // Since we haven't placed any holds, onHoldFunds and 
savingsAmountOnHold should be null or 0
+        assertEquals(0, BigDecimal.ZERO.compareTo(onHoldFunds), "onHoldFunds 
should be 0 when no holds are placed");
+        assertEquals(0, BigDecimal.ZERO.compareTo(savingsAmountOnHold), 
"savingsAmountOnHold should be 0 when no holds are placed");
+
+        // Verify calculation: availableBalance = accountBalance - onHoldFunds 
- savingsAmountOnHold
+        BigDecimal expectedAvailableBalance = 
accountBalance.subtract(onHoldFunds).subtract(savingsAmountOnHold);
+        assertEquals(0, expectedAvailableBalance.compareTo(availableBalance),
+                "availableBalance should equal accountBalance - onHoldFunds - 
savingsAmountOnHold");
+
+        // Verify availableBalance equals accountBalance when there are no 
holds
+        assertEquals(0, accountBalance.compareTo(availableBalance), 
"availableBalance should equal accountBalance when there are no holds");
+    }
+
+    @Test
+    public void testGroupAccountWithHold() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create a group
+        final Integer groupID = GroupHelper.createGroup(this.requestSpec, 
this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+
+        // Create a savings product
+        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);
+
+        // Apply for and activate a group savings account
+        final Integer savingsId = 
this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID, 
ACCOUNT_TYPE_GROUP);
+        Assertions.assertNotNull(savingsId);
+
+        HashMap savingsStatusHashMap = 
this.savingsAccountHelper.approveSavings(savingsId);
+        SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
+
+        savingsStatusHashMap = 
this.savingsAccountHelper.activateSavings(savingsId);
+        SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
+
+        // Make a deposit to create a balance
+        Integer depositTransactionId = (Integer) 
this.savingsAccountHelper.depositToSavingsAccount(savingsId, DEPOSIT_AMOUNT,
+                SavingsAccountHelper.TRANSACTION_DATE, 
CommonConstants.RESPONSE_RESOURCE_ID);
+        Assertions.assertNotNull(depositTransactionId);
+
+        // Place a hold on the account
+        Integer holdTransactionId = (Integer) 
this.savingsAccountHelper.holdAmountInSavingsAccount(savingsId, HOLD_AMOUNT, 
false,
+                SavingsAccountHelper.TRANSACTION_DATE, 
CommonConstants.RESPONSE_RESOURCE_ID);
+        Assertions.assertNotNull(holdTransactionId);
+
+        // Retrieve group accounts endpoint
+        final String GROUP_ACCOUNTS_URL = "/fineract-provider/api/v1/groups/" 
+ groupID + "/accounts?" + Utils.TENANT_IDENTIFIER;
+        HashMap groupAccountsResponse = 
Utils.performServerGet(this.requestSpec, this.responseSpec, GROUP_ACCOUNTS_URL, 
"");
+        Assertions.assertNotNull(groupAccountsResponse);
+
+        // Find our savings account in the response
+        ArrayList<HashMap> savingsAccounts = (ArrayList<HashMap>) 
groupAccountsResponse.get("savingsAccounts");
+        HashMap account = null;
+        for (HashMap acc : savingsAccounts) {
+            if (acc.get("id").equals(savingsId)) {
+                account = acc;
+                break;
+            }
+        }
+        Assertions.assertNotNull(account, "Savings account should be in the 
response");
+
+        // Parse fields
+        BigDecimal accountBalance = new 
BigDecimal(account.get("accountBalance").toString());
+        BigDecimal onHoldFunds = account.get("onHoldFunds") != null ? new 
BigDecimal(account.get("onHoldFunds").toString())
+                : BigDecimal.ZERO;
+        BigDecimal savingsAmountOnHold = account.get("savingsAmountOnHold") != 
null
+                ? new BigDecimal(account.get("savingsAmountOnHold").toString())
+                : BigDecimal.ZERO;
+        BigDecimal availableBalance = new 
BigDecimal(account.get("availableBalance").toString());
+
+        // Verify the hold amount is reflected in savingsAmountOnHold
+        assertEquals(0, new 
BigDecimal(HOLD_AMOUNT).compareTo(savingsAmountOnHold), "savingsAmountOnHold 
should equal the hold amount");
+
+        // Verify the calculation is correct: availableBalance = 
accountBalance - onHoldFunds - savingsAmountOnHold
+        BigDecimal expectedAvailableBalance = 
accountBalance.subtract(onHoldFunds).subtract(savingsAmountOnHold);
+        assertEquals(0, expectedAvailableBalance.compareTo(availableBalance),
+                "availableBalance should equal accountBalance - onHoldFunds - 
savingsAmountOnHold");
+    }
+
+    /**
+     * Test that verifies group savings accounts can be used as guarantors 
when loan products have guarantee
+     * requirements configured with zero minimum percentages.
+     * <p>
+     * Group accounts work with guarantees when minimum percentages are 0%, 
avoiding the self-guarantee validation logic
+     * that expects guarantor.entityId to match loan.clientId (which fails for 
group accounts where client_id = null).
+     * <p>
+     * By using {@code withOnHoldFundDetails("0","0","0")}, we enable 
guarantee fund holds (isHoldGuaranteeFunds = true)
+     * but set all minimum percentage requirements to 0%. This allows:
+     * <ul>
+     * <li>Validation to run (validateGuarantorBusinessRules() is called)</li>
+     * <li>Group accounts to pass validation (no mandatory minimums to 
check)</li>
+     * <li>Automatic holds to be placed on guarantor accounts upon loan 
disbursement</li>
+     * </ul>
+     */
+    @Test
+    public void testGroupAccountAsGuarantorWithGuaranteeHolds() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+        this.loanTransactionHelper = new 
LoanTransactionHelper(this.requestSpec, this.responseSpec);
+        final GuarantorHelper guarantorHelper = new 
GuarantorHelper(this.requestSpec, this.responseSpec);
+
+        // Create a borrower client
+        final Integer borrowerClientID = 
ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        ClientHelper.verifyClientCreatedOnServer(this.requestSpec, 
this.responseSpec, borrowerClientID);
+
+        // Create a group and associate the borrower with it
+        Integer groupID = GroupHelper.createGroup(this.requestSpec, 
this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+        GroupHelper.associateClient(this.requestSpec, this.responseSpec, 
groupID.toString(), borrowerClientID.toString());
+
+        // Create a GROUP savings account (owned by group, not individual 
client)
+        final Integer savingsProductID = 
createSavingsProduct(this.requestSpec, this.responseSpec, 
MINIMUM_OPENING_BALANCE, null, null,
+                "false");
+        final Integer guarantorSavingsId = 
this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID,
+                ACCOUNT_TYPE_GROUP);
+        Assertions.assertNotNull(guarantorSavingsId);
+
+        HashMap savingsStatusHashMap = 
this.savingsAccountHelper.approveSavings(guarantorSavingsId);
+        SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
+
+        savingsStatusHashMap = 
this.savingsAccountHelper.activateSavings(guarantorSavingsId);
+        SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
+
+        // Deposit funds into the group account
+        final String depositAmount = "10000";
+        Integer depositTransactionId = (Integer) 
this.savingsAccountHelper.depositToSavingsAccount(guarantorSavingsId, 
depositAmount,
+                SavingsAccountHelper.TRANSACTION_DATE, 
CommonConstants.RESPONSE_RESOURCE_ID);
+        Assertions.assertNotNull(depositTransactionId);
+
+        // Create loan product with guarantee requirements but zero minimum 
percentages
+        // This allows group accounts to be used as guarantors while enabling 
automatic holds
+        final String loanProductJSON = new 
LoanProductTestBuilder().withPrincipal("10000").withNumberOfRepayments("12")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("2")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0") // 0% mandatory, 0% 
self, 0% external
+                .build(null);
+        final Integer loanProductID = 
loanTransactionHelper.getLoanProductId(loanProductJSON);
+        Assertions.assertNotNull(loanProductID);
+
+        // Create a basic loan for the borrower
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal("10000").withLoanTermFrequency("12")
+                
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1")
+                
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments()
+                
.withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
+                .withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE_PLUS_ONE)
+                .build(borrowerClientID.toString(), loanProductID.toString(), 
null);
+        final Integer loanID = 
loanTransactionHelper.getLoanId(loanApplicationJSON);
+        Assertions.assertNotNull(loanID);
+
+        // Create a guarantor linking the group savings account to the loan
+        final String guaranteeAmount = "5000";
+        final String guarantorJSON = new GuarantorTestBuilder()
+                
.existingCustomerWithGuaranteeAmount(String.valueOf(borrowerClientID), 
String.valueOf(guarantorSavingsId), guaranteeAmount)
+                .build();
+        Integer guarantorId = guarantorHelper.createGuarantor(loanID, 
guarantorJSON);
+        Assertions.assertNotNull(guarantorId, "Guarantor with group savings 
account created successfully");
+
+        // Approve the loan - THIS is when the hold is placed (not on 
disbursement!)
+        HashMap loanStatusHashMap = 
loanTransactionHelper.approveLoan(SavingsAccountHelper.TRANSACTION_DATE, 
loanID);
+        LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
+        // Verify the group savings account has an automatic hold equal to the 
guarantee amount after approval
+        HashMap savingsDetails = 
this.savingsAccountHelper.getSavingsDetails(guarantorSavingsId);
+        Object onHoldFundsObj = savingsDetails.get("onHoldFunds");
+        BigDecimal onHoldFunds = onHoldFundsObj != null ? new 
BigDecimal(onHoldFundsObj.toString()) : BigDecimal.ZERO;
+        final BigDecimal expectedHoldAmount = new BigDecimal(guaranteeAmount);
+        Assertions.assertEquals(expectedHoldAmount, onHoldFunds.setScale(0, 
RoundingMode.HALF_UP),
+                "Group account should have automatic guarantor hold equal to 
guarantee amount after loan approval");
+    }
+
 }

Reply via email to