# Occasions & Discount Templates — Design Spec

## Overview

Add two owner/admin pricing tools:

1. **Occasions** — admin-managed labels (Wedding, Birthday, etc.). Owners can attach an occasion-specific nightly price to any of their units. When an owner creates a manual reservation, they can pick an occasion and the reservation uses the occasion price instead of the normal unit price.
2. **Discount Templates** — owner-created reusable discount rules that can be assigned to multiple buildings and/or units. A template applies automatically to a reservation when its active date window overlaps the reservation dates and its total-amount condition is met.

## Decisions

- **Occasion price replaces the normal nightly price** (`base_price` / `offer_price`) when an occasion is selected.
- **Who selects the occasion?** Owners/employees when creating a manual reservation. Online customer bookings do not use occasions in the first version.
- **Most occasions are created by admins directly.** Owner requests are supported but optional; request fields are nullable.
- **Discount template targets are polymorphic and many-to-many** — one template can be assigned to many `Building` and/or `Unit` records. Floor support is intentionally excluded.
- **Discount condition is based on reservation total amount** (before the template's own discount is applied).
- **Date windows** can be either a fixed `start_date` / `end_date` range or a relative `duration_days` window counted from today. All matching fields are nullable.
- **Precedence:**
  1. Occasion price (if selected)
  2. Discount templates (matching date window + total-amount condition)
  3. Promo code
- **Multiple matching templates:** all matching templates are applied sequentially, but total discount is capped at 100% of the pre-promo subtotal so the net price never goes below zero.

## Data Model

### `occasions`

| Column | Type | Notes |
|---|---|---|
| `id` | bigIncrements | |
| `name` | string | e.g. "Wedding", "Birthday" |
| `slug` | string unique | URL-safe identifier |
| `status` | enum('pending','approved','rejected') | default `pending` for requests, admin can set `approved`/`rejected` |
| `requested_by_owner_id` | foreignId → owners, nullable | null when created directly by admin |
| `approved_by_user_id` | foreignId → users, nullable | set when admin approves/rejects |
| `approved_at` | timestamp, nullable | |
| `rejection_reason` | text, nullable | |
| timestamps | | |

### `occasion_prices`

| Column | Type | Notes |
|---|---|---|
| `id` | bigIncrements | |
| `unit_id` | foreignId → units | |
| `occasion_id` | foreignId → occasions | |
| `price` | decimal(10,2) | nightly price for this unit + occasion |
| timestamps | | |
| unique(`unit_id`, `occasion_id`) | | |

### `discount_templates`

| Column | Type | Notes |
|---|---|---|
| `id` | bigIncrements | |
| `owner_id` | foreignId → owners | template owner |
| `name` | string | human-readable name |
| `discount_type` | enum('fixed','percentage') | |
| `discount_amount` | decimal(10,2) | |
| `condition_operator` | enum('=','>','<','>=','<='), nullable | no condition if null |
| `condition_amount` | decimal(10,2), nullable | |
| `start_date` | date, nullable | fixed window start |
| `end_date` | date, nullable | fixed window end |
| `duration_days` | unsignedInteger, nullable | relative window length from today |
| `is_active` | boolean | default true |
| timestamps | | |

### `discount_template_assignments`

Polymorphic many-to-many link.

| Column | Type | Notes |
|---|---|---|
| `id` | bigIncrements | |
| `discount_template_id` | foreignId → discount_templates | |
| `assignable_type` | enum('App\\Models\\Building','App\\Models\\Unit') | |
| `assignable_id` | unsignedBigInteger | |
| timestamps | | |
| unique(`discount_template_id`, `assignable_type`, `assignable_id`) | | |

## API Endpoints

### Admin occasions

| Method | Endpoint | Action |
|---|---|---|
| GET | `/api/v1/admin/occasions` | list all occasions |
| POST | `/api/v1/admin/occasions` | create an 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 requested occasion |
| POST | `/api/v1/admin/occasions/{occasion}/reject` | reject a requested occasion |

### Owner/employee occasions

| Method | Endpoint | Action |
|---|---|---|
| GET | `/api/v1/occasions` | list approved occasions |
| POST | `/api/v1/occasion-requests` | owner requests a new occasion |

### Owner/employee occasion prices

| Method | Endpoint | Action |
|---|---|---|
| GET | `/api/v1/occasion-prices` | list prices for owner's units |
| POST | `/api/v1/occasion-prices` | set a unit's price for an occasion |
| PATCH | `/api/v1/occasion-prices/{occasionPrice}` | update price |
| DELETE | `/api/v1/occasion-prices/{occasionPrice}` | remove price |

### Owner/employee discount templates

| Method | Endpoint | Action |
|---|---|---|
| GET | `/api/v1/discount-templates` | list owner's templates |
| POST | `/api/v1/discount-templates` | create template |
| GET | `/api/v1/discount-templates/{template}` | show template with assignments |
| PATCH | `/api/v1/discount-templates/{template}` | update template |
| DELETE | `/api/v1/discount-templates/{template}` | delete template |
| POST | `/api/v1/discount-templates/{template}/toggle` | enable/disable |

### Reservation payloads

Add optional fields:

- `occasion_id` — used when creating a manual reservation.
- Existing reservation endpoints do not change otherwise.

## Pricing Flow

```text
nightlyPrice = unit.offer_price ?? unit.base_price
if (occasionPrice exists for unit + occasion):
    nightlyPrice = occasionPrice.price

subtotal = nightlyPrice * nights

for each active discount template assigned to the unit's building or the unit itself:
    if any night of the reservation falls inside the template's active window:
        if condition is null OR subtotal matches condition:
            subtotal = applyDiscount(subtotal, template)
            subtotal = max(0, subtotal)

total = subtotal
if (promoCode):
    total = applyPromo(total, promoCode)
```

## Authorization

- **Occasions**
  - Admin: full CRUD + approve/reject.
  - Owner/employee: read approved, create requests.
- **Occasion prices**
  - Owner/employee can only manage prices for units they own/employ.
- **Discount templates**
  - Owner/employee can only manage templates they own; assignments must target their own buildings/units.

Policies follow existing patterns (`before()` super-admin, `ownsX()` helpers, `isApprovedOwner()` / `isActiveEmployee()` + Spatie permissions).

## Testing

- Unit tests for `DiscountTemplate` matching logic (date window, condition, multiple templates cap).
- Feature tests for admin occasion CRUD and approval.
- Feature tests for owner occasion-price CRUD.
- Feature tests for discount template CRUD and assignment.
- Feature tests confirming reservation total changes when occasion / discount template is applied.
