From 05662e64d59815e7c904dfc682964993c7db41b9 Mon Sep 17 00:00:00 2001 From: Ralph Hopman <1891004+rhopman@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:15:34 +0100 Subject: [PATCH] FINERACT-2473: Add available balance to group accounts endpoint (#5468) --- .../data/SavingsAccountSummaryData.java | 36 ++- ...sReadPlatformServiceJpaRepositoryImpl.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 3cc739b87..009e634eb 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 7c1f50873..9f6600d4f 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 8f27280b3..a7805647e 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 69043adff..f1f609a51 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 savingsAccounts = (ArrayList) 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 savingsAccounts = (ArrayList) 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. + *

+ * 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). + *

+ * By using {@code withOnHoldFundDetails("0","0","0")}, we enable guarantee fund holds (isHoldGuaranteeFunds = true) + * but set all minimum percentage requirements to 0%. This allows: + *

    + *
  • Validation to run (validateGuarantorBusinessRules() is called)
  • + *
  • Group accounts to pass validation (no mandatory minimums to check)
  • + *
  • Automatic holds to be placed on guarantor accounts upon loan disbursement
  • + *
+ */ + @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"); + } + }