# SYSTEM MAP — ClubPHP ERP

> Complete reference for AI-assisted development. Covers architecture, modules, database, patterns, and cross-cutting concerns.
> Last updated: 2026-05-03

---

## 1. ARCHITECTURE OVERVIEW

```
public/index.php → App::boot() → Router::dispatch() → Middleware → Controller → Response
cli.php          → App (no session) → MigrationRunner / SeederRunner / CronRunner
```

**Stack:** Custom PHP 8.1+ framework, MySQL 8, plain PDO, no ORM relations, bcmath for money, Arabic-first RTL.

**Boot sequence:** timezone(Africa/Cairo) → .env → config files → DB connect → session → system_config overrides → module bootstrap.php (sorted) → module Routes.php

**Key singletons:** `App::getInstance()` → `->db()`, `->session()`, `->router()`, `->currentEmployee()`, `->currentBranch()`

---

## 2. MODULE INVENTORY (50 modules)

### Membership Domain
| Module | Route Prefix | Controllers | Key Permissions | Events |
|--------|-------------|-------------|-----------------|--------|
| **Members** | `/members` | MemberController, MemberApiController | member.view/create/edit/archive/search/view_financial/change_status/pay_form_fee/pay_membership/fill_form | Listens: member.profile_data |
| **Spouses** | `/members/{id}/spouses` | SpouseController | spouse.add/edit/remove/view | — |
| **Children** | `/members/{id}/children` | ChildController | child.add/edit/remove/view/freeze/separate | — |
| **Temporary** | `/temporary` | TemporaryController | temp.add/edit/remove/view | Listens: member.profile_data |
| **Seasonal** | `/seasonal` | SeasonalController | (uses temp.*) | Listens: member.profile_data |
| **Sports** | `/sports` | SportsController | (uses temp.*) | — |
| **Honorary** | `/honorary` | HonoraryController | (uses member.*) | — |
| **Foreign** | `/foreign` | ForeignController | — | — |
| **Interviews** | `/interviews` | InterviewController | interview.view/schedule/decide/conduct | — |
| **Carnets** | `/carnets` | CarnetController | carnet.print/replace/view_log | — |

### Financial Domain
| Module | Route Prefix | Controllers | Key Permissions | Events |
|--------|-------------|-------------|-----------------|--------|
| **Payments** | `/payments` | PaymentController | payment.process_cash/check/visa/view/void_receipt/refund | Dispatches: sms.send, member.activated; Listens: payment.completed |
| **Cashier** | `/cashier` | CashierController | cashier.view_queue/process_payment/cancel_request | Dispatches: installment.plan_created, member.activated, divorce/death/waiver/transfer/spouse/child.fee_paid; Listens: payment_request.completed |
| **Receipts** | `/receipts` | ReceiptController | receipt.view/print/void | — |
| **Installments** | `/installments` | InstallmentController | installment.view/create_plan/record_payment/modify_plan | — |
| **Subscriptions** | `/subscriptions` | SubscriptionController | subscription.view/collect/exempt/generate_batch | — |
| **Fines** | `/violations`, `/fines` | ViolationController, FineController | fine.view/impose/collect/waive | — |
| **Sales** | `/sales` | SaleController, PackageController, SaleReportController | sales.view/create/void/refund, package.view/manage, report.sales | — |
| **Pricing** | `/pricing` | PricingController, SpecialDiscountController | (uses rules.*) | — |
| **Rules** | `/rules` | RuleController | rules.view/edit/create/deactivate + pricing.* + special_discounts.* | — |
| **ServiceCatalog** | `/catalog` | CatalogController | (uses pricing.*) | — |

### Accounting Domain
| Module | Route Prefix | Controllers | Key Permissions | Events |
|--------|-------------|-------------|-----------------|--------|
| **Accounting** | `/accounting` | 7 controllers (Reports, FiscalYear, COA, CostCenter, BankAccount, Journal, BankRecon, PeriodClosing) | 28 permissions | Listens: payment.completed/voided, sale.completed/voided, hr.payroll.paid, subscription.paid, fine.imposed/paid, installment.plan_created/paid, sale.refunded, procurement.* events, rental.deposit_collected/refunded |

### Life Events Domain
| Module | Route Prefix | Controllers | Key Permissions |
|--------|-------------|-------------|-----------------|
| **Transfers** | `/transfers` | TransferController | transfer.view/create/approve + separation.* + waiver.* |
| **Divorce** | `/divorce` | DivorceController | (uses transfer.*) |
| **Death** | `/death` | DeathController | (uses transfer.*) |
| **Waiver** | `/waivers` | WaiverController | (uses transfer.*) |

### HR Domain
| Module | Route Prefix | Controllers | Key Permissions | Events |
|--------|-------------|-------------|-----------------|--------|
| **HR** | `/hr` | 18 controllers (Department, JobTitle, EmployeeProfile, Contract, Salary, Attendance, Leave, Payroll, Insurance, Tax, Disciplinary, Loan, EndOfService, Performance, EmployeeDocument, Holiday, WorkSchedule, HrReport) | 35 permissions | Dispatches: hr.leave.submitted/approved/rejected, hr.contract.renewed; Listens: employee.created, hr.leave.approved/rejected, hr.payroll.paid, hr.loan.approved |

