# Cashier Module — Architecture Map

> **Last updated:** 2026-06-11
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Cashier module is the **front-desk payment queue** for membership-related payments. It provides:

- Payment request queue management (view, process, cancel, requeue)
- Session management (open/close cashier shifts)
- Payment request creation (from any module via `PaymentRequestService::createRequest`)
- Payment completion (delegates to `PaymentService::processPayment`)
- Post-payment event orchestration (member activation, dependent activation, life-event processing)
- Settlement receiving (from sub-treasuries)
- Bank deposit management (create, confirm, reject)
- Custody balance tracking

It acts as the **orchestrator bridge** between:
- Modules that CREATE payment obligations (Members, Spouses, Children, Transfers, Death, Divorce, etc.)
- The Payments module that PROCESSES payments
- The Treasury module that manages physical cash handling

The Cashier module does **NOT**:
- Process payments directly (delegates to `PaymentService`)
- Own the payments/receipts tables (owned by Payments module)
- Manage treasury infrastructure (owned by Treasury module)
- Post journal entries (owned by Accounting module)

---

## 2. Directory & File Structure

```
app/Modules/Cashier/
├── bootstrap.php                          # Events + menu + permissions registration
├── Routes.php                             # Web routes (18 routes)
├── Controllers/
│   ├── CashierController.php             # Queue, process, complete, cancel, requeue, session mgmt
│   ├── DepositController.php             # Bank deposit CRUD + confirm/reject
│   └── SettlementReceiveController.php   # Settlement receiving from sub-treasuries
├── Models/
│   └── PaymentRequest.php                # Model (pendingForMember, completedForMember)
├── Services/
│   ├── PaymentRequestService.php         # Core: createRequest, processRequest, cancelRequest, requeueRequest
│   └── DepositService.php                # Bank deposit lifecycle (create, confirm, reject)
└── Views/
    ├── queue.php                          # Payment queue with filters + session controls
    ├── process.php                        # Single payment request processing form
    ├── deposits/
    │   ├── index.php                      # All deposits list
    │   ├── create.php                     # New deposit form
    │   └── show.php                       # Deposit detail + confirm/reject actions
    └── settlements/
        └── index.php                      # Pending settlements from sub-treasuries
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `payment_requests` Table (103 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| request_number | varchar(50) | NO | UNI | Format: PRQ-YYYY-NNNNNN |
| member_id | bigint unsigned | YES | MUL | FK to members (null for non-member payments) |
| payment_type | varchar(50) | NO | MUL | Same types as payments table |
| amount | decimal(15,2) | NO | | |
| currency | varchar(3) | NO | | Default: 'EGP' |
| description_ar | text | YES | | Arabic description |
| related_entity_type | varchar(100) | YES | MUL | spouses/children/temporary_members/etc. |
| related_entity_id | bigint unsigned | YES | | FK to related entity |
| status | varchar(30) | NO | MUL | Default: 'pending' |
| payment_id | bigint unsigned | YES | | FK to payments (set on completion) |
| receipt_number | varchar(50) | YES | | Set on completion |
| requested_by | bigint unsigned | NO | MUL | FK to employees (who created request) |
| processed_by | bigint unsigned | YES | | FK to employees (cashier who collected) |
| processed_at | timestamp | YES | | When payment was collected |
| branch_id | bigint unsigned | YES | MUL | FK to branches |
| notes | text | YES | | JSON or text (installment_months for down_payment) |
| is_voided | tinyint(1) | NO | | Default: 0 |
| voided_at | timestamp | YES | | |
| voided_by | bigint unsigned | YES | | |
| void_reason | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

**Status lifecycle:**
```
pending → processing → completed
pending → cancelled
cancelled → pending (requeue)
completed → cancelled (with payment void)
```

**Current data distribution (status):**
- completed: 58
- pending: 30
- cancelled: 8
- voided: 7

### 3.2 `treasury_deposits` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| deposit_number | varchar(30) | NO | UNI | Format: DEP-YYYY-NNNNNN |
| treasury_id | bigint unsigned | NO | MUL | FK to treasuries (always main treasury) |
| bank_account_id | bigint unsigned | YES | | FK to bank_accounts |
| amount | decimal(14,2) | NO | | |
| deposit_date | date | NO | MUL | |
| bank_receipt_serial | varchar(100) | YES | | Bank's own receipt number |
| bank_receipt_path | varchar(255) | YES | | Uploaded bank receipt scan |
| deposited_by | bigint unsigned | NO | | FK to employees |
| deposited_at | timestamp | NO | | |
| confirmed_by | bigint unsigned | YES | | FK to employees (accounting mgr) |
| confirmed_at | timestamp | YES | | |
| status | enum | NO | MUL | pending_deposit/deposited/confirmed/rejected |
| rejection_reason | text | YES | | |
| journal_entry_id | bigint unsigned | YES | | FK to journal_entries (set on confirm) |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `treasury_settlements` Table (related — owned by Treasury module)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| settlement_number | varchar(30) | NO | UNI | Format: STL-YYYY-NNNNNN |
| from_treasury_id | bigint unsigned | NO | MUL | Source sub-treasury |
| to_treasury_id | bigint unsigned | NO | MUL | Main treasury |
| session_id | bigint unsigned | NO | MUL | Source session |
| amount | decimal(14,2) | NO | | |
| settled_by | bigint unsigned | NO | | Employee from sub-treasury |
| received_by | bigint unsigned | YES | | Main treasury cashier |
| settled_at | timestamp | NO | | |
| received_at | timestamp | YES | | |
| status | enum | NO | MUL | pending/received/rejected |
| rejection_reason | text | YES | | |
| receipt_count | int unsigned | NO | | Number of receipts in settlement |
| journal_entry_id | bigint unsigned | YES | | FK to journal_entries |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

---

## 4. Payment Request Lifecycle

### 4.1 Duplex Prevention
Before creating a request, the system checks for existing pending/processing/completed requests with the same `(member_id, payment_type, related_entity_type, related_entity_id)` combination. Prevents double-charging.

### 4.2 Atomic Lock Pattern
When a cashier starts processing, the request status is immediately set to `processing` to prevent concurrent double-execution:
```sql
UPDATE payment_requests SET status = 'processing' WHERE id = ? AND status = 'pending'
```

### 4.3 Cancellation with Payment Void
If a completed request is cancelled, its linked payment is voided first (cascade). This triggers `payment.voided` which cascades to membership deactivation if applicable.

### 4.4 Requeue
Cancelled (non-voided) requests can be returned to `pending` status. The system re-checks for duplicates before allowing requeue.

---

## 5. Core Business Flows

### 5.1 Standard Payment Queue Flow

```
1. Module creates payment request:
   PaymentRequestService::createRequest([
     member_id, payment_type, amount, description_ar,
     related_entity_type, related_entity_id, notes
   ])
   → Validates, generates PRQ-YYYY-NNNNNN number
   → Inserts with status='pending'
   → Dispatches: payment_request.created

