# Occasions, Occasion Prices & Discount Templates — Full Use-Case & Flow Documentation

> **Scope:** This document describes the complete behavior, data flow, API usage, and business rules for the *Occasions*, *Occasion Prices*, and *Discount Templates* features implemented in the Turista platform.

---

## Table of Contents

1. [Glossary](#1-glossary)
2. [High-Level Architecture](#2-high-level-architecture)
3. [Data Model Deep Dive](#3-data-model-deep-dive)
4. [Feature 1 — Occasions](#4-feature-1--occasions)
5. [Feature 2 — Occasion Prices](#5-feature-2--occasion-prices)
6. [Feature 3 — Discount Templates](#6-feature-3--discount-templates)
7. [Pricing Engine Flow](#7-pricing-engine-flow)
8. [Reservation Pricing Integration](#8-reservation-pricing-integration)
9. [API Reference](#9-api-reference)
10. [Authorization Matrix](#10-authorization-matrix)
11. [End-to-End Use Cases](#11-end-to-end-use-cases)
12. [Testing Coverage](#12-testing-coverage)

---

## 1. Glossary

| Term | Meaning |
|---|---|
| **Occasion** | An admin-managed label (e.g., Wedding, Eid, Birthday) that can trigger a special nightly price for a unit. |
| **Occasion Price** | A per-unit, per-occasion nightly price override. When the occasion is selected for a reservation, this price replaces the unit's `base_price` / `offer_price`. |
| **Discount Template** | A reusable discount rule owned by an owner. It can target multiple buildings and/or units and applies automatically when its date window and total-amount condition match a reservation. |
| **Assignment** | A polymorphic link between a discount template and a `Building` or `Unit`. |
| **PricingService** | Service that computes the effective nightly price, applies occasions, and applies matching discount templates. |
| **ReservationService** | Service that orchestrates reservation creation/recalculation and delegates price computation to `PricingService`. |
| **Pre-promo subtotal** | The reservation subtotal after occasion pricing and discount templates, but before any promo code is applied. |

---

## 2. High-Level Architecture

```text
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Admin Portal                                    │
│  • Create / edit / delete occasions                                          │
│  • Approve or reject owner-requested occasions                               │
└─────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Owner Portal                                    │
│  • Request a new occasion                                                    │
│  • Set per-unit occasion prices                                              │
│  • Create discount templates and assign them to buildings/units              │
└─────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                            Pricing Engine                                    │
│  PricingService.calculateSubtotal(unit, checkIn, checkOut, occasion)         │
│    1. Determine nightly price (base/offer or occasion price)                 │
│    2. Compute subtotal = nights × nightly price                              │
│    3. Find matching discount templates                                       │
│    4. Apply templates and cap total discount at 100%                         │
└─────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Reservation / Invoice                                │
│  ReservationService stores discount_amount on the reservation                │
│  Invoices & receipts mirror discount_amount as their discount line           │
│  Promo codes are applied after templates and only affect total_price         │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## 3. Data Model Deep Dive

### 3.1 `occasions` table

| Column | Type | Purpose |
|---|---|---|
| `id` | bigint PK | Unique identifier. |
| `name` | string | Human-readable label, e.g., "Wedding". |
| `slug` | string unique | URL-safe identifier. |
| `status` | enum pending/approved/rejected | Lifecycle state. |
| `start_date` | nullable date | Fixed window start for the occasion. |
| `end_date` | nullable date | Fixed window end for the occasion. |
| `days_of_week` | nullable json | Recurring days (0=Sunday … 6=Saturday). e.g. `[5,6]` for weekend. |
| `requested_by_owner_id` | nullable FK → owners | Set when an owner requests the occasion; `null` when created by admin. |
| `approved_by_user_id` | nullable FK → users | Admin who approved/rejected the occasion. |
| `approved_at` | timestamp nullable | Approval/rejection timestamp. |
| `rejection_reason` | text nullable | Reason provided when rejecting. |

**Lifecycle states:**
- `pending` — Owner request waiting for admin review.
- `approved` — Available for use in occasion prices and reservations.
- `rejected` — Admin declined the request; reason stored.

### 3.2 `occasion_prices` table

| Column | Type | Purpose |
|---|---|---|
| `id` | bigint PK | Unique identifier. |
| `unit_id` | FK → units | The unit this price applies to. |
| `occasion_id` | FK → occasions | The occasion this price applies to. |
| `price` | decimal(10,2) | Nightly price for this unit+occasion combination. |

**Uniqueness:** One price per `(unit_id, occasion_id)` pair.

### 3.3 `discount_templates` table

| Column | Type | Purpose |
|---|---|---|
| `id` | bigint PK | Unique identifier. |
| `owner_id` | FK → owners | Template owner. |
| `name` | string | Human-readable name. |
| `discount_type` | enum fixed/percentage | How the discount is calculated. |
| `discount_amount` | decimal(10,2) | Fixed amount or percentage value. |
| `condition_operator` | nullable enum =/>/<>=/<= | Optional operator for a total-amount condition. |
| `condition_amount` | nullable decimal(10,2) | Threshold for the condition. |
| `start_date` | nullable date | Fixed window start. |
| `end_date` | nullable date | Fixed window end. |
| `duration_days` | nullable unsigned int | Relative window length from today. |
| `is_active` | boolean default true | Enable/disable switch. |

**Window rules:**
- If only `duration_days` is provided, on save the template computes:
  - `start_date = today`
  - `end_date = today + duration_days`
- If `start_date` and/or `end_date` are provided, they are used directly.
- If no dates and no `duration_days`, the template is always active (subject to `is_active`).

### 3.4 `discount_template_assignments` table

| Column | Type | Purpose |
|---|---|---|
| `id` | bigint PK | Unique identifier. |
| `discount_template_id` | FK → discount_templates | The template. |
| `assignable_type` | string | `App\Models\Building` or `App\Models\Unit`. |
| `assignable_id` | bigint | The building or unit ID. |

**Uniqueness:** One assignment per `(template, type, id)` triple.

### 3.5 `reservations` table additions

| Column | Type | Purpose |
|---|---|---|
| `occasion_id` | nullable FK → occasions | Selected occasion for the reservation. |
| `discount_amount` | decimal(10,2) default 0 | Total discount from matching discount templates. |

### 3.6 Model relations

| Model | New relations |
|---|---|
| `Occasion` | `requestedBy()` → Owner, `approvedBy()` → User, scopes `approved()` / `pending()`, `isOccasionDate()`, `splitRange()` |
| `OccasionPrice` | `unit()` → Unit, `occasion()` → Occasion |
| `DiscountTemplate` | `owner()` → Owner, `buildings()` / `units()` morph-to-many, `assignments()` → HasMany |
| `DiscountTemplateAssignment` | `discountTemplate()` → DiscountTemplate, `assignable()` → MorphTo |
| `Unit` | `occasionPrices()` → HasMany, `discountTemplates()` → MorphToMany |
| `Building` | `discountTemplates()` → MorphToMany |
| `Reservation` | `occasion()` → BelongsTo |

---

## 4. Feature 1 — Occasions

### 4.1 Purpose
Provide a controlled vocabulary of special-event labels. Admins own the canonical list; owners may request additions.

### 4.2 Actor: Admin

**Create occasion directly**
```http
POST /api/v1/admin/occasions
{
  "name": "Wedding",
  "slug": "wedding",
  "status": "approved"
}
```
Result: occasion is immediately `approved` and usable.

**List all occasions**
```http
GET /api/v1/admin/occasions
```
Returns paginated list including pending, approved, and rejected occasions.

**Update occasion**
```http
PATCH /api/v1/admin/occasions/{occasion}
{
  "name": "Luxury Wedding",
  "slug": "luxury-wedding",
  "status": "approved"
}
```

**Delete occasion**
```http
DELETE /api/v1/admin/occasions/{occasion}
```
Cascades to related `occasion_prices` because of `cascadeOnDelete`.

**Approve owner request**
```http
POST /api/v1/admin/occasions/{occasion}/approve
```
Sets:
- `status = approved`
- `approved_by_user_id = auth()->id()`
- `approved_at = now()`
- `rejection_reason = null`

**Reject owner request**
```http
POST /api/v1/admin/occasions/{occasion}/reject
{
  "rejection_reason": "Not suitable for platform."
}
```
Sets:
- `status = rejected`
- `approved_by_user_id = auth()->id()`
- `approved_at = now()`
- `rejection_reason = <provided>`

### 4.3 Actor: Owner

**List approved occasions**
```http
GET /api/v1/occasions
```
Only returns `status = approved`.

**Request a new occasion**
```http
POST /api/v1/occasion-requests
{
  "name": "Graduation Party"
}
```
Backend behavior:
- Generates slug: `Str::slug($name) . '-' . Str::random(6)`
- Sets `status = pending`
- Sets `requested_by_owner_id = auth()->user()->owner->id`

### 4.4 Validation rules

**Admin store/update**
- `name`: required, string, max 255
- `slug`: required, string, max 255, unique (ignore self on update)
- `status`: required, in `[pending, approved, rejected]`

**Owner request**
- `name`: required, string, max 255

### 4.5 Authorization

| Action | Allowed roles |
|---|---|
| `viewAny` / `view` | super_admin, admin, approved owner, active employee |
| `create`, `update`, `delete` | super_admin, admin |
| `request` | approved owner |
| `approve`, `reject` | super_admin, admin |

---

## 5. Feature 2 — Occasion Prices

### 5.1 Purpose
Allow an owner to define a special nightly price for each of their units for a specific approved occasion.

### 5.2 Example scenario

Unit `Sea View 101` normally costs **$100/night**. For the occasion **New Year's Eve**, the owner wants to charge **$250/night**. They create an occasion price:

```http
POST /api/v1/occasion-prices
{
  "unit_id": 101,
  "occasion_id": 7,
  "price": 250
}
```

### 5.3 CRUD endpoints

**Create**
```http
POST /api/v1/occasion-prices
{
  "unit_id": <unit-id>,
  "occasion_id": <approved-occasion-id>,
  "price": 250.00
}
```
Backend enforces that the unit belongs to the authenticated owner/employee.

**List**
```http
GET /api/v1/occasion-prices
```
Returns only prices for units owned by the authenticated owner/employee, paginated.

**Show**
```http
GET /api/v1/occasion-prices/{occasionPrice}
```

**Update**
```http
PATCH /api/v1/occasion-prices/{occasionPrice}
{
  "price": 275.00
}
```

**Delete**
```http
DELETE /api/v1/occasion-prices/{occasionPrice}
```

### 5.4 Validation rules

**Store**
- `unit_id`: required, exists in `units`
- `occasion_id`: required, exists in `occasions`
- `price`: required, numeric, min 0

**Update**
- `price`: required, numeric, min 0

### 5.5 Authorization

| Action | Allowed roles |
|---|---|
| `viewAny` / `create` | approved owner, active employee |
| `view` / `update` / `delete` | super_admin, owner/employee who owns the unit |

---

## 6. Feature 3 — Discount Templates

### 6.1 Purpose
Allow an owner to create reusable discount rules that automatically apply to reservations when:
1. The template is assigned to the reservation's unit or building.
2. The reservation dates overlap the template's active window.
3. The reservation's pre-discount subtotal satisfies the optional condition.

### 6.2 Template structure

**Fixed discount**
```json
{
  "name": "Summer 50 Off",
  "discount_type": "fixed",
  "discount_amount": 50
}
```
Deducts $50 from the subtotal.

**Percentage discount**
```json
{
  "name": "Summer 20% Off",
  "discount_type": "percentage",
  "discount_amount": 20
}
```
Deducts 20% of the subtotal.

**Conditional discount**
```json
{
  "name": "Big Booking Bonus",
  "discount_type": "fixed",
  "discount_amount": 100,
  "condition_operator": ">=",
  "condition_amount": 500
}
```
Only applies when the pre-discount subtotal is at least $500.

**Date-bounded discount**
```json
{
  "name": "July Special",
  "discount_type": "percentage",
  "discount_amount": 15,
  "start_date": "2026-07-01",
  "end_date": "2026-07-31"
}
```

**Relative window discount**
```json
{
  "name": "Launch Week Deal",
  "discount_type": "fixed",
  "discount_amount": 30,
  "duration_days": 7
}
```
On save, expands to `start_date = today`, `end_date = today + 7 days`.

### 6.3 CRUD endpoints

**Create with assignments**
```http
POST /api/v1/discount-templates
{
  "name": "Summer Sale",
  "discount_type": "fixed",
  "discount_amount": 50,
  "assignments": [
    { "type": "building", "id": 3 },
    { "type": "unit", "id": 12 }
  ]
}
```

**List**
```http
GET /api/v1/discount-templates
```
Owner/employee sees only their own templates.

**Show**
```http
GET /api/v1/discount-templates/{discount_template}
```
Includes `assignments` array with normalized `type` (`building`/`unit`) and `id`.

**Update with assignments**
```http
PUT /api/v1/discount-templates/{discount_template}
{
  "name": "Updated Sale",
  "discount_type": "percentage",
  "discount_amount": 15,
  "assignments": [
    { "type": "unit", "id": 12 }
  ]
}
```
Assignments are fully replaced on every update.

**Delete**
```http
DELETE /api/v1/discount-templates/{discount_template}
```
Cascades assignments because of `cascadeOnDelete`.

**Toggle active state**
```http
POST /api/v1/discount-templates/{discount_template}/toggle
```
Flips `is_active` true ↔ false.

### 6.4 Validation rules

- `name`: required, string, max 255
- `discount_type`: required, in `[fixed, percentage]`
- `discount_amount`: required, numeric, min 0
- `condition_operator`: nullable, in `[=, >, <, >=, <=]`
- `condition_amount`: nullable, numeric, min 0, required_with:condition_operator
- `start_date`: nullable, date, required_with:end_date
- `end_date`: nullable, date, after_or_equal:start_date
- `duration_days`: nullable, integer, min 1
- `assignments`: nullable, array
- `assignments.*.type`: required_with:assignments, in `[building, unit]`
- `assignments.*.id`: required_with:assignments, integer

### 6.5 Assignment ownership enforcement

On create/update, each assignment target is verified:
- `Building`: `building.owner_id === authenticated_owner_id`
- `Unit`: `unit.building.owner_id === authenticated_owner_id`

If any target does not belong to the owner, validation fails with:
```json
{
  "message": "The given data was invalid.",
  "errors": {
    "assignments": ["The selected building/unit does not belong to you."]
  }
}
```

### 6.6 Authorization

| Action | Allowed roles |
|---|---|
| `viewAny` / `create` | approved owner, active employee |
| `view` / `update` / `delete` / `toggle` | super_admin, owner/employee who owns the template |

---

## 7. Pricing Engine Flow

### 7.1 Entry point

```php
$result = PricingService::calculateSubtotal(
    $unit,
    $checkIn,   // Carbon
    $checkOut,  // Carbon
    $occasion   // ?Occasion
);
```

Returns:
```php
[
    'subtotal'        => float, // after occasion + templates
    'discount_amount' => float, // total template discount
    'occasion_price'  => float|null,
]
```

### 7.2 Step-by-step algorithm

**Step 1 — Count nights**
```php
$nights = count(UnitAvailabilityService::dateRange(
    $checkIn->format('Y-m-d'),
    $checkOut->format('Y-m-d')
));
```
The date range is exclusive of the checkout date (standard hotel convention).

**Step 2 — Split nights into normal and occasion buckets**

```php
$normalPrice = $unit->offer_price ?? $unit->base_price;
$occasionPrice = null;

if ($occasion) {
    $occasionPriceModel = $unit->occasionPrices()
        ->where('occasion_id', $occasion->id)
        ->first();

    $occasionPrice = $occasionPriceModel?->price;
}

if ($occasion && $occasionPrice !== null) {
    $split = $occasion->splitRange($checkIn, $checkOut);
    $normalNights = $split['normal'];
    $occasionNights = $split['occasion'];
} else {
    $normalNights = $nights;
    $occasionNights = 0;
}
```

A night is an occasion night when:
- the reservation has an occasion selected,
- the unit has an occasion price for that occasion, and
- the night falls inside the occasion's `start_date`/`end_date` window or matches one of its `days_of_week` values.

If no occasion price exists for the unit+occasion pair, all nights are priced normally.

**Step 3 — Compute pre-template subtotal**

```php
$originalSubtotal = ($normalNights * $normalPrice) + ($occasionNights * $occasionPrice);
```

**Step 4 — Find matching discount templates**

Templates must:
1. Belong to the unit's owner (`discount_templates.owner_id = unit.building.owner_id`).
2. Be active (`is_active = true`).
3. Be assigned to the unit's building OR the unit itself.
4. Have an active date window overlapping the reservation dates.
5. Satisfy the optional total-amount condition.

**Date overlap check**
```php
public function isActiveForRange(Carbon $checkIn, Carbon $checkOut): bool
{
    $date = $checkIn->copy();
    while ($date->lte($checkOut)) {
        if ($this->isActiveForDate($date)) {
            return true;
        }
        $date->addDay();
    }
    return false;
}
```
A template matches if **any single night** of the reservation falls inside its window.

**Condition check**
```php
public function conditionMatches(float $subtotal): bool
{
    if ($this->condition_operator === null || $this->condition_amount === null) {
        return true;
    }

    return match ($this->condition_operator) {
        '='  => $subtotal == $this->condition_amount,
        '>'  => $subtotal >  $this->condition_amount,
        '<'  => $subtotal <  $this->condition_amount,
        '>=' => $subtotal >= $this->condition_amount,
        '<=' => $subtotal <= $this->condition_amount,
        default => false,
    };
}
```
The condition is evaluated against the **pre-template subtotal**.

**Step 5 — Apply templates and cap discount**

Each template's discount is computed from the original subtotal, then summed:
```php
$discountAmount = 0.00;
foreach ($templates as $template) {
    $discountAmount += ($originalSubtotal - $template->apply($originalSubtotal));
}

if ($discountAmount > $originalSubtotal) {
    $discountAmount = $originalSubtotal;
}

$subtotal = $originalSubtotal - $discountAmount;
```

The cap guarantees the net price never goes below zero.

### 7.3 Precedence summary

1. Occasion price (if selected, defined, and the night matches the occasion dates)
2. Base / offer price (fallback for non-occasion nights)
3. Discount templates (active + matching + condition satisfied)
4. Promo code (applied later by `ReservationService`)

---

## 8. Reservation Pricing Integration

### 8.1 Where pricing is invoked

`ReservationService` uses `PricingService` in three places:
1. `calculateTotal(...)` — public price preview.
2. `persistReservation(...)` — customer online booking.
3. `recalculateReservation(...)` — date/unit/promo changes.

### 8.2 `calculateTotal` signature

```php
public function calculateTotal(
    int $unitId,
    string $checkIn,
    string $checkOut,
    ?PromoCode $promoCode = null,
    ?Occasion $occasion = null,
): float
```

Flow:
1. Call `PricingService::calculateSubtotal(...)`.
2. If promo code provided, apply promo discount on top of the subtotal.
3. Return final total.

### 8.3 Creating a reservation with an occasion

During `persistReservation`:
1. Read `occasion_id` from validated request data.
2. Resolve the occasion through `Occasion::approved()->findOrFail(...)`.
3. Remove `occasion_id` from the data before mass assignment.
4. Compute pricing once:
   ```php
   $pricing = PricingService::calculateSubtotal(
       $unit,
       Carbon::parse($data['check_in_date']),
       Carbon::parse($data['check_out_date']),
       $occasion
   );
   ```
5. Apply promo code on top if present.
6. Create reservation with:
   - `total_price = $totalPrice`
   - `discount_amount = $pricing['discount_amount']`
   - `promo_code_id = $promoCode?->id`

### 8.4 Recalculating a reservation

When dates, unit, or promo code change:
1. Resolve the reservation's occasion if `occasion_id` is set.
2. Recompute pricing via `PricingService`.
3. Update `total_price` and `discount_amount` on the reservation.
4. Update the owner invoice:
   - `price` = nightly price
   - `quantity` = nights
   - `total_price` = original subtotal
   - `discount` = `reservation->discount_amount`
   - `net_price` = `total_price - discount`
   - `remaining_amount` = `net_price - paid_amount`
5. Mirror the invoice to the customer receipt.

### 8.5 Invoice / receipt discount semantics

- `invoice.discount` and `receipt.discount` reflect **template/occasion-level discounts** (`reservation->discount_amount`).
- Promo-code discounts reduce `reservation->total_price` but are **not** shown on the invoice discount line.
- Example:
  - Subtotal after templates: $270
  - Template discount: $0
  - Promo code 10%: -$27
  - `reservation->total_price = $243`
  - `invoice->discount = 0`
  - `invoice->net_price = $270`

This separation allows the invoice to show the owner's pricing adjustments while the reservation total reflects the customer's final charge.

### 8.6 Promo code helper

```php
private function applyPromoCodeDiscount(float $total, PromoCode $promoCode): float
{
    $discount = $promoCode->discount_type === 'percentage'
        ? $total * ($promoCode->discount_value / 100)
        : $promoCode->discount_value;

    return round(max(0, $total - min($discount, $total)), 2);
}
```
Promo discount is capped so the total never goes negative.

---

## 9. API Reference

### 9.1 Admin occasions

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/admin/occasions` | List all occasions |
| POST | `/api/v1/admin/occasions` | Create occasion directly |
| PATCH | `/api/v1/admin/occasions/{occasion}` | Update occasion |
| DELETE | `/api/v1/admin/occasions/{occasion}` | Delete occasion |
| POST | `/api/v1/admin/occasions/{occasion}/approve` | Approve a pending request |
| POST | `/api/v1/admin/occasions/{occasion}/reject` | Reject a pending request |

### 9.2 Owner/employee occasions

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/occasions` | List approved occasions |
| POST | `/api/v1/occasion-requests` | Request a new occasion |

### 9.3 Owner/employee occasion prices

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/occasion-prices` | List prices for owner's units |
| POST | `/api/v1/occasion-prices` | Set a unit's price for an occasion |
| GET | `/api/v1/occasion-prices/{occasionPrice}` | Show a price |
| PATCH | `/api/v1/occasion-prices/{occasionPrice}` | Update price |
| DELETE | `/api/v1/occasion-prices/{occasionPrice}` | Remove price |

### 9.4 Owner/employee discount templates

| Method | Endpoint | Description |
|---|---|---|
| GET | `/api/v1/discount-templates` | List owner's templates |
| POST | `/api/v1/discount-templates` | Create template |
| GET | `/api/v1/discount-templates/{discount_template}` | Show template with assignments |
| PUT | `/api/v1/discount-templates/{discount_template}` | Update template |
| DELETE | `/api/v1/discount-templates/{discount_template}` | Delete template |
| POST | `/api/v1/discount-templates/{discount_template}/toggle` | Enable/disable |

### 9.5 Authentication middleware

- Admin endpoints: `role:super_admin|admin`
- Owner/employee endpoints: `role:owner|employee`
- Per-resource authorization enforced by policies.

---

## 10. Authorization Matrix

| Resource | Action | super_admin | admin | approved owner | active employee |
|---|---|:---:|:---:|:---:|:---:|
| Occasion | viewAny/view | ✅ | ✅ | ✅ | ✅ |
| Occasion | create/update/delete | ✅ | ✅ | ❌ | ❌ |
| Occasion | request | ❌ | ❌ | ✅ | ❌ |
| Occasion | approve/reject | ✅ | ✅ | ❌ | ❌ |
| OccasionPrice | viewAny/create | ✅ | ✅ | ✅ | ✅ |
| OccasionPrice | view/update/delete (own unit) | ✅ | ✅ | ✅ | ✅ |
| OccasionPrice | view/update/delete (other unit) | ✅ | ❌ | ❌ | ❌ |
| DiscountTemplate | viewAny/create | ✅ | ✅ | ✅ | ✅ |
| DiscountTemplate | view/update/delete/toggle (own) | ✅ | ✅ | ✅ | ✅ |
| DiscountTemplate | view/update/delete/toggle (other) | ✅ | ❌ | ❌ | ❌ |

`super_admin` bypasses all checks via the `before()` hook in every policy.

---

## 11. End-to-End Use Cases

### Use Case 1 — Owner requests a new occasion

1. Owner opens the app and goes to *Occasions*.
2. Clicks *Request New Occasion*.
3. Enters name: `"National Day"`.
4. App sends:
   ```http
   POST /api/v1/occasion-requests
   { "name": "National Day" }
   ```
5. Backend creates occasion with:
   - `slug = national-day-a3f9k2`
   - `status = pending`
   - `requested_by_owner_id = <owner-id>`
6. Admin receives notification.
7. Admin reviews and sends:
   ```http
   POST /api/v1/admin/occasions/{id}/approve
   ```
8. Occasion becomes `approved` and appears in owner's occasion list.

### Use Case 2 — Owner sets an occasion price

1. Owner goes to *Unit Pricing*.
2. Selects unit `Sea View 101`.
3. Selects occasion `National Day`.
4. Enters price `400`.
5. App sends:
   ```http
   POST /api/v1/occasion-prices
   {
     "unit_id": 101,
     "occasion_id": 9,
     "price": 400
   }
   ```
6. Backend verifies the unit belongs to the owner and creates the price.

### Use Case 3 — Reservation uses occasion price

1. A customer searches for `Sea View 101` for `2026-12-01` → `2026-12-04`.
2. Admin/owner creates the reservation and selects occasion `National Day`.
3. `ReservationService::persistReservation` resolves the approved occasion.
4. `PricingService` finds the occasion price `400`.
5. Calculation:
   - Nights = 3
   - All nights match the occasion window
   - Nightly price = 400 (occasion override)
   - Subtotal = 1200
   - No matching discount templates
   - `discount_amount = 0`
6. Reservation created with `total_price = 1200`.

### Use Case 3b — Reservation overlaps normal and occasion dates

1. Unit `Sea View 101` base price is **$100/night**.
2. Owner creates occasion `National Day` for `2026-12-02` → `2026-12-03`.
3. Owner sets occasion price to **$400/night**.
4. Customer books `2026-12-01` → `2026-12-04`.
5. `PricingService` splits the 3 nights:
   - 1 normal night × $100 = $100
   - 2 occasion nights × $400 = $800
6. Pre-template subtotal = $900.

### Use Case 4 — Owner creates a seasonal discount

1. Owner creates a discount template:
   ```http
   POST /api/v1/discount-templates
   {
     "name": "Summer 2026 20% Off",
     "discount_type": "percentage",
     "discount_amount": 20,
     "start_date": "2026-06-01",
     "end_date": "2026-08-31",
     "assignments": [
       { "type": "building", "id": 3 }
     ]
   }
   ```
2. Template is active for any reservation in summer 2026 at building 3.

### Use Case 5 — Conditional discount applies only to large bookings

1. Owner creates:
   ```http
   POST /api/v1/discount-templates
   {
     "name": "Big Booking Bonus",
     "discount_type": "fixed",
     "discount_amount": 100,
     "condition_operator": ">=",
     "condition_amount": 1000,
     "assignments": [
       { "type": "unit", "id": 12 }
     ]
   }
   ```
2. Customer books unit 12 for 5 nights at $200/night.
3. Pre-template subtotal = $1000.
4. Condition `>= 1000` is satisfied.
5. Discount = $100.
6. Final pre-promo subtotal = $900.

### Use Case 6 — Multiple discounts capped at 100%

1. Owner creates two templates:
   - `60% off` assigned to unit 5
   - `60% off` assigned to building 1 (which contains unit 5)
2. Customer books unit 5 for 2 nights at $150/night.
3. Pre-template subtotal = $300.
4. Both templates match.
5. Raw discount = 60% × 300 + 60% × 300 = $360.
6. Capped at $300.
7. Final pre-promo subtotal = $0.

### Use Case 7 — Date change triggers recalculation

1. Reservation exists for unit 7, `2026-08-01` → `2026-08-04`, with a matching template.
2. Customer/owner updates checkout to `2026-08-05`.
3. `ReservationService::updateReservation` detects the date change.
4. `recalculateReservation` runs:
   - Recomputes nights = 4
   - Recomputes subtotal and discount
   - Updates `reservation.total_price`
   - Updates `reservation.discount_amount`
   - Updates invoice/receipt line items
5. Payment status is re-evaluated based on existing payments.

---

## 12. Testing Coverage

| Test File | Tests | Focus |
|---|---|---|
| `tests/Unit/Services/PricingServiceTest.php` | 10 | Base price, offer price, occasion override, partial occasion ranges, recurring occasion days, template matching, conditions, 100% cap, date windows |
| `tests/Feature/Admin/OccasionManagementTest.php` | 8 | Admin CRUD, owner request, approve/reject, authorization |
| `tests/Feature/Owner/OccasionPriceTest.php` | 6 | CRUD, ownership enforcement, reservation pricing integration |
| `tests/Feature/Owner/DiscountTemplateTest.php` | 8 | CRUD, toggle, assignment ownership, reservation pricing integration |

**Suite result:** 492 tests passed, 1558 assertions.

---

## 13. Files Added / Modified

### New files

```
database/migrations/2026_06_30_120000_create_occasions_table.php
database/migrations/2026_06_30_120001_create_occasion_prices_table.php
database/migrations/2026_06_30_120002_create_discount_templates_table.php
database/migrations/2026_06_30_120003_create_discount_template_assignments_table.php
database/migrations/2026_06_30_120004_add_discount_amount_to_reservations_table.php
database/migrations/2026_06_30_120005_add_occasion_id_to_reservations_table.php
app/Models/Occasion.php
app/Models/OccasionPrice.php
app/Models/DiscountTemplate.php
app/Models/DiscountTemplateAssignment.php
app/Services/PricingService.php
app/Facades/PricingService.php
app/Policies/OccasionPolicy.php
app/Policies/OccasionPricePolicy.php
app/Policies/DiscountTemplatePolicy.php
app/Http/Requests/Admin/StoreOccasionRequest.php
app/Http/Requests/Admin/UpdateOccasionRequest.php
app/Http/Requests/Owner/StoreOccasionRequest.php
app/Http/Requests/Owner/StoreOccasionPriceRequest.php
app/Http/Requests/Owner/UpdateOccasionPriceRequest.php
app/Http/Requests/Owner/StoreDiscountTemplateRequest.php
app/Http/Requests/Owner/UpdateDiscountTemplateRequest.php
app/Http/Controllers/Api/Admin/AdminOccasionController.php
app/Http/Controllers/Api/Owner/OwnerOccasionController.php
app/Http/Controllers/Api/Owner/OccasionPriceController.php
app/Http/Controllers/Api/Owner/DiscountTemplateController.php
app/Http/Resources/OccasionResource.php
app/Http/Resources/OccasionPriceResource.php
app/Http/Resources/DiscountTemplateResource.php
tests/Unit/Services/PricingServiceTest.php
tests/Feature/Admin/OccasionManagementTest.php
tests/Feature/Owner/OccasionPriceTest.php
tests/Feature/Owner/DiscountTemplateTest.php
```

### Modified files

```
app/Models/Unit.php
app/Models/Building.php
app/Models/Reservation.php
app/Services/ReservationService.php
app/Providers/AppServiceProvider.php
routes/api.php
tests/Feature/ReservationFixesTest.php
```

---

*Document generated after implementation completion. All behaviors verified by the test suite.*