### Operations Domain
| Module | Route Prefix | Controllers | Key Permissions |
|--------|-------------|-------------|-----------------|
| **Facilities** | `/facilities` | FacilityController | facility.view/manage |
| **Academies** | `/academies` | AcademyController | academy.view/manage/enroll |
| **Disciplines** | `/disciplines` | SportsDashboardController, DisciplineController | discipline.view/manage |
| **Reservations** | `/reservations` | ReservationController | reservation.view/create/confirm/cancel |
| **Rentals** | `/rentals` | RentalController | rental.view/manage_entity/approve/manage_contract/manage_deposit |
| **ActivitySubscriptions** | `/activity-subscriptions` | ActivitySubscriptionController | activity_sub.view/collect/exempt/generate/manage_pricing |
| **PlayerAffairs** | `/players` | PlayerController, AttendanceController | player.view/search/register/edit/manage_card/view_medical/manage_medical/record_attendance |

### Inventory & Procurement Domain
| Module | Route Prefix | Controllers | Key Permissions |
|--------|-------------|-------------|-----------------|
| **Inventory** | `/inventory` | 10 controllers (Warehouse, Category, Item, StockMovement, StockTransfer, StockAudit, Supplier, PurchaseOrder, Asset, InventoryReport) | 18 permissions |
| **Procurement** | `/procurement` | 7 controllers (Dashboard, Requisition, GRN, VendorInvoice, VendorPayment, RTV, Report) | 16 permissions |

### System Domain
| Module | Route Prefix | Controllers | Key Permissions |
|--------|-------------|-------------|-----------------|
| **Auth** | `/` (login/logout) | AuthController | auth.force_logout |
| **Dashboard** | `/dashboard` | DashboardController | — |
| **Roles** | `/roles` | RoleController | role.view/create/edit |
| **Users** | `/users` | UserController, UserPermissionController | user.view/create/edit/deactivate/assign_role/manage_permissions |
| **Settings** | `/settings` | SettingsController, BrandingController | settings.view/edit/backup |
| **Branches** | `/branches` | BranchController | settings.* |
| **Notifications** | `/notifications` | NotificationController | sms.send_single/send_bulk/view_log/edit_templates |
| **Reports** | `/reports` | ReportController | report.view_membership/financial/operations/audit/sports/sports_financial/export |
| **Audit** | `/audit` | AuditController | — (Listens: auth.login/logout/password_changed) |
| **Archive** | `/archive` | ArchiveController | archive.view/take_snapshot/compare/number_chain |
| **Documents** | `/documents` | DocumentController | document.upload/view/delete (Listens: member.profile_data) |
| **Forms** | `/forms` | FormController, FormBuilderController | forms.view/edit_schema/create_schema |
| **Workflow** | `/workflows` | WorkflowController | workflow.view/transition/manage |
| **Support** | `/support` | TicketController | support.view/create/reply/assign/close/manage |

---

## 3. EVENT BUS MAP

### Events Dispatched
| Event | Source | Data |
|-------|--------|------|
| `member.created` | Model auto | `{id, form_number, ...}` |
| `member.updated` | Model auto | `{id, changed_fields}` |
| `member.activated` | Cashier/Payments | `{member_id}` |
| `member.profile_data` | MemberController | `{member_id}` — enrichment point |
| `payment.completed` | PaymentService | `{payment_id, member_id, amount, type}` |
| `payment.voided` | PaymentService | `{payment_id}` |
| `payment_request.completed` | PaymentRequestService | `{request_id}` |
| `sale.completed` | SaleService | `{sale_id, amount}` |
| `sale.voided` | SaleService | `{sale_id}` |
| `sale.refunded` | SaleService | `{sale_id, refund_id}` |
| `fine.imposed` | FineService | `{fine_id, amount}` |
| `fine.paid` | FineService | `{fine_id}` |
| `subscription.paid` | SubscriptionService | `{subscription_id}` |
| `installment.plan_created` | Cashier | `{plan_id, total}` |
| `installment.paid` | InstallmentService | `{installment_id}` |
| `hr.leave.submitted` | LeaveService | `{leave_id}` |
| `hr.leave.approved` | LeaveService | `{leave_id}` |
| `hr.leave.rejected` | LeaveService | `{leave_id}` |
| `hr.payroll.paid` | PayrollService | `{payroll_run_id, total_net}` |
| `hr.contract.renewed` | ContractService | `{contract_id}` |
| `hr.loan.approved` | LoanService | `{loan_id}` |
| `procurement.invoice_approved` | ProcurementService | `{invoice_id}` |
| `procurement.payment_completed` | ProcurementService | `{payment_id}` |
| `procurement.payment_voided` | ProcurementService | `{payment_id}` |
| `procurement.rtv_completed` | ProcurementService | `{rtv_id}` |
| `rental.deposit_collected` | RentalService | `{contract_id}` |
| `rental.deposit_refunded` | RentalService | `{contract_id}` |
| `player.card_activated` | PlayerCardService | `{player_id}` |
| `player.medical_updated` | MedicalRecordService | `{player_id}` |
| `sms.send` | Payments/various | `{phone, template, data}` |
| `auth.login` | AuthController | `{employee_id}` |
| `auth.logout` | AuthController | `{employee_id}` |

