From ada24d74970f0f4e9ced4ff8d36d3e69c3983320 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Thu, 28 May 2026 22:28:30 +0530 Subject: [PATCH] feat(cashier): implement expected balance for logged-in cashier, settlement checks, and reconciliation rules --- .../api/CashReconciliationApiResource.java | 55 +++++++++++++++++++ .../api/CashSettlementApiResource.java | 40 ++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java index 69983ee93..05e9e6207 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreconciliation/api/CashReconciliationApiResource.java @@ -157,6 +157,61 @@ public class CashReconciliationApiResource { return gson.toJson(result); } + /** + * GET /cashreconciliation/expected/me + * Same as /expected but auto-detects cashierId from the authenticated user. + */ + @GET + @Path("expected/me") + public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); + final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + Long appUserId; + try { + appUserId = context.authenticatedUser().getId(); + } catch (Exception e) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.not.authenticated", "Not authenticated."); + } + Long cashierId; + try { + cashierId = jdbcTemplate.queryForObject( + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + Long.class, appUserId); + } catch (Exception e) { + Map empty = new LinkedHashMap<>(); + empty.put("cashierId", null); + empty.put("asOfDate", dateFilter); + empty.put("expectedTotal", java.math.BigDecimal.ZERO); + empty.put("denominations", new ArrayList<>()); + return gson.toJson(empty); + } + // Delegate to the existing /expected logic + return getExpected(cashierId, dateFilter); + } + + /** + * GET /cashreconciliation/check?cashierId=X&date=yyyy-MM-dd + * Returns whether reconciliation has been done today for a cashier. + */ + @GET + @Path("check") + public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", + "cashierId query param is required."); + } + final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)", + Integer.class, cashierId, reconDate); + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("date", reconDate); + result.put("reconciled", count != null && count > 0); + return gson.toJson(result); + } + @POST @Transactional public String create(String body) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java index b6952145e..92d8975a6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashsettlement/api/CashSettlementApiResource.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.math.BigDecimal; import java.time.LocalDate; @@ -22,6 +23,7 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.springframework.jdbc.core.JdbcTemplate; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement; import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlementDetail; @@ -46,6 +48,7 @@ public class CashSettlementApiResource { private final PlatformSecurityContext context; private final GlDispatchService glDispatchService; private final PlatformTransactionManager transactionManager; + private final JdbcTemplate jdbcTemplate; private final Gson gson = new Gson(); @POST @@ -75,6 +78,24 @@ public class CashSettlementApiResource { final String currencyCode = json.has("currencyCode") && !json.get("currencyCode").isJsonNull() ? json.get("currencyCode").getAsString() : "INR"; + // Require reconciliation before settlement + Integer reconCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)", + Integer.class, fromCashierId, settlementDate.toString()); + if (reconCount == null || reconCount == 0) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.reconciliation.required", + "Reconciliation must be completed before settlement for cashier " + fromCashierId + " on " + settlementDate + ". Please complete today's reconciliation first."); + } + + // Prevent duplicate settlement for same cashier+date + Integer existingCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)", + Integer.class, fromCashierId, settlementDate.toString()); + if (existingCount != null && existingCount > 0) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.duplicate", + "A settlement for cashier " + fromCashierId + " on " + settlementDate + " already exists. Reverse the existing settlement first."); + } + final JsonArray denominations = json.has("denominations") && json.get("denominations").isJsonArray() ? json.getAsJsonArray("denominations") : new JsonArray(); @@ -168,6 +189,25 @@ public class CashSettlementApiResource { return gson.toJson(result); } + @GET + @Path("check") + public String check(@QueryParam("cashierId") Long cashierId, @QueryParam("date") String date) { + context.authenticatedUser().validateHasPermissionTo("READ_CASHSETTLEMENT"); + if (cashierId == null) { + throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required", + "cashierId query param is required."); + } + final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString(); + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)", + Integer.class, cashierId, settlementDate); + Map result = new LinkedHashMap<>(); + result.put("cashierId", cashierId); + result.put("date", settlementDate); + result.put("alreadySettled", count != null && count > 0); + return gson.toJson(result); + } + @GET public String getAll() { List list = repository.findAllByOrderByCreatedAtDesc();