# Payments Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Payments module is the **central payment processor** for the entire ERP. Every monetary transaction passes through `PaymentService::processPayment()`. It manages:

- Processing all payment types (17+ categories: membership, forms, installments, fines, etc.)
- Receipt generation (sequential numbering per year: `REC-YYYY-NNNNNN`)
- Payment voiding (with 24-hour window for non-super-admins)
- Payment history and daily reporting
- Balance calculation across all member obligations
- Lifecycle orchestration (activation, deactivation via `PaymentLifecycleService`)
- Installment plan creation (on down_payment)
- API for external systems

It does **NOT** directly manage:
- Payment requests / queuing (Cashier module)
- Treasury sessions, custody, settlements (Treasury module)
- Journal entries / GL posting (Accounting module — listens to `payment.completed`)
- Installment schedule tracking (Installments module)
- Member activation logic (Members/MembershipPaymentGuard — called by PaymentLifecycleService)

---

## 2. Directory & File Structure

```
app/Modules/Payments/
├── bootstrap.php                          # Events + menu + permissions registration
├── Routes.php                             # Web routes (7 routes)
├── ApiRoutes.php                          # REST API v1 (4 endpoints)
├── provides.php                           # ModuleRegistry: PaymentProcessorInterface
├── PaymentProcessorAdapter.php            # Interface adapter for cross-module use
├── Controllers/
│   ├── PaymentController.php             # Main controller (index, process, store, void, history, dailyReport)
│   └── Api/
│       └── PaymentApiV1Controller.php    # External REST API (CRUD + void)
├── Models/
│   └── Payment.php                       # Model (search, getForMember, getDailyTotals)
├── Services/
│   ├── PaymentService.php                # THE central processor — processPayment, voidPayment, hasPaid, totalPaid
│   ├── PaymentLifecycleService.php       # Orchestrates payment→membership transitions
│   └── BalanceCalculator.php             # Aggregates all member financial obligations
└── Views/
    ├── history.php                        # Payment list / single payment view
    ├── process.php                        # Payment form for a member (shows all options)
    ├── daily-report.php                   # Daily cash report with filters
    └── _partials/
        └── payment-summary.php            # Reusable payment summary component
```

---

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

### 3.1 `payments` Table (412 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| member_id | bigint unsigned | YES | MUL | FK to members (null for guest payments) |
| player_id | bigint unsigned | YES | MUL | FK to sports players |
| receipt_id | bigint unsigned | YES | MUL | FK to receipts |
| payment_type | varchar(50) | NO | MUL | See payment types list below |
| amount | decimal(15,2) | NO | | Always > 0 |
| currency | varchar(3) | NO | | Default: 'EGP' |
| payment_method | varchar(30) | NO | MUL | cash/check/visa/bank_transfer |
| check_number | varchar(50) | YES | | For check payments |
| check_bank | varchar(200) | YES | | Bank name on check |
| check_date | date | YES | | Check maturity date |
| check_status | varchar(30) | YES | MUL | pending/cleared/bounced |
| visa_reference | varchar(100) | YES | | Visa transaction reference |
| transfer_reference | varchar(100) | YES | | Bank transfer reference |
| transfer_bank | varchar(200) | YES | | Transfer source bank |
| related_entity_type | varchar(100) | YES | MUL | spouses/children/temporary_members/etc. |
| related_entity_id | bigint unsigned | YES | | FK to related entity |
| notes | text | YES | | |
| payment_date | date | NO | MUL | Date payment was made |
| received_by_employee_id | bigint unsigned | YES | | FK to employees |
| treasury_id | bigint unsigned | YES | MUL | FK to treasuries (which cashbox) |
| session_id | bigint unsigned | YES | MUL | FK to treasury_sessions |
| is_voided | tinyint(1) | NO | MUL | Default: 0 |
| voided_at | timestamp | YES | | |
| voided_by | bigint unsigned | YES | | FK to employees |
| void_reason | text | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |
| created_by | bigint unsigned | YES | | FK to employees |
| updated_by | bigint unsigned | YES | | FK to employees |

