# Pricing Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Pricing module is the **unified pricing administration dashboard** for the ERP. It manages:
- Membership price configuration by branch + qualification (via `pricing_configs` table)
- Special discounts (percentage-based member discounts with document requirements)
- Annual subscription rate management (member, spouse, child, temp — per financial year)
- Membership price tiers (high/medium/no qualification)
- Rule versioning UI (adding historical rule versions through the Pricing dashboard)
- Pricing calculation engine (membership, child, spouse, separation, installment fees)
- Service fee lookups (delegates to `service_catalog`)
- Discount rules (quantity/stackable discounts for group membership)

It does **NOT** directly manage:
- Business rules (Rules module owns `business_rules` table — Pricing reads/updates via RuleEngine)
- Service catalog entries (ServiceCatalog module owns CRUD — Pricing updates amounts via dashboard)
- Payments or billing (Members/Payments modules)
- Subscriptions (Subscriptions module)

---

## 2. Directory & File Structure

```
app/Modules/Pricing/
├── bootstrap.php                              # Menu + permission registration
├── Routes.php                                 # Web routes (16 routes)
├── Controllers/
│   ├── PricingDashboardController.php        # Main dashboard: rules by category, annual rates, membership prices
│   ├── PricingController.php                 # Legacy pricing_configs CRUD
│   └── SpecialDiscountController.php         # Special discount CRUD + toggle active
├── Models/
│   ├── PricingConfig.php                     # pricing_configs model (branch+qual pricing)
│   ├── SpecialDiscount.php                   # special_discounts model with search/pagination
│   └── DiscountRule.php                      # discount_rules model (group/quantity discounts)
├── Services/
│   ├── PricingEngine.php                     # Core calculation engine (membership, child, spouse, separation, installment)
│   └── RuleFieldMapper.php                   # Maps rule JSON fields to UI form controls + category config
└── Views/
    ├── dashboard.php                         # Main pricing dashboard (grouped rules + annual rates + membership prices)
    ├── index.php                             # Legacy pricing configs list
    ├── edit.php                              # Edit single pricing config
    └── special_discounts/
        ├── index.php                         # Special discount list with filters
        └── form.php                          # Create/edit special discount form
```

---

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

### 3.1 `pricing_configs` Table (12 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| branch_id | bigint unsigned | NO | MUL | FK to branches |
| qualification_id | bigint unsigned | NO | MUL | FK to qualifications |
| membership_type | varchar(50) | NO | MUL | Default: 'working' |
| price | decimal(15,2) | NO | | Membership value |
| currency | varchar(3) | NO | | Default: 'EGP' |
| effective_from | date | NO | MUL | Start of validity |
| effective_to | date | YES | | End of validity (NULL = current) |
| is_active | tinyint(1) | NO | | Default: 1 |
| notes | 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 |

### 3.2 `special_discounts` Table (0 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| name_ar | varchar(200) | NO | | Arabic name |
| name_en | varchar(200) | YES | | English name |
| discount_percentage | decimal(5,2) | NO | | Default: 0.00 |
| requires_document | tinyint(1) | NO | | Default: 0 |
| description | text | YES | | |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| 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 |

### 3.3 `discount_rules` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| discount_code | varchar(50) | NO | MUL | Unique code |
| name_ar | varchar(200) | NO | | Arabic name |
| name_en | varchar(200) | YES | | English name |
| discount_type | varchar(30) | NO | | Default: 'percentage' |
| value | decimal(15,2) | NO | | Discount value |
| min_quantity | int unsigned | YES | | Min quantity for eligibility |
| max_quantity | int unsigned | YES | | Max quantity cap |
| stackable | tinyint(1) | NO | | Default: 0 |
| max_discount_percentage | decimal(5,2) | YES | | Cap on total discount |
| branch_id | bigint unsigned | YES | MUL | FK to branches (NULL = global) |
| effective_from | date | NO | | |
| effective_to | date | YES | | |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | On update cascade |
| created_by | bigint unsigned | YES | | |
| updated_by | bigint unsigned | YES | | |

### 3.4 Related Tables (cross-module)

| Table | Owner Module | Relationship |
|-------|--------------|--------------|
| business_rules | Rules | Read/updated by PricingDashboardController via RuleEngine |
| service_catalog | ServiceCatalog | Updated by PricingDashboardController (annual rates, membership prices) |
| branches | Branches | Referenced by pricing_configs.branch_id |
| qualifications | (lookup) | Referenced by pricing_configs.qualification_id |
| members | Members | References special_discounts.id via special_discount_id |

---

