# Treasury Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Treasury module manages the **sub-treasury (cashier window) operations** for the club. It handles:
- Payment collection from a queue of payment requests
- Cashier session management (open/close shifts)
- Cash custody tracking per employee (running balance ledger)
- Settlement of collected cash to the main treasury
- Direct payment creation and collection (create-and-collect shortcut)
- Dashboard with real-time statistics (today's collections, pending queue, custody balance)

It does **NOT** directly manage:
- The main treasury (handled by Cashier module)
- Payment creation/logic (delegates to PaymentService)
- Payment requests (created by other modules via Cashier/PaymentRequestService)
- Accounting journal entries (Accounting listens to events)
- Receipt generation (Payments module handles via PaymentService)
- Bank deposits (Cashier/DepositService handles that)

---

## 2. Directory & File Structure

```
app/Modules/Treasury/
├── bootstrap.php                              # Menu + permissions + payment.voided listener
├── Routes.php                                 # 15 web routes
├── Controllers/
│   ├── TreasuryController.php                # Dashboard, queue, process, collect, custody
│   ├── SessionController.php                 # Session CRUD (index, current, open, close)
│   └── SettlementController.php              # Settlement CRUD (index, create, store, show)
├── Models/
│   ├── Treasury.php                          # Treasury model
│   ├── TreasuryCustodyLog.php                # Custody log model
│   ├── TreasuryDeposit.php                   # Deposit model
│   ├── TreasurySession.php                   # Session model
│   └── TreasurySettlement.php                # Settlement model
├── Services/
│   ├── TreasuryService.php                   # Core: collectPayment, queue, dashboard stats
│   ├── SessionService.php                    # Open/close sessions, session history
│   ├── SettlementService.php                 # Initiate/receive/reject settlements
│   └── CustodyService.php                    # Custody balance ledger (collection/settlement/void)
└── Views/
    ├── dashboard.php                         # Treasury dashboard
    ├── queue.php                             # Payment request queue
    ├── process.php                           # Process single payment request
    ├── create_and_collect.php                # Direct payment creation
    ├── sessions/
    │   ├── index.php                         # Session history
    │   └── current.php                       # Current open session detail
    ├── settlements/
    │   ├── index.php                         # Settlement history
    │   ├── create.php                        # New settlement form
    │   └── show.php                          # Settlement detail
    └── custody/
        └── index.php                         # Custody log
```

---

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

### 3.1 `treasuries` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(20) | NO | UNI | 'SUB_SA', 'SUB_MEM', or main |
| name_ar | varchar(100) | NO | | Arabic name |
| name_en | varchar(100) | YES | | English name |
| type | enum('main','sub') | NO | MUL | Main or sub-treasury |
| account_code | varchar(20) | NO | | Accounting account code |
| branch_id | bigint unsigned | YES | MUL | FK to branches |
| is_active | tinyint(1) | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

**Treasury codes:**
- `SUB_SA` — Sub-treasury for Sports Activity payments
- `SUB_MEM` — Sub-treasury for Membership payments
- `type='main'` — Main treasury (receives settlements)

### 3.2 `treasury_sessions` Table (6 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| treasury_id | bigint unsigned | NO | MUL | FK to treasuries |
| cashier_id | bigint unsigned | NO | MUL | FK to employees |
| session_number | varchar(30) | NO | UNI | Format: TSS-YYYY-NNNNNN |
| opened_at | timestamp | NO | | Session start time |
| closed_at | timestamp | YES | | Session end time (null if open) |
| opening_balance | decimal(14,2) | NO | | Default: 0.00 |
| closing_balance | decimal(14,2) | YES | | Calculated on close |
| total_collected | decimal(14,2) | NO | | Running total during session |
| total_receipts | int unsigned | NO | | Receipt count |
| status | enum('open','closed','settled') | NO | MUL | Session lifecycle |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

**Status lifecycle:** open → closed → settled

### 3.3 `treasury_settlements` Table

| 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 | FK to treasuries (sub) |
| to_treasury_id | bigint unsigned | NO | MUL | FK to treasuries (main) |
| session_id | bigint unsigned | NO | MUL | FK to treasury_sessions |
| amount | decimal(14,2) | NO | | Settlement amount |
| settled_by | bigint unsigned | NO | | FK to employees (cashier) |
| received_by | bigint unsigned | YES | | FK to employees (main treasury) |
| settled_at | timestamp | NO | | When cashier initiated |
| received_at | timestamp | YES | | When main treasury received |
| status | enum('pending','received','rejected') | NO | MUL | |
| rejection_reason | text | YES | | |
| receipt_count | int unsigned | NO | | Number of receipts in session |
| journal_entry_id | bigint unsigned | YES | | FK to journal_entries |
| notes | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

**Status lifecycle:** pending → received | rejected

### 3.4 `treasury_custody_log` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| treasury_id | bigint unsigned | NO | MUL | FK to treasuries |
| employee_id | bigint unsigned | NO | MUL | FK to employees |
| action | enum | NO | | collection, settlement_out, settlement_in, deposit, deposit_confirmed, void_reversal |
| amount | decimal(14,2) | NO | | Transaction amount |
| balance_after | decimal(14,2) | NO | | Running balance after this action |
| reference_type | varchar(30) | YES | | 'payment', 'settlement', 'deposit' |
| reference_id | bigint unsigned | YES | | FK to referenced entity |
| description_ar | varchar(255) | YES | | |
| created_at | timestamp | NO | MUL | |

**Action types:**
- `collection` — Cash received from member payment (+)
- `settlement_out` — Cash sent to main treasury (-)
- `settlement_in` — Cash received from sub-treasury (+)
- `deposit` — Cash deposited to bank (-)
- `deposit_confirmed` — Bank deposit confirmed (informational)
- `void_reversal` — Payment voided, custody reversed (-)

---

## 4. Core Business Flows

### 4.1 Cashier Session Lifecycle

```
1. POST /treasury/sessions/open
   - Validates: no open session exists for this treasury
   - Creates session (status='open', opening_balance=0)
   - Generates session number (TSS-YYYY-NNNNNN)
   - Dispatches: treasury.session.opened

2. [Cashier collects payments during shift]
   - total_collected and total_receipts incremented per payment

3. POST /treasury/sessions/{id}/close
   - Recalculates total from payments WHERE session_id=? AND is_voided=0 AND payment_method='cash'
   - Sets closing_balance, status='closed'
   - Dispatches: treasury.session.closed
```

### 4.2 Payment Collection (from queue)

```
1. GET /treasury/queue — shows pending payment_requests
   - Filtered by treasury type:
     - SUB_SA: activity_subscription, hourly_booking, sports_registration, sa_form_fee, sa_subscription, sports_subscription
     - SUB_MEM: form_fee, membership_fee, down_payment, addition_fee, annual_subscription, divorce_fee, death_fee, waiver_fee, separation_fee, fine, carnet_replacement, seasonal_fee

2. GET /treasury/queue/{id} — process form for specific request

3. POST /treasury/queue/{id}/collect
   - TreasuryService::collectPayment(sessionId, requestId, paymentMethod)
   - Marks payment_request as 'processing'
   - Calls PaymentService::processPayment() with treasury_id + session_id
   - On success: marks request 'completed', updates session totals
   - If cash: records CustodyService::recordCollection()
   - Dispatches: payment_request.completed, treasury.payment.collected
```

### 4.3 Create-and-Collect (direct payment)

```
POST /treasury/collect
- Creates a payment_request AND immediately collects it
- Used for ad-hoc payments not queued by other modules
- Validates: member_id, payment_type, amount > 0, payment_method
- TreasuryService::createAndCollectPayment() → PaymentRequestService::createRequest() → collectPayment()
```

### 4.4 Settlement (sub → main treasury)

```
1. GET /treasury/settlements/create — shows closed sessions available for settlement

2. POST /treasury/settlements
   - SettlementService::initiateSettlement(sessionId)
   - Validates: session is 'closed', no existing settlement, amount > 0
   - Creates settlement (status='pending')
   - Updates session status to 'settled'
   - Records CustodyService::recordSettlementOut() (reduces cashier custody)
   - Dispatches: treasury.settlement.initiated

3. [Main treasury receives/rejects via Cashier module]
   - SettlementService::receiveSettlement() → CustodyService::recordSettlementIn()
   - SettlementService::rejectSettlement() → reverses custody, resets session to 'closed'
   - Dispatches: treasury.settlement.received or treasury.settlement.rejected
```

### 4.5 Payment Void Reversal (event-driven)

```
When a payment is voided (payment.voided event):
1. Treasury bootstrap listener fires (priority 40)
2. Checks: payment has treasury_id, payment_method='cash'
3. Calls CustodyService::recordVoidReversal()
4. Reduces cashier's custody balance by voided amount
```

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `treasury.session.opened` | `{session_id, treasury_id, cashier_id}` | SessionService::openSession |
| `treasury.session.closed` | `{session_id, treasury_id, cashier_id, total_collected, total_receipts}` | SessionService::closeSession |
| `payment_request.completed` | `{request_id, request_number, member_id, payment_type, amount, payment_id, receipt_number, related_entity_type, related_entity_id}` | TreasuryService::collectPayment |
| `treasury.payment.collected` | `{payment_id, treasury_id, session_id, cashier_id, amount, receipt_number}` | TreasuryService::collectPayment |
| `treasury.settlement.initiated` | `{settlement_id, from_treasury_id, to_treasury_id, amount, session_id}` | SettlementService::initiateSettlement |
| `treasury.settlement.received` | `{settlement_id, from_treasury_id, to_treasury_id, amount, received_by}` | SettlementService::receiveSettlement |
| `treasury.settlement.rejected` | `{settlement_id, reason, rejected_by}` | SettlementService::rejectSettlement |

---

## 6. Events Consumed

| Event | Source Module | Action |
|-------|-------------|--------|
| `payment.voided` | Payments | CustodyService::recordVoidReversal() — reduces custody balance (priority 40) |

---

## 7. Cross-Module Dependencies

### 7.1 Treasury IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Payments | PaymentService::processPayment() (core payment creation) |
| Cashier | PaymentRequestService::createRequest(), ::getPaymentTypeLabel() |

### 7.2 Other modules that IMPORT FROM Treasury:

| Module | What it imports |
|--------|----------------|
| Accounting | Listens to treasury.settlement.received, treasury.deposit.confirmed (GL posting) |
| Cashier | TreasuryService (getSubTreasury, getMainTreasury), SessionService, SettlementService, CustodyService |
| Cashier | DepositController/DepositService uses CustodyService |
| Cashier | SettlementReceiveController uses SettlementService |
| Payments | References treasury_id and session_id in payments table |
| Tutorials | References Treasury for training materials |

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| treasury.view_dashboard | View treasury dashboard |
| treasury.collect_payment | Access queue and collect payments |
| treasury.open_session | Open a new cashier session |
| treasury.close_session | Close an open session |
| treasury.initiate_settlement | Create a settlement to main treasury |
| treasury.view_custody | View custody balance and log |
| treasury.view_sessions | View session history |
| treasury.view_settlements | View settlement history |

---

## 9. Number Generation

| Type | Format | Table.Column | Example |
|------|--------|--------------|---------|
| Session | TSS-YYYY-NNNNNN | treasury_sessions.session_number | TSS-2026-000006 |
| Settlement | STL-YYYY-NNNNNN | treasury_settlements.settlement_number | STL-2026-000001 |

Both use MAX(SUBSTRING(...)) pattern for sequential numbering.

---

## 10. Treasury Queue Routing

Payment requests are routed to different sub-treasuries based on `payment_type`:

| Sub-Treasury | Code | Handles Payment Types |
|-------------|------|----------------------|
| Sports Activity | SUB_SA | activity_subscription, hourly_booking, sports_registration, sa_form_fee, sa_subscription, sports_subscription |
| Membership | SUB_MEM | form_fee, membership_fee, down_payment, addition_fee, annual_subscription, divorce_fee, death_fee, waiver_fee, separation_fee, fine, carnet_replacement, seasonal_fee |

---

## 11. High-Risk Areas

### 11.1 Custody Balance Integrity (CRITICAL)
- Custody balance is a running ledger (balance_after on each entry)
- No recalculation mechanism — if any entry has wrong balance_after, all subsequent entries are wrong
- Multiple sources can modify: collections, settlements, voids, deposits
- Race condition possible: two payments collected simultaneously could read same "current balance"

### 11.2 Session-Treasury Binding
- Only one open session allowed per treasury at a time
- If session close fails mid-operation, treasury becomes locked
- No "force close" mechanism for stuck sessions

### 11.3 Settlement State Machine
- Settlement rejected → session reverts to 'closed' + custody reversed
- If custody reversal fails, session stays settled but custody is wrong
- No idempotency protection on receive/reject

### 11.4 Payment Request Processing Lock
- payment_request marked 'processing' before PaymentService call
- If PaymentService fails, request reverted to 'pending'
- Brief window where request appears "processing" — could confuse multiple cashiers

### 11.5 Event Listener Failure (payment.voided)
- Treasury's void reversal listener is wrapped in try/catch
- If it fails, payment is voided but custody isn't reversed
- Logger captures the error but no automatic retry

---

## 12. Known Patterns & Gotchas

1. **Sub-treasury per department**: The system uses separate sub-treasuries (SUB_SA, SUB_MEM) to segregate cash by department
2. **Cash-only custody**: CustodyService only records cash payments — card/check payments don't affect custody balance
3. **One-to-one session-settlement**: Each session can have exactly one settlement. Re-settling requires the first to be rejected
4. **Dashboard recalculates**: getDashboardStats queries payments table directly — may differ slightly from session.total_collected due to timing
5. **Main treasury is passive**: The main treasury doesn't have its own session/queue — it only receives settlements
6. **Session totals are incremental**: total_collected is updated with `+= amount` on each collection, then recalculated from scratch on session close (potential discrepancy during session)
7. **6 sessions in production**: Active system with 6 historical sessions

---

## Appendix: Files to Check Before Modifying Treasury

1. `app/Modules/Payments/Services/PaymentService.php` — Creates payments, triggers payment.voided
2. `app/Modules/Cashier/Services/PaymentRequestService.php` — Creates payment requests
3. `app/Modules/Cashier/Controllers/SettlementReceiveController.php` — Receives settlements at main treasury
4. `app/Modules/Cashier/Services/DepositService.php` — Bank deposits from custody
5. `app/Modules/Accounting/bootstrap.php` — GL posting on settlement received/deposit confirmed
6. `app/Modules/Accounting/Services/Reports/TreasuryReportService.php` — Treasury reporting