**Current data distribution (payment_type, non-voided):**
- installment: 202
- form_fee: 74
- membership_fee: 45
- annual_subscription: 26
- down_payment: 21
- waiver_fee: 12
- addition_fee: 10
- death_fee: 8
- sports_registration: 8
- divorce_fee: 2
- other/seasonal/separation: 4

### 3.2 `receipts` Table (412 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| receipt_number | varchar(50) | NO | UNI | Format: REC-YYYY-NNNNNN |
| member_id | bigint unsigned | YES | MUL | FK to members |
| payment_id | bigint unsigned | YES | MUL | FK to payments |
| receipt_type | varchar(30) | NO | MUL | Default: 'payment' |
| amount | decimal(15,2) | NO | | |
| amount_in_words_ar | text | YES | | Arabic text representation |
| description_ar | text | YES | | |
| issued_by_employee_id | bigint unsigned | YES | | FK to employees |
| issued_at | timestamp | NO | MUL | |
| is_voided | tinyint(1) | NO | MUL | Default: 0 |
| voided_at | timestamp | YES | | |
| voided_by | bigint unsigned | YES | | |
| void_reason | text | YES | | |
| print_count | int unsigned | NO | | Default: 0 |
| last_printed_at | timestamp | YES | | |
| last_printed_by | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |

---

## 4. Payment Types Reference

| Type Code | Arabic Label | Typical Amount |
|-----------|-------------|----------------|
| form_fee | رسوم استمارة | 505 EGP |
| membership_fee | قيمة العضوية | 150K/225K/300K |
| addition_fee | رسوم إضافة | 5-30% of membership |
| annual_subscription | اشتراك سنوي | 492-527 EGP |
| development_fee | رسوم تنمية | 35 EGP |
| down_payment | مقدم تقسيط | 25%+ of membership |
| installment | قسط شهري | Variable |
| fine | غرامة | 1,000-10,000 EGP |
| separation_fee | رسوم فصل | 2.5-30% |
| divorce_fee | رسوم طلاق | 10-50% |
| death_fee | رسوم نقل وفاة | Form + subscription |
| waiver_fee | رسوم تنازل | 30% |
| carnet_replacement | بدل فاقد كارنيه | Variable |
| seasonal_fee | رسوم عضوية موسمية | Variable |
| sports_conversion | رسوم تحويل رياضي | 50% |
| inventory_sale | مبيعات مخزون | Variable |
| activity_subscription | اشتراك نشاط | Variable |
| other | أخرى | Variable |

---

## 5. Core Business Flows

### 5.1 Payment Processing (PaymentService::processPayment)

```
1. Validate: amount > 0, payment_type present, member exists (if member_id set)
2. Begin transaction
3. Generate receipt number (REC-YYYY-NNNNNN sequential)
4. Insert into `payments` table
5. Insert into `receipts` table (Arabic amount in words)
6. Link receipt_id back to payment
7. Commit
8. Dispatch: payment.completed event
   → Accounting auto-posts journal entry (Dr. Cash, Cr. Revenue)
   → StatementIntegrationService records customer transaction
   → DailyCashMovementService records cash inflow
   → Payments/bootstrap.php handles workflow transition (if membership)
```

### 5.2 Payment Void (PaymentService::voidPayment)

```
1. Verify payment exists and is not already voided
2. Check 24-hour void window (super admin exempt)
3. Begin transaction
4. Set is_voided=1 on payment + receipt
5. Void any linked payment_requests
6. Commit
7. Dispatch: payment.voided event
   → Cashier/bootstrap.php → PaymentLifecycleService::onPaymentVoided
     → MembershipPaymentGuard::deactivateMember (if membership/down_payment)
     → MembershipPaymentGuard::deactivateDependent (if addition_fee)
     → Revert life-event status (if separation/divorce/death/waiver)
   → Accounting reverses journal entry
```

### 5.3 Membership Payment Lifecycle (PaymentLifecycleService)

