# Phase 1: Foundation & Schema

## Objective
Set up all models, migrations, enums, morph maps, role, middleware, and route skeleton for the entire Hotel Manager (PMS) module. This phase creates the data foundation that all subsequent phases build upon.

---

## Tasks

### 1.1 Add `is_hotel_manager` to Employee Model

**File:** `app/Models/Employee.php`

Add boolean field (same pattern as `is_shop_manager`, `is_restaurant_manager`):
```php
// In $fillable
'is_hotel_manager',

// In $casts
'is_hotel_manager' => 'boolean',
```

**File:** `app/Models/User.php`
```php
public function getIsHotelManagerAttribute(): bool
{
    return $this->employee?->is_hotel_manager ?? false;
}
```

Update `getEmployeeType()` and `getEmployeeTypes()` in Employee model.

### 1.2 Create New Enums

**File:** `app/Enums/HotelRoomStatus.php`
```php
enum HotelRoomStatus: string
{
    case AVAILABLE = 'available';
    case OCCUPIED = 'occupied';
    case RESERVED = 'reserved';
    case MAINTENANCE = 'maintenance';
    case OUT_OF_ORDER = 'out_of_order';
}
```

**File:** `app/Enums/HotelHousekeepingStatus.php`
```php
enum HotelHousekeepingStatus: string
{
    case CLEAN = 'clean';
    case DIRTY = 'dirty';
    case INSPECTED = 'inspected';
    case IN_PROGRESS = 'in_progress';
    case OUT_OF_ORDER = 'out_of_order';
}
```

**File:** `app/Enums/HotelReservationStatus.php`
```php
enum HotelReservationStatus: string
{
    case PENDING = 'pending';
    case CONFIRMED = 'confirmed';
    case CHECKED_IN = 'checked_in';
    case CHECKED_OUT = 'checked_out';
    case CANCELLED = 'cancelled';
    case NO_SHOW = 'no_show';
}
```

**File:** `app/Enums/HotelRateSeason.php`
```php
enum HotelRateSeason: string
{
    case LOW = 'low';
    case MID = 'mid';
    case HIGH = 'high';
    case SPECIAL = 'special';
}
```

**File:** `app/Enums/HotelExtraType.php`
```php
enum HotelExtraType: string
{
    case ROOM_SERVICE = 'room_service';
    case MINIBAR = 'minibar';
    case SPA = 'spa';
    case LAUNDRY = 'laundry';
    case PARKING = 'parking';
    case TRANSFER = 'transfer';
    case OTHER = 'other';
}
```

**File:** `app/Enums/HotelBedType.php`
```php
enum HotelBedType: string
{
    case SINGLE = 'single';
    case DOUBLE = 'double';
    case QUEEN = 'queen';
    case KING = 'king';
    case TWIN = 'twin';
    case SOFA_BED = 'sofa_bed';
    case BUNK = 'bunk';
}
```

**File:** `app/Enums/HotelChannelType.php`
```php
enum HotelChannelType: string
{
    case DIRECT = 'direct';
    case BOOKING_COM = 'booking_com';
    case AIRBNB = 'airbnb';
    case EXPEDIA = 'expedia';
    case OTHER = 'other';
}
```

**File:** `app/Enums/HotelReservationSource.php`
```php
enum HotelReservationSource: string
{
    case DIRECT_APP = 'direct_app';
    case DIRECT_WALKIN = 'direct_walkin';
    case DIRECT_PHONE = 'direct_phone';
    case BOOKING_COM = 'booking_com';
    case AIRBNB = 'airbnb';
    case EXPEDIA = 'expedia';
    case OTHER = 'other';
}
```

All enums must have `label()` method with translation keys, use `HasOptions` and `HasEnumStaticMethods` traits.

### 1.3 Update OrderableType Enum

**File:** `app/Enums/OrderableType.php`

Add:
```php
case HOTEL_RESERVATION = 'hotel_reservation';
case HOTEL_EXTRA = 'hotel_extra';
```

Update `label()`, `labelForOrderable()`, and `code()` methods.

### 1.4 Create All Models

#### Hotel Model
**File:** `app/Models/Hotel.php`