2. Cashier views queue:
   GET /cashier → CashierController@queue
   → Lists pending/processing requests
   → Shows session status + custody balance

3. Cashier processes request:
   GET /cashier/{id} → shows details + payment method selection
   POST /cashier/{id}/complete
   → If treasury system: TreasuryService::collectPayment()
   → If no treasury: PaymentRequestService::processRequest()
     → Sets status='processing' (atomic lock)
     → Calls PaymentService::processPayment()
     → Sets status='completed', links payment_id
     → Dispatches: payment_request.completed

4. Post-completion (Cashier/bootstrap.php listens to payment_request.completed):
   → membership_fee/down_payment/foreign_membership_fee/sports_membership_fee: PaymentLifecycleService → activate member
   → addition_fee: PaymentLifecycleService → activate dependent
   → seasonal_fee: Activate seasonal membership + activate seasonal-type members
   → divorce/death/waiver/separation fee: Update case status to 'fee_paid'
```

### 5.2 Bank Deposit Flow

```
1. Main treasury cashier creates deposit:
   POST /cashier/deposits → DepositService::createDeposit()
   → Validates amount, gets main treasury
   → Generates DEP-YYYY-NNNNNN
   → Records custody deduction
   → Status: 'deposited'
   → Dispatches: treasury.deposit.created

