FINERACT-2482: On-hold transactions API returns empty results for group savings accounts

This commit is contained in:
Ralph Hopman
2026-02-14 13:36:24 +01:00
committed by Arnold Gálovics
parent 6269cbbb79
commit 9a69cc4826
2 changed files with 150 additions and 3 deletions
@@ -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:
* <ul>
* <li>The endpoint returns hold transactions for group savings accounts</li>
* <li>The savingsClientName field is populated with the group name (not null/blank)</li>
* <li>Transaction details (amount, type, date) are correct</li>
* <li>The response includes pagination information</li>
* </ul>
*/
@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<HashMap> pageItems = (ArrayList<HashMap>) 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)");
}
}