# Rentals Module — Architecture Map

> **Last updated:** 2026-07-18
> **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 + MenuRegistry (sidebar under العمليات, order 290)
├── Routes.php                             # Web routes (17 routes)
├── Controllers/
│   └── RentalController.php              # Single controller (entities + contracts + deposits + invoices)
├── Models/
│   ├── RentalEntity.php                  # Tenant entity model (soft delete)
│   ├── RentalContract.php                # Contract model (soft delete)
│   ├── RentalBooking.php                 # Individual booking slots (no soft delete)
│   └── RentalInvoice.php                 # Monthly invoice model (no soft delete)
├── Services/
│   ├── RentalContractService.php         # Contract creation, activation, booking generation
│   ├── RentalDepositService.php          # Deposit collect/refund lifecycle
│   └── RentalInvoiceService.php          # Invoice generation, late fee calc, mark paid
└── 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 (VAT + utilities + late fee + live calc)
    ├── contract_show.php                 # Contract detail with invoices + bookings + deposit
    ├── invoice_form.php                  # Create invoice for a contract
    └── invoice_show.php                  # Invoice detail + pay action
```

---

## 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 (net rental) |
| vat_percentage | decimal(5,2) | NO | | 1.00 | Always 1% by law |
| vat_amount | decimal(15,2) | NO | | 0.00 | total_amount * vat_percentage / 100 |
| utilities_percentage | decimal(5,2) | NO | | 0.00 | % of monthly base for utilities |
| utilities_amount | decimal(15,2) | NO | | 0.00 | total_amount * utilities_percentage / 100 |
| grand_total | decimal(15,2) | NO | | 0.00 | total + utilities + vat (full contract) |
| late_fee_type | varchar(10) | NO | | none | none/daily/weekly/monthly |
| late_fee_rate | decimal(5,2) | NO | | 0.00 | % of invoice total per late period |
| deposit_percentage | decimal(5,2) | NO | | 10.00 | % of grand_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.4 `rental_invoices` Table

| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| invoice_number | varchar(50) | NO | UNI | | Format: RI-YYYYMM-XXXX |
| contract_id | bigint unsigned | NO | MUL | | FK to rental_contracts |
| entity_id | bigint unsigned | NO | MUL | | FK to rental_entities |
| period_start | date | NO | | | Billing period start |
| period_end | date | NO | | | Billing period end |
| due_date | date | NO | MUL | | Payment due date |
| base_amount | decimal(15,2) | NO | | 0.00 | Monthly share of net rental |
| utilities_amount | decimal(15,2) | NO | | 0.00 | Utilities surcharge |
| vat_amount | decimal(15,2) | NO | | 0.00 | 1% of (base + utilities) |
| late_fee_amount | decimal(15,2) | NO | | 0.00 | Set at payment if overdue |
| total_amount | decimal(15,2) | NO | | 0.00 | base + utilities + vat + late_fee |
| status | varchar(20) | NO | MUL | unpaid | unpaid/paid/overdue/cancelled |
| paid_at | timestamp | YES | | | |
| payment_id | bigint unsigned | YES | | | FK to external payment |
| notes | text | YES | | | |
| journal_entry_id | bigint unsigned | YES | | | FK to journal_entries |
| 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 |
| GET | /rentals/contracts/{id}/invoices/create | RentalController@createInvoice | rental.manage_contract |
| POST | /rentals/contracts/{id}/invoices | RentalController@storeInvoice | rental.manage_contract |
| POST | /rentals/contracts/{id}/invoices/bulk-generate | RentalController@bulkGenerateInvoices | rental.manage_contract |
| GET | /rentals/invoices/{id} | RentalController@showInvoice | rental.view |
| POST | /rentals/invoices/{id}/pay | RentalController@payInvoice | rental.manage_contract |

---

## 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 |
| `rental.invoice_paid` | `{invoice_id, contract_id, payment_id, base_amount, utilities_amount, vat_amount, late_fee_amount, total_amount, paid_at}` | RentalInvoiceService::markPaid |

---

## 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 |
| `rental.invoice_paid` | Accounting | Dr. Cash, Cr. RentalRevenue(410521) + ServiceRevenue(410515) + TaxPayable(230804) + FineRevenue(410512) |

---

## 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 Invoice Late Fee Applied at Payment Time
- Late fee is NOT stored on the invoice at generation — it is calculated in `RentalInvoiceService::calcLateFee()` when `markPaid()` is called
- If `paid_at` ≤ `due_date`, late fee = 0
- If overdue: periods = ceil(days / period_length); fee = total × rate% × periods

---

## 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%) |
| RENTAL_VAT_PCT | VAT percentage on rental invoices (default: 1.00%, seeded via Phase_95_001) |

---

## 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