```
Full cash (membership_fee):
  → MembershipPaymentGuard::activateMember()
  → MembershipPaymentGuard::activateIncludedDependents()
  → Dispatches: member.activated

Down payment (down_payment):
  → transitionToPendingCheques()
  → createInstallmentPlan() (schedule + interest)
  → Dispatches: member.pending_cheques
  → [Later: ChequeService submits cheques → member.activated]
```

### 5.4 Balance Calculation (BalanceCalculator::getSummary)

Aggregates across ALL obligation sources for a member:
1. Total paid (all time, non-voided)
2. Form fee status (paid/unpaid)
3. Membership fee remaining (value - paid - down_payment)
4. Active installment plan (total, paid, remaining, overdue count, next due)
5. Unpaid subscriptions (pending + overdue)
6. Unpaid fines (imposed + appeal_upheld)
7. Total outstanding (sum of all above)

Also provides `canPrintCarnet()` — blocks if unpaid subscriptions, overdue installments, or unpaid fines.

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `payment.completed` | `{payment_id, receipt_id, receipt_number, member_id, payment_type, amount, method, treasury_id}` | PaymentService::processPayment |
| `payment.voided` | `{payment_id, member_id, payment_type, amount, reason}` | PaymentService::voidPayment |
| `installment.plan_created` | `{plan_id, member_id, total_amount}` | PaymentLifecycleService, PaymentController |
| `member.activated` | `{member_id, membership_number}` | PaymentController (direct payment path) |
| `member.pending_cheques` | `{member_id}` | Cashier/bootstrap.php (via lifecycle) |

---

## 7. Events Consumed

