FINERACT-2473: Add available balance to group accounts endpoint (#5468)
This commit is contained in:
+27
-9
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.accountdetails.data;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountApplicationTimelineData;
|
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.
|
* Immutable data object for savings accounts.
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SavingsAccountSummaryData {
|
public class SavingsAccountSummaryData {
|
||||||
|
|
||||||
@@ -40,7 +42,27 @@ public class SavingsAccountSummaryData {
|
|||||||
private final String shortProductName;
|
private final String shortProductName;
|
||||||
private final SavingsAccountStatusEnumData status;
|
private final SavingsAccountStatusEnumData status;
|
||||||
private final CurrencyData currency;
|
private final CurrencyData currency;
|
||||||
|
/**
|
||||||
|
* The total balance of the savings account.
|
||||||
|
*/
|
||||||
private final BigDecimal accountBalance;
|
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
|
// differentiate Individual, JLG or Group account
|
||||||
private final EnumOptionData accountType;
|
private final EnumOptionData accountType;
|
||||||
private final SavingsAccountApplicationTimelineData timeline;
|
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,
|
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 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) {
|
final EnumOptionData depositType, final SavingsAccountSubStatusEnumData subStatus, final LocalDate lastActiveTransactionDate) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.accountNo = accountNo;
|
this.accountNo = accountNo;
|
||||||
@@ -63,18 +86,13 @@ public class SavingsAccountSummaryData {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.accountBalance = accountBalance;
|
this.accountBalance = accountBalance;
|
||||||
|
this.onHoldFunds = onHoldFunds;
|
||||||
|
this.savingsAmountOnHold = savingsAmountOnHold;
|
||||||
|
this.availableBalance = availableBalance;
|
||||||
this.accountType = accountType;
|
this.accountType = accountType;
|
||||||
this.timeline = timeline;
|
this.timeline = timeline;
|
||||||
this.depositType = depositType;
|
this.depositType = depositType;
|
||||||
this.subStatus = subStatus;
|
this.subStatus = subStatus;
|
||||||
this.lastActiveTransactionDate = lastActiveTransactionDate;
|
this.lastActiveTransactionDate = lastActiveTransactionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAccountNo() {
|
|
||||||
return accountNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -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.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_type_enum as accountType, ");
|
||||||
accountsSummary.append("sa.account_balance_derived as accountBalance, ");
|
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("sa.submittedon_date as submittedOnDate,");
|
||||||
accountsSummary.append("sbu.username as submittedByUsername,");
|
accountsSummary.append("sbu.username as submittedByUsername,");
|
||||||
@@ -377,6 +379,18 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
final String shortProductName = rs.getString("shortProductName");
|
final String shortProductName = rs.getString("shortProductName");
|
||||||
final Integer statusId = JdbcSupport.getInteger(rs, "statusEnum");
|
final Integer statusId = JdbcSupport.getInteger(rs, "statusEnum");
|
||||||
final BigDecimal accountBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accountBalance");
|
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 SavingsAccountStatusEnumData status = SavingsEnumerations.status(statusId);
|
||||||
final Integer accountType = JdbcSupport.getInteger(rs, "accountType");
|
final Integer accountType = JdbcSupport.getInteger(rs, "accountType");
|
||||||
final EnumOptionData accountTypeData = AccountEnumerations.loanType(accountType);
|
final EnumOptionData accountTypeData = AccountEnumerations.loanType(accountType);
|
||||||
@@ -433,7 +447,8 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
|
|||||||
activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname);
|
activatedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname);
|
||||||
|
|
||||||
return new SavingsAccountSummaryData(id, accountNo, externalId, productId, productName, shortProductName, status, currency,
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
@@ -415,6 +415,14 @@ final class GroupsApiResourceSwagger {
|
|||||||
public String productName;
|
public String productName;
|
||||||
public GetGroupsGroupIdAccountsSavingStatus status;
|
public GetGroupsGroupIdAccountsSavingStatus status;
|
||||||
public GetGroupsGroupIdAccountsSavingCurrency currency;
|
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;
|
public GetGroupsGroupIdAccountsSavingAccountType accountType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+260
@@ -26,6 +26,7 @@ import io.restassured.http.ContentType;
|
|||||||
import io.restassured.specification.RequestSpecification;
|
import io.restassured.specification.RequestSpecification;
|
||||||
import io.restassured.specification.ResponseSpecification;
|
import io.restassured.specification.ResponseSpecification;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
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.charges.ChargesHelper;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
|
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
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.loans.LoanTransactionHelper;
|
||||||
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
|
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
|
||||||
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
|
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 MINIMUM_OPENING_BALANCE = "1000.0";
|
||||||
public static final String PRINCIPAL = "5000";
|
public static final String PRINCIPAL = "5000";
|
||||||
public static final String GUARANTEE_AMOUNT = "500";
|
public static final String GUARANTEE_AMOUNT = "500";
|
||||||
|
public static final String HOLD_AMOUNT = "300";
|
||||||
public static final String ACCOUNT_TYPE_GROUP = "GROUP";
|
public static final String ACCOUNT_TYPE_GROUP = "GROUP";
|
||||||
|
|
||||||
private ResponseSpecification responseSpec;
|
private ResponseSpecification responseSpec;
|
||||||
@@ -1021,4 +1024,261 @@ public class GroupSavingsIntegrationTest {
|
|||||||
"Should find at least one on-hold transaction with savingsClientName populated (group name)");
|
"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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user