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:
co-authored by
Claude Sonnet 4.6
parent
ada24d7497
commit
49d64004cc
+54
@@ -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 "
|
||||
|
||||
+17
-2
@@ -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 : ""));
|
||||
|
||||
+24
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user