```php
final class Hotel extends BaseModel implements HasMedia
{
    use HasPublicId, HasFactory, InteractsWithMedia, SoftDeletes,
        LogsActivity, HasDatabaseTranslations, HasPlaceScope;

    protected $fillable = [
        'place_id', 'name', 'description', 'slug', 'stars',
        'check_in_time', 'check_out_time', 'opening_hours',
        'policies', 'contact_email', 'contact_phone',
        'free_cancellation_hours', 'cancellation_penalty_percentage',
        'is_active',
    ];

    protected $casts = [
        'stars' => 'integer',
        'opening_hours' => 'array',
        'policies' => 'array',
        'free_cancellation_hours' => 'integer',
        'cancellation_penalty_percentage' => 'integer',
        'is_active' => 'boolean',
    ];

    protected array $translatable = ['name', 'description'];
}
```

**Relations:**
- `place()` BelongsTo Place
- `roomTypes()` HasMany HotelRoomType
- `rooms()` HasMany HotelRoom
- `seasonalRates()` HasMany HotelSeasonalRate
- `reservations()` HasMany HotelReservation
- `extras()` HasMany HotelExtra
- `channelMappings()` HasMany HotelChannelMapping
- `orders()` HasMany Order (via hotel_id)

**Methods:**
- `scopeActive($query)` — filter is_active = true

**Media:** `logo`, `photos` collections

#### HotelRoomType Model
**File:** `app/Models/HotelRoomType.php`

```php
final class HotelRoomType extends BaseModel implements HasMedia
{
    use HasPublicId, HasFactory, SoftDeletes, LogsActivity,
        HasDatabaseTranslations, InteractsWithMedia;

    protected $fillable = [
        'hotel_id', 'name', 'description', 'slug',
        'money_price_amount', 'money_price_currency',
        'member_money_price_amount', 'member_money_price_currency',
        'resort_money_price_amount', 'resort_money_price_currency',
        'max_occupancy', 'max_adults', 'max_children',
        'bed_type', 'amenities', 'surface_sqm',
        'is_active', 'sort_order',
    ];

    protected $casts = [
        'money_price' => AsMoney::class,
        'member_money_price' => AsMoney::class,
        'resort_money_price' => AsMoney::class,
        'max_occupancy' => 'integer',
        'max_adults' => 'integer',
        'max_children' => 'integer',
        'bed_type' => HotelBedType::class,
        'amenities' => 'array',
        'surface_sqm' => 'integer',
        'is_active' => 'boolean',
        'sort_order' => 'integer',
    ];

    protected array $translatable = ['name', 'description'];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `rooms()` HasMany HotelRoom
- `seasonalRates()` HasMany HotelSeasonalRate
- `reservations()` HasMany HotelReservation
- `channelMappings()` HasMany HotelChannelMapping

**Media:** `photos` collection

#### HotelRoom Model
**File:** `app/Models/HotelRoom.php`

```php
final class HotelRoom extends BaseModel
{
    use HasPublicId, HasFactory, SoftDeletes, LogsActivity;

    protected $fillable = [
        'hotel_id', 'room_type_id', 'number', 'floor',
        'status', 'housekeeping_status', 'notes', 'is_active',
    ];

    protected $casts = [
        'status' => HotelRoomStatus::class,
        'housekeeping_status' => HotelHousekeepingStatus::class,
        'is_active' => 'boolean',
    ];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `roomType()` BelongsTo HotelRoomType
- `reservations()` BelongsToMany HotelReservation (via pivot)
- `housekeepingLogs()` HasMany HotelHousekeepingLog
- `reservationExtras()` HasMany HotelReservationExtra

#### HotelSeasonalRate Model
**File:** `app/Models/HotelSeasonalRate.php`

```php
final class HotelSeasonalRate extends BaseModel
{
    use HasPublicId, HasFactory, SoftDeletes, HasDatabaseTranslations;

    protected $fillable = [
        'hotel_id', 'room_type_id', 'name', 'season',
        'start_date', 'end_date',
        'money_price_amount', 'money_price_currency',
        'member_money_price_amount', 'member_money_price_currency',
        'resort_money_price_amount', 'resort_money_price_currency',
        'min_stay', 'max_stay', 'is_active',
    ];

    protected $casts = [
        'season' => HotelRateSeason::class,
        'start_date' => 'date',
        'end_date' => 'date',
        'money_price' => AsMoney::class,
        'member_money_price' => AsMoney::class,
        'resort_money_price' => AsMoney::class,
        'min_stay' => 'integer',
        'max_stay' => 'integer',
        'is_active' => 'boolean',
    ];

    protected array $translatable = ['name'];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `roomType()` BelongsTo HotelRoomType

**Scopes:**
- `scopeForDate($query, Carbon $date)` — rates covering a specific date
- `scopeActive($query)`

#### HotelReservation Model
**File:** `app/Models/HotelReservation.php`

Implements `Orderable` interface for room charge prepayment.

```php
final class HotelReservation extends BaseModel implements Orderable
{
    use HasPublicId, HasFactory, SoftDeletes, LogsActivity;

