# Rentals Module — Architecture Map

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

---

## 1. Purpose & Responsibilities

The Rentals module manages **facility rentals to external entities** (schools, universities, embassies, corporations, etc.). It handles:
- Rental entity (tenant) registration and management
- Contract creation with financial calculations (subtotal, bulk discount, deposit)
- Contract approval workflow
- Booking slot generation from contracts
- Deposit lifecycle (collection, refund request, refund processing)
- Technical report submission tracking (gates deposit refund)

It does **NOT** directly manage:
- Facility definitions (Facilities module owns those)
- Payments/cashier processing (relies on external payment IDs)
- Accounting journal entries (Accounting module listens to events)
- Member/individual reservations (Reservations module handles those)

---

## 2. Directory & File Structure

```
app/Modules/Rentals/
├── bootstrap.php                          # Permission registration (5 permissions)
├── Routes.php                             # Web routes (13 routes)
├── Controllers/
│   └── RentalController.php              # Single controller (entities + contracts + deposits)
├── Models/
│   ├── RentalEntity.php                  # Tenant entity model (soft delete)
│   ├── RentalContract.php                # Contract model (soft delete)
│   └── RentalBooking.php                 # Individual booking slots (no soft delete)
├── Services/
│   ├── RentalContractService.php         # Contract creation, activation, booking generation
│   └── RentalDepositService.php          # Deposit collect/refund lifecycle
└── Views/
    ├── index.php                         # Contract list with filters
    ├── entities.php                      # Entity list with filters
    ├── entity_form.php                   # Create/edit entity form
    ├── entity_show.php                   # Entity detail with contracts
    ├── contract_form.php                 # Create contract form
    └── contract_show.php                 # Contract detail with bookings + deposit status
```

---

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

### 3.1 `rental_entities` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| entity_type | varchar(50) | NO | MUL | | school/university/embassy/federation/private/corporate |
| name_ar | varchar(300) | NO | | | Arabic name |
| name_en | varchar(300) | YES | | | English name |
| contact_person | varchar(200) | YES | | | |
| contact_phone | varchar(30) | YES | | | |
| contact_email | varchar(200) | YES | | | |
| address | text | YES | | | |
| tax_number | varchar(50) | YES | | | |
| official_letter_received | tinyint(1) | NO | | 0 | |
| board_approved | tinyint(1) | NO | | 0 | |
| approval_date | date | YES | | | |
| status | varchar(20) | NO | MUL | pending | pending/approved/active/suspended/blacklisted |
| notes | text | YES | | | |
| is_archived | tinyint(1) | NO | | 0 | Soft delete |
| archived_at | timestamp | YES | | | |
| archived_by | bigint unsigned | YES | | | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |
| created_by | bigint unsigned | YES | | | FK to employees |
| updated_by | bigint unsigned | YES | | | FK to employees |

### 3.2 `rental_contracts` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| contract_number | varchar(50) | NO | UNI | | Format: RC-YYYYMM-XXXX |
| entity_id | bigint unsigned | NO | MUL | | FK to rental_entities |
| facility_id | bigint unsigned | NO | MUL | | FK to facilities |
| activity_type | varchar(30) | NO | | practice | |
| start_date | date | NO | MUL | | |
| end_date | date | NO | | | |
| total_units | int unsigned | NO | | 0 | Number of booking units |
| unit_rate | decimal(15,2) | NO | | 0.00 | Rate per unit |
| time_tier | varchar(5) | NO | | AM | AM or PM |
| subtotal | decimal(15,2) | NO | | 0.00 | units * rate |
| discount_percentage | decimal(5,2) | NO | | 0.00 | Bulk discount % |
| discount_amount | decimal(15,2) | NO | | 0.00 | |
| total_amount | decimal(15,2) | NO | | 0.00 | subtotal - discount |
| deposit_percentage | decimal(5,2) | NO | | 10.00 | % of total for deposit |
| deposit_amount | decimal(15,2) | NO | | 0.00 | |
| deposit_status | varchar(20) | NO | | pending | pending/collected/partially_refunded/refunded |
| deposit_payment_id | bigint unsigned | YES | MUL | | FK to external payment |
| technical_report_submitted | tinyint(1) | NO | | 0 | Gates deposit refund |
| status | varchar(20) | NO | MUL | draft | draft/pending_approval/approved/active/completed/cancelled/terminated |
| approved_by | bigint unsigned | YES | | | FK to employees |
| approved_at | timestamp | YES | | | |
| terms_json | json | YES | | | Contract terms as JSON |
| notes | text | YES | | | |
| is_archived | tinyint(1) | NO | | 0 | Soft delete |
| archived_at | timestamp | YES | | | |
| archived_by | bigint unsigned | YES | | | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |
| created_by | bigint unsigned | YES | | | |
| updated_by | bigint unsigned | YES | | | |

### 3.3 `rental_bookings` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| contract_id | bigint unsigned | NO | MUL | | FK to rental_contracts |
| facility_id | bigint unsigned | NO | MUL | | FK to facilities |
| booking_date | date | NO | MUL | | |
| start_time | time | NO | | | |
| end_time | time | NO | | | |
| status | varchar(20) | NO | | scheduled | scheduled/completed/cancelled/no_show |
| notes | text | YES | | | |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |

---

## 4. Routes

