diff --git a/docs/teller-cash-management/issues.md b/docs/teller-cash-management/issues.md
new file mode 100644
index 00000000000..9556ae4c21e
--- /dev/null
+++ b/docs/teller-cash-management/issues.md
@@ -0,0 +1,348 @@
+# Teller Cash Management Redesign — Planned Issues
+
+This document lists all 10 planned issues for the Teller Cash Management redesign. Issues are ordered from highest to lowest priority and in dependency order (bugs first, then enhancements).
+
+---
+
+## Summary Table
+
+| # | Title | Type | Priority | Depends On |
+|---|-------|------|----------|------------|
+| 1 | Bug: PUT /tellers drops debitAccountId and creditAccountId | Bug | High | — |
+| 2 | Bug: fromDate/toDate query parameters are ignored on cashier transactions endpoints | Bug | High | — |
+| 3 | Bug: m_cashiers unique constraint (staff_id, teller_id) prevents cashier re-assignment to the same teller | Bug | High | — |
+| 4 | Enhancement: Add m_cashier_sessions table and JPA entity | Enhancement | High | #3 |
+| 5 | Enhancement: Add cashier_session_id FK to m_loan_transactions and m_savings_account_transaction | Enhancement | High | #4 |
+| 6 | Enhancement: Implement Cashier Session lifecycle API endpoints | Enhancement | High | #4, #5 |
+| 7 | Bug/Enhancement: Route cash GL through Teller Cash (11140) when a cashier session is open | Bug + Enhancement | High | #4, #5, #6 |
+| 8 | Enhancement: Auto-post GL variance journal entry on unbalanced settlement | Enhancement | Medium | #6, #7 |
+| 9 | Bug: Cashier assignment expiry causes silent failure with no notification | Bug | Medium | — |
+| 10 | Enhancement: Multi-teller concurrent session support | Enhancement | Medium | #4, #5, #6, #7 |
+
+---
+
+## Issue 1 — Bug: PUT /tellers drops debitAccountId and creditAccountId
+
+**Labels:** `bug`
+**Spec Reference:** §2.5 — API Bugs Discovered, Phase 2
+
+### Problem
+
+The `Teller.update()` method in `Teller.java` does not handle updating `debitAccountId` and `creditAccountId` from a PUT payload. Requests to update these fields via `PUT /tellers/{id}` are silently ignored, even though the fields exist in the database (`debit_account_id`, `credit_account_id` on `m_tellers`) and on the JPA entity.
+
+### Code Reference
+- File: `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java`
+- Method: `update()` — lines 94–153
+- The method handles: `officeId`, `name`, `description`, `startDate`, `endDate`, `status`
+- ❌ Never reads or persists `debitAccountId` or `creditAccountId`
+
+### Workaround
+Direct SQL only:
+```sql
+UPDATE m_tellers SET debit_account_id = ?, credit_account_id = ? WHERE id = ?;
+```
+
+### Fix Required
+- Update `Teller.update()` to read and persist `debitAccountId` and `creditAccountId` (look up via `GLAccountRepository`)
+- Update `TellerCommandFromApiJsonDeserializer` to allow and validate these fields
+- Update the PUT request DTO to include these fields
+
+---
+
+## Issue 2 — Bug: fromDate/toDate query parameters are ignored on cashier transactions endpoints
+
+**Labels:** `bug`
+**Spec Reference:** §2.5 — API Bugs Discovered, Phase 2
+
+### Problem
+
+The endpoints `/tellers/{id}/cashiers/{cashierId}/transactions` and `/tellers/{id}/cashiers/{cashierId}/summaryandtransactions` accept `currencyCode` but completely ignore `fromDate` and `toDate` — they are not even declared as `@QueryParam` in the API resource. The SQL always uses the cashier assignment dates (`c.start_date` / `c.end_date`) regardless of what is passed in the URL.
+
+### Code References
+
+- File: `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java`
+ - Method: `getTransactionsForCashier()` — `fromDate`/`toDate` are absent from the method signature
+- File: `fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java`
+ - Method: `retrieveCashierTransactions()` — lines 314–354
+ - `fromDate` and `toDate` exist in the method signature but are **never referenced** in the SQL string or params array
+
+### Impact
+It is impossible to narrow a cashier's transactions to a sub-range of their assignment period. All queries always return the full assignment window.
+
+### Fix Required
+- Add `@QueryParam("fromDate")` and `@QueryParam("toDate")` (with `@QueryParam("dateFormat")` and `@QueryParam("locale")`) to `getTransactionsForCashier()` and `getTransactionsWithSummaryForCashier()` in `TellerApiResource.java`
+- Pass the parsed dates to the service method
+- Bind them into the SQL WHERE clauses, replacing `c.start_date`/`c.end_date` when provided
+
+---
+
+## Issue 3 — Bug: m_cashiers unique constraint (staff_id, teller_id) prevents cashier re-assignment to the same teller
+
+**Labels:** `bug`
+**Spec Reference:** §2.1 — Data Model Limitations, Phase 2
+
+### Problem
+
+The `m_cashiers` table enforces a unique constraint on `(staff_id, teller_id)`, meaning a staff member can only ever be assigned to a given teller **once in the database's lifetime**. Re-assigning the same cashier to the same teller on a new date period is impossible without deleting the old record first.
+
+### Code Reference
+- File: `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java`
+- Lines 48–51
+
+### Real-world Impact
+If a cashier is assigned to Teller 1 for March, they cannot be assigned to Teller 1 again in April without deleting the March assignment. This blocks normal shift rotation in any real MFI environment.
+
+### Fix Required
+- Drop the unique constraint `ux_cashiers_staff_teller` from `m_cashiers`
+- Provide a Liquibase migration to remove the constraint
+- Rely on the existing date-overlap check in `CashierTransactionDataValidator.validateCashierAllowedDateAndTime()` as the only guard against duplicate active assignments
+
+---
+
+## Issue 4 — Enhancement: Add m_cashier_sessions table and JPA entity
+
+**Labels:** `enhancement`
+**Spec Reference:** §3.2.1, §3.3 — New Data Model, Phase 3
+**Depends On:** #3
+
+### Feature: Cashier Session as a First-Class Entity
+
+Introduce a `m_cashier_sessions` table and corresponding JPA entity. This is the **foundational change** for the entire session-aware teller redesign. All subsequent issues (GL routing, transaction isolation, session API endpoints) depend on this.
+
+### Schema
+```sql
+CREATE TABLE m_cashier_sessions (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ cashier_id BIGINT NOT NULL,
+ teller_id BIGINT NOT NULL,
+ user_id BIGINT NOT NULL,
+ office_id BIGINT NOT NULL,
+ session_date DATE NOT NULL,
+ opened_at TIMESTAMP NOT NULL,
+ closed_at TIMESTAMP NULL,
+ opening_allocation DECIMAL(19,6) NOT NULL DEFAULT 0,
+ total_settled DECIMAL(19,6) NOT NULL DEFAULT 0,
+ status VARCHAR(20) NOT NULL, -- OPEN, SETTLED, CLOSED
+ opening_txn_id BIGINT NULL,
+ closing_txn_id BIGINT NULL,
+ currency_code VARCHAR(3) NOT NULL,
+ created_by BIGINT NOT NULL,
+ created_date TIMESTAMP NOT NULL
+);
+```
+
+### Deliverables
+- Liquibase changeset for `m_cashier_sessions`
+- `CashierSession.java` JPA entity
+- `CashierSessionRepository.java`
+- `CashierSessionStatus` enum: `OPEN`, `SETTLED`, `CLOSED`
+
+### Business Rules (from Spec §3.5.1)
+- A cashier can only have ONE open session per day per teller
+- A user cannot open a new session if they have an unsettled session from a previous day
+
+---
+
+## Issue 5 — Enhancement: Add cashier_session_id FK to m_loan_transactions and m_savings_account_transaction
+
+**Labels:** `enhancement`
+**Spec Reference:** §3.2.2 — Alter existing tables, Phase 3
+**Depends On:** #4
+
+### Feature: Link Transactions to Cashier Sessions
+
+Add a `cashier_session_id` nullable foreign key column to both `m_loan_transactions` and `m_savings_account_transaction`. This is what enables true per-session transaction isolation and accurate session-level reconciliation.
+
+### Schema
+```sql
+ALTER TABLE m_loan_transactions
+ ADD COLUMN cashier_session_id BIGINT NULL,
+ ADD CONSTRAINT fk_loan_txn_cashier_session
+ FOREIGN KEY (cashier_session_id) REFERENCES m_cashier_sessions(id);
+
+ALTER TABLE m_savings_account_transaction
+ ADD COLUMN cashier_session_id BIGINT NULL,
+ ADD CONSTRAINT fk_sav_txn_cashier_session
+ FOREIGN KEY (cashier_session_id) REFERENCES m_cashier_sessions(id);
+```
+
+### Deliverables
+- Liquibase changeset for both ALTER statements
+- Update `LoanTransaction.java` JPA entity to include `@ManyToOne CashierSession cashierSession`
+- Update `SavingsAccountTransaction.java` JPA entity similarly
+
+---
+
+## Issue 6 — Enhancement: Implement Cashier Session lifecycle API endpoints
+
+**Labels:** `enhancement`
+**Spec Reference:** §3.3, §4 — Session Lifecycle + API Endpoints, Phase 4
+**Depends On:** #4, #5
+
+### New Endpoints Required
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `POST` | `/tellers/{id}/cashiers/{cId}/sessions` | Open a new cashier session |
+| `GET` | `/tellers/{id}/cashiers/{cId}/sessions/active` | Get the currently open session |
+| `GET` | `/tellers/{id}/cashiers/{cId}/sessions` | List all sessions (paginated) |
+| `POST` | `/tellers/{id}/cashiers/{cId}/sessions/{sId}/close` | Close session, post variance entry if applicable |
+| `GET` | `/tellers/{id}/cashiers/{cId}/sessions/{sId}/summary` | Full session summary with GL reconciliation |
+| `GET` | `/users/{id}/session/active` | Resolve active session for logged-in user |
+| `GET` | `/tellers/branch/{officeId}/dashboard` | Supervisor view — all open sessions and positions |
+
+### Session Lifecycle (Spec §3.3)
+
+| Step | Action | GL Entry |
+|------|--------|----------|
+| 1 | Open Session | None — record created |
+| 2 | Allocate Cash | DR 11140 Teller Cash / CR 11130 Vault |
+| 3 | Process Transactions | Auto-linked to open session |
+| 4 | Settle Cash | DR 11130 Vault / CR 11140 Teller Cash |
+| 5 | Close Session | Variance journal if applicable |
+
+### Deliverables
+- `CashierSessionApiResource.java`
+- `CashierSessionWritePlatformService.java` + implementation
+- `CashierSessionReadPlatformService.java` + implementation
+- Command wrapper additions in `CommandWrapperBuilder`
+- `CashierSessionData.java` DTO
+- `CashierSessionSummaryData.java` DTO
+
+---
+
+## Issue 7 — Bug/Enhancement: Route cash GL through Teller Cash (11140) when a cashier session is open
+
+**Labels:** `bug`, `enhancement`
+**Spec Reference:** §2.3, §3.4 — GL Routing Fix, Phase 5
+**Depends On:** #4, #5, #6
+
+### Problem + Feature: GL Routing for Cash Transactions
+
+Currently, all cash repayments and disbursements post to `11130 Cash on Hand (Vault)` regardless of whether a cashier session is active. They should route through `11140 Teller Cash` when an active session exists.
+
+### Current Behaviour (Wrong)
+```
+-- Cash repayment with active cashier session:
+DEBIT 11130 Cash on Hand (Vault) ← wrong, bypasses teller
+CREDIT 11210 Gross Loan Portfolio
+```
+
+### Expected Behaviour
+```
+-- Cash repayment with active cashier session:
+DEBIT 11140 Teller Cash ← correct, routes through cashier drawer
+CREDIT 11210 Gross Loan Portfolio
+```
+
+### Files to Update
+- `fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForLoan.java`
+- `fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorForSavings.java`
+
+### Financial Activity Mapping Required
+| ID | Activity | GL Code | Account Name |
+|----|----------|---------|--------------|
+| 101 | cashAtMainVault | 11130 | Cash on Hand (Vault) |
+| 102 | cashAtTeller | 11140 | Teller Cash — Head Office |
+
+---
+
+## Issue 8 — Enhancement: Auto-post GL variance journal entry on unbalanced settlement
+
+**Labels:** `enhancement`
+**Spec Reference:** §3.5.3 — Variance Handling, Phase 7
+**Depends On:** #6, #7
+
+### Feature: Variance Journal Entry on Settlement
+
+When a cashier settles and the amount returned does not equal the expected cash on hand, the system must automatically calculate and post a correcting GL journal entry.
+
+### Variance Formula (Spec §3.5.3)
+```
+Expected Cash = Opening Allocation + Cash In − Cash Out
+Variance = Settled Amount − Expected Cash
+```
+
+### GL Entries
+
+**Short settlement (cashier returns less than expected):**
+```sql
+DEBIT 53920 Cash Shortage — Teller [variance amount]
+CREDIT 11140 Teller Cash — Head Office [variance amount]
+```
+
+**Over settlement (cashier returns more than expected):**
+```sql
+DEBIT 11140 Teller Cash — Head Office [variance amount]
+CREDIT 43210 Miscellaneous Income [variance amount]
+```
+
+### GL Accounts Required
+| GL Code | Account Name | Type |
+|---------|-------------|------|
+| 53920 | Cash Shortage — Teller | Expense — Detail |
+| 43210 | Miscellaneous Income | Income — Detail |
+
+### Business Rules
+- If variance ≠ 0, a mandatory supervisor note is required before settlement is accepted
+- The correcting journal must be auto-posted as part of the session close flow
+
+### Implementation Location
+- `TellerWritePlatformServiceJpaImpl.java` — `settleCashFromCashier()` method
+- Or new `CashierSessionWritePlatformServiceImpl.java` session close handler
+
+---
+
+## Issue 9 — Bug: Cashier assignment expiry causes silent failure with no notification
+
+**Labels:** `bug`
+**Spec Reference:** §7 — Known Limitations, Phase 9
+
+### Problem
+
+When a cashier's `end_date` on `m_cashiers` is reached, operations fail silently. No notification is sent, no warning is shown, and errors returned to users are unclear.
+
+### Current Behaviour
+- Cashier assignment expires silently at midnight on `end_date`
+- Subsequent allocation, repayment, or settlement calls fail without a helpful error
+- No API response distinguishes "cashier assignment expired" from other errors
+- No advance warning when expiry is approaching
+
+### Files Involved
+- `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java`
+- `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java`
+- `fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java`
+
+### Fix Required
+- On any teller operation, check if `cashier.getEndDate()` is in the past and return `422 Unprocessable Entity` with a clear message
+- On cashier retrieval, include an `expiryWarning` flag in the response if `end_date` is within 7 days
+
+---
+
+## Issue 10 — Enhancement: Multi-teller concurrent session support
+
+**Labels:** `enhancement`
+**Spec Reference:** §8 — Implementation Plan Phase 10
+**Depends On:** #4, #5, #6, #7
+
+### Feature: Allow Cashier to Hold Concurrent Sessions on Multiple Tellers
+
+The proposed session model (Issue #6) assumes one active session per cashier per day. To support cashiers rotating between multiple teller stations within the same day, the session model needs to permit concurrent sessions on different tellers.
+
+### Required Changes
+
+**Session uniqueness rule change:**
+- Current proposed rule: one open session per cashier per day
+- New rule: one open session per cashier **per teller** per day
+
+**Session lookup change:**
+```java
+// Required (this issue):
+cashierSessionRepository.findOpenSessionByUserAndTeller(userId, tellerId, officeId, date);
+```
+
+### Deliverables
+- Update `CashierSessionRepository.findOpenSessionByUser()` to accept `tellerId`
+- Update session open validation to check uniqueness per `(cashier_id, teller_id, session_date)`
+- Update GL routing in `AccountingProcessorForLoan` and `AccountingProcessorForSavings` to resolve teller account from the active session's `teller_id`
+- Add `teller_id` index on `m_cashier_sessions`
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java
index d190e9f18d0..4a91eae95c6 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntry.java
@@ -3,15 +3,13 @@
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
+ * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
@@ -91,6 +89,7 @@ public class JournalEntry extends AbstractAuditableWithUTCDateTimeCustom {
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
+ @Setter
@Column(name = "description", length = 500)
private String description;
@@ -154,4 +153,4 @@ public boolean isCreditEntry() {
return JournalEntryType.CREDIT.getValue().equals(this.type);
}
-}
+}
\ No newline at end of file
diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java
index b078042ab49..07aabee61c6 100644
--- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java
+++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/journalentry/domain/JournalEntryRepository.java
@@ -57,11 +57,11 @@ public interface JournalEntryRepository extends JpaRepository findTrialBalanceLinesForDate(@Param("transactionDate") LocalDate transactionDate);
diff --git a/fineract-branch/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/CashierAssignmentExpiredExceptionMapper.java b/fineract-branch/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/CashierAssignmentExpiredExceptionMapper.java
new file mode 100644
index 00000000000..905ac276292
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/CashierAssignmentExpiredExceptionMapper.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.exceptionmapper;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import jakarta.ws.rs.ext.Provider;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
+import org.apache.fineract.organisation.teller.exception.CashierAssignmentExpiredException;
+import org.springframework.stereotype.Component;
+
+@Provider
+@Component
+@Slf4j
+public class CashierAssignmentExpiredExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(final CashierAssignmentExpiredException exception) {
+ log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception));
+ final ApiParameterError error = ApiParameterError.generalError("error.msg.cashier.assignment.expired", exception.getMessage());
+ return Response.status(422).entity(error).type(MediaType.APPLICATION_JSON).build();
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/CashierSessionApiResource.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/CashierSessionApiResource.java
new file mode 100644
index 00000000000..1f56fc059e3
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/CashierSessionApiResource.java
@@ -0,0 +1,168 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.api;
+
+import com.google.gson.JsonObject;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+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.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.service.CommandWrapperBuilder;
+import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.organisation.teller.data.CashierSessionData;
+import org.apache.fineract.organisation.teller.data.CashierSessionSummaryData;
+import org.apache.fineract.organisation.teller.service.CashierSessionReadPlatformService;
+import org.springframework.stereotype.Component;
+
+@Path("/v1/tellers")
+@Component
+@Tag(name = "Cashier Session Management", description = "Endpoints for managing cashier session lifecycle.")
+@RequiredArgsConstructor
+public class CashierSessionApiResource {
+
+ private static final String RESOURCE_NAME = "CASHIERSESSION";
+
+ private final CashierSessionReadPlatformService readService;
+ private final PortfolioCommandSourceWritePlatformService commandWritePlatformService;
+ private final PlatformSecurityContext context;
+
+ /**
+ * POST /tellers/{id}/cashiers/{cId}/sessions — Open a new cashier session.
+ */
+ @POST
+ @Path("{tellerId}/cashiers/{cashierId}/sessions")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Open a cashier session", description = "Opens a new cashier session for the given cashier on the given teller.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public CommandProcessingResult openSession(
+ @PathParam("tellerId") @Parameter(description = "tellerId") final Long tellerId,
+ @PathParam("cashierId") @Parameter(description = "cashierId") final Long cashierId,
+ @QueryParam("currencyCode") @Parameter(description = "currencyCode") final String currencyCode) {
+ context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
+ final JsonObject json = new JsonObject();
+ json.addProperty("currencyCode", currencyCode != null ? currencyCode : "");
+ final CommandWrapper request = new CommandWrapperBuilder()
+ .openCashierSession(tellerId, cashierId)
+ .withJson(json.toString())
+ .build();
+ return commandWritePlatformService.logCommandSource(request);
+ }
+
+ /**
+ * GET /tellers/{id}/cashiers/{cId}/sessions/active — Get the currently open session.
+ */
+ @GET
+ @Path("{tellerId}/cashiers/{cashierId}/sessions/active")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Get active cashier session", description = "Returns the currently open session for the given cashier on the given teller.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public Optional getActiveSession(
+ @PathParam("tellerId") @Parameter(description = "tellerId") final Long tellerId,
+ @PathParam("cashierId") @Parameter(description = "cashierId") final Long cashierId) {
+ context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
+ return readService.findActiveSession(cashierId, tellerId);
+ }
+
+ /**
+ * GET /tellers/{id}/cashiers/{cId}/sessions — List all sessions (paginated).
+ */
+ @GET
+ @Path("{tellerId}/cashiers/{cashierId}/sessions")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "List cashier sessions", description = "Returns all sessions for the given cashier on the given teller.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public List listSessions(
+ @PathParam("tellerId") @Parameter(description = "tellerId") final Long tellerId,
+ @PathParam("cashierId") @Parameter(description = "cashierId") final Long cashierId) {
+ context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
+ return readService.findAllSessions(cashierId, tellerId);
+ }
+
+ /**
+ * POST /tellers/{id}/cashiers/{cId}/sessions/{sId}/close — Close session.
+ */
+ @POST
+ @Path("{tellerId}/cashiers/{cashierId}/sessions/{sessionId}/close")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Close a cashier session", description = "Closes the specified cashier session.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public CommandProcessingResult closeSession(
+ @PathParam("tellerId") @Parameter(description = "tellerId") final Long tellerId,
+ @PathParam("cashierId") @Parameter(description = "cashierId") final Long cashierId,
+ @PathParam("sessionId") @Parameter(description = "sessionId") final Long sessionId) {
+ context.authenticatedUser().validateHasPermissionTo("CLOSE_CASHIERSESSION");
+ final CommandWrapper request = new CommandWrapperBuilder()
+ .closeCashierSession(tellerId, cashierId, sessionId)
+ .withJson("{}")
+ .build();
+ return commandWritePlatformService.logCommandSource(request);
+ }
+
+ /**
+ * GET /tellers/{id}/cashiers/{cId}/sessions/{sId}/summary — Full session summary with GL reconciliation.
+ */
+ @GET
+ @Path("{tellerId}/cashiers/{cashierId}/sessions/{sessionId}/summary")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Get session summary", description = "Returns a full session summary with GL reconciliation for the given session.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public CashierSessionSummaryData getSessionSummary(
+ @PathParam("tellerId") @Parameter(description = "tellerId") final Long tellerId,
+ @PathParam("cashierId") @Parameter(description = "cashierId") final Long cashierId,
+ @PathParam("sessionId") @Parameter(description = "sessionId") final Long sessionId) {
+ context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
+ return readService.getSessionSummary(sessionId);
+ }
+
+ /**
+ * GET /tellers/branch/{officeId}/dashboard — Supervisor view: all open sessions and positions.
+ */
+ @GET
+ @Path("branch/{officeId}/dashboard")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Get branch dashboard", description = "Returns all open cashier sessions for the given office (supervisor view).")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public List getBranchDashboard(
+ @PathParam("officeId") @Parameter(description = "officeId") final Long officeId) {
+ context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
+ return readService.findOpenSessionsByOffice(officeId);
+
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java
index 35053c7c43e..c35ca01400f 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/TellerApiResource.java
@@ -40,6 +40,8 @@
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
+import java.util.Locale;
+import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -289,18 +291,27 @@ public Page getTransactionsForCashier(
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
- @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
+ @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
+ @QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDateStr,
+ @QueryParam("toDate") @Parameter(description = "toDate") final String toDateStr,
+ @QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat,
+ @QueryParam("locale") @Parameter(description = "locale") final String locale) {
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
.sortOrder(sortOrder).build();
- return this.readPlatformService.retrieveCashierTransactions(cashierId, false, null, null, currencyCode, searchParameters);
+ final LocalDate fromDate = parseDateParam(fromDateStr, "fromDate", dateFormat, locale);
+ final LocalDate toDate = parseDateParam(toDateStr, "toDate", dateFormat, locale);
+ return this.readPlatformService.retrieveCashierTransactions(cashierId, false, fromDate, toDate, currencyCode, searchParameters,
+ null);
}
@GET
@Path("{tellerId}/cashiers/{cashierId}/summaryandtransactions")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
- @Operation(summary = "Retrieve Transactions With Summary For Cashier", description = "")
+ @Operation(summary = "Retrieve Transactions With Summary For Cashier", description = "Returns a combined summary and transaction list for a cashier. "
+ + "When sessionId is omitted this is the legacy all-time view. "
+ + "When sessionId is provided only transactions belonging to that session are included (Option A).")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.GetTellersTellerIdCashiersCashiersIdSummaryAndTransactionsResponse.class))) })
public CashierTransactionsWithSummaryData getTransactionsWithSummaryForCashier(
@@ -310,13 +321,20 @@ public CashierTransactionsWithSummaryData getTransactionsWithSummaryForCashier(
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
- @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
+ @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
+ @QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDateStr,
+ @QueryParam("toDate") @Parameter(description = "toDate") final String toDateStr,
+ @QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat,
+ @QueryParam("locale") @Parameter(description = "locale") final String locale,
+ @QueryParam("sessionId") @Parameter(description = "Optional cashier session ID. When provided, only transactions for that session are returned. When omitted, all transactions for the cashier are returned (backwards-compatible behaviour).") final Long sessionId) {
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
.sortOrder(sortOrder).build();
+ final LocalDate fromDate = parseDateParam(fromDateStr, "fromDate", dateFormat, locale);
+ final LocalDate toDate = parseDateParam(toDateStr, "toDate", dateFormat, locale);
- return this.readPlatformService.retrieveCashierTransactionsWithSummary(cashierId, false, null, null, currencyCode,
- searchParameters);
+ return this.readPlatformService.retrieveCashierTransactionsWithSummary(cashierId, false, fromDate, toDate, currencyCode,
+ searchParameters, sessionId);
}
@GET
@@ -366,4 +384,12 @@ public Collection getJournalData(@PathParam("tellerId") @Para
return this.readPlatformService.fetchTellerJournals(tellerId, cashierDate, dateRangeHolder.getStartDate(),
dateRangeHolder.getEndDate());
}
+
+ private LocalDate parseDateParam(final String dateStr, final String parameterName, final String dateFormat, final String locale) {
+ if (dateStr == null || dateFormat == null || locale == null) {
+ return null;
+ }
+ final Locale localeObj = JsonParserHelper.localeFromString(locale);
+ return JsonParserHelper.convertFrom(dateStr, parameterName, dateFormat, localeObj);
+ }
}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/UserSessionApiResource.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/UserSessionApiResource.java
new file mode 100644
index 00000000000..a006522ee66
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/api/UserSessionApiResource.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.api;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.organisation.teller.data.CashierSessionData;
+import org.apache.fineract.organisation.teller.service.CashierSessionReadPlatformService;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.stereotype.Component;
+
+@Path("/v1/users")
+@Component
+@Tag(name = "User Session Management", description = "Endpoint for resolving the active cashier session for a user.")
+@RequiredArgsConstructor
+public class UserSessionApiResource {
+
+ private final CashierSessionReadPlatformService readService;
+ private final PlatformSecurityContext context;
+
+ /**
+ * GET /users/{id}/session/active — Resolve active session for logged-in user.
+ */
+ @GET
+ @Path("{userId}/session/active")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Get active session for user", description = "Returns the currently open cashier session for the given user.")
+ @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
+ public Optional getActiveSessionForUser(
+ @PathParam("userId") @Parameter(description = "userId") final Long userId) {
+ final AppUser currentUser = context.authenticatedUser();
+ final Long officeId = currentUser.getOffice().getId();
+ return readService.findActiveSessionForUser(userId, officeId);
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierData.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierData.java
index 1024962f8fc..25344d3d10f 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierData.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierData.java
@@ -53,6 +53,8 @@ public final class CashierData implements Serializable {
private String startTime;
private String endTime;
+ private boolean expiryWarning;
+
// Template fields
private String officeName;
private String tellerName;
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionData.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionData.java
new file mode 100644
index 00000000000..8633d269eb6
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionData.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.apache.fineract.organisation.teller.domain.CashierSessionStatus;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class CashierSessionData implements Serializable {
+
+ private Long id;
+ private Long cashierId;
+ private Long tellerId;
+ private Long userId;
+ private Long officeId;
+ private LocalDate sessionDate;
+ private LocalDateTime openedAt;
+ private LocalDateTime closedAt;
+ private BigDecimal openingAllocation;
+ private BigDecimal totalSettled;
+ private CashierSessionStatus status;
+ private Long openingTxnId;
+ private Long closingTxnId;
+ private String currencyCode;
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionSummaryData.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionSummaryData.java
new file mode 100644
index 00000000000..a9f87d660e9
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierSessionSummaryData.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class CashierSessionSummaryData implements Serializable {
+
+ private CashierSessionData session;
+ private BigDecimal openingAllocation;
+ private BigDecimal totalCashIn;
+ private BigDecimal totalCashOut;
+ private BigDecimal expectedCash;
+ private BigDecimal settledAmount;
+ private BigDecimal variance;
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierTransactionDataValidator.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierTransactionDataValidator.java
index 7667fdfba50..317697ef405 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierTransactionDataValidator.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/data/CashierTransactionDataValidator.java
@@ -24,12 +24,14 @@
import java.util.HashMap;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.organisation.teller.domain.Cashier;
import org.apache.fineract.organisation.teller.domain.Teller;
import org.apache.fineract.organisation.teller.exception.CashierAlreadyAllocated;
+import org.apache.fineract.organisation.teller.exception.CashierAssignmentExpiredException;
import org.apache.fineract.organisation.teller.exception.CashierDateRangeOutOfTellerDateRangeException;
import org.apache.fineract.organisation.teller.exception.CashierInsufficientAmountException;
import org.apache.fineract.organisation.teller.service.TellerManagementReadPlatformService;
@@ -58,7 +60,7 @@ public CashierTransactionDataValidator(final TellerManagementReadPlatformService
public void validateSettleCashAndCashOutTransactions(final Long cashierId, String currencyCode, final BigDecimal transactionAmount) {
final SearchParameters searchParameters = SearchParameters.builder().build();
final CashierTransactionsWithSummaryData cashierTxnWithSummary = this.tellerManagementReadPlatformService
- .retrieveCashierTransactionsWithSummary(cashierId, false, null, null, currencyCode, searchParameters);
+ .retrieveCashierTransactionsWithSummary(cashierId, false, null, null, currencyCode, searchParameters, null);
if (MathUtil.isGreaterThan(transactionAmount, cashierTxnWithSummary.getNetCash())) {
throw new CashierInsufficientAmountException();
}
@@ -70,6 +72,24 @@ public void validateSettleCashAndCashOutTransactions(final Long cashierId, JsonC
validateSettleCashAndCashOutTransactions(cashierId, currencyCode, transactionAmount);
}
+ public void validateAllocateCashTransactions(final Long cashierId, final JsonCommand command) {
+ // Verify cashier exists (throws exception if not found)
+ final CashierData cashierData = this.tellerManagementReadPlatformService.findCashier(cashierId);
+
+ // Check cashier assignment is not expired
+ if (cashierData.getEndDate() != null && cashierData.getEndDate().isBefore(DateUtils.getLocalDateOfTenant())) {
+ final String cashierName = cashierData.getStaffName() != null ? cashierData.getStaffName() : String.valueOf(cashierId);
+ throw new CashierAssignmentExpiredException(cashierName, cashierData.getEndDate());
+ }
+
+ // Validate currency code is present
+ final String currencyCode = command.stringValueOfParameterNamed("currencyCode");
+ if (currencyCode == null || currencyCode.isBlank()) {
+ throw new GeneralPlatformDomainRuleException("error.msg.cashier.allocation.currency.code.required",
+ "Currency code is required for cashier cash allocation.");
+ }
+ }
+
public void validateCashierAllowedDateAndTime(final Cashier cashier, final Teller teller) {
Long staffId = cashier.getStaff().getId();
final LocalDate fromDate = cashier.getStartDate();
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java
index 4089a805977..7bab8d91e75 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Cashier.java
@@ -26,7 +26,6 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
-import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -48,8 +47,7 @@
* @since 2.0.0
*/
@Entity
-@Table(name = "m_cashiers", uniqueConstraints = {
- @UniqueConstraint(name = "ux_cashiers_staff_teller", columnNames = { "staff_id", "teller_id" }) })
+@Table(name = "m_cashiers")
@Getter
@Setter
@NoArgsConstructor
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSession.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSession.java
new file mode 100644
index 00000000000..6734bdf44da
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSession.java
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.organisation.office.domain.Office;
+
+@Entity
+@Table(name = "m_cashier_sessions")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class CashierSession extends AbstractPersistableCustom {
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "cashier_id", nullable = false)
+ private Cashier cashier;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "teller_id", nullable = false)
+ private Teller teller;
+
+ @Column(name = "user_id", nullable = false)
+ private Long userId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "office_id", nullable = false)
+ private Office office;
+
+ @Column(name = "session_date", nullable = false)
+ private LocalDate sessionDate;
+
+ @Column(name = "opened_at", nullable = false)
+ private LocalDateTime openedAt;
+
+ @Column(name = "closed_at")
+ private LocalDateTime closedAt;
+
+ @Column(name = "opening_allocation", precision = 19, scale = 6, nullable = false)
+ private BigDecimal openingAllocation = BigDecimal.ZERO;
+
+ @Column(name = "total_settled", precision = 19, scale = 6, nullable = false)
+ private BigDecimal totalSettled = BigDecimal.ZERO;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "status", length = 20, nullable = false)
+ private CashierSessionStatus status;
+
+ @Column(name = "opening_txn_id")
+ private Long openingTxnId;
+
+ @Column(name = "closing_txn_id")
+ private Long closingTxnId;
+
+ @Column(name = "currency_code", length = 3, nullable = false)
+ private String currencyCode;
+
+ @Column(name = "supervisor_note", length = 500, nullable = true)
+ private String supervisorNote;
+
+ @Column(name = "created_by", nullable = false)
+ private Long createdBy;
+
+ @Column(name = "created_date", nullable = false)
+ private LocalDateTime createdDate;
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionRepository.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionRepository.java
new file mode 100644
index 00000000000..73683b08eab
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionRepository.java
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.domain;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface CashierSessionRepository extends JpaRepository {
+
+ // Find OPEN session for a cashier+teller+date (enforces one session per station per day)
+ @Query("SELECT cs FROM CashierSession cs WHERE cs.cashier.id = :cashierId AND cs.teller.id = :tellerId AND cs.sessionDate = :sessionDate AND cs.status = org.apache.fineract.organisation.teller.domain.CashierSessionStatus.OPEN")
+ Optional findOpenSession(@Param("cashierId") Long cashierId, @Param("tellerId") Long tellerId,
+ @Param("sessionDate") LocalDate sessionDate);
+
+ // Find all OPEN sessions by user on any teller (for GL routing; supports multi-teller scenario)
+ @Query("SELECT cs FROM CashierSession cs WHERE cs.userId = :userId AND cs.office.id = :officeId AND cs.sessionDate = :sessionDate AND cs.status = org.apache.fineract.organisation.teller.domain.CashierSessionStatus.OPEN")
+ List findOpenSessionByUser(@Param("userId") Long userId, @Param("officeId") Long officeId,
+ @Param("sessionDate") LocalDate sessionDate);
+
+ // Find OPEN session by user on a specific teller (for per-teller GL routing)
+ @Query("SELECT cs FROM CashierSession cs WHERE cs.userId = :userId AND cs.teller.id = :tellerId AND cs.office.id = :officeId AND cs.sessionDate = :sessionDate AND cs.status = org.apache.fineract.organisation.teller.domain.CashierSessionStatus.OPEN")
+ Optional findOpenSessionByUserAndTeller(@Param("userId") Long userId, @Param("tellerId") Long tellerId,
+ @Param("officeId") Long officeId, @Param("sessionDate") LocalDate sessionDate);
+
+ // Find any unsettled OPEN sessions from prior days (blocks opening new session)
+ @Query("SELECT cs FROM CashierSession cs WHERE cs.userId = :userId AND cs.sessionDate < :today AND cs.status = org.apache.fineract.organisation.teller.domain.CashierSessionStatus.OPEN")
+ List findUnsettledPriorSessions(@Param("userId") Long userId, @Param("today") LocalDate today);
+
+ // List all sessions for a cashier on a teller
+ List findByCashierIdAndTellerIdOrderBySessionDateDesc(Long cashierId, Long tellerId);
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionStatus.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionStatus.java
new file mode 100644
index 00000000000..16aa636f8c4
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierSessionStatus.java
@@ -0,0 +1,25 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.domain;
+
+public enum CashierSessionStatus {
+ OPEN,
+ SETTLED,
+ CLOSED
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransaction.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransaction.java
index 098a1c68319..090772920af 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransaction.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransaction.java
@@ -59,6 +59,10 @@ public class CashierTransaction extends AbstractPersistableCustom {
@JoinColumn(name = "cashier_id", nullable = false)
private Cashier cashier;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "cashier_session_id", nullable = true)
+ private CashierSession cashierSession;
+
@Column(name = "txn_type", nullable = false)
private Integer txnType;
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransactionRepository.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransactionRepository.java
index 5e3dbbadaa9..62c745c365e 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransactionRepository.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/CashierTransactionRepository.java
@@ -18,10 +18,24 @@
*/
package org.apache.fineract.organisation.teller.domain;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
public interface CashierTransactionRepository
extends JpaRepository, JpaSpecificationExecutor {
- // no added behavior
+
+ @Query("SELECT COALESCE(SUM(ct.txnAmount), 0) FROM CashierTransaction ct WHERE ct.cashier.id = :cashierId AND ct.txnType = :txnType AND ct.txnDate = :sessionDate")
+ BigDecimal sumAmountByCashierAndTxnTypeAndDate(@Param("cashierId") Long cashierId, @Param("txnType") Integer txnType,
+ @Param("sessionDate") LocalDate sessionDate);
+
+ @Query("SELECT COALESCE(SUM(ct.txnAmount), 0) FROM CashierTransaction ct WHERE ct.cashierSession.id = :sessionId AND ct.txnType = :txnType")
+ BigDecimal sumAmountBySessionAndTxnType(@Param("sessionId") Long sessionId, @Param("txnType") Integer txnType);
+
+ @Query("SELECT COALESCE(SUM(ct.txnAmount), 0) FROM CashierTransaction ct WHERE ct.cashierSession.id = :sessionId AND ct.txnType IN :txnTypes")
+ BigDecimal sumAmountBySessionAndTxnTypes(@Param("sessionId") Long sessionId, @Param("txnTypes") List txnTypes);
}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java
index 91e22d10e27..63dd895b80b 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java
@@ -91,9 +91,10 @@ public static Teller fromJson(final Office tellerOffice, final JsonCommand comma
.setStatus(status.getValue());
}
- public Map update(Office tellerOffice, final JsonCommand command) {
+ public Map update(Office tellerOffice, GLAccount newDebitAccount, GLAccount newCreditAccount,
+ final JsonCommand command) {
- final Map actualChanges = new LinkedHashMap<>(7);
+ final Map actualChanges = new LinkedHashMap<>(9);
final String dateFormatAsInput = command.dateFormat();
final String localeAsInput = command.locale();
@@ -150,6 +151,22 @@ public Map update(Office tellerOffice, final JsonCommand command
}
}
+ final String debitAccountIdParamName = "debitAccountId";
+ final Long currentDebitAccountId = this.debitAccount != null ? this.debitAccount.getId() : null;
+ if (command.isChangeInLongParameterNamed(debitAccountIdParamName, currentDebitAccountId)) {
+ final Long newValue = command.longValueOfParameterNamed(debitAccountIdParamName);
+ actualChanges.put(debitAccountIdParamName, newValue);
+ this.debitAccount = newDebitAccount;
+ }
+
+ final String creditAccountIdParamName = "creditAccountId";
+ final Long currentCreditAccountId = this.creditAccount != null ? this.creditAccount.getId() : null;
+ if (command.isChangeInLongParameterNamed(creditAccountIdParamName, currentCreditAccountId)) {
+ final Long newValue = command.longValueOfParameterNamed(creditAccountIdParamName);
+ actualChanges.put(creditAccountIdParamName, newValue);
+ this.creditAccount = newCreditAccount;
+ }
+
return actualChanges;
}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierAssignmentExpiredException.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierAssignmentExpiredException.java
new file mode 100644
index 00000000000..fdf2f4047ca
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierAssignmentExpiredException.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.exception;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+@SuppressWarnings("serial")
+public class CashierAssignmentExpiredException extends RuntimeException {
+
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+ public CashierAssignmentExpiredException(final String cashierName, final LocalDate expiryDate) {
+ super("Cashier assignment for " + cashierName + " expired on " + expiryDate.format(DATE_FORMATTER)
+ + ". Please renew the assignment.");
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionAlreadyOpenException.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionAlreadyOpenException.java
new file mode 100644
index 00000000000..a0d2ae1a4d9
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionAlreadyOpenException.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class CashierSessionAlreadyOpenException extends AbstractPlatformDomainRuleException {
+
+ public CashierSessionAlreadyOpenException(Long cashierId, Long tellerId) {
+ super("cashier.session.already.open",
+ "An open session already exists for cashier " + cashierId + " on teller " + tellerId + " today.");
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionNotFoundException.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionNotFoundException.java
new file mode 100644
index 00000000000..1ad947cbe18
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionNotFoundException.java
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
+
+public class CashierSessionNotFoundException extends AbstractPlatformResourceNotFoundException {
+
+ private static final String ERROR_MESSAGE_CODE = "error.msg.cashier.session.not.found";
+ private static final String DEFAULT_ERROR_MESSAGE = "Cashier session with identifier {0,number,long} not found!";
+
+ public CashierSessionNotFoundException(Long sessionId) {
+ super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE, sessionId);
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionUnsettledPriorDayException.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionUnsettledPriorDayException.java
new file mode 100644
index 00000000000..e845cf44b8b
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/CashierSessionUnsettledPriorDayException.java
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.exception;
+
+import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class CashierSessionUnsettledPriorDayException extends AbstractPlatformDomainRuleException {
+
+ public CashierSessionUnsettledPriorDayException() {
+ super("cashier.session.unsettled.prior.day",
+ "Cannot open a new session: there is an unsettled open session from a prior day.");
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/CloseCashierSessionCommandHandler.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/CloseCashierSessionCommandHandler.java
new file mode 100644
index 00000000000..b7377b480a3
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/CloseCashierSessionCommandHandler.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.handler;
+
+import java.math.BigDecimal;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.organisation.teller.service.CashierSessionWritePlatformService;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "CASHIERSESSION", action = "CLOSECASHIERSESSION")
+public class CloseCashierSessionCommandHandler implements NewCommandSourceHandler {
+
+ private final CashierSessionWritePlatformService cashierSessionWritePlatformService;
+
+ @Override
+ public CommandProcessingResult processCommand(final JsonCommand command) {
+ final Long sessionId = command.entityId();
+ final BigDecimal settledAmount = command.bigDecimalValueOfParameterNamed("settledAmount");
+ final String supervisorNote = command.stringValueOfParameterNamed("supervisorNote");
+ return cashierSessionWritePlatformService.closeSession(sessionId, settledAmount, supervisorNote);
+ }
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/DeleteCashierAllocationCommandHandler.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/DeleteCashierAllocationCommandHandler.java
index adcb9109eef..a0e62a3f835 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/DeleteCashierAllocationCommandHandler.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/DeleteCashierAllocationCommandHandler.java
@@ -30,7 +30,7 @@
* Handles a delete cashier command.
*
* @author Markus Geiss
- * @see org.apache.fineract.organisation.teller.service.CashierWritePlatformService
+ * @see org.apache.fineract.organisation.teller.service.TellerWritePlatformService
* @since 2.0.0
*/
@Service
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/ModifyCashierCommandHandler.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/OpenCashierSessionCommandHandler.java
similarity index 62%
rename from fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/ModifyCashierCommandHandler.java
rename to fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/OpenCashierSessionCommandHandler.java
index cabda9a1473..7389e8492e3 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/ModifyCashierCommandHandler.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/handler/OpenCashierSessionCommandHandler.java
@@ -19,25 +19,25 @@
package org.apache.fineract.organisation.teller.handler;
import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.apache.fineract.organisation.teller.service.CashierWritePlatformService;
+import org.apache.fineract.organisation.teller.service.CashierSessionWritePlatformService;
+import org.springframework.stereotype.Service;
-/**
- * Handles a modify cashier command.
- *
- * @author Markus Geiss
- * @see org.apache.fineract.organisation.teller.service.CashierWritePlatformService
- * @since 2.0.0
- */
+@Service
@RequiredArgsConstructor
-public class ModifyCashierCommandHandler implements NewCommandSourceHandler {
+@CommandType(entity = "CASHIERSESSION", action = "OPENCASHIERSESSION")
+public class OpenCashierSessionCommandHandler implements NewCommandSourceHandler {
- private final CashierWritePlatformService writePlatformService;
+ private final CashierSessionWritePlatformService cashierSessionWritePlatformService;
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
- return this.writePlatformService.modifyCashier(command.entityId(), command);
+ final Long tellerId = command.entityId();
+ final Long cashierId = command.subentityId();
+ final String currencyCode = command.stringValueOfParameterNamed("currencyCode");
+ return cashierSessionWritePlatformService.openSession(tellerId, cashierId, currencyCode);
}
}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/serialization/TellerCommandFromApiJsonDeserializer.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/serialization/TellerCommandFromApiJsonDeserializer.java
index 5123699c17f..adf8f2db416 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/serialization/TellerCommandFromApiJsonDeserializer.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/serialization/TellerCommandFromApiJsonDeserializer.java
@@ -68,13 +68,15 @@ public final class TellerCommandFromApiJsonDeserializer {
public static final String TXN_DATE = "txnDate";
public static final String TXN_NOTE = "txnNote";
public static final String TELLER = "teller";
+ public static final String DEBIT_ACCOUNT_ID = "debitAccountId";
+ public static final String CREDIT_ACCOUNT_ID = "creditAccountId";
private static final String START_TIME = "startTime";
/**
* The parameters supported for this command.
*/
private static final Set SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList(OFFICE_ID, NAME, DESCRIPTION, START_DATE, END_DATE,
STATUS, DATE_FORMAT, LOCALE, IS_FULL_DAY, STAFF_ID, HOUR_START_TIME, MIN_START_TIME, HOUR_END_TIME, MIN_END_TIME, TXN_AMOUNT,
- TXN_DATE, TXN_NOTE, ENTITY_TYPE, ENTITY_ID, CURRENCY_CODE));
+ TXN_DATE, TXN_NOTE, ENTITY_TYPE, ENTITY_ID, CURRENCY_CODE, DEBIT_ACCOUNT_ID, CREDIT_ACCOUNT_ID));
private final FromJsonHelper fromApiJsonHelper;
@Autowired
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformService.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformService.java
new file mode 100644
index 00000000000..de5a185475b
--- /dev/null
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformService.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.service;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.organisation.teller.data.CashierSessionData;
+import org.apache.fineract.organisation.teller.data.CashierSessionSummaryData;
+
+public interface CashierSessionReadPlatformService {
+
+ Optional findActiveSession(Long cashierId, Long tellerId);
+
+ List findAllSessions(Long cashierId, Long tellerId);
+
+ CashierSessionData findSessionById(Long sessionId);
+
+ CashierSessionSummaryData getSessionSummary(Long sessionId);
+
+ Optional findActiveSessionForUser(Long userId, Long officeId);
+
+ List findOpenSessionsByOffice(Long officeId);
+}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierWritePlatformService.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformService.java
similarity index 63%
rename from fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierWritePlatformService.java
rename to fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformService.java
index ce7a7196b86..ef3f7ee1730 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierWritePlatformService.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformService.java
@@ -18,24 +18,14 @@
*/
package org.apache.fineract.organisation.teller.service;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import java.math.BigDecimal;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-public class CashierWritePlatformService {
+public interface CashierSessionWritePlatformService {
- public CommandProcessingResult allocateCashierToTeller(JsonCommand command) {
- // TODO Auto-generated method stub
- return null;
- }
+ CommandProcessingResult openSession(Long tellerId, Long cashierId, String currencyCode);
- public CommandProcessingResult deleteCashier(Long entityId) {
- // TODO Auto-generated method stub
- return null;
- }
-
- public CommandProcessingResult modifyCashier(Long entityId, JsonCommand command) {
- // TODO Auto-generated method stub
- return null;
- }
+ CommandProcessingResult closeSession(Long sessionId);
+ CommandProcessingResult closeSession(Long sessionId, BigDecimal settledAmount, String supervisorNote);
}
diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformService.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformService.java
index 4b7fa79a9fe..4e10c60e31a 100644
--- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformService.java
+++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformService.java
@@ -56,9 +56,13 @@ public interface TellerManagementReadPlatformService {
Collection retrieveCashiersForTellers(Long tellerId);
Page retrieveCashierTransactions(Long cashierId, boolean includeAllTellers, LocalDate fromDate,
- LocalDate toDate, String currencyCode, SearchParameters searchParameters);
+ LocalDate toDate, String currencyCode, SearchParameters searchParameters, Long sessionId);
+ /**
+ * Option A: sessionId is optional. When null, returns the all-time legacy view for the cashier (backwards
+ * compatible). When non-null, filters results to the specified cashier session only.
+ */
CashierTransactionsWithSummaryData retrieveCashierTransactionsWithSummary(Long cashierId, boolean includeAllTellers, LocalDate fromDate,
- LocalDate toDate, String currencyCode, SearchParameters searchParameters);
+ LocalDate toDate, String currencyCode, SearchParameters searchParameters, Long sessionId);
}
diff --git a/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/module-changelog-master.xml b/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/module-changelog-master.xml
index 035b136c7da..f86dfd82081 100644
--- a/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/module-changelog-master.xml
+++ b/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/module-changelog-master.xml
@@ -23,4 +23,6 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+
diff --git a/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/parts/4001_drop_unique_constraint_cashiers_staff_teller.xml b/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/parts/4001_drop_unique_constraint_cashiers_staff_teller.xml
new file mode 100644
index 00000000000..03ed36a333d
--- /dev/null
+++ b/fineract-branch/src/main/resources/db/changelog/tenant/module/branch/parts/4001_drop_unique_constraint_cashiers_staff_teller.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
index 5e972e532d9..5d0f7c0e558 100644
--- a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
+++ b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml
@@ -81,6 +81,7 @@
org.apache.fineract.organisation.teller.domain.CashierTransaction
+ org.apache.fineract.organisation.teller.domain.CashierSessionorg.apache.fineract.organisation.teller.domain.Tellerorg.apache.fineract.organisation.teller.domain.Cashierorg.apache.fineract.organisation.teller.domain.TellerTransaction
diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
index 01e6b6d2513..dcd9477a98f 100644
--- a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
+++ b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java
@@ -250,6 +250,8 @@ private static void init() {
commandStrategies.put(CommandContext
.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/interest-pauses\\/" + NUMBER_REGEX).method(PUT).build(),
"updateLoanInterestPauseByExternalIdCommandStrategy");
+ commandStrategies.put(CommandContext.resource("v1\\/journalentries" + OPTIONAL_COMMAND_PARAM_REGEX).method(POST).build(),
+ "createJournalEntryCommandStrategy");
}
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index ff96583fb54..895f5e394ce 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -2881,6 +2881,24 @@ public CommandWrapperBuilder settleCashFromCashier(final Long tellerId, final Lo
return this;
}
+ public CommandWrapperBuilder openCashierSession(final Long tellerId, final Long cashierId) {
+ this.actionName = "OPENCASHIERSESSION";
+ this.entityName = "CASHIERSESSION";
+ this.entityId = tellerId;
+ this.subentityId = cashierId;
+ this.href = "/tellers/" + tellerId + "/cashiers/" + cashierId + "/sessions";
+ return this;
+ }
+
+ public CommandWrapperBuilder closeCashierSession(final Long tellerId, final Long cashierId, final Long sessionId) {
+ this.actionName = "CLOSECASHIERSESSION";
+ this.entityName = "CASHIERSESSION";
+ this.entityId = sessionId;
+ this.subentityId = cashierId;
+ this.href = "/tellers/" + tellerId + "/cashiers/" + cashierId + "/sessions/" + sessionId + "/close";
+ return this;
+ }
+
public CommandWrapperBuilder deleteRole(Long roleId) {
this.actionName = "DELETE";
this.entityName = "ROLE";
diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/search/data/SearchData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/search/data/SearchData.java
index 0438156f78b..34706c6081f 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/search/data/SearchData.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/search/data/SearchData.java
@@ -35,10 +35,11 @@ public class SearchData {
private final EnumOptionData entityStatus;
private final String parentType;
private final String subEntityType;
+ private final Long productId;
public SearchData(final Long entityId, final String entityAccountNo, final String entityExternalId, final String entityName,
final String entityType, final Long parentId, final String parentName, final String parentType, final String entityMobileNo,
- final EnumOptionData entityStatus, final String subEntityType) {
+ final EnumOptionData entityStatus, final String subEntityType, final Long productId) {
this.entityId = entityId;
this.entityAccountNo = entityAccountNo;
@@ -51,6 +52,7 @@ public SearchData(final Long entityId, final String entityAccountNo, final Strin
this.entityMobileNo = entityMobileNo;
this.entityStatus = entityStatus;
this.subEntityType = subEntityType;
+ this.productId = productId;
}
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 168ab04ab32..8706694bb5e 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -155,6 +155,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom
+ * For cash payment types ({@code isCashPayment == true}):
+ *
+ *
If the current user has one or more open cashier sessions for the given office on the transaction date (i.e.
+ * the cashier is active at one or more teller windows), the {@code CASH_AT_TELLER} (11140) financial-activity GL
+ * account is returned.
+ *
Otherwise, the {@code CASH_AT_MAINVAULT} (11130) financial-activity GL account is returned as fallback.
+ *
+ *
+ * Returns an empty {@link Optional} when {@code paymentTypeId} is {@code null} or the payment type is not a cash
+ * payment, signalling callers to fall back to the product-specific fund-source mapping.
+ *
+ * @param paymentTypeId
+ * the payment type id; may be {@code null}
+ * @param officeId
+ * the office for which to look up the cashier session
+ * @param transactionDate
+ * the transaction date
+ * @return an {@link Optional} containing the resolved {@link GLAccount} (always present for cash payment types,
+ * empty for non-cash or null payment types)
+ */
+ public Optional resolveCashGLAccount(final Long paymentTypeId, final Long officeId, final LocalDate transactionDate) {
+ if (paymentTypeId == null) {
+ return Optional.empty();
+ }
+ final PaymentType paymentType = paymentTypeRepositoryWrapper.findOneWithNotFoundDetection(paymentTypeId);
+ if (paymentType.getIsCashPayment() == null || !paymentType.getIsCashPayment()) {
+ return Optional.empty();
+ }
+ final Long currentUserId = securityContext.authenticatedUser().getId();
+ final List activeSessions = cashierSessionRepository.findOpenSessionByUser(currentUserId, officeId,
+ transactionDate);
+ final int financialActivityId = !activeSessions.isEmpty() ? FinancialActivity.CASH_AT_TELLER.getValue()
+ : FinancialActivity.CASH_AT_MAINVAULT.getValue();
+ return Optional.of(financialActivityAccountRepository.findByFinancialActivityTypeWithNotFoundDetection(financialActivityId)
+ .getGlAccount());
+ }
+
public BigDecimal createCreditJournalEntryOrReversalForClientPayments(final Office office, final String currencyCode,
final Long clientId, final Long transactionId, final LocalDate transactionDate, final Boolean isReversal,
final List clientChargePaymentDTOs) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
index 6081ab9e039..081357b22a9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java
@@ -24,6 +24,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
@@ -476,8 +477,15 @@ private void createJournalEntriesForDisbursements(final LoanDTO loanDTO, final L
this.helper.createCreditJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount());
} else {
- this.helper.createCreditJournalEntryForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId,
- paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount());
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId, loanTransactionDTO.getOfficeId(),
+ transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ loanTransactionDTO.getAmount(), cashGLAccount.get());
+ } else {
+ this.helper.createCreditJournalEntryForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId,
+ paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount());
+ }
}
}
@@ -816,8 +824,15 @@ private void createJournalEntriesForLoanRepayments(LoanDTO loanDTO, LoanTransact
}
} else {
- this.helper.createDebitJournalEntryForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId,
- paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId,
+ loanTransactionDTO.getOfficeId(), transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ totalDebitAmount, cashGLAccount.get());
+ } else {
+ this.helper.createDebitJournalEntryForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(),
+ loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
+ }
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java
index 6b5f7f52e40..a70bba887d5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForSavings.java
@@ -21,10 +21,12 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
+import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.SavingsDTO;
import org.apache.fineract.accounting.journalentry.data.SavingsTransactionDTO;
@@ -69,15 +71,32 @@ public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) {
amount.subtract(overdraftAmount), isReversal);
}
} else {
- this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
- CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId, transactionId,
- transactionDate, overdraftAmount, isReversal);
- if (isPositive) {
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId,
+ savingsTransactionDTO.getOfficeId(), transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createCashBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ this.helper.createCashBasedCreditJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ if (isPositive) {
+ this.helper.createCashBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal);
+ this.helper.createCashBasedCreditJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal);
+ }
+ } else {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
- savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate,
- amount.subtract(overdraftAmount), isReversal);
+ CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(),
+ CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ if (isPositive) {
+ this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
+ savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate,
+ amount.subtract(overdraftAmount), isReversal);
+ }
}
}
} else if (savingsTransactionDTO.getTransactionType().isDeposit() && savingsTransactionDTO.isOverdraftTransaction()) {
@@ -93,15 +112,32 @@ public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) {
amount.subtract(overdraftAmount), isReversal);
}
} else {
- this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
- CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
- transactionId, transactionDate, overdraftAmount, isReversal);
- if (isPositive) {
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId,
+ savingsTransactionDTO.getOfficeId(), transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createCashBasedDebitJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ this.helper.createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ if (isPositive) {
+ this.helper.createCashBasedDebitJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal);
+ this.helper.createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, amount.subtract(overdraftAmount), isReversal);
+ }
+ } else {
this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
- savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate,
- amount.subtract(overdraftAmount), isReversal);
+ CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
+ CashAccountsForSavings.OVERDRAFT_PORTFOLIO_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, overdraftAmount, isReversal);
+ if (isPositive) {
+ this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
+ savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate,
+ amount.subtract(overdraftAmount), isReversal);
+ }
}
}
}
@@ -113,9 +149,19 @@ else if (savingsTransactionDTO.getTransactionType().isDeposit()) {
FinancialActivity.LIABILITY_TRANSFER.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
} else {
- this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
- savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId,
+ savingsTransactionDTO.getOfficeId(), transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createCashBasedDebitJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, amount, isReversal);
+ this.helper.createCashBasedCreditJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, amount, isReversal);
+ } else {
+ this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_REFERENCE.getValue(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
+ savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
+ }
}
}
@@ -132,9 +178,19 @@ else if (savingsTransactionDTO.getTransactionType().isWithdrawal()) {
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
} else {
- this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
- CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
- savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
+ final Optional cashGLAccount = this.helper.resolveCashGLAccount(paymentTypeId,
+ savingsTransactionDTO.getOfficeId(), transactionDate);
+ if (cashGLAccount.isPresent()) {
+ this.helper.createCashBasedDebitJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), savingsProductId, paymentTypeId, savingsId,
+ transactionId, transactionDate, amount, isReversal);
+ this.helper.createCashBasedCreditJournalEntryForSavings(office, currencyCode, cashGLAccount.get(), savingsId,
+ transactionId, transactionDate, amount, isReversal);
+ } else {
+ this.helper.createCashBasedJournalEntriesAndReversalsForSavings(office, currencyCode,
+ CashAccountsForSavings.SAVINGS_CONTROL.getValue(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue(),
+ savingsProductId, paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal);
+ }
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
index dad040d9f2f..4efe8496ef8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java
@@ -337,13 +337,19 @@ public CommandProcessingResult revertJournalEntry(final JsonCommand command) {
// is the transaction Id valid
final List journalEntries = this.glJournalEntryRepository
.findUnReversedManualJournalEntriesByTransactionId(command.getTransactionId());
- String reversalComment = command.stringValueOfParameterNamed("comments");
if (journalEntries.size() <= 1) {
throw new JournalEntriesNotFoundException(command.getTransactionId());
}
- final String reversalTransactionId = revertJournalEntry(journalEntries, reversalComment);
- return new CommandProcessingResultBuilder().withTransactionId(reversalTransactionId).build();
+
+ // For manual journal entries, simply mark all entries in the transaction as reversed.
+ // No new offsetting entries should be created — this is not a transaction adjustment.
+ for (final JournalEntry journalEntry : journalEntries) {
+ journalEntry.setReversed(true);
+ helper.persistJournalEntry(journalEntry);
+ }
+
+ return new CommandProcessingResultBuilder().withTransactionId(command.getTransactionId()).build();
}
@Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
index 57c1a1f2a0f..825273a2eae 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java
@@ -47,11 +47,13 @@
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
+import org.apache.fineract.organisation.teller.domain.CashierSessionRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
+import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepositoryWrapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -67,10 +69,11 @@ public AccountingProcessorHelper accountingProcessorHelper(JournalEntryRepositor
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository, GLClosureRepository closureRepository,
GLAccountRepository glAccountRepository, OfficeRepository officeRepository,
AccountTransfersReadPlatformService accountTransfersReadPlatformService, ChargeRepositoryWrapper chargeRepositoryWrapper,
- BusinessEventNotifierService businessEventNotifierService) {
+ BusinessEventNotifierService businessEventNotifierService, PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper,
+ CashierSessionRepository cashierSessionRepository, PlatformSecurityContext securityContext) {
return new AccountingProcessorHelper(glJournalEntryRepository, accountMappingRepository, financialActivityAccountRepository,
closureRepository, glAccountRepository, officeRepository, accountTransfersReadPlatformService, chargeRepositoryWrapper,
- businessEventNotifierService);
+ businessEventNotifierService, paymentTypeRepositoryWrapper, cashierSessionRepository, securityContext);
}
@Bean
diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateJournalEntryCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateJournalEntryCommandStrategy.java
new file mode 100644
index 00000000000..ea6c790f0d7
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/CreateJournalEntryCommandStrategy.java
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.batch.command.internal;
+
+import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
+
+import com.google.common.base.Splitter;
+import jakarta.ws.rs.core.UriInfo;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.accounting.journalentry.api.JournalEntriesApiResource;
+import org.apache.fineract.batch.command.CommandStrategy;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+/**
+ * Implements {@link org.apache.fineract.batch.command.CommandStrategy} to handle creation of journal entries. It
+ * passes the contents of the body from the BatchRequest to
+ * {@link org.apache.fineract.accounting.journalentry.api.JournalEntriesApiResource} and gets back the response. This
+ * class will also catch any errors raised by {@link org.apache.fineract.accounting.journalentry.api.JournalEntriesApiResource}
+ * and map those errors to appropriate status codes in BatchResponse.
+ *
+ * @see org.apache.fineract.batch.command.CommandStrategy
+ * @see org.apache.fineract.batch.domain.BatchRequest
+ * @see org.apache.fineract.batch.domain.BatchResponse
+ */
+@Component
+@RequiredArgsConstructor
+public class CreateJournalEntryCommandStrategy implements CommandStrategy {
+
+ private final JournalEntriesApiResource journalEntriesApiResource;
+
+ @Override
+ public BatchResponse execute(final BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) {
+
+ final BatchResponse response = new BatchResponse();
+ final String responseBody;
+
+ response.setRequestId(request.getRequestId());
+ response.setHeaders(request.getHeaders());
+
+ // Extract command parameter if present
+ String commandParam = null;
+ final List pathParameters = Splitter.on('/').splitToList(relativeUrlWithoutVersion(request));
+
+ // Check if there's a query parameter with command
+ if (pathParameters.size() > 1) {
+ final String lastPart = pathParameters.get(pathParameters.size() - 1);
+ final Pattern commandPattern = Pattern.compile("\\?command=([\\w\\-]+)");
+ final Matcher commandMatcher = commandPattern.matcher(lastPart);
+
+ if (commandMatcher.find()) {
+ commandParam = commandMatcher.group(1);
+ }
+ }
+
+ // Calls 'createGLJournalEntry' function from 'JournalEntriesApiResource' to create a new journal entry
+ responseBody = journalEntriesApiResource.createGLJournalEntry(request.getBody(), commandParam);
+
+ response.setStatusCode(HttpStatus.SC_OK);
+ // Sets the body of the response after the successful creation of the journal entry
+ response.setBody(responseBody);
+
+ return response;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformServiceImpl.java
new file mode 100644
index 00000000000..8a13499156f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionReadPlatformServiceImpl.java
@@ -0,0 +1,160 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.service;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
+import org.apache.fineract.organisation.teller.data.CashierSessionData;
+import org.apache.fineract.organisation.teller.data.CashierSessionSummaryData;
+import org.apache.fineract.organisation.teller.domain.CashierSessionStatus;
+import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
+import org.apache.fineract.organisation.teller.domain.CashierTxnType;
+import org.apache.fineract.organisation.teller.exception.CashierSessionNotFoundException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+
+@RequiredArgsConstructor
+public class CashierSessionReadPlatformServiceImpl implements CashierSessionReadPlatformService {
+
+ private final JdbcTemplate jdbcTemplate;
+ private final CashierTransactionRepository cashierTransactionRepository;
+
+ private static final class CashierSessionMapper implements RowMapper {
+
+ public String schema() {
+ return "cs.id as id, cs.cashier_id as cashier_id, cs.teller_id as teller_id, "
+ + "cs.user_id as user_id, cs.office_id as office_id, cs.session_date as session_date, "
+ + "cs.opened_at as opened_at, cs.closed_at as closed_at, "
+ + "cs.opening_allocation as opening_allocation, cs.total_settled as total_settled, "
+ + "cs.status as status, cs.opening_txn_id as opening_txn_id, "
+ + "cs.closing_txn_id as closing_txn_id, cs.currency_code as currency_code "
+ + "from m_cashier_sessions cs ";
+ }
+
+ @Override
+ public CashierSessionData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
+ final Long id = rs.getLong("id");
+ final Long cashierId = rs.getLong("cashier_id");
+ final Long tellerId = rs.getLong("teller_id");
+ final Long userId = rs.getLong("user_id");
+ final Long officeId = rs.getLong("office_id");
+ final LocalDate sessionDate = JdbcSupport.getLocalDate(rs, "session_date");
+
+ final Timestamp openedAtTs = rs.getTimestamp("opened_at");
+ final LocalDateTime openedAt = openedAtTs != null ? openedAtTs.toLocalDateTime() : null;
+
+ final Timestamp closedAtTs = rs.getTimestamp("closed_at");
+ final LocalDateTime closedAt = closedAtTs != null ? closedAtTs.toLocalDateTime() : null;
+
+ final BigDecimal openingAllocation = rs.getBigDecimal("opening_allocation");
+ final BigDecimal totalSettled = rs.getBigDecimal("total_settled");
+ final String statusStr = rs.getString("status");
+ final CashierSessionStatus status = statusStr != null ? CashierSessionStatus.valueOf(statusStr) : null;
+ final Long openingTxnId = rs.getLong("opening_txn_id");
+ final Long closingTxnId = rs.getLong("closing_txn_id");
+ final String currencyCode = rs.getString("currency_code");
+
+ return new CashierSessionData(id, cashierId, tellerId, userId, officeId, sessionDate, openedAt, closedAt, openingAllocation,
+ totalSettled, status, openingTxnId == 0 ? null : openingTxnId, closingTxnId == 0 ? null : closingTxnId, currencyCode);
+ }
+ }
+
+ @Override
+ public Optional findActiveSession(final Long cashierId, final Long tellerId) {
+ try {
+ final CashierSessionMapper mapper = new CashierSessionMapper();
+ final String sql = "select " + mapper.schema()
+ + " where cs.cashier_id = ? and cs.teller_id = ? and cs.status = 'OPEN' and cs.session_date = CURRENT_DATE";
+ return Optional.ofNullable(jdbcTemplate.queryForObject(sql, mapper, cashierId, tellerId));
+ } catch (EmptyResultDataAccessException e) {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public List findAllSessions(final Long cashierId, final Long tellerId) {
+ final CashierSessionMapper mapper = new CashierSessionMapper();
+ final String sql = "select " + mapper.schema()
+ + " where cs.cashier_id = ? and cs.teller_id = ? order by cs.session_date desc";
+ return jdbcTemplate.query(sql, mapper, cashierId, tellerId);
+ }
+
+ @Override
+ public CashierSessionData findSessionById(final Long sessionId) {
+ try {
+ final CashierSessionMapper mapper = new CashierSessionMapper();
+ final String sql = "select " + mapper.schema() + " where cs.id = ?";
+ return jdbcTemplate.queryForObject(sql, mapper, sessionId);
+ } catch (EmptyResultDataAccessException e) {
+ throw new CashierSessionNotFoundException(sessionId);
+ }
+ }
+
+ @Override
+ public CashierSessionSummaryData getSessionSummary(final Long sessionId) {
+ final CashierSessionData session = findSessionById(sessionId);
+
+ // Cash-in: ALLOCATE (101) — cash allocated to cashier, SETTLE (102) — cash settled from client,
+ // INWARD_CASH_TXN (103) — direct cash-in from savings/loan/client transactions.
+ // Cash-out: OUTWARD_CASH_TXN (104) — cash paid out for withdrawals/disbursements.
+ final BigDecimal totalCashIn = cashierTransactionRepository.sumAmountBySessionAndTxnTypes(sessionId,
+ List.of(CashierTxnType.ALLOCATE.getId(), CashierTxnType.SETTLE.getId(), CashierTxnType.INWARD_CASH_TXN.getId()));
+ final BigDecimal totalCashOut = cashierTransactionRepository.sumAmountBySessionAndTxnTypes(sessionId,
+ List.of(CashierTxnType.OUTWARD_CASH_TXN.getId()));
+
+ final BigDecimal openingAllocation = session.getOpeningAllocation() != null ? session.getOpeningAllocation() : BigDecimal.ZERO;
+ final BigDecimal safeTotalCashIn = totalCashIn != null ? totalCashIn : BigDecimal.ZERO;
+ final BigDecimal safeTotalCashOut = totalCashOut != null ? totalCashOut : BigDecimal.ZERO;
+
+ final BigDecimal expectedCash = openingAllocation.add(safeTotalCashIn).subtract(safeTotalCashOut);
+ final BigDecimal settledAmount = session.getTotalSettled() != null ? session.getTotalSettled() : BigDecimal.ZERO;
+ final BigDecimal variance = settledAmount.subtract(expectedCash);
+
+ return new CashierSessionSummaryData(session, openingAllocation, safeTotalCashIn, safeTotalCashOut, expectedCash, settledAmount,
+ variance);
+ }
+
+ @Override
+ public Optional findActiveSessionForUser(final Long userId, final Long officeId) {
+ try {
+ final CashierSessionMapper mapper = new CashierSessionMapper();
+ final String sql = "select " + mapper.schema()
+ + " where cs.user_id = ? and cs.office_id = ? and cs.status = 'OPEN' and cs.session_date = CURRENT_DATE";
+ return Optional.ofNullable(jdbcTemplate.queryForObject(sql, mapper, userId, officeId));
+ } catch (EmptyResultDataAccessException e) {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public List findOpenSessionsByOffice(final Long officeId) {
+ final CashierSessionMapper mapper = new CashierSessionMapper();
+ final String sql = "select " + mapper.schema() + " where cs.office_id = ? and cs.status = 'OPEN'";
+ return jdbcTemplate.query(sql, mapper, officeId);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformServiceImpl.java
new file mode 100644
index 00000000000..9eb1ccdb599
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/CashierSessionWritePlatformServiceImpl.java
@@ -0,0 +1,215 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.organisation.teller.service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
+import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
+import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
+import org.apache.fineract.accounting.glaccount.domain.GLAccount;
+import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
+import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
+import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
+import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.organisation.office.domain.Office;
+import org.apache.fineract.organisation.teller.domain.Cashier;
+import org.apache.fineract.organisation.teller.domain.CashierRepository;
+import org.apache.fineract.organisation.teller.domain.CashierSession;
+import org.apache.fineract.organisation.teller.domain.CashierSessionRepository;
+import org.apache.fineract.organisation.teller.domain.CashierSessionStatus;
+import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository;
+import org.apache.fineract.organisation.teller.domain.Teller;
+import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
+import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
+import org.apache.fineract.organisation.teller.exception.CashierSessionAlreadyOpenException;
+import org.apache.fineract.organisation.teller.exception.CashierSessionNotFoundException;
+import org.apache.fineract.organisation.teller.exception.CashierSessionUnsettledPriorDayException;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.transaction.annotation.Transactional;
+
+@RequiredArgsConstructor
+public class CashierSessionWritePlatformServiceImpl implements CashierSessionWritePlatformService {
+
+ private static final String GL_CODE_CASH_SHORTAGE_TELLER = "53920";
+ private static final String GL_CODE_MISCELLANEOUS_INCOME = "43210";
+
+ private final PlatformSecurityContext context;
+ private final CashierSessionRepository cashierSessionRepository;
+ private final CashierRepository cashierRepository;
+ private final TellerRepositoryWrapper tellerRepositoryWrapper;
+ private final CashierTransactionRepository cashierTransactionRepository;
+ private final JournalEntryRepository glJournalEntryRepository;
+ private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper;
+ private final GLAccountRepositoryWrapper glAccountRepositoryWrapper;
+
+ @Override
+ @Transactional
+ public CommandProcessingResult openSession(final Long tellerId, final Long cashierId, final String currencyCode) {
+ final AppUser currentUser = context.authenticatedUser();
+
+ final Cashier cashier = cashierRepository.findById(cashierId)
+ .orElseThrow(() -> new CashierNotFoundException(cashierId));
+ final Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId);
+
+ final LocalDate today = DateUtils.getBusinessLocalDate();
+
+ // Business rule: one OPEN session per cashier per teller per day
+ cashierSessionRepository.findOpenSession(cashierId, tellerId, today).ifPresent(s -> {
+ throw new CashierSessionAlreadyOpenException(cashierId, tellerId);
+ });
+
+ // Business rule: cannot open if prior day has unsettled session
+ final List unsettled = cashierSessionRepository.findUnsettledPriorSessions(currentUser.getId(), today);
+ if (!unsettled.isEmpty()) {
+ throw new CashierSessionUnsettledPriorDayException();
+ }
+
+ final LocalDateTime now = LocalDateTime.now();
+ final CashierSession session = new CashierSession()
+ .setCashier(cashier)
+ .setTeller(teller)
+ .setUserId(currentUser.getId())
+ .setOffice(teller.getOffice())
+ .setSessionDate(today)
+ .setOpenedAt(now)
+ .setStatus(CashierSessionStatus.OPEN)
+ .setCurrencyCode(currencyCode != null ? currencyCode : "")
+ .setCreatedBy(currentUser.getId())
+ .setCreatedDate(now);
+
+ cashierSessionRepository.save(session);
+
+ return new CommandProcessingResultBuilder()
+ .withEntityId(session.getId())
+ .withOfficeId(teller.getOffice().getId())
+ .build();
+ }
+
+ @Override
+ @Transactional
+ public CommandProcessingResult closeSession(final Long sessionId) {
+ return closeSession(sessionId, null, null);
+ }
+
+ @Override
+ @Transactional
+ public CommandProcessingResult closeSession(final Long sessionId, final BigDecimal settledAmount, final String supervisorNote) {
+ context.authenticatedUser();
+
+ final CashierSession session = cashierSessionRepository.findById(sessionId)
+ .orElseThrow(() -> new CashierSessionNotFoundException(sessionId));
+
+ final BigDecimal resolvedSettledAmount = settledAmount != null ? settledAmount : BigDecimal.ZERO;
+
+ // Compute expected cash: openingAllocation + sumCashIn - sumCashOut
+ // Cash-in types: 1 (legacy direct cash-in), 101 (ALLOCATE), 102 (SETTLE from client)
+ // Cash-out types: 2 (legacy direct cash-out), 201 (legacy savings cash-out), 202 (legacy loan cash-out)
+ final BigDecimal sumCashIn = cashierTransactionRepository.sumAmountBySessionAndTxnTypes(
+ sessionId, List.of(1, 101, 102));
+ final BigDecimal sumCashOut = cashierTransactionRepository.sumAmountBySessionAndTxnTypes(
+ sessionId, List.of(2, 201, 202));
+
+ final BigDecimal openingAllocation = session.getOpeningAllocation() != null ? session.getOpeningAllocation() : BigDecimal.ZERO;
+ final BigDecimal safeCashIn = sumCashIn != null ? sumCashIn : BigDecimal.ZERO;
+ final BigDecimal safeCashOut = sumCashOut != null ? sumCashOut : BigDecimal.ZERO;
+
+ final BigDecimal expectedCash = openingAllocation.add(safeCashIn).subtract(safeCashOut);
+ final BigDecimal variance = resolvedSettledAmount.subtract(expectedCash);
+ final boolean hasVariance = variance.compareTo(BigDecimal.ZERO) != 0;
+
+ // Validate: supervisor note required when variance != 0
+ if (hasVariance && (supervisorNote == null || supervisorNote.isBlank())) {
+ throw new PlatformApiDataValidationException(
+ "validation.msg.cashierSession.supervisorNote.required",
+ "A supervisor note is required when a variance exists between settled amount and expected cash.",
+ "supervisorNote");
+ }
+
+ // Post GL variance journal entry if needed
+ if (hasVariance) {
+ postVarianceJournalEntry(session, variance, supervisorNote);
+ }
+
+ // Update session fields
+ session.setTotalSettled(resolvedSettledAmount);
+ if (hasVariance) {
+ session.setSupervisorNote(supervisorNote);
+ }
+ session.setStatus(CashierSessionStatus.SETTLED);
+ session.setClosedAt(LocalDateTime.now());
+
+ cashierSessionRepository.save(session);
+
+ return new CommandProcessingResultBuilder()
+ .withEntityId(sessionId)
+ .build();
+ }
+
+ private void postVarianceJournalEntry(final CashierSession session, final BigDecimal variance, final String note) {
+
+ final FinancialActivityAccount tellerCashAccount = financialActivityAccountRepositoryWrapper
+ .findByFinancialActivityTypeWithNotFoundDetection(FinancialActivity.CASH_AT_TELLER.getValue());
+ final GLAccount tellerCashGlAccount = tellerCashAccount.getGlAccount();
+
+ final Office office = session.getOffice();
+ final String currencyCode = session.getCurrencyCode();
+ final LocalDate entryDate = session.getSessionDate();
+
+ final String transactionId = java.util.UUID.randomUUID().toString().replace("-", "");
+
+ final String description = note != null ? note : "Session variance adjustment";
+
+ final GLAccount debitAccount;
+ final GLAccount creditAccount;
+
+ if (variance.compareTo(BigDecimal.ZERO) < 0) {
+ // Short settlement: cashier returned less than expected
+ // DEBIT Cash Shortage - Teller (53920) / CREDIT Teller Cash (11140)
+ debitAccount = glAccountRepositoryWrapper.findOneByGlCodeWithNotFoundDetection(GL_CODE_CASH_SHORTAGE_TELLER);
+ creditAccount = tellerCashGlAccount;
+ } else {
+ // Over settlement: cashier returned more than expected
+ // DEBIT Teller Cash (11140) / CREDIT Miscellaneous Income (43210)
+ debitAccount = tellerCashGlAccount;
+ creditAccount = glAccountRepositoryWrapper.findOneByGlCodeWithNotFoundDetection(GL_CODE_MISCELLANEOUS_INCOME);
+ }
+
+ final BigDecimal absVariance = variance.abs();
+
+ final JournalEntry debitEntry = JournalEntry.createNew(office, null, debitAccount, currencyCode,
+ transactionId, false, entryDate, JournalEntryType.DEBIT, absVariance, description,
+ null, null, null, null, null, null, null);
+
+ final JournalEntry creditEntry = JournalEntry.createNew(office, null, creditAccount, currencyCode,
+ transactionId, false, entryDate, JournalEntryType.CREDIT, absVariance, description,
+ null, null, null, null, null, null, null);
+
+ glJournalEntryRepository.saveAndFlush(debitEntry);
+ glJournalEntryRepository.saveAndFlush(creditEntry);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java
index aa694751a0a..db6e5e71fb4 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java
@@ -23,10 +23,13 @@
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
@@ -268,16 +271,68 @@ public CashierTransactionData retrieveCashierTxnTemplate(Long cashierId) {
@Override
public CashierTransactionsWithSummaryData retrieveCashierTransactionsWithSummary(final Long cashierId, final boolean includeAllTellers,
- final LocalDate fromDate, final LocalDate toDate, final String currencyCode, final SearchParameters searchParameters) {
+ final LocalDate fromDate, final LocalDate toDate, final String currencyCode, final SearchParameters searchParameters,
+ final Long sessionId) {
sqlValidator.validate(searchParameters.getOrderBy());
sqlValidator.validate(searchParameters.getSortOrder());
final String nextDay = sqlGenerator.incrementDateByOneDay("c.end_date");
+ final String fromDateExpr = fromDate != null ? "?" : "c.start_date";
+ final String toDateExprForCashierTxn = toDate != null ? "?" : "c.end_date";
+ final String toDateExprForOtherTxn = toDate != null ? "?" : nextDay;
+
final CashierTransactionSummaryMapper ctsm = new CashierTransactionSummaryMapper();
- final String sql = "SELECT " + ctsm.cashierTxnSummarySchema(nextDay) + " LIMIT 1000";
+ final String sql = "SELECT " + ctsm.cashierTxnSummarySchema(fromDateExpr, toDateExprForCashierTxn, toDateExprForOtherTxn,
+ sessionId != null)
+ + " LIMIT 1000";
+
+ final List