2. Accounting manager confirms:
   POST /cashier/deposits/{id}/confirm → DepositService::confirmDeposit()
   → Status: 'confirmed'
   → Records confirmed custody
   → Dispatches: treasury.deposit.confirmed
     → Accounting auto-posts: Dr. Bank, Cr. Main Cash

3. If rejected:
   POST /cashier/deposits/{id}/reject → DepositService::rejectDeposit()
   → Status: 'rejected'
   → Reverses custody deduction (amount goes back to manager)
   → Dispatches: treasury.deposit.rejected
```

### 5.3 Settlement Receiving Flow

```
1. Sub-treasury creates settlement (Treasury module)
2. Main treasury receives:
   POST /cashier/settlements/{id}/receive
   → SettlementService::receiveSettlement()
   → Status: 'received', received_by set
   → Dispatches: treasury.settlement.received
     → Accounting auto-posts: Dr. Main Cash, Cr. Sub-Treasury Cash
3. If rejected:
   POST /cashier/settlements/{id}/reject
   → Settlement reverted, rejection reason recorded
```

### 5.4 Session Management

```
POST /cashier/session/open → SessionService::openSession()
  → Only one active session per treasury per employee
  → Required before collecting payments (when treasury system is active)

POST /cashier/session/close → SessionService::closeSession()
  → Calculates total collected during session
  → Creates settlement to main treasury if sub-treasury
