feat(cashier): fix current account transactions search and withdrawal validations
CurrentAccountTransactionsApiResource: - Fixed: search endpoint now queries m_current_account_transaction instead of m_savings_account_transaction (wrong table) - Added remarks column to search query (column exists in CA table) - Charge Deduction (type=7) mapped in transactionType value - Removed sat.remarks that caused 500 (column doesn't exist in savings table) CurrentAccountTransactionWritePlatformServiceImpl: - Added min required balance validation in withdrawal() - Throws GeneralPlatformDomainRuleException when balance after withdrawal would fall below minRequiredBalance (if enforceMinRequiredBalance=true) StandingInstructionReadPlatformServiceImpl: - Fixed SQL WHERE clause: fromAccountType + fromAccount condition now correctly adds WHERE when fromAccountId is set Liquibase 0315: - Fix standing instruction permission code (trim trailing space on id=449) - Grant READ permissions to CASHIER role for transaction entry screen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2371ad98b1
commit
edaeb674d5
+2
-1
@@ -264,7 +264,8 @@ public class StandingInstructionReadPlatformServiceImpl implements StandingInstr
|
||||
sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " ");
|
||||
sqlBuilder.append(this.standingInstructionMapper.schema());
|
||||
if (standingInstructionDTO.transferType() != null || standingInstructionDTO.clientId() != null
|
||||
|| standingInstructionDTO.clientName() != null) {
|
||||
|| standingInstructionDTO.clientName() != null
|
||||
|| (standingInstructionDTO.fromAccountType() != null && standingInstructionDTO.fromAccount() != null)) {
|
||||
sqlBuilder.append(" where ");
|
||||
}
|
||||
boolean addAndCaluse = false;
|
||||
|
||||
+78
@@ -198,6 +198,84 @@ public class CurrentAccountTransactionsApiResource {
|
||||
return toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /currentaccounts/{accountId}/transactions/search?fromDate=YYYY-MM-DD&toDate=YYYY-MM-DD&limit=20
|
||||
*
|
||||
* Date-filtered transaction search for current accounts — mirrors the savings account search
|
||||
* endpoint so the cashier transaction entry screen can use a consistent API across all types.
|
||||
*/
|
||||
@GET
|
||||
@Path("search")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Search current account transactions by date range")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String searchTransactions(
|
||||
@PathParam("accountId") final Long accountId,
|
||||
@QueryParam("fromDate") final String fromDate,
|
||||
@QueryParam("toDate") final String toDate,
|
||||
@QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") final int limit,
|
||||
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") final int offset) {
|
||||
|
||||
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
|
||||
|
||||
final StringBuilder sql = new StringBuilder(
|
||||
"SELECT cat.id, cat.transaction_type_enum, cat.transaction_date, cat.amount, "
|
||||
+ "cat.running_balance_derived, cat.is_reversed, cat.ref_no, cat.remarks "
|
||||
+ "FROM m_current_account_transaction cat "
|
||||
+ "WHERE cat.current_account_id = ? AND cat.is_reversed = false ");
|
||||
|
||||
final java.util.List<Object> params = new java.util.ArrayList<>();
|
||||
params.add(accountId);
|
||||
|
||||
if (fromDate != null && !fromDate.isBlank()) {
|
||||
sql.append("AND cat.transaction_date >= CAST(? AS DATE) ");
|
||||
params.add(fromDate);
|
||||
}
|
||||
if (toDate != null && !toDate.isBlank()) {
|
||||
sql.append("AND cat.transaction_date <= CAST(? AS DATE) ");
|
||||
params.add(toDate);
|
||||
}
|
||||
sql.append("ORDER BY cat.transaction_date DESC, cat.id DESC ");
|
||||
sql.append("LIMIT ? OFFSET ?");
|
||||
params.add(limit);
|
||||
params.add(offset);
|
||||
|
||||
final java.util.List<java.util.Map<String, Object>> rows = jdbcTemplate.query(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> {
|
||||
final java.util.Map<String, Object> row = new java.util.LinkedHashMap<>();
|
||||
row.put("id", rs.getLong("id"));
|
||||
|
||||
final int typeEnum = rs.getInt("transaction_type_enum");
|
||||
final java.util.Map<String, Object> txnType = new java.util.LinkedHashMap<>();
|
||||
txnType.put("id", typeEnum);
|
||||
txnType.put("deposit", typeEnum == 1);
|
||||
txnType.put("withdrawal", typeEnum == 2);
|
||||
txnType.put("value", typeEnum == 1 ? "Deposit" : typeEnum == 2 ? "Withdrawal"
|
||||
: typeEnum == 7 ? "Charge Deduction" : "Other");
|
||||
row.put("transactionType", txnType);
|
||||
|
||||
final java.sql.Date d = rs.getDate("transaction_date");
|
||||
if (d != null) {
|
||||
final java.time.LocalDate ld = d.toLocalDate();
|
||||
row.put("date", new int[]{ ld.getYear(), ld.getMonthValue(), ld.getDayOfMonth() });
|
||||
}
|
||||
row.put("amount", rs.getBigDecimal("amount"));
|
||||
row.put("runningBalance", rs.getBigDecimal("running_balance_derived"));
|
||||
row.put("reversed", rs.getBoolean("is_reversed"));
|
||||
row.put("refNo", rs.getString("ref_no"));
|
||||
row.put("remarks", rs.getString("remarks"));
|
||||
return row;
|
||||
},
|
||||
params.toArray());
|
||||
|
||||
final java.util.Map<String, Object> resp = new java.util.LinkedHashMap<>();
|
||||
resp.put("totalFilteredRecords", rows.size());
|
||||
resp.put("pageItems", rows);
|
||||
return new com.google.gson.Gson().toJson(resp);
|
||||
}
|
||||
|
||||
private boolean is(final String commandParam, final String commandValue) {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
}
|
||||
|
||||
+14
-4
@@ -132,11 +132,21 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
||||
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
|
||||
}
|
||||
|
||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
|
||||
|
||||
// Minimum required balance validation
|
||||
final BigDecimal newBalance = currentBalance.subtract(amount);
|
||||
if (account.isEnforceMinRequiredBalance() && account.getMinRequiredBalance() != null
|
||||
&& account.getMinRequiredBalance().compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (newBalance.compareTo(account.getMinRequiredBalance()) < 0) {
|
||||
throw new GeneralPlatformDomainRuleException(
|
||||
"error.msg.current.account.min.required.balance.breached",
|
||||
"Withdrawal of " + amount + " would breach the minimum required balance of "
|
||||
+ account.getMinRequiredBalance() + ". Balance after withdrawal would be " + newBalance,
|
||||
accountId);
|
||||
}
|
||||
}
|
||||
|
||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
|
||||
txn.setRunningBalanceDerived(newBalance);
|
||||
|
||||
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||
|
||||
+31
-13
@@ -10,16 +10,30 @@
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Grant additional READ permissions to the CASHIER role required for
|
||||
the Cashier Transaction Entry screen to display full account details:
|
||||
- READ_CLIENTKYC → PAN, Aadhaar, CKYC number
|
||||
- READ_CLIENTNOMINEE → nominee list per account
|
||||
- READ_ADDRESS → client address
|
||||
- READ_FAMILYMEMBERS → S/D/W/H/O (guardian/spouse name)
|
||||
- READ_SAVINGSACCOUNT → savings account detail fetch
|
||||
Fix: m_permission.code for READ_STANDINGINSTRUCTION (id=449) had a trailing
|
||||
space which caused permission checks to fail (exact string match).
|
||||
Must run BEFORE the permission grant below.
|
||||
-->
|
||||
<changeSet id="0315_fix_standing_instruction_permission_code" author="fineract">
|
||||
<sql>
|
||||
UPDATE m_permission
|
||||
SET code = TRIM(code)
|
||||
WHERE id = 449
|
||||
AND code != TRIM(code);
|
||||
</sql>
|
||||
<rollback><!-- no rollback needed — trimming is idempotent --></rollback>
|
||||
</changeSet>
|
||||
|
||||
<!--
|
||||
Grant READ permissions to CASHIER role for Cashier Transaction Entry screen:
|
||||
- READ_CLIENTKYC, READ_CLIENTNOMINEE, READ_ADDRESS, READ_FAMILYMEMBERS
|
||||
- READ_SAVINGSACCOUNT, READ_CLIENTIMAGE, READ_DOCUMENT
|
||||
- READ_SAVINGNOTE, READ_CLIENTNOTE
|
||||
- READ_STANDINGINSTRUCTION (id 449 — trailing space fixed above)
|
||||
-->
|
||||
<changeSet id="0315_grant_cashier_transaction_entry_permissions" author="fineract">
|
||||
<sql>
|
||||
-- Grant code-based permissions (includes READ_STANDINGINSTRUCTION after trim fix)
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
@@ -28,7 +42,12 @@
|
||||
'READ_CLIENTNOMINEE',
|
||||
'READ_ADDRESS',
|
||||
'READ_FAMILYMEMBERS',
|
||||
'READ_SAVINGSACCOUNT'
|
||||
'READ_SAVINGSACCOUNT',
|
||||
'READ_CLIENTIMAGE',
|
||||
'READ_DOCUMENT',
|
||||
'READ_SAVINGNOTE',
|
||||
'READ_CLIENTNOTE',
|
||||
'READ_STANDINGINSTRUCTION'
|
||||
)
|
||||
WHERE r.name = 'CASHIER'
|
||||
AND NOT EXISTS (
|
||||
@@ -42,11 +61,10 @@
|
||||
AND permission_id IN (
|
||||
SELECT id FROM m_permission
|
||||
WHERE code IN (
|
||||
'READ_CLIENTKYC',
|
||||
'READ_CLIENTNOMINEE',
|
||||
'READ_ADDRESS',
|
||||
'READ_FAMILYMEMBERS',
|
||||
'READ_SAVINGSACCOUNT'
|
||||
'READ_CLIENTKYC', 'READ_CLIENTNOMINEE', 'READ_ADDRESS',
|
||||
'READ_FAMILYMEMBERS', 'READ_SAVINGSACCOUNT', 'READ_CLIENTIMAGE',
|
||||
'READ_DOCUMENT', 'READ_SAVINGNOTE', 'READ_CLIENTNOTE',
|
||||
'READ_STANDINGINSTRUCTION'
|
||||
)
|
||||
);
|
||||
</rollback>
|
||||
|
||||
Reference in New Issue
Block a user