### Event Listeners (Hub: Accounting Module)
The Accounting module listens to 16+ events and auto-posts double-entry journal entries via `GLSyncService`. No other module needs to know about GL — it's fully event-driven.

---

## 4. DATABASE SCHEMA (151 tables)

### System Foundation (Phase 01-02)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `migrations` | Track schema versions | batch, migration_name |
| `system_config` | Runtime config overrides | config_key, config_value, value_type |
| `branches` | Multi-branch support | code, name_ar, name_en, address, phone |
| `employees` | System users | username, password_hash, email, branch_id, is_active |
| `roles` | Permission groups | role_code, name_ar, name_en |
| `role_permissions` | Role→Permission mapping | role_id, permission_key |
| `employee_roles` | Employee→Role mapping | employee_id, role_id, is_active |
| `employee_permissions` | Direct employee permissions | employee_id, permission_key, is_denied |
| `login_attempts` | Security tracking | username, ip_address, success |
| `active_sessions` | Session management | employee_id, session_id, ip_address |
| `password_history` | Password reuse prevention | employee_id, password_hash |
| `audit_trail` | All system changes | entity_type, entity_id, action, before/after_data_json |

### Master Data (Phase 03-04)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `governorates` | Egyptian governorates | code, name_ar |
| `countries` | Country list | code, name_ar, nationality_ar |
| `qualifications` | Education levels | code, name_ar (affects pricing) |

### Business Rules (Phase 05-06)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `business_rules` | All configurable rules | rule_code, category, data_type, current_value_json, branch_id |
| `rule_versions` | Rule change history | rule_id, version_number, old/new_value_json, change_reason |
| `pricing_configs` | Branch×Qualification pricing | branch_id, qualification_id, membership_type, price |
| `service_catalog` | Service fees | service_code, base_amount, percentage |
| `discount_rules` | Discount tiers | min_amount, max_amount, discount_percentage |
| `rule_overrides` | Branch-specific overrides | rule_id, branch_id, override_value_json |

### Members (Phase 07-09)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `members` | Core member data | form_number, membership_number, full_name_ar, national_id, membership_value, status, branch_id, qualification_id |
| `member_notes` | Internal notes | member_id, note_text, created_by |
| `spouses` | Member spouses | member_id, full_name_ar, national_id, nationality, marriage_date, status |
| `children` | Member children | member_id, full_name_ar, date_of_birth, relationship, classification, status |
| `temporary_members` | Temp membership | member_id, start_date, end_date, fee_amount |
| `seasonal_memberships` | Seasonal passes | member_id, season_year, start_date, end_date |
| `sports_members` | Sports membership | member_id, sport_type, status |
| `honorary_members` | Honorary membership | member_id, granted_by, grant_reason |
| `foreign_member_details` | Foreign member extras | member_id, passport_number, visa_type |

### Payments & Finance (Phase 10-12)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `payment_methods` | Available methods | code, name_ar (cash/visa/check/bank_transfer) |
| `payments` | All payments | payment_number, member_id, amount, payment_method, status |
| `receipts` | Payment receipts | receipt_number, payment_id, member_id, amount |
| `installment_plans` | Payment plans | member_id, total_amount, down_payment, months, interest_rate |
| `installment_schedule` | Plan installments | plan_id, installment_number, due_date, amount, status |
| `subscriptions` | Annual subscriptions | member_id, year, amount, status, due_date |
| `violations` | Violation types | code, name_ar, fine_amount |
| `fines` | Imposed fines | member_id, violation_id, amount, status |
| `payment_requests` | Cashier queue | request_number, member_id, payment_type, amount, status, requested_by, processed_by |

### Life Events (Phase 13-14)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `transfer_requests` | Membership transfers | source_member_id, target_member_id, transfer_type, status, fee_amount |
| `divorce_cases` | Divorce separations | member_id, spouse_id, divorce_date, spouse_new_member_id, status |
| `death_cases` | Death transfers | member_id, death_date, transferred_to_member_id, status |
| `waiver_requests` | Voluntary waivers | member_id, target_member_id, waiver_reason, status |

### Interviews & Documents (Phase 15-16)
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `interviews` | Membership interviews | member_id, scheduled_date, interviewer_id, result, notes |
| `carnets` | Member ID cards | member_id, carnet_number, issue_date, expiry_date, status |
| `documents` | Uploaded documents | member_id, document_type, file_path, original_filename |

### Notifications (Phase 17)
| Table | Purpose |
|-------|---------|
| `sms_templates` | SMS message templates |
| `sms_log` | Sent SMS history |
| `notification_queue` | Pending notifications |
| `report_definitions` | Report configurations |
| `scheduled_reports` | Auto-generated reports |