| Method | Path | Handler | Permission |
|--------|------|---------|------------|
| GET | /rentals | RentalController@index | rental.view |
| GET | /rentals/entities | RentalController@entities | rental.view |
| GET | /rentals/entities/create | RentalController@createEntity | rental.manage_entity |
| POST | /rentals/entities | RentalController@storeEntity | rental.manage_entity |
| GET | /rentals/entities/{id} | RentalController@showEntity | rental.view |
| GET | /rentals/entities/{id}/edit | RentalController@editEntity | rental.manage_entity |
| POST | /rentals/entities/{id} | RentalController@updateEntity | rental.manage_entity |
| GET | /rentals/contracts/create | RentalController@createContract | rental.manage_contract |
| POST | /rentals/contracts | RentalController@storeContract | rental.manage_contract |
| GET | /rentals/contracts/{id} | RentalController@showContract | rental.view |
| POST | /rentals/contracts/{id}/approve | RentalController@approveContract | rental.approve |
| POST | /rentals/contracts/{id}/deposit/collect | RentalController@collectDeposit | rental.manage_deposit |
| POST | /rentals/contracts/{id}/deposit/refund | RentalController@refundDeposit | rental.manage_deposit |

---

## 5. Core Business Flows

### 5.1 Contract Creation Flow

```
1. Register rental entity (entity_type, name, contact, board approval)
2. Create contract:
   - Select entity + facility
   - Set date range, total_units, unit_rate, time_tier
   - System calculates: subtotal, bulk discount (if eligible), total, deposit
3. Contract starts as 'draft'
4. Approve contract (sets approved_by, approved_at)
5. Activate contract
6. Generate booking slots (date/time for each unit)
```

### 5.2 Bulk Discount Logic

Controlled by `business_rules` table:
- `RENTAL_BULK_MIN_UNITS` (default: 24)
- `RENTAL_BULK_MIN_MONTHS` (default: 2)
- `RENTAL_BULK_DISCOUNT_PCT` (default: 15%)

If total_units >= min_units AND contract duration >= min_months, discount is applied.

### 5.3 Deposit Lifecycle

```
pending → collected (on payment)
collected → partially_refunded (refund requested, requires technical_report_submitted=1)
partially_refunded → refunded (on refund payment processing)
```

**Key invariant:** Deposit refund CANNOT be requested unless `technical_report_submitted = 1`.

### 5.4 Contract Number Format

`RC-YYYYMM-XXXX` (e.g., RC-202606-0001). Sequential within the month.

---

## 6. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `rental.contract_created` | `{contract_id, entity_id}` | RentalContractService::createContract |
| `rental.contract_activated` | `{contract_id}` | RentalContractService::activateContract |
| `rental.deposit_collected` | `{contract_id, payment_id}` | RentalDepositService::collectDeposit |
| `rental.deposit_refund_requested` | `{contract_id}` | RentalDepositService::requestRefund |
| `rental.deposit_refunded` | `{contract_id, payment_id}` | RentalDepositService::processRefund |

---

## 7. Events Consumed (by other modules)

| Event | Listener Module | Action |
|-------|----------------|--------|
| `rental.deposit_collected` | Accounting | Auto-post journal entry for deposit liability |
| `rental.deposit_refunded` | Accounting | Auto-post journal entry for deposit refund |

---

## 8. Cross-Module Dependencies

### 8.1 Rentals module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Facilities | `Facility::find()`, `Facility::allActive()` (facility lookup for contracts) |
| Rules | `RuleEngine::getValue()` (bulk discount business rule parameters) |

### 8.2 Other modules that IMPORT FROM Rentals:

| Module | What it imports |
|--------|----------------|
| Reservations | Queries `rental_bookings` table directly for conflict detection |
| Accounting | Listens to rental deposit events for journal entries |

---

## 9. Permissions

| Key | Description |
|-----|-------------|
| rental.view | View rental contracts and entities |
| rental.manage_entity | Create/edit rental entities (tenants) |
| rental.manage_contract | Create contracts |
| rental.approve | Approve contracts |
| rental.manage_deposit | Collect and refund deposits |

---

## 10. High-Risk Areas

### 10.1 Contract Number Sequencing
- Uses MAX() query on `contract_number` to determine next sequence
- Race condition possible if two contracts are created in the same month simultaneously
- Number is parsed by splitting on `-` and incrementing the last segment

### 10.2 Deposit Refund Gate
- Technical report submission is a boolean flag that gates refund eligibility
- If this flag is set incorrectly, deposits could be refunded prematurely
- No audit trail on who set `technical_report_submitted`

### 10.3 Booking-Reservation Conflict Detection
- The Reservations module queries `rental_bookings` directly via raw SQL
- If the Rentals table schema changes, the Reservations conflict detector will break silently (wrapped in try/catch)

### 10.4 No Menu Registration
- The Rentals module does NOT register a sidebar menu in `bootstrap.php`
- Only permissions are registered — navigation must be handled elsewhere or manually

---

## 11. Configuration Dependencies

| Rule Engine Keys | Purpose |
|-----------------|---------|
| RENTAL_BULK_MIN_UNITS | Minimum units for bulk discount eligibility (default: 24) |
| RENTAL_BULK_MIN_MONTHS | Minimum months for bulk discount eligibility (default: 2) |
| RENTAL_BULK_DISCOUNT_PCT | Bulk discount percentage (default: 15%) |

---

## 12. Known Patterns & Gotchas

1. **Single controller pattern**: All entities, contracts, and deposits handled by one controller (RentalController)
2. **No event listeners in bootstrap**: Rentals only dispatches events, never listens
3. **Facility dependency**: Contracts link to `facilities` table — if a facility is archived, existing contracts may display incorrectly
4. **Soft delete on entities/contracts, NOT on bookings**: RentalBooking has `$softDelete = false`
5. **terms_json is JSON column**: Stored as MySQL JSON type, decoded via `getTerms()` method
6. **Deposit amount calculated as percentage of total_amount (post-discount)**, not subtotal
