# ServiceCatalog Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The ServiceCatalog module is the **service price registry** for the ERP. It manages:
- All service/fee definitions with their prices (form fees, subscription rates, membership values, carnet fees, etc.)
- Time-bounded pricing (effective_from/effective_to for historical and future pricing)
- Branch-specific pricing with global fallback
- Multiple price types: fixed amount, percentage-based, annual amounts
- The `ServicePrice::getPrice()` API used by other modules to look up current fees

It does **NOT** directly manage:
- Fee calculations (Pricing module's PricingEngine handles logic)
- Business rules (Rules module)
- Special discounts (Pricing module owns those)
- Payment processing (Payments/Cashier modules)

---

## 2. Directory & File Structure

```
app/Modules/ServiceCatalog/
├── bootstrap.php                      # Empty — menu/permissions registered in Rules/bootstrap.php
├── Routes.php                         # Web routes (3 routes)
├── Controllers/
│   └── CatalogController.php         # Index, edit, update (simple CRUD)
├── Models/
│   └── ServicePrice.php              # service_catalog model with getPrice(), getServiceRow(), allActive()
└── Views/
    ├── index.php                     # Service list (all active services)
    └── edit.php                      # Edit single service (base_amount, percentage, annual_amount)
```

---

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

### 3.1 `service_catalog` Table (52 active rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| service_code | varchar(50) | NO | MUL | Unique-ish code (e.g., SVC_NEW_FORM) |
| name_ar | varchar(200) | NO | | Arabic service name |
| name_en | varchar(200) | YES | | English service name |
| price_type | varchar(50) | NO | | Default: 'fixed' (fixed/percentage) |
| base_amount | decimal(15,2) | YES | | Fixed price amount (NULL for percentage-only) |
| percentage | decimal(5,2) | YES | | Percentage value (NULL for fixed-only) |
| annual_amount | decimal(15,2) | YES | | Annual recurring amount |
| currency | varchar(3) | NO | | Default: 'EGP' |
| applies_to | varchar(100) | YES | | Target entity type |
| branch_id | bigint unsigned | YES | MUL | FK to branches (NULL = global) |
| effective_from | date | NO | | Start of validity |
| effective_to | date | YES | | End of validity (NULL = indefinite) |
| 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 |

---

## 4. Routes (3 endpoints)

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /catalog | CatalogController@index | pricing.view |
| GET | /catalog/{id}/edit | CatalogController@edit | pricing.edit |
| POST | /catalog/{id} | CatalogController@update | pricing.edit |

**Note:** ServiceCatalog reuses `pricing.view` and `pricing.edit` permissions — it does not have its own permission namespace.

---

## 5. ServicePrice Model — API Methods

### 5.1 `getPrice(serviceCode, default?, branchId?)`
- **Primary lookup method** used system-wide for fee amounts
- Branch-specific first → global fallback
- Date-bounded: only returns prices where `effective_from <= today <= effective_to`
- Returns `base_amount` string or `$default` if not found

### 5.2 `getServiceRow(serviceCode, branchId?)`
- Returns the full row (for percentage-based services needing all columns)
- Same branch → global fallback logic

### 5.3 `allActive()`
- Returns all active services with branch name (LEFT JOIN branches)
- Used by the index view

---

## 6. Service Code Catalog (52 active entries)

### Form Fees
| Code | Name | Amount |
|------|------|--------|
| SVC_NEW_FORM | Form fee (new membership) | 505.00 EGP |
| SVC_ADDITION_FORM | Addition form fee | 570.00 EGP |
| SVC_TRANSFER_FORM | Transfer/separation form fee | 570.00 EGP |
| SVC_DEATH_FORM | Death transfer form fee | 570.00 EGP |
| SVC_DIVORCE_FORM | Divorce form fee | 570.00 EGP |

### Membership Values (current — since 2024-07-01)
| Code | Name | Amount |
|------|------|--------|
| SVC_MEMBERSHIP_HIGH_QUAL | High qualification | 150,000.00 EGP |
| SVC_MEMBERSHIP_MED_QUAL | Medium qualification | 225,000.00 EGP |
| SVC_MEMBERSHIP_NO_QUAL | No qualification | 300,000.00 EGP |

### Membership Values (old — 2023-01-01 to 2024-06-30)
| Code | Name | Amount |
|------|------|--------|
| SVC_MEMBERSHIP_HIGH_QUAL_OLD | High qualification (old) | 114,000.00 EGP |
| SVC_MEMBERSHIP_MED_QUAL_OLD | Medium qualification (old) | 171,000.00 EGP |
| SVC_MEMBERSHIP_NO_QUAL_OLD | No qualification (old) | 228,000.00 EGP |

### Annual Subscriptions (2025/2026)
| Code | Name | Amount |
|------|------|--------|
| SVC_ANNUAL_MEMBER | Member annual subscription | 492.00 EGP |
| SVC_ANNUAL_SPOUSE | Spouse annual subscription | 492.00 EGP |
| SVC_ANNUAL_CHILD | Child annual subscription | 222.00 EGP |
| SVC_ANNUAL_TEMP | Temp member annual subscription | 222.00 EGP |

### Annual Subscriptions (historical)
| Code Pattern | Years Available | Notes |
|--------------|----------------|-------|
| SVC_ANNUAL_*_2023 | 2023/2024 | Member/Spouse: 410, Child/Temp: 185 |
| SVC_ANNUAL_*_2024 | 2024/2025 | Member/Spouse: 410, Child/Temp: 185 |
| SVC_ANNUAL_*_2025 | 2025/2026 | Member/Spouse: 492, Child/Temp: 222 |

### Other Services
| Code | Name | Amount |
|------|------|--------|
| SVC_DEV_FEE | Development fee | 35.00 EGP |
| SVC_MARTYRS_STAMP | Martyrs stamp | 5.00 EGP |
| SVC_CARNET_REPLACE | Carnet replacement | 200.00 EGP |
| SVC_CARNET_REPLACEMENT | Carnet replacement (alt code) | 200.00 EGP |
| SVC_FOREIGN_MEMBER | Foreign membership | 5,000.00 EGP |
| SVC_SEASONAL | Seasonal membership | NULL (percentage-based) |
| SVC_SEASONAL_MEMBER | Seasonal member fee | NULL (percentage-based) |

### Percentage-Based Services (base_amount = NULL)
| Code | Name | Notes |
|------|------|-------|
| SVC_CHILD_18 through SVC_CHILD_21 | Child fee by age | Percentage of membership value |
| SVC_CHILD_4TH | 4th+ child under 18 | Percentage of membership value |
| SVC_SPOUSE_2ND through SVC_SPOUSE_4TH | Spouse fees | Percentage of membership value |
| SVC_SPOUSE_BASE / SVC_SPOUSE_ACQUIRED / SVC_SPOUSE_FOREIGN | Spouse variants | Percentage of membership value |
| SVC_TEMP_MEMBER | Temporary member | Percentage of membership value |

---

## 7. Events Dispatched

None — ServiceCatalog does not dispatch events.

---

## 8. Events Consumed

None — ServiceCatalog does not listen for events.

---

## 9. Cross-Module Dependencies

### 9.1 ServiceCatalog IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| (None) | ServiceCatalog is self-contained — only uses App core (DB) |

### 9.2 Other modules that IMPORT FROM ServiceCatalog:

| Module | What it imports | Usage |
|--------|----------------|-------|
| Pricing | Reads/writes `service_catalog` directly via DB | PricingDashboardController updates annual rates + membership prices |
| Pricing | PricingEngine::getServiceFee() queries service_catalog | Fee lookups |
| Members | ServicePrice::getPrice() / FormFeeService | Form fee amounts |
| Members | BillingService | Subscription rates lookup |
| Members | MemberNumberGenerator / FormNumberGenerator | (indirect via service fees) |
| Death | service_catalog lookups | Death form fee |
| Divorce | DivorceFeeCalculator | Divorce form fee |
| Payments | BalanceCalculator | Service fee reference |
| Spouses | SpouseFeeCalculator | Spouse fee lookups |
| Subscriptions | SubscriptionGenerator | Annual subscription rates |
| Transfers | SeparationFeeCalculator | Transfer form fee |
| Audit | AuditService references service_catalog | Audit context |

---

## 10. Permissions

ServiceCatalog reuses the **Pricing module's permissions**:
- `pricing.view` — View service catalog
- `pricing.edit` — Edit service prices

No separate permission namespace.

---

## 11. Menu Structure

ServiceCatalog appears as a child item in the **Rules & Pricing** menu group (registered in `Rules/bootstrap.php`):
- Route: `/catalog`
- Label: "Service Catalog" / "كتالوج الخدمات"
- Order: 3 (after Rules Engine and Pricing)

---

## 12. High-Risk Areas

### 12.1 Dual Write Path — Pricing Dashboard vs. CatalogController
- `CatalogController@update` allows editing base_amount, percentage, annual_amount for any service
- `PricingDashboardController@saveAnnualRates` and `saveMembershipPrices` also write to service_catalog
- `PricingDashboardController@addYear` inserts new rows into service_catalog
- These two write paths have different validation logic — risk of inconsistency

### 12.2 service_code Not Truly Unique
- The `service_code` column is indexed (MUL) but NOT unique
- Multiple rows can share the same code with different effective_from/effective_to ranges
- Multiple rows can share the same code with different branch_id values
- `getPrice()` uses `ORDER BY effective_from DESC LIMIT 1` to pick the most recent valid entry
- If two entries overlap in date range, behavior depends on insertion order

### 12.3 Percentage-Based Services Have NULL base_amount
- Many services (spouse fees, child fees, temp member) have `base_amount = NULL`
- `getPrice()` returns NULL for these — callers must handle null
- Actual calculation uses the percentage field via PricingEngine + RuleEngine, NOT ServicePrice::getPrice()
- These catalog entries exist more as documentation/reference than as actual price lookups

### 12.4 No Validation on Update
- `CatalogController@update` accepts raw POST data with minimal validation
- No check that base_amount is non-negative
- No check that effective dates are logical
- No audit trail for price changes (no version history unlike business_rules)

### 12.5 Financial Year Boundaries
- Annual subscription services use convention: `SVC_ANNUAL_{TYPE}_{YEAR}`
- `effective_from` = July 1 of year, `effective_to` = June 30 of next year
- Non-year-suffixed codes (SVC_ANNUAL_MEMBER) also exist as "current" fallback
- `SubscriptionGenerator` must pick the correct year-specific rate

---

## 13. Known Patterns & Gotchas

1. **No bootstrap.php logic**: ServiceCatalog's bootstrap.php is empty — menu/permissions are registered by Rules module
2. **No soft delete**: Uses `is_active` flag instead
3. **Branch fallback**: `getPrice()` tries branch-specific first, then `branch_id IS NULL`
4. **Duplicate codes exist**: e.g., `SVC_CARNET_REPLACE` and `SVC_CARNET_REPLACEMENT` both active
5. **Old pricing preserved**: `_OLD` suffix codes keep historical prices (e.g., SVC_MEMBERSHIP_HIGH_QUAL_OLD)
6. **price_type field**: Exists as 'fixed' or 'percentage' but not heavily enforced in code
7. **applies_to field**: Exists but appears unused in current queries
8. **Currency always EGP**: Foreign member fee is stored as 5000 EGP (not USD) in service_catalog — USD handling is in business_rules

---

## Appendix: Files to Check Before Modifying ServiceCatalog

1. `app/Modules/Pricing/Controllers/PricingDashboardController.php` — Writes to service_catalog directly
2. `app/Modules/Pricing/Services/PricingEngine.php` — getServiceFee() queries service_catalog
3. `app/Modules/Members/Services/BillingService.php` — Subscription rate lookups
4. `app/Modules/Members/Services/FormFeeService.php` — Form fee amount
5. `app/Modules/Subscriptions/Services/SubscriptionGenerator.php` — Annual rates
6. `app/Modules/Subscriptions/Services/SubscriptionCalculator.php` — Rate lookups
7. `app/Modules/Transfers/Services/SeparationFeeCalculator.php` — Transfer form fee
