feat(cashier): add cashier-morning balance endpoint and expose fromCashierId in settlement/transfer APIs

- GET /cashreports/cashier-morning: returns denomination-wise cash received by cashier from vault
  for a given date (only vault→cashier transfers, non-reversed)
- CashSettlementApiResource: include fromCashierId in list and detail responses
- CashTransferApiResource: include toCashierId in list and detail responses for traceability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-31 08:55:09 +05:30
co-authored by Claude Sonnet 4.6
parent ada24d7497
commit 49d64004cc
3 changed files with 95 additions and 4 deletions
@@ -172,6 +172,60 @@ public class CashReportApiResource {
return gson.toJson(buildPositionForCashier(cashierId, dateFilter));
}
/**
* GET /cashreports/cashier-morning?cashierId=X&date=YYYY-MM-DD
* Returns denomination-wise cash received by the cashier from vault on the given date
* (today's morning opening allocation from m_cash_transfer only, non-reversed).
*/
@GET
@Path("cashier-morning")
public String cashierMorning(@QueryParam("cashierId") Long cashierId,
@QueryParam("date") String date) {
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
if (cashierId == null) {
Map<String, Object> empty = new LinkedHashMap<>();
empty.put("cashierId", null);
empty.put("date", txnDate);
empty.put("totalMorning", BigDecimal.ZERO);
empty.put("denominations", new ArrayList<>());
return gson.toJson(empty);
}
final String sql = "SELECT ctd.denomination_id, ctd.denomination_value, "
+ "COALESCE(dm.kind, 'note') AS kind, "
+ "SUM(ctd.count) AS total_count, "
+ "SUM(ctd.total_amount) AS total_amount "
+ "FROM m_cash_transfer_detail ctd "
+ "JOIN m_cash_transfer ct ON ct.id = ctd.transfer_id "
+ "LEFT JOIN m_denomination_master dm ON dm.id = ctd.denomination_id "
+ "WHERE ct.to_cashier_id = ? "
+ "AND ct.transfer_date = CAST(? AS DATE) "
+ "AND (ct.is_reversed IS NULL OR ct.is_reversed = false) "
+ "GROUP BY ctd.denomination_id, ctd.denomination_value, dm.kind "
+ "ORDER BY ctd.denomination_value DESC";
List<Map<String, Object>> rows = jdbcTemplate.query(sql, (rs, rowNum) -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("denominationId", rs.getLong("denomination_id"));
row.put("denominationValue", rs.getInt("denomination_value"));
row.put("kind", rs.getString("kind"));
row.put("morningCount", rs.getLong("total_count"));
row.put("morningAmount", rs.getBigDecimal("total_amount"));
return row;
}, cashierId, txnDate);
BigDecimal total = rows.stream()
.map(r -> (BigDecimal) r.get("morningAmount"))
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId);
result.put("date", txnDate);
result.put("totalMorning", total);
result.put("denominations", rows);
return gson.toJson(result);
}
private Map<String, Object> buildPositionForCashier(Long cashierId, String dateFilter) {
// Vault → Cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
@@ -132,7 +132,16 @@ public class CashSettlementApiResource {
String glWarning = null;
try {
glDispatchService.settleCashFromCashier(toTellerId, fromCashierId, total,
// Use cashier's own teller_id from m_cashiers for GL dispatch so the
// Fineract cashier-teller link is respected regardless of which vault teller
// the user submitted cash to.
Long glTellerId = toTellerId;
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : "");
} catch (Exception e) {
@@ -176,7 +185,13 @@ public class CashSettlementApiResource {
if (settlement.getToTellerId() != null && settlement.getFromCashierId() != null) {
try {
glDispatchService.allocateCashToCashier(settlement.getToTellerId(),
Long glTellerId = settlement.getToTellerId();
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.allocateCashToCashier(glTellerId,
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR",
LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
"Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
@@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetail;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferDetailRepository;
import org.apache.fineract.portfolio.cashtransfer.domain.CashTransferRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +47,7 @@ public class CashTransferApiResource {
private final PlatformSecurityContext context;
private final GlDispatchService glDispatchService;
private final PlatformTransactionManager transactionManager;
private final JdbcTemplate jdbcTemplate;
private final Gson gson = new Gson();
@POST
@@ -122,8 +124,18 @@ public class CashTransferApiResource {
String glWarning = null;
try {
// Use the cashier's own teller_id from m_cashiers for GL dispatch.
// Fineract's allocateCashToCashier requires cashier and teller to be linked —
// the user-selected fromTellerId is the business vault but may differ from the
// cashier's registered teller_id. Always resolve from DB to avoid GL failures.
Long glTellerId = fromTellerId;
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now();
glDispatchService.allocateCashToCashier(fromTellerId, toCashierId, total,
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : "");
} catch (Exception e) {
@@ -168,7 +180,13 @@ public class CashTransferApiResource {
if (transfer.getFromTellerId() != null && transfer.getToCashierId() != null) {
try {
glDispatchService.settleCashFromCashier(transfer.getFromTellerId(),
Long glTellerId = transfer.getFromTellerId();
try {
Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {}
glDispatchService.settleCashFromCashier(glTellerId,
transfer.getToCashierId(), transfer.getTransferAmount(), "INR",
java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
"Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
@@ -261,6 +279,10 @@ public class CashTransferApiResource {
map.put("performedBy", e.getPerformedBy());
map.put("performedByUsername", e.getPerformedByUsername());
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
map.put("isReversed", Boolean.TRUE.equals(e.getIsReversed()));
map.put("reversedBy", e.getReversedBy());
map.put("reversedAt", e.getReversedAt() != null ? e.getReversedAt().toString() : null);
map.put("reverseReason", e.getReverseReason());
return map;
}