```

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `payment_request.created` | `{request_id, request_number, member_id, payment_type, amount}` | PaymentRequestService::createRequest |
| `payment_request.completed` | `{request_id, request_number, member_id, payment_type, amount, payment_id, receipt_number, related_entity_type, related_entity_id}` | PaymentRequestService::processRequest |
| `payment_request.cancelled` | `{request_id, member_id, payment_type, related_entity_type, related_entity_id}` | PaymentRequestService::cancelRequest |
| `treasury.deposit.created` | `{deposit_id, treasury_id, amount, deposited_by}` | DepositService::createDeposit |
| `treasury.deposit.confirmed` | `{deposit_id, treasury_id, amount, confirmed_by, bank_account_id}` | DepositService::confirmDeposit |
| `treasury.deposit.rejected` | `{deposit_id, reason, rejected_by}` | DepositService::rejectDeposit |
| `member.activated` | `{member_id, membership_number}` | Cashier/bootstrap.php (via PaymentLifecycleService) |
| `member.pending_cheques` | `{member_id}` | Cashier/bootstrap.php (via PaymentLifecycleService) |
| `spouse.fee_paid` | `{spouse_id, member_id, payment_id}` | Cashier/bootstrap.php |
| `child.fee_paid` | `{child_id, member_id, payment_id}` | Cashier/bootstrap.php |
| `temporary.fee_paid` | `{temp_id, member_id, payment_id}` | Cashier/bootstrap.php |
| `seasonal.fee_paid` | `{seasonal_id, member_id, payment_id}` | Cashier/bootstrap.php |
| `divorce.fee_paid` | `{case_id, payment_id}` | Cashier/bootstrap.php |
| `death.fee_paid` | `{case_id, payment_id}` | Cashier/bootstrap.php |
| `waiver.fee_paid` | `{waiver_id, payment_id}` | Cashier/bootstrap.php |
| `transfer.fee_paid` | `{transfer_id, payment_id}` | Cashier/bootstrap.php |

---

## 7. Events Consumed

| Event | Listener Location | Action |
|-------|-------------------|--------|
| `payment.voided` | Cashier/bootstrap.php | Delegates to PaymentLifecycleService::onPaymentVoided |
| `payment_request.cancelled` | Cashier/bootstrap.php | Delegates to PaymentLifecycleService::onPaymentRequestCancelled |
| `payment_request.completed` | Cashier/bootstrap.php | Master orchestrator for post-payment actions |

---

## 8. Cross-Module Dependencies

### 8.1 Cashier module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Payments | PaymentService (processPayment for completing requests) |
| Payments | PaymentLifecycleService (used in bootstrap listeners) |
| Treasury | TreasuryService (getMembershipTreasury, getQueueForTreasury, collectPayment) |
| Treasury | SessionService (open/close/current session) |
| Treasury | CustodyService (getCurrentBalance, recordDeposit, recordDepositConfirmed) |
| Treasury | SettlementService (receive/reject settlements) |

### 8.2 Other modules that IMPORT FROM Cashier:

| Module | What it imports |
|--------|----------------|
| Members | PaymentRequestService (create form_fee, membership_fee, addition_fee requests) |
| Children | PaymentRequestService (create addition_fee requests) |
| Spouses | PaymentRequestService (create addition_fee requests) |
| Temporary | PaymentRequestService (create addition_fee requests) |
| Death | PaymentRequestService (create death_fee requests) |
| Divorce | PaymentRequestService (create divorce_fee requests) |
| Transfers | PaymentRequestService (create separation_fee requests) |
| Waiver | PaymentRequestService (create waiver_fee requests) |
| Carnets | PaymentRequestService (create carnet_replacement requests) |
| Seasonal | PaymentRequestService (create seasonal_fee requests) |
| SportsActivity | PaymentRequestService (registration, booking, subscription requests) |
| Treasury | PaymentRequestService (queue integration) |
| Audit | PaymentRequestService (audit trail integration) |

---

## 9. Routes

| Method | Path | Controller@Action | Permission |
|--------|------|-------------------|------------|
| GET | /cashier | CashierController@queue | cashier.view_queue |
| GET | /cashier/{id} | CashierController@process | cashier.process_payment |
| POST | /cashier/{id}/complete | CashierController@complete | cashier.process_payment |
| POST | /cashier/{id}/cancel | CashierController@cancel | cashier.cancel_request |
| POST | /cashier/{id}/requeue | CashierController@requeue | cashier.cancel_request |
| POST | /cashier/session/open | CashierController@openSession | cashier.process_payment |
| POST | /cashier/session/close | CashierController@closeSession | cashier.process_payment |
| GET | /cashier/settlements | SettlementReceiveController@index | cashier.receive_settlement |
| POST | /cashier/settlements/{id}/receive | SettlementReceiveController@receive | cashier.receive_settlement |
| POST | /cashier/settlements/{id}/reject | SettlementReceiveController@reject | cashier.receive_settlement |
| GET | /cashier/deposits | DepositController@index | cashier.manage_deposits |
| GET | /cashier/deposits/create | DepositController@create | cashier.manage_deposits |
| POST | /cashier/deposits | DepositController@store | cashier.manage_deposits |
| GET | /cashier/deposits/{id} | DepositController@show | cashier.manage_deposits |
| POST | /cashier/deposits/{id}/confirm | DepositController@confirm | cashier.confirm_deposit |
| POST | /cashier/deposits/{id}/reject | DepositController@reject | cashier.confirm_deposit |

---

## 10. Permissions

| Key | Description |
|-----|-------------|
| cashier.view_queue | View membership treasury payment queue |
| cashier.process_payment | Collect payment from queue + manage sessions |
| cashier.cancel_request | Cancel or requeue payment requests |
| cashier.receive_settlement | Receive/reject settlements from sub-treasuries |
| cashier.manage_deposits | Create and view bank deposits |
| cashier.confirm_deposit | Confirm/reject bank deposits (accounting manager role) |
| cashier.view_custody | View treasury custody balance |

---

## 11. High-Risk Areas

### 11.1 Cashier/bootstrap.php Event Orchestration (CRITICAL)
- The `payment_request.completed` listener is the **single orchestration point** for all post-payment actions
- Handles 6+ different payment types with different post-completion logic
- If this listener fails silently: payment is recorded but member/dependent never activates
- All logic is wrapped in try/catch — failures are logged but NOT re-thrown

### 11.2 Atomic Lock Race Condition
- The `UPDATE ... SET status = 'processing' WHERE status = 'pending'` prevents double-processing
- However, the check is not done via SELECT FOR UPDATE — there is a tiny window between status check and actual payment
- In practice, the queue is used by one cashier at a time, so this is low-risk

### 11.3 Cancel Completed Request (Cascade Void)
- Cancelling a completed request VOIDS the linked payment
- This triggers `payment.voided` which may DEACTIVATE a member
- If a member has been active for months and their original payment request is cancelled, they get deactivated
- This is intentional but dangerous — should only be used for same-day corrections

### 11.4 Deposit Rejection Custody Reversal
- When a deposit is rejected, the amount goes back to the treasury manager's custody
- If the custody tracking is out of sync (e.g., due to manual override), this can create negative balances

### 11.5 Requeue Without Re-validation
- When a cancelled request is re-queued, the amount is NOT re-validated against current pricing
- If fees have changed since the original request was created, the re-queued request may have a stale amount

---

## 12. Known Patterns & Gotchas

1. **Two paths for payment processing**: If treasury system is configured, payments go through `TreasuryService::collectPayment()`. If not, they go directly through `PaymentRequestService::processRequest()`. Both ultimately call `PaymentService::processPayment()`.
2. **Session required for treasury path**: If the treasury system is active, a cashier must have an open session before collecting. Without a session, the redirect goes back to the queue with an error.
3. **receipt_number stored on request**: After processing, the receipt number is stored on the payment_request row for quick reference — avoids joining through payment_id.
4. **Notes field for installment metadata**: For `down_payment` requests, the `notes` field contains JSON with `installment_months` — used by PaymentLifecycleService to create the plan with the correct duration.
5. **Branch scoping**: Queue can be filtered by branch_id (from current employee's branch) — multi-branch deployment support.
6. **Non-member payments allowed**: Payment types like `sports_registration`, `hourly_booking`, `sa_form_fee` can have null member_id (sports activity registrations for non-members).
7. **Return-to-sender pattern**: The cancel action accepts a `return_to` POST parameter to redirect back to the originating page (e.g., `/members/{id}`).
8. **Print receipt on completion**: The complete action redirects with `?print_receipt={receiptId}` query param — the queue view triggers auto-print dialog.

---

## 13. Sidebar Menu Position

| Menu Item | Route | Permission | Order |
|-----------|-------|------------|-------|
| خزنة العضويات (Membership Treasury) | /cashier | cashier.view_queue | 300 |
| ├ طابور الدفع (Payment Queue) | /cashier | cashier.view_queue | 1 |
| ├ استلام تسويات (Receive Settlements) | /cashier/settlements | cashier.receive_settlement | 2 |
| └ إيداعات البنك (Bank Deposits) | /cashier/deposits | cashier.manage_deposits | 3 |

---

## 14. Relationship to Treasury Module

The Cashier module is the **UI layer** for the membership treasury. The Treasury module provides the **infrastructure**:
- `TreasuryService` — treasury CRUD, queue routing, payment collection
- `SessionService` — shift management (open, close, handover)
- `CustodyService` — per-employee cash balance tracking
- `SettlementService` — inter-treasury cash transfers

When no Treasury module treasuries are configured, Cashier falls back to basic `PaymentRequestService` flow (no sessions, no custody, no settlements).