### Facilities & Sports (Phase 18-21)
| Table | Purpose |
|-------|---------|
| `sport_disciplines` | Available sports |
| `facilities` | Physical facilities |
| `facility_time_slots` | Booking slots |
| `facility_blackout_dates` | Closed dates |
| `academies` | Training academies |
| `academy_levels` | Skill levels |
| `academy_schedules` | Class schedules |
| `players` | Athlete records |
| `player_disciplines` | Player→Sport mapping |
| `academy_enrollments` | Academy registrations |
| `player_medical_records` | Medical data |
| `player_attendance` | Training attendance |
| `reservations` | Facility bookings |
| `rental_entities` | Rentable spaces |
| `rental_contracts` | Lease agreements |
| `rental_bookings` | Rental reservations |
| `activity_subscriptions` | Activity passes |
| `activity_pricing` | Activity fees |

### Inventory & Procurement (Phase 25-34)
| Table | Purpose |
|-------|---------|
| `warehouses` | Storage locations |
| `item_categories` | Item classification |
| `inventory_items` | Stock items |
| `item_warehouse_stock` | Stock levels per warehouse |
| `stock_movements` | In/out movements |
| `item_batches` | Batch tracking |
| `asset_register` | Fixed assets |
| `depreciation_entries` | Asset depreciation |
| `stock_transfers` | Inter-warehouse transfers |
| `stock_audits` | Physical counts |
| `suppliers` | Vendor master |
| `purchase_orders` | POs |
| `purchase_requisitions` | PRs |
| `goods_received_notes` | GRNs |
| `vendor_invoices` | Vendor bills |
| `vendor_payments` | Vendor payments |
| `return_to_vendor` | RTVs |
| `packages` | Sale packages |
| `sales` | POS sales |
| `sale_items` | Sale line items |
| `sale_refunds` | Refund records |

### HR (Phase 26-33)
| Table | Purpose |
|-------|---------|
| `hr_departments` | Org structure |
| `hr_job_titles` | Position definitions |
| `hr_salary_structures` | Pay structures |
| `hr_salary_components` | Earnings/deductions |
| `hr_employee_profiles` | Extended employee data |
| `hr_employee_salary_details` | Individual salary setup |
| `hr_contracts` | Employment contracts |
| `hr_employee_documents` | Employee documents |
| `hr_work_schedules` | Shift patterns |
| `hr_employee_schedules` | Assigned schedules |
| `hr_attendance` | Clock in/out |
| `hr_leave_types` | Leave categories |
| `hr_leave_balances` | Remaining leave |
| `hr_leave_requests` | Leave applications |
| `hr_sick_leave_records` | Sick leave |
| `hr_holidays` | Public holidays |
| `hr_disciplinary_actions` | Warnings/penalties |
| `hr_employee_loans` | Employee loans |
| `hr_loan_installments` | Loan repayments |
| `hr_payroll_periods` | Pay periods |
| `hr_payroll_runs` | Payroll execution |
| `hr_payroll_components_log` | Payslip breakdown |
| `hr_insurance_records` | Social insurance |
| `hr_tax_records` | Tax calculations |
| `hr_end_of_service` | Termination settlements |
| `hr_performance_cycles` | Review periods |
| `hr_performance_reviews` | Performance ratings |
| `hr_salary_adjustments` | Pay changes |

### Accounting (Phase 27-30)
| Table | Purpose |
|-------|---------|
| `fiscal_years` | Financial years |
| `chart_of_accounts` | GL accounts (hierarchical) |
| `cost_centers` | Cost allocation |
| `bank_accounts` | Bank accounts |
| `journal_entries` | GL journal headers |
| `journal_entry_lines` | Debit/credit lines |
| `account_balances` | Running balances |
| `bank_reconciliations` | Bank recon headers |
| `bank_reconciliation_items` | Recon matches |
| `accounts_payable` | AP ledger |
| `accounts_receivable` | AR ledger |
| `period_closings` | Period close records |

### Forms & Workflow (Phase 22-23)
| Table | Purpose |
|-------|---------|
| `form_field_types` | Dynamic form field types |
| `form_schemas` | Form definitions (JSON schema) |
| `form_submissions` | Submitted form data |
| `workflow_definitions` | Workflow state machines |
| `workflow_instances` | Active workflows |
| `workflow_transition_log` | State change history |

### Support & Discounts (Phase 35-39)
| Table | Purpose |
|-------|---------|
| `special_discounts` | Discount types (e.g., disabled, military) |
| `support_tickets` | Help desk tickets |
| `support_ticket_replies` | Ticket responses |
| `support_ticket_attachments` | Ticket files |

---

## 5. CORE FRAMEWORK API

### App (Singleton)
```php
App::getInstance()->boot()           // Initialize everything
App::getInstance()->db()             // Database instance
App::getInstance()->session()        // Session instance
App::getInstance()->router()         // Router instance
App::getInstance()->config('key')    // Config with dot notation
App::getInstance()->currentEmployee()// Authenticated user
App::getInstance()->currentBranch()  // Active branch
App::getInstance()->basePath()       // /path/to/clubphp
```

