diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountOnHoldTransactionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountOnHoldTransactionReadPlatformServiceImpl.java
index b19f00d99..57090513b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountOnHoldTransactionReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountOnHoldTransactionReadPlatformServiceImpl.java
@@ -94,16 +94,18 @@ public class DepositAccountOnHoldTransactionReadPlatformServiceImpl implements D
sqlBuilder.append(" tr.id as transactionId, tr.transaction_type_enum as transactionType, ");
sqlBuilder.append(" tr.transaction_date as transactionDate, tr.amount as transactionAmount,");
sqlBuilder.append(" tr.is_reversed as reversed, sa.account_no as savingsAccNum, ");
- sqlBuilder.append("sc.display_name as savingsClientName, ml.id as loanid, sa.id as savingid, ");
- sqlBuilder.append(" ml.account_no as loanAccountNum, lc.display_name as loanClientName");
+ sqlBuilder.append(" COALESCE(sc.display_name, sg.display_name) as savingsClientName, ml.id as loanid, sa.id as savingid, ");
+ sqlBuilder.append(" ml.account_no as loanAccountNum, COALESCE(lc.display_name, lg.display_name) as loanClientName");
sqlBuilder.append(" from m_savings_account sa ");
sqlBuilder.append(" join m_deposit_account_on_hold_transaction tr on sa.id = tr.savings_account_id ");
- sqlBuilder.append(" join m_client sc on sc.id = sa.client_id");
+ sqlBuilder.append(" left join m_client sc on sc.id = sa.client_id");
+ sqlBuilder.append(" left join m_group sg on sg.id = sa.group_id");
sqlBuilder.append(" left join m_guarantor_transaction gt on gt.deposit_on_hold_transaction_id = tr.id ");
sqlBuilder.append(" left join m_guarantor_funding_details mgfd on mgfd.id=gt.guarantor_fund_detail_id");
sqlBuilder.append(" left join m_portfolio_account_associations pa on pa.id=mgfd.account_associations_id");
sqlBuilder.append(" left join m_loan ml on ml.id = pa.loan_account_id");
sqlBuilder.append(" left join m_client lc on lc.id = ml.client_id");
+ sqlBuilder.append(" left join m_group lg on lg.id = ml.group_id");
this.schemaSql = sqlBuilder.toString();
}
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 b2354792d..69043adff 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
@@ -43,10 +43,15 @@ import org.apache.fineract.integrationtests.common.GroupHelper;
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
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.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
import org.apache.fineract.integrationtests.common.savings.SavingsTestLifecycleExtension;
+import org.apache.fineract.integrationtests.guarantor.GuarantorHelper;
+import org.apache.fineract.integrationtests.guarantor.GuarantorTestBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -65,12 +70,16 @@ public class GroupSavingsIntegrationTest {
public static final String WITHDRAW_AMOUNT = "1000";
public static final String WITHDRAW_AMOUNT_ADJUSTED = "500";
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 ACCOUNT_TYPE_GROUP = "GROUP";
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
private SavingsAccountHelper savingsAccountHelper;
private PaymentTypeHelper paymentTypeHelper;
+ private LoanTransactionHelper loanTransactionHelper;
+ private GuarantorHelper guarantorHelper;
private static final Logger LOG = LoggerFactory.getLogger(GroupSavingsIntegrationTest.class);
@BeforeEach
@@ -81,6 +90,10 @@ public class GroupSavingsIntegrationTest {
this.requestSpec.header("Fineract-Platform-TenantId", "default");
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.paymentTypeHelper = new PaymentTypeHelper();
+ // Use a default responseSpec for loan operations that doesn't enforce status code
+ ResponseSpecification loanResponseSpec = new ResponseSpecBuilder().build();
+ this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, loanResponseSpec);
+ this.guarantorHelper = new GuarantorHelper(this.requestSpec, loanResponseSpec);
}
@Test
@@ -876,4 +889,136 @@ public class GroupSavingsIntegrationTest {
return map;
}
+ /**
+ * Test that verifies the /savingsaccounts/{savingsId}/onholdtransactions API endpoint works correctly for GROUP
+ * savings accounts when used as guarantor collateral:
+ *
+ * - The endpoint returns hold transactions for group savings accounts
+ * - The savingsClientName field is populated with the group name (not null/blank)
+ * - Transaction details (amount, type, date) are correct
+ * - The response includes pagination information
+ *
+ */
+ @Test
+ public void testOnHoldTransactionsApiForGroupSavingsAccount() {
+ this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
+
+ // Create a client who will take out the loan
+ final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
+ Assertions.assertNotNull(clientID);
+
+ // Create a group with a savings account that will act as collateral
+ final Integer groupID = GroupHelper.createGroup(this.requestSpec, this.responseSpec, true);
+ Assertions.assertNotNull(groupID);
+
+ // Create a savings product
+ final Integer savingsProductID = createSavingsProduct(this.requestSpec, this.responseSpec, MINIMUM_OPENING_BALANCE, null, null,
+ "false");
+ Assertions.assertNotNull(savingsProductID);
+
+ // Create 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);
+
+ // After activation, the account has MINIMUM_OPENING_BALANCE (1000) that can be used as collateral
+ // No need for additional deposit
+
+ // Create a loan product with hold funds enabled
+ // Note: Using 0,0,0 to bypass bug FINERACT-2476 where group accounts can't be guarantors
+ // with non-zero guarantee requirements. This test focuses on the SQL query fix.
+ LoanProductTestBuilder loanProductBuilder = new LoanProductTestBuilder().withPrincipal(PRINCIPAL).withNumberOfRepayments("4")
+ .withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("2")
+ .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+ .withOnHoldFundDetails("0", "0", "0");
+ final String loanProductJSON = loanProductBuilder.build(null);
+ final Integer loanProductID = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+ Assertions.assertNotNull(loanProductID);
+
+ // Apply for a loan for the client
+ final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(PRINCIPAL).withLoanTermFrequency("4")
+ .withLoanTermFrequencyAsWeeks().withNumberOfRepayments("4").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+ .withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+ .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+ .withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+ .build(clientID.toString(), loanProductID.toString(), null);
+ final Integer loanID = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+ Assertions.assertNotNull(loanID);
+ LOG.info("Created loan with ID: {}", loanID);
+
+ // Add the group savings account as guarantor collateral for the loan
+ // Use GUARANTEE_AMOUNT (500) as guarantee amount (less than the MINIMUM_OPENING_BALANCE of 1000)
+ String guarantorJSON = new GuarantorTestBuilder()
+ .existingCustomerWithGuaranteeAmount(String.valueOf(groupID), String.valueOf(savingsId), GUARANTEE_AMOUNT).build();
+
+ LOG.info("Guarantor JSON: {}", guarantorJSON);
+ LOG.info("Loan ID: {}, Group ID: {}, Savings ID: {}", loanID, groupID, savingsId);
+
+ Integer guarantorId = this.guarantorHelper.createGuarantor(loanID, guarantorJSON);
+ LOG.info("Guarantor ID: {}", guarantorId);
+ Assertions.assertNotNull(guarantorId, "Guarantor creation should return a valid ID");
+
+ // Approve the loan - this will create the hold transaction on the group savings account
+ HashMap loanStatusHashMap = this.loanTransactionHelper.approveLoan(SavingsAccountHelper.TRANSACTION_DATE, loanID);
+ Assertions.assertNotNull(loanStatusHashMap);
+
+ // Call the on-hold transactions API endpoint
+ final String ON_HOLD_TRANSACTIONS_URL = "/fineract-provider/api/v1/savingsaccounts/" + savingsId + "/onholdtransactions?"
+ + Utils.TENANT_IDENTIFIER;
+ HashMap onHoldTransactionsResponse = Utils.performServerGet(this.requestSpec, this.responseSpec, ON_HOLD_TRANSACTIONS_URL, "");
+ Assertions.assertNotNull(onHoldTransactionsResponse);
+
+ // Verify the response structure
+ Assertions.assertTrue(onHoldTransactionsResponse.containsKey("totalFilteredRecords"),
+ "Response should contain totalFilteredRecords");
+ Assertions.assertTrue(onHoldTransactionsResponse.containsKey("pageItems"), "Response should contain pageItems");
+
+ // Verify we have at least one transaction (the guarantor hold we just created)
+ Integer totalRecords = (Integer) onHoldTransactionsResponse.get("totalFilteredRecords");
+ Assertions.assertTrue(totalRecords > 0, "Should have at least one on-hold transaction");
+
+ // Get the page items
+ ArrayList pageItems = (ArrayList) onHoldTransactionsResponse.get("pageItems");
+ Assertions.assertNotNull(pageItems, "pageItems should not be null");
+ Assertions.assertFalse(pageItems.isEmpty(), "pageItems should not be empty");
+
+ LOG.info("Found {} on-hold transactions", pageItems.size());
+
+ // Verify that at least one transaction has the group name populated
+ boolean foundTransactionWithGroupName = false;
+ for (HashMap transaction : pageItems) {
+ LOG.info("Transaction: {}", transaction);
+ String savingsClientName = (String) transaction.get("savingsClientName");
+ if (savingsClientName != null && !savingsClientName.isBlank()) {
+ foundTransactionWithGroupName = true;
+
+ // Verify transaction details
+ Assertions.assertNotNull(transaction.get("amount"), "Transaction amount should not be null");
+
+ // Verify savings account details are present
+ String savingsAccNum = (String) transaction.get("savingsAccountNo");
+ Assertions.assertNotNull(savingsAccNum, "savingsAccountNo should not be null");
+
+ // Verify savings ID matches
+ Integer savingsIdFromTransaction = (Integer) transaction.get("savingsId");
+ Assertions.assertEquals(savingsId, savingsIdFromTransaction, "savingsId should match");
+
+ // Verify transaction date is present
+ Assertions.assertNotNull(transaction.get("transactionDate"), "transactionDate should not be null");
+
+ LOG.info("SUCCESS: Found on-hold transaction with group name '{}' for group savings account", savingsClientName);
+ break;
+ }
+ }
+
+ Assertions.assertTrue(foundTransactionWithGroupName,
+ "Should find at least one on-hold transaction with savingsClientName populated (group name)");
+ }
+
}