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");
+ }
+
}