### Database
```php
$db->select($sql, $params)          // Returns array of rows
$db->selectOne($sql, $params)       // Returns ?array (first row)
$db->insert($table, $data)          // Returns int (last ID)
$db->update($table, $data, $where, $params)  // Returns affected count
$db->delete($table, $where, $params)         // Returns affected count
$db->query($sql, $params)           // Returns PDOStatement
$db->raw($sql)                      // Execute without params
$db->beginTransaction() / commit() / rollBack()
$db->tableExists($table)            // Cached check
// Audit hooks:
$db->onAfterInsert(fn($table, $data, $id))
$db->onBeforeUpdate(fn($table, $data, $entityId, $oldRecord))
$db->onAfterUpdate(fn($table, $data, $entityId, $oldRecord, $newRecord))
$db->onAfterDelete(fn($table, $oldRecord, $entityId))
```

### QueryBuilder (Immutable, fluent)
```php
Model::query()
    ->where('col', '=', $val)        // AND WHERE
    ->orWhere('col', '=', $val)      // OR WHERE
    ->whereIn('col', $arr)           // WHERE IN
    ->whereNull('col')               // IS NULL
    ->whereNotNull('col')            // IS NOT NULL
    ->whereBetween('col', $min, $max)
    ->whereRaw('sql', $bindings)     // Raw WHERE
    ->join($table, $on)              // INNER JOIN
    ->leftJoin($table, $on)          // LEFT JOIN
    ->orderBy('col', 'ASC')
    ->groupBy('col')
    ->having('raw')
    ->limit($n)->offset($n)
    ->when($bool, fn($q))           // Conditional
    ->withArchived()                 // Include soft-deleted
    ->get()                          // array of rows
    ->first()                        // ?array
    ->count()                        // int
    ->sum('col')                     // string
    ->paginate($perPage, $page)      // ['data' => [], 'pagination' => []]
```

### Model (Active Record)
```php
// Static properties to override:
protected static string $table = 'table_name';
protected static array $fillable = ['col1', 'col2'];
protected static bool $timestamps = true;    // created_at, updated_at
protected static bool $softDelete = true;    // is_archived
protected static bool $dispatchEvents = true;// fires {singular}.created/updated/archived

// Methods:
Model::find($id)                    // ?static
Model::findOrFail($id)              // static (throws RuntimeException)
Model::create($data)                // static (filtered by $fillable)
Model::query()                      // QueryBuilder
$model->save()                      // bool
$model->update($data)               // bool (filtered by $fillable)
$model->archive()                   // bool (soft delete)
$model->toArray()                   // array
```

### Controller
```php
$this->view('Module.Views.template', $data)  // HTML response
$this->json($data)                           // JSON response
$this->redirect($url)                        // 302 redirect
$this->back()                                // Redirect to referer
$this->validate($data, $rules)              // Validate or flash errors
$this->authorize('permission.key')           // Check or throw 403
$this->currentEmployee()                     // ?object
```

### Response (Fluent builder)
```php
$this->redirect('/path')
    ->withSuccess('msg')             // Flash success alert
    ->withError('msg')               // Flash error alert
    ->withWarning('msg')             // Flash warning alert
    ->withInput($data)               // Flash old input
// Also: ->html($content), ->json($data), ->download($path, $name)
```

### Request
```php
$request->method()                   // GET/POST/PUT/DELETE
$request->path()                     // /members/123
$request->get('key', $default)       // GET param
$request->post('key', $default)      // POST param
$request->input('key', $default)     // POST or GET
$request->all()                      // All input merged
$request->file('key')                // ?array (uploaded file)
$request->isAjax()                   // bool
$request->ip()                       // Client IP
$request->routeParam('id')           // Route parameter
```

### Template
```php
// In views:
$__template->layout('Layout.main')
$__template->section('content') / $__template->endSection()
$__template->yield('content', $default)
$__template->include('Module.Views.partial', $data)
$__template->includeIf('path', $data)

// Path resolution (dot notation):
'Layout.main'              → app/Shared/Layout/main.php
'Members.Views.show'       → app/Modules/Members/Views/show.php
'Shared.Components.alerts' → app/Shared/Components/alerts.php
```

### EventBus
```php
EventBus::listen('event.name', fn($data), $priority)  // Higher priority = first
EventBus::dispatch('event.name', $data)                // Returns handler results
EventBus::dispatchAsync('event.name', $data)           // Queue for async
EventBus::hasListeners('event.name')                   // bool
```

### Session
```php
$session->get('key', $default) / ->set('key', $val) / ->has('key') / ->remove('key')
$session->flash('key', $val)         // One-time data
$session->getFlash('key')            // Get and delete
$session->getAlerts()                // Get flashed alerts array
$session->regenerate() / ->destroy()
```