## 4. Routes (16 endpoints)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /pricing | PricingDashboardController@index | pricing.view |
| POST | /pricing/section/{category} | PricingDashboardController@saveSection | pricing.edit |
| POST | /pricing/annual-rates | PricingDashboardController@saveAnnualRates | pricing.edit |
| POST | /pricing/annual-rates/add-year | PricingDashboardController@addYear | pricing.edit |
| POST | /pricing/membership-prices | PricingDashboardController@saveMembershipPrices | pricing.edit |
| GET | /pricing/rule/{ruleCode}/versions | PricingDashboardController@ruleVersions | pricing.view |
| POST | /pricing/rule/{ruleCode}/add-version | PricingDashboardController@addRuleVersion | pricing.edit |
| GET | /pricing/configs | PricingController@index | pricing.view |
| GET | /pricing/configs/{id}/edit | PricingController@edit | pricing.edit |
| POST | /pricing/configs/{id} | PricingController@update | pricing.edit |
| GET | /pricing/special-discounts | SpecialDiscountController@index | pricing.special_discounts.view |
| GET | /pricing/special-discounts/create | SpecialDiscountController@create | pricing.special_discounts.create |
| POST | /pricing/special-discounts | SpecialDiscountController@store | pricing.special_discounts.create |
| GET | /pricing/special-discounts/{id}/edit | SpecialDiscountController@edit | pricing.special_discounts.edit |
| POST | /pricing/special-discounts/{id} | SpecialDiscountController@update | pricing.special_discounts.edit |
| POST | /pricing/special-discounts/{id}/toggle | SpecialDiscountController@toggleActive | pricing.special_discounts.edit |

---

## 5. PricingEngine — Core Calculation Service

The `PricingEngine` is a **static utility class** used across the system for all fee calculations:

### 5.1 `getMembershipPrice(branchId, qualificationCode, date)`
- Looks up `pricing_configs` for branch + qualification + membership_type='working'
- Time-bound (effective_from/effective_to)
- Returns: `{price, currency, config_id, error}`

### 5.2 `calculateChildFee(membershipValue, childAge, childOrder)`
- Uses RuleEngine rules: INITIAL_FREE_CHILDREN_COUNT, CHILD_INCLUDED_MAX_AGE, CHILD_MAX_AGE
- Age-bracket percentages: CHILD_4TH_UNDER_18_FEE, CHILD_FEE_AGE_18/19/20/21
- Returns: `{fee, rule_applied, percentage, classification}`
- Classification: included | dependent_with_fee | regular | temporary | not_accepted

### 5.3 `calculateSpouseFee(membershipValue, spouseOrder, nationality, marriageDate, membershipAcquisitionDate, memberType)`
- 1st spouse: free
- Foreign/acquired: SPOUSE_FOREIGN_FEE / SPOUSE_ACQUIRED_MEMBER_FEE
- 2nd/3rd/4th+: SPOUSE_2ND_FEE / SPOUSE_3RD_FEE / SPOUSE_4TH_FEE
- Retroactive annual fee: calculated from marriage-to-membership year gap
- Returns: `{percentage_fee, annual_fee, total, years_count, rule_applied}`

### 5.4 `calculateSeparationFee(newMembershipValue, yearsSinceAcquisition)`
- Uses: SEPARATION_FEE_YEAR_1 through SEPARATION_FEE_YEAR_6_PLUS
- Returns: `{fee, percentage, rule_applied, years}`

### 5.5 `calculateInstallmentPlan(totalAmount, downPayment, months)`
- Uses: INSTALLMENT_INTEREST_RATE (annual → monthly)
- Generates full amortization schedule
- Returns: `{total_amount, down_payment, remaining, annual_rate, total_interest, monthly_payment, months, schedule[]}`

### 5.6 `getServiceFee(serviceCode, branchId)` / `getFormFee(formType)`
- Lookups from `service_catalog` table
- Branch-specific → global fallback
- Code mapping: NEW_MEMBERSHIP → SVC_NEW_FORM, ADDITION → SVC_ADDITION_FORM, TRANSFER_SEPARATION → SVC_TRANSFER_FORM

---

## 6. Events Dispatched

None — the Pricing module does not dispatch events.

---

## 7. Events Consumed

None — the Pricing module does not listen for events.

---

## 8. Cross-Module Dependencies

### 8.1 Pricing module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Rules | RuleEngine (get, require, requireValue, update, addHistoricalVersion, getVersionHistory) |
| ServiceCatalog | Reads/writes `service_catalog` table directly (annual rates, membership prices) |

### 8.2 Other modules that IMPORT FROM Pricing:

| Module | What it imports |
|--------|----------------|
| Members | PricingEngine (getMembershipPrice), SpecialDiscount model |
| Children | PricingEngine (calculateChildFee), ChildFeeCalculator |
| Spouses | SpouseFeeCalculator uses PricingEngine |
| Transfers | SeparationFeeCalculator uses PricingEngine |
| Carnets | PricingEngine (getServiceFee for carnet replacement) |
| Sports | SportsConversionCalculator uses PricingEngine |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| pricing.view | View pricing dashboard and configs |
| pricing.edit | Edit prices, fees, and rules |
| pricing.special_discounts.view | View special discounts list |
| pricing.special_discounts.create | Create new special discounts |
| pricing.special_discounts.edit | Edit/toggle special discounts |

---

## 10. Configuration Dependencies

### Rule Engine Categories Used by Pricing Dashboard