    protected $fillable = [
        'hotel_id', 'user_id', 'room_type_id',
        'check_in', 'check_out', 'nights',
        'adults', 'children', 'special_requests',
        'status', 'source', 'external_id',
        'total_price_amount', 'total_price_currency',
        'order_item_id',
        'confirmed_at', 'checked_in_at', 'checked_out_at',
        'cancelled_at', 'cancellation_reason',
    ];

    protected $casts = [
        'check_in' => 'date',
        'check_out' => 'date',
        'nights' => 'integer',
        'adults' => 'integer',
        'children' => 'integer',
        'status' => HotelReservationStatus::class,
        'source' => HotelReservationSource::class,
        'total_price' => AsMoney::class,
        'confirmed_at' => 'immutable_datetime',
        'checked_in_at' => 'immutable_datetime',
        'checked_out_at' => 'immutable_datetime',
        'cancelled_at' => 'immutable_datetime',
    ];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `user()` BelongsTo User
- `roomType()` BelongsTo HotelRoomType
- `rooms()` BelongsToMany HotelRoom (via `hotel_reservation_rooms` pivot)
- `extras()` HasMany HotelReservationExtra
- `orderItem()` BelongsTo OrderItem (nullable)

**Methods:**
- `confirm()`, `checkIn()`, `checkOut()`, `cancel(string $reason)`, `markNoShow()`
- `canBeCancelled(): bool`
- `totalExtrasAmount(): Money` — sum of extras charges

**Orderable interface methods:**
- `orderableMoneyPrice(...)` — returns total_price (rate × nights)
- `orderableCreditsPrice(...)` — returns credits equivalent
- `orderableCreditsTaxes(...)` — returns tax amount
- `orderableConstraints()` — `[RequiresMetadata(['check_in', 'check_out', 'adults']), CheckInMustBeTodayOrFuture(), HotelRoomTypeMustBeAvailable()]`

#### HotelReservationRoom (Pivot Model)
**File:** `app/Models/HotelReservationRoom.php`

Simple pivot model between HotelReservation and HotelRoom.

#### HotelExtra Model
**File:** `app/Models/HotelExtra.php`

Implements `Orderable` interface.

```php
final class HotelExtra extends BaseModel implements Orderable
{
    use HasPublicId, HasFactory, SoftDeletes,
        HasDatabaseTranslations;

    protected $fillable = [
        'hotel_id', 'name', 'description', 'type',
        'money_price_amount', 'money_price_currency',
        'member_money_price_amount', 'member_money_price_currency',
        'is_active', 'sort_order',
    ];

    protected $casts = [
        'type' => HotelExtraType::class,
        'money_price' => AsMoney::class,
        'member_money_price' => AsMoney::class,
        'is_active' => 'boolean',
        'sort_order' => 'integer',
    ];

    protected array $translatable = ['name', 'description'];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `reservationExtras()` HasMany HotelReservationExtra

**Orderable interface methods:**
- `orderableMoneyPrice(...)` — returns money_price or member_money_price
- `orderableConstraints()` — `[new RequiresMetadata(['quantity'])]`

#### HotelReservationExtra Model
**File:** `app/Models/HotelReservationExtra.php`

```php
final class HotelReservationExtra extends BaseModel
{
    use HasPublicId, HasFactory;

    protected $fillable = [
        'reservation_id', 'extra_id', 'room_id',
        'quantity', 'unit_price_amount', 'unit_price_currency',
        'notes', 'billed_at', 'order_item_id',
    ];