### Validator
```php
$result = (new Validator())->validate($data, [
    'name' => 'required|string|max:200',
    'age'  => 'required|integer|min:1|max:120',
    'email' => 'nullable|email|unique:employees,email',
    'nid'   => 'required|national_id',
    'phone' => 'required|phone_eg',
]);
$result->passes() / ->fails() / ->errors() / ->validated()

// Available rules: required, string, integer, numeric, email, date,
// min:N, max:N, between:M,N, in:a,b,c, not_in:a,b,c, digits:N,
// digits_between:M,N, regex:pattern, confirmed, unique:table,col[,except],
// exists:table,col, phone_eg, national_id, arabic_text, english_text,
// date_before_today, date_after_today, date_before:field, date_after:field,
// file, file_max:kb, file_types:ext1,ext2, array, json, decimal:N, nullable
```

### Global Helpers
```php
e($text)                    // HTML escape
money($amount)              // Currency format (e.g., "1,500.00 ج.م")
csrf_field()                // <input type="hidden" ...>
old('field', $default)      // Flashed input
arabic_date($date)          // Localized date
now() / today()             // Current datetime/date strings
age_from_dob($dob)          // ['years' => N, 'months' => M, 'days' => D]
generate_uuid()             // UUID v4
url('/path')                // Full URL with base
flash_success/error/warning($msg) // Shorthand flash
```

---

## 6. CODING PATTERNS & CONVENTIONS

### File Structure
```
app/Modules/{Name}/
├── bootstrap.php              — PermissionRegistry, MenuRegistry, EventBus::listen
├── Routes.php                 — return [[METHOD, PATH, HANDLER, MIDDLEWARE, PERM], ...]
├── Controllers/XxxController.php
├── Models/Xxx.php
├── Services/XxxService.php
└── Views/
    ├── index.php
    ├── show.php
    ├── create.php
    ├── edit.php
    └── _partials/
```

### Route Definition
```php
return [
    ['GET',  '/prefix',       'Module\Controllers\Ctrl@index',  ['auth'],         'perm.view'],
    ['GET',  '/prefix/create','Module\Controllers\Ctrl@create', ['auth'],         'perm.create'],
    ['POST', '/prefix',       'Module\Controllers\Ctrl@store',  ['auth', 'csrf'], 'perm.create'],
    ['GET',  '/prefix/{id}',  'Module\Controllers\Ctrl@show',   ['auth'],         'perm.view'],
    ['POST', '/prefix/{id}',  'Module\Controllers\Ctrl@update', ['auth', 'csrf'], 'perm.edit'],
];
```

### Controller Pattern
```php
public function store(Request $request): Response
{
    $this->authorize('module.create');
    $data = $request->all();

    // Validate
    $errors = [];
    if (empty($data['name'])) $errors[] = 'الاسم مطلوب';
    if (!empty($errors)) {
        $session = App::getInstance()->session();
        $session->flash('_alerts', array_map(fn($e) => ['type'=>'error','message'=>$e], $errors));
        return $this->redirect('/prefix/create')->withInput($data);
    }

    // Create
    $model = Model::create($data);

    return $this->redirect('/prefix/' . $model->id)
        ->withSuccess('تم الإنشاء بنجاح');
}
```

### Service Pattern (Static, final)
```php
final class XxxService
{
    public static function doSomething(int $id, array $data): array
    {
        $db = App::getInstance()->db();
        // ... business logic with bcmath ...
        return ['success' => true, 'id' => $newId];
        // OR: return ['success' => false, 'error' => 'Arabic message'];
    }
}
```

### Financial Calculations (ALWAYS bcmath)
```php
// Percentage of value
$fee = bcdiv(bcmul($membershipValue, $percentage, 4), '100', 2);

// Addition
$total = bcadd($subtotal, $tax, 2);

// Comparison
if (bccomp($amount, '0.00', 2) > 0) { /* positive */ }

// NEVER use float for money. NEVER cast to (float).
```

### Bootstrap Pattern
```php
<?php
declare(strict_types=1);

use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
use App\Core\EventBus;

PermissionRegistry::register('module_name', [
    'perm.view'   => ['ar' => 'عرض', 'en' => 'View'],
    'perm.create' => ['ar' => 'إنشاء', 'en' => 'Create'],
]);

MenuRegistry::register('menu_key', [
    'label_ar'   => 'القائمة',
    'icon'       => 'lucide-icon-name',
    'route'      => '/prefix',
    'permission' => 'perm.view',
    'order'      => 500,
    'children'   => [...],
]);

EventBus::listen('some.event', function(array $data) {
    // React to event
}, priority: 100);
```

### Migration Pattern
```php
<?php
declare(strict_types=1);
return [
    'up' => "
        CREATE TABLE IF NOT EXISTS `table_name` (
            `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            `code` VARCHAR(50) NOT NULL,
            `name_ar` VARCHAR(255) NOT NULL,
            `is_active` TINYINT(1) NOT NULL DEFAULT 1,
            `is_archived` TINYINT(1) NOT NULL DEFAULT 0,
            `archived_at` TIMESTAMP NULL,
            `archived_by` BIGINT UNSIGNED NULL,
            `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            `created_by` BIGINT UNSIGNED NULL,
            `updated_by` BIGINT UNSIGNED NULL,
            INDEX `idx_active` (`is_active`, `is_archived`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ",
    'down' => "DROP TABLE IF EXISTS `table_name`",
];
```

