# Phase 6: Extras & Room Service — DONE

**Completed:** 2026-03-16

## Summary

Extras management (minibar, spa, laundry, parking...) with CRUD via Filament admin, manager API for charging extras to checked-in reservations, customer browsing, and integration with the folio/checkout system. Includes 1 action, 2 API resources, 1 form request, 4 controllers, 2 Filament relation managers, and 8 Pest tests.

## Files Created

### Actions (1)
- `app/Actions/HotelManager/AddExtraToReservationAction.php` — Validates reservation is CHECKED_IN, resolves member pricing, creates HotelReservationExtra with billed_at timestamp.

### API Resources (2)
- `app/Http/Resources/HotelManager/HotelExtraResource.php` — Extra detail with name, type, price, member_price, is_active.
- `app/Http/Resources/HotelManager/HotelReservationExtraResource.php` — Extra charge on reservation with extra info, room, quantity, unit_price, total_price, billed_at, is_billed flag.

### Form Requests (1)
- `app/Http/Requests/HotelManager/AddReservationExtraRequest.php` — extra_id, quantity (int ≥ 1), room_id (optional), notes (optional).

### Manager Controllers (3)
- `app/Http/Controllers/Api/Employee/HotelManager/ListExtrasController.php` — GET `/manage/extras` with optional type filter.
- `app/Http/Controllers/Api/Employee/HotelManager/AddReservationExtraController.php` — POST `/manage/reservations/{reservation}/extras`, validates checked-in, resolves extra + room by public_id.
- `app/Http/Controllers/Api/Employee/HotelManager/RemoveReservationExtraController.php` — DELETE `/manage/reservations/{reservation}/extras/{reservationExtra}`, validates not yet billed.

### Customer Controllers (1)
- `app/Http/Controllers/Api/Hotel/ListHotelExtrasController.php` — GET `/{hotel}/extras`, active extras only.

### Filament Relation Managers (2)
- `app/Filament/Resources/HotelResource/RelationManagers/ExtrasRelationManager.php` — Full CRUD with translatable name/description, type filter, price fields, sort order.
- `app/Filament/Resources/HotelResource/RelationManagers/ReservationExtrasRelationManager.php` — Read-only table showing extras charged to a reservation with billed/invoiced status.

### Tests (1)
- `tests/Feature/Controllers/HotelManager/ExtrasTest.php` — 8 tests: list extras, add extra to checked-in reservation, cannot add to non-checked-in, extras in folio at checkout, remove unbilled extra, cannot remove billed extra, customer browsing (active only), standard pricing for non-member.

## Files Modified

- `app/Filament/Resources/HotelResource.php` — Added ExtrasRelationManager to relation managers.
- `routes/api.php` — Added 3 manager routes + 1 customer route for extras.

## API Routes

### Hotel Manager
| Method | URL | Controller |
|--------|-----|-----------|
| GET | `/v1/places/{place}/hotels/{hotel}/manage/extras` | ListExtrasController |
| POST | `/v1/places/{place}/hotels/{hotel}/manage/reservations/{reservation}/extras` | AddReservationExtraController |
| DELETE | `/v1/places/{place}/hotels/{hotel}/manage/reservations/{reservation}/extras/{reservationExtra}` | RemoveReservationExtraController |

### Customer
| Method | URL | Controller |
|--------|-----|-----------|
| GET | `/v1/places/{place}/hotels/{hotel}/extras` | ListHotelExtrasController |

## Quality Checks

- **Pint:** PASS (0 issues after auto-fix)
- **PHPStan level 7:** PASS (0 errors)
- **Pest tests:** 89 passing (18 Phase 1 + 14 Phase 2 + 11 Phase 3 + 8 Phase 7 + 13 Phase 4 + 17 Phase 5 + 8 Phase 6)

## Design Decisions

- **AnonymousResourceCollection with $wrap=null**: The `ApiResource` base class sets `$wrap = null`, which causes `::collection()` to serialize incorrectly for list endpoints. Used `->resolve()` + `response()->json()` pattern instead.
- **Member pricing**: The `AddExtraToReservationAction` checks `$reservation->user->is_member` (computed accessor based on active membership) and uses `HotelExtra::orderableMoneyPrice($isMember)` which falls back to standard pricing when no member price is set.
- **Billed vs unbilled**: Extras can only be removed if `order_item_id` is null (not yet included in a checkout folio order). Once billed, they are immutable.
- **Reuse existing Form Requests**: POS/Cash payment requests reused from ShopManager (Phase 5).

## Notes

- No commits created — user will commit manually.
- Swagger annotations present on all 4 new controllers.
- All Filament relation managers follow existing patterns (BaseRelationManager, HasTranslatableFields).