| Category | Description | Rule Count |
|----------|-------------|------------|
| financial | Form fees, development fee, installments | 11 |
| spouse_fee | Spouse addition percentages + annual flat fees | 11 |
| children_fee | Child fees by age bracket + free limits | 12 |
| separation_fee | Separation/waiver fees by year | 8 |
| divorce | Divorce case fees | 9 |
| penalty | Late fines, violation limits, drop rules | 14 |
| discount | Group/branch discounts | 6 |
| foreign | Foreign/honorary/sports membership fees | 6 |
| temporary | Temp/seasonal membership rules | 5 |
| membership | Nanny/temp membership fees | 3 |
| age | Age limits and thresholds | 17 |
| workflow | Collection schedules, form expiry, freeze rules | 11 |

### Service Catalog Codes Managed via Dashboard

| Code Pattern | Purpose |
|--------------|---------|
| SVC_ANNUAL_MEMBER_{YEAR} | Annual member subscription |
| SVC_ANNUAL_SPOUSE_{YEAR} | Annual spouse subscription |
| SVC_ANNUAL_CHILD_{YEAR} | Annual child subscription |
| SVC_ANNUAL_TEMP_{YEAR} | Annual temp subscription |
| SVC_MEMBERSHIP_HIGH_QUAL | Membership value (high qualification) |
| SVC_MEMBERSHIP_MED_QUAL | Membership value (medium qualification) |
| SVC_MEMBERSHIP_NO_QUAL | Membership value (no qualification) |

---

## 11. High-Risk Areas

### 11.1 PricingEngine (CRITICAL)
- **Central fee calculation authority** — used by Members, Children, Spouses, Transfers, Sports
- All calculations use bcmath for precision (bcmul, bcdiv, bcadd, bcsub)
- If PricingEngine returns wrong values, members pay incorrect amounts
- Heavy dependency on RuleEngine: if a required rule is missing, throws RuntimeException

### 11.2 Dashboard Writes to Cross-Module Tables
- `saveAnnualRates()` writes directly to `service_catalog` (ServiceCatalog's table)
- `saveMembershipPrices()` writes directly to `service_catalog`
- `addYear()` inserts new rows into `service_catalog`
- `saveSection()` updates business rules via `RuleEngine::update()` (Rules' table)
- These bypass the ServiceCatalog and Rules controllers — no additional validation

### 11.3 Rule Version Management
- `addRuleVersion()` adds historical rule versions to `business_rules`
- Overlap detection prevents overlapping date ranges, but off-by-one errors in effective_from/effective_to can create gaps
- Changing a rule's value immediately affects all downstream fee calculations system-wide

### 11.4 pricing_configs vs service_catalog Dual Source
- Membership prices exist in BOTH `pricing_configs` (branch+qualification matrix) AND `service_catalog` (SVC_MEMBERSHIP_* entries)
- `PricingEngine::getMembershipPrice()` uses `pricing_configs`
- `Members/BillingService` may use `service_catalog` for same data
- Keeping both in sync is manual — risk of divergence

### 11.5 Discount Percentage Validation
- Special discount percentage validated as 0.01-100% with bccomp
- No validation that applying discount won't result in negative membership value
- No cap on combined discounts (special + board offer + group)

---

## 12. Known Patterns & Gotchas

1. **No soft delete**: Neither `pricing_configs`, `special_discounts`, nor `discount_rules` use soft delete — records use `is_active` flag and `effective_to` date instead
2. **Dashboard is the primary UI**: The `/pricing` dashboard groups rules by category and renders editable forms via `RuleFieldMapper` — individual rule editing via `/rules` is secondary
3. **PricingEngine is stateless**: All methods are static, no caching of calculation results
4. **JSON value format**: Rule values are stored as JSON objects (e.g., `{"percentage": "10", "annual_flat": "150"}`) — PricingEngine expects specific keys
5. **Branch-specific fallback**: Both PricingEngine and RuleEngine try branch-specific first, then global — important for multi-branch pricing
6. **No events dispatched**: Price changes are NOT broadcast to other modules — downstream modules always read current prices at calculation time
7. **Currency hardcoded**: Almost all pricing assumes EGP except foreign member rules (USD)

---

## Appendix: Files to Check Before Modifying Pricing

1. `app/Modules/Members/Services/BillingService.php` — Aggregates all fees, calls PricingEngine
2. `app/Modules/Children/Services/ChildFeeCalculator.php` — Uses PricingEngine.calculateChildFee
3. `app/Modules/Spouses/Services/SpouseFeeCalculator.php` — Uses PricingEngine.calculateSpouseFee
4. `app/Modules/Transfers/Services/SeparationFeeCalculator.php` — Uses PricingEngine.calculateSeparationFee
5. `app/Modules/Installments/Services/InstallmentCalculator.php` — Uses PricingEngine.calculateInstallmentPlan
6. `app/Modules/Rules/Services/RuleEngine.php` — All rule reads/writes go through here
7. `app/Modules/ServiceCatalog/Models/ServicePrice.php` — getPrice() method parallels PricingEngine.getServiceFee()
