diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java index 04ac6bea6..367433ddf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper; import org.apache.fineract.portfolio.cashexchange.domain.CashExchange; import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail; @@ -45,6 +46,7 @@ public class CashExchangeApiResource { private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; private final DenominationBreakdownWriter denominationBreakdownWriter; + private final CashierRoleHelper cashierRoleHelper; private final Gson gson = new Gson(); @POST @@ -174,10 +176,30 @@ public class CashExchangeApiResource { } @GET - public String getAll(@QueryParam("accountId") Long accountId) { - List list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId) + public String getAll( + @QueryParam("accountId") Long accountId, + @QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset, + @QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") int limit) { + final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering(); + List all = accountId != null + ? repository.findByAccountIdOrderByCreatedAtDesc(accountId) : repository.findAllByOrderByCreatedAtDesc(); - return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + if (scopedCashierId != null) { + all = all.stream() + .filter(e -> scopedCashierId.equals(e.getCashierId())) + .collect(Collectors.toList()); + } + final int total = all.size(); + final int safeOffset = Math.max(0, Math.min(offset, total)); + final int safeTo = Math.min(safeOffset + Math.max(1, limit), total); + final List> page = all.subList(safeOffset, safeTo) + .stream().map(this::toMap).collect(Collectors.toList()); + final Map resp = new LinkedHashMap<>(); + resp.put("totalFilteredRecords", total); + resp.put("offset", safeOffset); + resp.put("limit", limit); + resp.put("pageItems", page); + return gson.toJson(resp); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java index 39803e8f7..a3d76e4f5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashintercashier/api/CashInterCashierApiResource.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.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper; import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashier; import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetail; import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierDetailRepository; @@ -40,6 +42,7 @@ public class CashInterCashierApiResource { private final CashInterCashierRepository repository; private final CashInterCashierDetailRepository detailRepository; private final PlatformSecurityContext context; + private final CashierRoleHelper cashierRoleHelper; private final Gson gson = new Gson(); @POST @@ -143,9 +146,28 @@ public class CashInterCashierApiResource { } @GET - public String getAll() { - return gson.toJson(repository.findAllByOrderByCreatedAtDesc().stream() - .map(this::toMap).collect(Collectors.toList())); + public String getAll( + @QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset, + @QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") int limit) { + final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering(); + List all = repository.findAllByOrderByCreatedAtDesc(); + if (scopedCashierId != null) { + all = all.stream() + .filter(t -> scopedCashierId.equals(t.getFromCashierId()) + || scopedCashierId.equals(t.getToCashierId())) + .collect(Collectors.toList()); + } + final int total = all.size(); + final int safeOffset = Math.max(0, Math.min(offset, total)); + final int safeTo = Math.min(safeOffset + Math.max(1, limit), total); + final List> page = all.subList(safeOffset, safeTo) + .stream().map(this::toMap).collect(Collectors.toList()); + final Map resp = new LinkedHashMap<>(); + resp.put("totalFilteredRecords", total); + resp.put("offset", safeOffset); + resp.put("limit", limit); + resp.put("pageItems", page); + return gson.toJson(resp); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java index a89eb956f..54b35a956 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper; import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @@ -27,6 +28,7 @@ public class CashReportApiResource { private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; private final DenominationBreakdownWriter denominationBreakdownWriter; + private final CashierRoleHelper cashierRoleHelper; private final Gson gson = new Gson(); @GET @@ -115,10 +117,15 @@ public class CashReportApiResource { @GET @Path("excess") - public String excess(@QueryParam("threshold") BigDecimal threshold, @QueryParam("fromDate") String fromDate, - @QueryParam("toDate") String toDate) { + public String excess(@QueryParam("threshold") BigDecimal threshold, + @QueryParam("fromDate") String fromDate, + @QueryParam("toDate") String toDate, + @jakarta.ws.rs.DefaultValue("0") @QueryParam("offset") int offset, + @jakarta.ws.rs.DefaultValue("20") @QueryParam("limit") int limit) { final BigDecimal effectiveThreshold = threshold != null ? threshold : BigDecimal.valueOf(50000); final boolean hasDateRange = fromDate != null && !fromDate.isBlank() && toDate != null && !toDate.isBlank(); + // CASHIER role: scope to this cashier's transactions only + final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering(); // Savings / Current / FD / RD transactions final StringBuilder sql = new StringBuilder(); @@ -154,6 +161,10 @@ public class CashReportApiResource { exSql.append("WHERE (is_reversed IS NULL OR is_reversed = false) AND exchange_amount >= ? "); final List exArgs = new ArrayList<>(); exArgs.add(effectiveThreshold); + if (scopedCashierId != null) { + exSql.append("AND cashier_id = ? "); + exArgs.add(scopedCashierId); + } if (hasDateRange) { exSql.append("AND exchange_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) "); exArgs.add(fromDate); @@ -181,7 +192,17 @@ public class CashReportApiResource { combined.addAll(exchangeRows); combined.sort((a, b) -> ((BigDecimal) b.get("totalAmount")).compareTo((BigDecimal) a.get("totalAmount"))); - return gson.toJson(combined); + final int total = combined.size(); + final int safeOffset = Math.max(0, Math.min(offset, total)); + final int safeTo = Math.min(safeOffset + Math.max(1, limit), total); + final List> page = combined.subList(safeOffset, safeTo); + + final Map resp = new LinkedHashMap<>(); + resp.put("totalFilteredRecords", total); + resp.put("offset", safeOffset); + resp.put("limit", limit); + resp.put("pageItems", page); + return gson.toJson(resp); } /** @@ -276,6 +297,12 @@ public class CashReportApiResource { @QueryParam("asOfDate") String asOfDate) { final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString(); + // CASHIER role: always scope to their own position only + final Long scopedId = cashierRoleHelper.getCashierIdForFiltering(); + if (scopedId != null) { + return gson.toJson(buildPositionForCashier(scopedId, dateFilter)); + } + if (cashierId != null) { return gson.toJson(buildPositionForCashier(cashierId, dateFilter)); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/CashierRoleHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/CashierRoleHelper.java new file mode 100644 index 000000000..a05be4c2f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/service/CashierRoleHelper.java @@ -0,0 +1,77 @@ +package org.apache.fineract.portfolio.cashreport.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * Helper service for cashier role detection and cashier ID resolution. + * + * Regular CASHIER role → isCashierOnly() = true → data scoped to that cashier + * HEAD CASHIER / Admin → isCashierOnly() = false → all data visible + */ +@Service +@RequiredArgsConstructor +public class CashierRoleHelper { + + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + + /** + * Returns true ONLY for users whose SOLE meaningful role is "CASHIER". + * Users with ALL_FUNCTIONS permission or a "head cashier" role return false. + */ + public boolean isCashierOnly() { + try { + AppUser user = context.authenticatedUser(); + + // Check via DB: if user's roles have ALL_FUNCTIONS permission → super-admin, not cashier-only + Long allFunctionsCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_appuser_role aur " + + "JOIN m_role_permission rp ON rp.role_id = aur.role_id " + + "JOIN m_permission p ON p.id = rp.permission_id " + + "WHERE aur.appuser_id = ? AND p.code = 'ALL_FUNCTIONS'", + Long.class, user.getId()); + if (allFunctionsCount != null && allFunctionsCount > 0) return false; + + // Check role name is exactly "CASHIER" (case-insensitive) + return user.getRoles().stream().anyMatch(r -> { + String name = r.getName().toLowerCase().trim(); + return name.equals("cashier"); + }); + } catch (Exception e) { + return false; + } + } + + /** + * Returns the m_cashiers.id for the currently authenticated user, + * or null if the user has no associated cashier record. + */ + public Long getCurrentCashierId() { + try { + Long userId = context.authenticatedUser().getId(); + return 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 = ? ORDER BY mc.id DESC LIMIT 1", + Long.class, userId); + } catch (EmptyResultDataAccessException e) { + return null; + } catch (Exception e) { + return null; + } + } + + /** + * Convenience: returns the cashier ID only when the user is cashier-only. + * Returns null for admin/head-cashier users (no filtering needed). + */ + public Long getCashierIdForFiltering() { + return isCashierOnly() ? getCurrentCashierId() : null; + } +} 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 514bed804..a9b8b64d9 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 @@ -23,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.apache.fineract.portfolio.cashreport.service.CashierRoleHelper; import org.springframework.jdbc.core.JdbcTemplate; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.cashsettlement.domain.CashSettlement; @@ -49,6 +50,7 @@ public class CashSettlementApiResource { private final GlDispatchService glDispatchService; private final PlatformTransactionManager transactionManager; private final JdbcTemplate jdbcTemplate; + private final CashierRoleHelper cashierRoleHelper; private final Gson gson = new Gson(); @POST @@ -224,9 +226,27 @@ public class CashSettlementApiResource { } @GET - public String getAll() { - List list = repository.findAllByOrderByCreatedAtDesc(); - return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + public String getAll( + @QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset, + @QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") int limit) { + final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering(); + List all = repository.findAllByOrderByCreatedAtDesc(); + if (scopedCashierId != null) { + all = all.stream() + .filter(s -> scopedCashierId.equals(s.getFromCashierId())) + .collect(Collectors.toList()); + } + final int total = all.size(); + final int safeOffset = Math.max(0, Math.min(offset, total)); + final int safeTo = Math.min(safeOffset + Math.max(1, limit), total); + final List> page = all.subList(safeOffset, safeTo) + .stream().map(this::toMap).collect(Collectors.toList()); + final Map resp = new LinkedHashMap<>(); + resp.put("totalFilteredRecords", total); + resp.put("offset", safeOffset); + resp.put("limit", limit); + resp.put("pageItems", page); + return gson.toJson(resp); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java index 00085fd92..359aad652 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashtransfer/api/CashTransferApiResource.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.apache.fineract.portfolio.cashreport.service.CashierRoleHelper; import org.apache.fineract.portfolio.teller.service.GlDispatchService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.cashtransfer.domain.CashTransfer; @@ -48,6 +50,7 @@ public class CashTransferApiResource { private final GlDispatchService glDispatchService; private final PlatformTransactionManager transactionManager; private final JdbcTemplate jdbcTemplate; + private final CashierRoleHelper cashierRoleHelper; private final Gson gson = new Gson(); @POST @@ -200,9 +203,27 @@ public class CashTransferApiResource { } @GET - public String getAll() { - List list = repository.findAllByOrderByCreatedAtDesc(); - return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + public String getAll( + @QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset, + @QueryParam("limit") @jakarta.ws.rs.DefaultValue("20") int limit) { + final Long scopedCashierId = cashierRoleHelper.getCashierIdForFiltering(); + List all = repository.findAllByOrderByCreatedAtDesc(); + if (scopedCashierId != null) { + all = all.stream() + .filter(t -> scopedCashierId.equals(t.getToCashierId())) + .collect(Collectors.toList()); + } + final int total = all.size(); + final int safeOffset = Math.max(0, Math.min(offset, total)); + final int safeTo = Math.min(safeOffset + Math.max(1, limit), total); + final List> page = all.subList(safeOffset, safeTo) + .stream().map(this::toMap).collect(Collectors.toList()); + final Map resp = new LinkedHashMap<>(); + resp.put("totalFilteredRecords", total); + resp.put("offset", safeOffset); + resp.put("limit", limit); + resp.put("pageItems", page); + return gson.toJson(resp); } @GET diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index cb6012d2f..b5a108dcd 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -342,4 +342,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0315_grant_cashier_transaction_entry_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0315_grant_cashier_transaction_entry_permissions.xml new file mode 100644 index 000000000..131425a5c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0315_grant_cashier_transaction_entry_permissions.xml @@ -0,0 +1,55 @@ + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ( + 'READ_CLIENTKYC', + 'READ_CLIENTNOMINEE', + 'READ_ADDRESS', + 'READ_FAMILYMEMBERS', + 'READ_SAVINGSACCOUNT' + ) + WHERE r.name = 'CASHIER' + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + DELETE FROM m_role_permission + WHERE role_id = (SELECT id FROM m_role WHERE name = 'CASHIER') + AND permission_id IN ( + SELECT id FROM m_permission + WHERE code IN ( + 'READ_CLIENTKYC', + 'READ_CLIENTNOMINEE', + 'READ_ADDRESS', + 'READ_FAMILYMEMBERS', + 'READ_SAVINGSACCOUNT' + ) + ); + + + +