    protected $casts = [
        'quantity' => 'integer',
        'unit_price' => AsMoney::class,
        'billed_at' => 'immutable_datetime',
    ];
}
```

**Relations:**
- `reservation()` BelongsTo HotelReservation
- `extra()` BelongsTo HotelExtra
- `room()` BelongsTo HotelRoom (nullable)
- `orderItem()` BelongsTo OrderItem (nullable)

**Computed:**
- `totalPrice(): Money` — unit_price × quantity

#### HotelHousekeepingLog Model
**File:** `app/Models/HotelHousekeepingLog.php`

```php
final class HotelHousekeepingLog extends BaseModel
{
    use HasFactory;

    protected $fillable = [
        'room_id', 'employee_id',
        'from_status', 'to_status',
        'notes', 'started_at', 'completed_at',
    ];

    protected $casts = [
        'from_status' => HotelHousekeepingStatus::class,
        'to_status' => HotelHousekeepingStatus::class,
        'started_at' => 'immutable_datetime',
        'completed_at' => 'immutable_datetime',
    ];
}
```

**Relations:**
- `room()` BelongsTo HotelRoom
- `employee()` BelongsTo Employee (nullable)

#### HotelChannelMapping Model
**File:** `app/Models/HotelChannelMapping.php`

```php
final class HotelChannelMapping extends BaseModel
{
    use HasPublicId, HasFactory, SoftDeletes;

    protected $fillable = [
        'hotel_id', 'room_type_id', 'channel',
        'external_property_id', 'external_room_type_id',
        'sync_rates', 'sync_availability', 'sync_reservations',
        'last_synced_at', 'is_active',
    ];