| Event | Listener Location | Action |
|-------|-------------------|--------|
| `payment.completed` | Payments/bootstrap.php (priority 100) | Workflow transition for membership payments |
| `member.profile_data` | Payments/bootstrap.php (priority 50) | Inject payment history into member profile |
| `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 | Membership activation, dependent activation, life-event status updates |

---

## 8. Cross-Module Dependencies

### 8.1 Payments module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Members | MemberNumberGenerator (assign membership number on activation) |
| Members | MembershipPaymentGuard (activation/deactivation logic) |
| Rules | RuleEngine (INSTALLMENT_INTEREST_RATE) |
| ServiceCatalog | ServicePrice (form fee amount lookup) |

### 8.2 Other modules that IMPORT FROM Payments:

| Module | What it imports |
|--------|----------------|
| Cashier | PaymentService (processPayment via PaymentRequestService) |
| Accounting | PaymentService (in ReportController for label lookups) |
| Members | PaymentService (referenced), BalanceCalculator |
| Installments | PaymentService (installment payments) |
| Subscriptions | PaymentService (subscription payments) |
| Fines | PaymentService (fine collection) |
| Death | PaymentService (death fee payment) |
| Divorce | PaymentService (divorce fee payment) |
| Transfers | PaymentService (separation fee payment) |
| Waiver | PaymentService (waiver fee payment) |
| Sales | PaymentService (inventory sale payments) |
| SportsActivity | PaymentService (booking/registration payments) |
| ActivitySubscriptions | PaymentService (activity subscription payments) |
| Foreign | PaymentService (foreign membership) |
| Receipts | Payment model (receipt linkage) |
| Treasury | PaymentService (treasury-routed payments) |

### 8.3 Interface Contract

Payments exposes `PaymentProcessorInterface` via `provides.php` / `PaymentProcessorAdapter`:
- `processPayment(array $data): array`
- `voidPayment(int $paymentId, string $reason): array`
- `hasPaid(int $memberId, string $paymentType, ...): bool`
- `totalPaid(int $memberId, ?string $paymentType): string`
- `generateReceiptNumber(): string`
- `getPaymentTypeLabel(string $type): string`

---

## 9. Routes

### Web Routes (7)

| Method | Path | Controller@Action | Permission |
|--------|------|-------------------|------------|
| GET | /payments | PaymentController@index | payment.view |
| GET | /payments/daily-report | PaymentController@dailyReport | payment.view |
| GET | /payments/process/{memberId} | PaymentController@process | payment.create |
| POST | /payments/process/{memberId} | PaymentController@store | payment.create |
| GET | /payments/member/{memberId} | PaymentController@memberHistory | payment.view |
| GET | /payments/{id} | PaymentController@show | payment.view |
| POST | /payments/{id}/void | PaymentController@void | payment.void |

### API v1 Routes (4)

| Method | Path | Permission |
|--------|------|------------|
| GET | /api/v1/payments | payment.view |
| GET | /api/v1/payments/{id} | payment.view |
| POST | /api/v1/payments | payment.process_cash |
| POST | /api/v1/payments/{id}/void | payment.void_receipt |

---

## 10. Permissions

| Key | Description |
|-----|-------------|
| payment.process_cash | Process cash payment |
| payment.process_check | Process check payment |
| payment.process_visa | Process visa payment |
| payment.view | View payments list and details |
| payment.create | Create new payment (direct, non-queue) |
| payment.void | Void a payment |
| payment.collect | Collect payment (cashier) |
| payment.void_receipt | Void receipt |
| payment.refund | Refund payment |

**Super Admin gates:**
- Void after 24-hour window

---

## 11. High-Risk Areas

### 11.1 PaymentService::processPayment (CRITICAL)
- **Single entry point** for ALL money coming into the system
- Creates both payment and receipt atomically in a transaction
- If this fails silently: money received but no record, or record created but event not dispatched
- Receipt number generation uses MAX() query — race conditions possible under concurrent use

### 11.2 PaymentLifecycleService (CRITICAL)
- **Only connector** between payment state changes and membership activation/deactivation
- Called from Cashier/bootstrap.php event listeners
- If broken: members pay but never activate, or voided payments leave members active
- Also creates installment plans — if interest calculation is wrong, long-term financial impact

### 11.3 24-Hour Void Window
- Non-super-admin users cannot void after 24 hours
- Check is based on `created_at` timestamp comparison
- If server timezone is wrong: window incorrectly enforced
- Super admin bypass has no audit trail beyond the standard void_reason

### 11.4 Receipt Number Sequencing
- Format: REC-YYYY-NNNNNN (e.g., REC-2026-000412)
- Generated via MAX()+1 on `receipt_number` LIKE 'REC-YYYY-%'
- Under concurrent load: duplicate receipt numbers possible (no unique DB constraint enforcement at generation time, but column IS unique)

### 11.5 Dual Payment Paths
- **Direct path**: PaymentController@store → PaymentService::processPayment (bypasses queue)
- **Queue path**: Cashier → PaymentRequestService::processRequest → PaymentService::processPayment
- Both paths exist for historical reasons; the queue path is preferred but the direct path still works
- PaymentController@store also does member activation directly (duplicates PaymentLifecycleService logic)

---

## 12. Known Patterns & Gotchas

1. **Every payment creates a receipt**: There is always a 1:1 relationship between payments and receipts
2. **Voiding cascades**: Voiding a payment also voids its receipt AND marks linked payment_requests as voided
3. **payment.completed fires BEFORE lifecycle**: The event is dispatched before any membership activation happens — listeners must not assume the member is already active
4. **Amount validation uses bcmath**: All monetary comparisons use `bccomp` with 2 decimal places
5. **NULL member_id allowed**: Guest payments (sports bookings, guest entries) can have null member_id
6. **player_id column**: Added for sports module — allows payments against players who are not members
7. **treasury_id and session_id**: When payment is processed through treasury system, these link to the cashbox and shift
8. **PaymentController has redundant activation logic**: The `handleMembershipPayment` method in the controller duplicates what PaymentLifecycleService does — this is legacy code from before the queue system was built

---

## 13. Configuration Dependencies

| Config/Rule | Purpose |
|-------------|---------|
| INSTALLMENT_INTEREST_RATE | Interest rate for installment plans (default: 22%) |
| SVC_NEW_FORM | Form fee service catalog price (505.00) |

---

## 14. Financial Year Convention

Receipt numbers reset each calendar year (REC-YYYY-NNNNNN).
Payment date is the date money was received, not necessarily the date the system processed it.
