feat(cashier): implement expected balance for logged-in cashier, settlement checks, and reconciliation rules

This commit is contained in:
Jaanu423
2026-05-28 22:28:30 +05:30
parent b89e653bcb
commit ada24d7497
2 changed files with 95 additions and 0 deletions
@@ -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<String, Object> 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<String, Object> 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) {
@@ -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<String, Object> 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<CashSettlement> list = repository.findAllByOrderByCreatedAtDesc();