    protected $casts = [
        'channel' => HotelChannelType::class,
        'sync_rates' => 'boolean',
        'sync_availability' => 'boolean',
        'sync_reservations' => 'boolean',
        'last_synced_at' => 'immutable_datetime',
        'is_active' => 'boolean',
    ];
}
```

**Relations:**
- `hotel()` BelongsTo Hotel
- `roomType()` BelongsTo HotelRoomType

### 1.5 Create All Migrations

See `00-MASTER-PLAN.md` § Schema Migrations for all 12 migration definitions.

All migrations must have `down()` method for rollback.

### 1.6 Update Morph Maps

**File:** `app/Providers/AppServiceProvider.php`

Add to morph map:
```php
'hotel' => Hotel::class,
'hotel_room_type' => HotelRoomType::class,
'hotel_room' => HotelRoom::class,
'hotel_reservation' => HotelReservation::class,
'hotel_extra' => HotelExtra::class,
```

### 1.7 Create EnsureHotelManager Middleware

**File:** `app/Http/Middleware/EnsureHotelManagerMiddleware.php`

Logic (same pattern as EnsureRestaurantManagerMiddleware):
1. Verify authenticated user is an active employee
2. Verify employee has `is_hotel_manager = true`
3. Verify employee is attached to the requested place
4. Verify `{hotel}` route parameter belongs to the place
5. Return 403 if any check fails
6. Bind resolved Hotel to request for downstream controllers

### 1.8 Register Route Groups

**File:** `routes/api.php`

```php
// Hotel Manager routes (employee)
Route::prefix('v1/places/{place}/hotels/{hotel}')
    ->middleware([
        'auth:sanctum',
        sprintf('ability:%s', User::ABILITY_FULL),
        EnsureHotelManagerMiddleware::class,
    ])
    ->group(function () {
        // Phase 2: Room Types & Rooms
        // Phase 3: Rates & Availability
        // Phase 4: Reservations (manager)
        // Phase 5: Check-in/out & Payments
        // Phase 6: Extras
        // Phase 7: Housekeeping
        // Phase 8: Dashboard
        // Phase 9: Channel Manager
    });

// Hotel Customer routes
Route::prefix('v1/places/{place}/hotels')
    ->middleware(['auth:sanctum', 'customer'])
    ->group(function () {
        // Phase 2: Hotel/room browsing (customer)
        // Phase 4: Reservations (customer)
    });
```

### 1.9 Create Model Factories

Create factories for all main models:
- `HotelFactory`
- `HotelRoomTypeFactory`
- `HotelRoomFactory`
- `HotelSeasonalRateFactory`
- `HotelReservationFactory`
- `HotelExtraFactory`
- `HotelReservationExtraFactory`
- `HotelHousekeepingLogFactory`
- `HotelChannelMappingFactory`

### 1.10 Add Hotel Relation to Place

**File:** `app/Models/Place.php`

```php
/** @return HasMany<Hotel> */
public function hotels(): HasMany
{
    return $this->hasMany(Hotel::class);
}
```

### 1.11 Add Hotel Fields to Order

**File:** `app/Models/Order.php`

Add to `$fillable`: `hotel_id`, `hotel_reservation_id`
Add relation: `hotel()` BelongsTo Hotel (nullable)
Add relation: `hotelReservation()` BelongsTo HotelReservation (nullable)

### 1.12 Add Employee Role Migration

Migration to add `is_hotel_manager` boolean to `employees` table and seed permissions.

### 1.13 Tests

**File:** `tests/Feature/Controllers/HotelManager/HotelManagerAuthorizationTest.php`

Test cases:
- [ ] Hotel manager can access `/v1/places/{place}/hotels/{hotel}/*` routes
- [ ] Regular employee without hotel_manager role gets 403
- [ ] Customer user gets 403
- [ ] Coach user gets 403
- [ ] Hotel manager at Place A cannot access Place B's hotel routes
- [ ] Hotel manager cannot access hotel belonging to different place
- [ ] All model factories create valid records
- [ ] Morph map resolves correctly for new types

---

## Files Created/Modified

| Action | File |
|---|---|
| Create | `app/Enums/HotelRoomStatus.php` |
| Create | `app/Enums/HotelHousekeepingStatus.php` |
| Create | `app/Enums/HotelReservationStatus.php` |
| Create | `app/Enums/HotelRateSeason.php` |
| Create | `app/Enums/HotelExtraType.php` |
| Create | `app/Enums/HotelBedType.php` |
| Create | `app/Enums/HotelChannelType.php` |
| Create | `app/Enums/HotelReservationSource.php` |
| Modify | `app/Enums/OrderableType.php` |
| Create | `app/Models/Hotel.php` |
| Create | `app/Models/HotelRoomType.php` |
| Create | `app/Models/HotelRoom.php` |
| Create | `app/Models/HotelSeasonalRate.php` |
| Create | `app/Models/HotelReservation.php` |
| Create | `app/Models/HotelReservationRoom.php` |
| Create | `app/Models/HotelExtra.php` |
| Create | `app/Models/HotelReservationExtra.php` |
| Create | `app/Models/HotelHousekeepingLog.php` |
| Create | `app/Models/HotelChannelMapping.php` |
| Create | `app/Http/Middleware/EnsureHotelManagerMiddleware.php` |
| Modify | `app/Models/Place.php` |
| Modify | `app/Models/Order.php` |
| Modify | `app/Models/Employee.php` |
| Modify | `app/Models/User.php` |
| Modify | `app/Providers/AppServiceProvider.php` |
| Modify | `routes/api.php` |
| Create | 12 migration files |
| Create | 9 factory files |
| Create | `tests/Feature/Controllers/HotelManager/HotelManagerAuthorizationTest.php` |

---

## Quality Checks

**Do NOT run `./gitCheck.sh`.** Instead, run lint and PHPStan only on new/modified files:
```bash
vendor/bin/pint app/Models/Hotel.php app/Models/HotelRoomType.php ... (all new/modified files)
vendor/bin/phpstan analyse app/Models/Hotel.php app/Models/HotelRoomType.php ... --level=7
```

Do NOT commit. The user will commit manually.

---

## Acceptance Criteria

- [ ] All 8 enums created with `label()` and `HasOptions`
- [ ] All 10 models created with proper relations, casts, fillable
- [ ] All 12 migrations run without errors, `down()` works
- [ ] HotelReservation implements Orderable correctly
- [ ] HotelExtra implements Orderable correctly
- [ ] Morph maps resolve for all new types
- [ ] EnsureHotelManagerMiddleware blocks unauthorized access
- [ ] Route groups registered and accessible
- [ ] All factories create valid records
- [ ] `is_hotel_manager` recognized on Employee model
- [ ] Place→hotels() relation works
- [ ] Order→hotel(), hotelReservation() fields work
- [ ] All tests pass
- [ ] `vendor/bin/pint` passes on all new/modified files
- [ ] `vendor/bin/phpstan --level=7` passes on all new/modified files