### Seed Pattern
```php
<?php
declare(strict_types=1);
return function (\App\Core\Database $db) {
    $exists = $db->selectOne("SELECT id FROM table WHERE code = ?", ['CODE']);
    if ($exists) return;
    $db->insert('table', [
        'code' => 'CODE',
        'name_ar' => 'الاسم',
        'is_active' => 1,
        'created_at' => date('Y-m-d H:i:s'),
    ]);
};
```

### Standard Table Columns
Every entity table includes:
- `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
- `is_archived` TINYINT(1) DEFAULT 0 (soft delete)
- `archived_at` TIMESTAMP NULL
- `archived_by` BIGINT UNSIGNED NULL
- `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- `created_by` BIGINT UNSIGNED NULL
- `updated_by` BIGINT UNSIGNED NULL
- `branch_id` BIGINT UNSIGNED NULL (for branch isolation; NULL = global)

### Naming Conventions
| Item | Convention | Example |
|------|-----------|---------|
| Table | snake_case plural | `payment_requests` |
| Column | snake_case | `membership_value` |
| Model | PascalCase singular | `PaymentRequest` |
| Controller | PascalCase + Controller | `PaymentController` |
| Service | PascalCase + Service | `PaymentRequestService` |
| Permission | module.action | `cashier.process_payment` |
| Route | kebab-case | `/activity-subscriptions` |
| Migration | Phase_NN_NNN_description | `Phase_38_001_create_payment_requests_table.php` |
| Seed | Phase_NN_NNN_seed_description | `Phase_40_001_seed_master_document_compliance.php` |
| Rule code | UPPER_SNAKE_CASE | `CHILD_INCLUDED_MAX_COUNT` |
| Event | dot.separated | `payment.completed` |

---

## 7. BUSINESS RULES ENGINE

All configurable values live in `business_rules` table, accessed via `RuleEngine`:

```php
// Get full rule data (returns decoded JSON)
$data = RuleEngine::get('RULE_CODE');        // e.g., ['percentage' => '10.00']

// Get single value from rule
$val = RuleEngine::getValue('RULE_CODE', 'value');  // e.g., 3
```

### Key Rule Codes
| Code | Category | Type | Value |
|------|----------|------|-------|
| `INITIAL_FREE_CHILDREN_COUNT` | children_fee | integer | 3 |
| `CHILD_INCLUDED_MAX_AGE` | age | integer | 18 |
| `CHILD_MAX_AGE` | age | integer | 25 |
| `CHILD_FEE_AGE_18/19/20` | children_fee | percentage | 5-10% |
| `CHILD_FEE_AGE_21` | children_fee | percentage | 15% |
| `CHILD_4TH_UNDER_18_FEE` | children_fee | percentage | 5% |
| `STEPCHILD_FEE` | children_fee | percentage | 10% |
| `SPOUSE_BASE_MEMBER_FEE` | spouse_fee | percentage | 15% |
| `SPOUSE_ACQUIRED_MEMBER_FEE` | spouse_fee | percentage | 50% |
| `SPOUSE_FOREIGN_FEE` | spouse_fee | percentage | 15% |
| `SPOUSE_2ND_FEE` | spouse_fee | percentage+annual_flat | 10% + 150/yr |
| `SPOUSE_3RD_FEE` | spouse_fee | percentage+annual_flat | 20% + 200/yr |
| `SPOUSE_4TH_FEE` | spouse_fee | percentage+annual_flat | 30% + 300/yr |
| `FORM_ADDITION_FEE` | financial | amount | varies |
| `MAX_SPOUSES_MALE_MEMBER` | spouse_fee | integer | 4 |
| `MAX_SPOUSES_FEMALE_MEMBER` | spouse_fee | integer | 1 |
| `SPOUSE_MIN_AGE` | spouse_fee | integer | 18 |
| `CHILD_NID_REQUIRED_AGE` | age | integer | 16 |
| `CHILD_MIN_AGE_GAP` | age | integer | 15 |
| `CHILD_MAX_AGE_GAP` | age | integer | 60 |
| `DEATH_TRANSFER_WINDOW_MONTHS` | death | integer | 12 |
| `FREEZE_MAX_YEARS` | workflow | integer | 3 |
| `FREEZE_ANNUAL_FEE_PERCENTAGE` | workflow | percentage | 50% |
| `CARNET_REPLACEMENT_FEE` | financial | amount | 200.00 |
| `BOARD_APPROVAL_THRESHOLD` | workflow | amount | 50,000.00 |
| `SEPARATION_FEE_YEAR_1..5` | transfer | percentage | varies |
| `INSTALLMENT_INTEREST_RATE` | financial | percentage | 22% |

---

## 8. PAYMENT & CASHIER WORKFLOW

```
Service Desk                           Cashier Queue                    System
─────────────                          ─────────────                    ──────
Creates payment_request  ─────────►  Appears in /cashier queue
  (status: pending)                     │
                                        ▼
                                   Cashier opens request
                                   Selects payment method
                                   Clicks "Complete"
                                        │
                                        ▼
                              PaymentRequestService::processRequest()
                                        │
                                        ├── PaymentService::processPayment()
                                        │       ├── Creates payment record
                                        │       ├── Creates receipt
                                        │       └── Dispatches payment.completed
                                        │
                                        ├── Updates request status → completed
                                        ├── Dispatches domain event (e.g., member.activated)
                                        │
                                        ▼
                              Accounting module (via EventBus)
                                        │
                                        ├── GLSyncService posts journal entry
                                        └── Updates account_balances
```

### Member Activation Flow
```
1. member created (status: draft)
2. form_fee payment_request → cashier pays → form unlocked
3. form filled (status: form_filled)
4. membership_fee payment_request → cashier pays
5. member.activated event fired → status: active
```

---

## 9. MULTI-BRANCH ISOLATION

- `branch_id` column on most data tables
- `NULL` branch_id = company-wide (rules, roles, master data)
- Employee bound to one branch via `employees.branch_id`
- `App::getInstance()->currentBranch()` set by AuthMiddleware
- Queries filter by branch: `WHERE branch_id = ?`
- Pricing is branch-specific: `pricing_configs.branch_id`
- Reports support cross-branch aggregation for admins

---

## 10. AUTHORIZATION MODEL

```
Employee ──► employee_roles ──► roles ──► role_permissions ──► permission_key
         └── employee_permissions (direct grant/deny overrides)
```

- Route-level: 5th parameter in route array → checked in AuthMiddleware
- Controller-level: `$this->authorize('perm.key')` → throws 403
- View-level: Check employee permissions before rendering buttons/sections
- Super admin: role_code = 'super_admin' bypasses all checks

---

## 11. AUDIT SYSTEM

Automatic via Database hooks registered in Audit module bootstrap:
- Every INSERT/UPDATE/DELETE on non-system tables → `audit_trail` record
- Stores: who, when, what table, what entity, before/after JSON, changed fields
- Ignores: migrations, seeds, sessions, audit_trail itself
- Sensitive fields (password) masked in logs

---

## 12. KEY SERVICES REFERENCE

| Service | Module | Purpose |
|---------|--------|---------|
| `PricingEngine` | Pricing | Calculate child/spouse/separation/installment fees |
| `RuleEngine` | Rules | Read business rules from DB |
| `PaymentService` | Payments | Process payments, create receipts |
| `PaymentRequestService` | Cashier | Cashier queue management |
| `BillingService` | Members | Combined invoice calculation |
| `FormFeeService` | Members | Form fee logic, free slot detection |
| `ChildFeeCalculator` | Children | Child addition fee calculation |
| `SpouseFeeCalculator` | Spouses | Spouse addition fee calculation |
| `GLSyncService` | Accounting | Auto-post journal entries from events |
| `JournalService` | Accounting | Manual journal entry creation |
| `MemberSearchService` | Members | Full-text member search |
| `MemberNumberGenerator` | Members | Sequential membership numbers |
| `FormNumberGenerator` | Members | Sequential form numbers |
| `NationalIdParser` | Members | Extract DOB/gender/governorate from NID |
| `AuditService` | Audit | Manual audit logging + hook registration |
| `AttachmentService` | Support | File upload handling |
| `PayrollCalculationService` | HR | Salary computation |
| `LeaveService` | HR | Leave request workflow |

---

## 13. COMMON PITFALLS

1. **CLI context**: `App::getInstance()->db()` is null. Seeds receive `Database` as param.
2. **Table existence**: Never `SHOW TABLES LIKE ?` — use `information_schema.tables` query.
3. **Soft delete column**: Not all tables have `is_archived`. Check migration first.
4. **strict_types**: All files declare it. Type mismatches = fatal error.
5. **Empty string to DECIMAL**: Convert `''` to `null` before DB insert/update.
6. **bcmath strings**: Always pass strings to bcmath functions, never floats.
7. **Route params**: Come as strings. Cast: `(int) $id` before DB queries.
8. **CSRF**: Required on all POST/PUT/DELETE. Missing = 419 error.
9. **Flash messages**: Consumed on read. Don't read twice.
10. **Event priority**: Higher number = runs first. Default = 0.

---

## 14. ADDING A NEW MODULE (Checklist)

1. Create `app/Modules/{Name}/` directory structure
2. Create `bootstrap.php` — register permissions, menu, event listeners
3. Create `Routes.php` — define all routes
4. Create Controller(s) extending `App\Core\Controller`
5. Create Model(s) extending `App\Core\Model` with `$table`, `$fillable`
6. Create migration `database/migrations/Phase_NN_NNN_create_xxx_table.php`
7. Create seed if needed `database/seeds/Phase_NN_NNN_seed_xxx.php`
8. Create Views using `$__template->layout('Layout.main')`
9. Run `php cli.php migrate` then `php cli.php seed`
10. Verify via UI — check permissions, menu visibility, CRUD operations
