# Owner Manual Reservation for Registered Customers Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Extend the existing owner/employee guest booking routes so a phone lookup can return a registered customer (skipping pending-customer creation) while still requiring OTP consent before any reservation is created.

**Architecture:** Keep all behavior inside the existing route group at `routes/api.php:49-54`. `POST pending-customers` (and `POST reservations/on-arrival/prepare`) become the unified intake step: lookup by phone, return registered customer data when found or create a `PendingCustomer` when not, and **always send OTP**. `POST verify-otp` marks either a cache flag (registered customer) or `PendingCustomer.is_verified` (guest). `POST reservations/on-arrival` creates the reservation against `Customer::class` or `PendingCustomer::class` depending on which verification path completed.

**Tech Stack:** Laravel 11, PHP 8.2+, Pest PHP, Sanctum, Spatie Permission, Laravel Cache, Spatie Media Library.

## Global Constraints

- All changes stay in the existing owner/employee middleware group (`auth:sanctum`, `verified`, `role:owner|employee`) — **no new route prefix outside this group**.
- Existing routes to extend (do not replace):
  - `POST pending-customers`
  - `POST reservations/on-arrival/prepare`
  - `POST verify-otp`
  - `POST reservations/on-arrival`
- OTP is **always** sent on intake, even when a registered customer is found.
- A reservation for a registered customer may only be created after OTP verification succeeds.
- Registered customer lookup matches `users.phone` for users with the `customer` role and a `customers` row.
- When a registered customer is found, **do not** create a `PendingCustomer` record.
- When no registered customer is found, keep the existing pending-customer creation behavior (`name` required).
- Owner booking verification for registered customers is stored in cache (`owner_booking_verified:{phone}`) with a 30-minute TTL and consumed (one-time) when the reservation is created.
- Registered-customer reservations set `reservation_method` to `'manual'`; guest on-arrival reservations set `'on_arrival'`.
- `POST reservations/on-arrival` must validate that the target `unit_id` belongs to a building owned by the authenticated owner (or the employee's owner).
- Follow existing Pest patterns in `tests/Feature/PendingCustomerFlowTest.php`.

---

## File Map

| File | Responsibility |
|------|----------------|
| `app/Models/Customer.php` | `findByPhone()` helper |
| `app/Services/OwnerBookingVerificationService.php` | Cache-based OTP-consent flag for registered customers |
| `app/Http/Controllers/Api/PendingCustomerController.php` | Branch intake + OTP; branch verify-otp |
| `app/Http/Controllers/ReservationController.php` | Branch prepare + on-arrival store |
| `app/Http/Requests/PendingCustomerRequest.php` | Make `name` optional (required only for new guests, enforced in controller) |
| `app/Http/Requests/OnArrivalReservationPrepareRequest.php` | Prepare-step validation without photo |
| `app/Http/Requests/OnArrivalReservationRequest.php` | Accept registered OR pending verification; unit ownership check |
| `app/Services/ReservationService.php` | Optional `$reservationMethod` argument |
| `routes/api.php` | **No new routes** — only existing group |
| `tests/Feature/PendingCustomerFlowTest.php` | Extended coverage |

---

## End-to-End Flow

```text
POST /api/pending-customers { phone, name?, whatsapp? }
        |
        +-- registered customer found
        |       -> return customer data + OTP (no PendingCustomer row)
        |
        +-- not found
                -> require name, create PendingCustomer + OTP

POST /api/verify-otp { phone, otp }
        |
        +-- registered customer
        |       -> cache owner_booking_verified:{phone} = true
        |
        +-- pending guest
                -> PendingCustomer.is_verified = true

POST /api/reservations/on-arrival { phone, unit_id, dates, photo, ... }
        |
        +-- registered + cache verified
        |       -> Reservation(customer_type=Customer, reservation_method=manual)
        |
        +-- pending + is_verified
                -> Reservation(customer_type=PendingCustomer, reservation_method=on_arrival)
```

Same branching applies to `POST /api/reservations/on-arrival/prepare` for the combined prepare path.

---

### Task 1: Customer phone lookup helper

**Files:**
- Modify: `app/Models/Customer.php`
- Test: `tests/Unit/Models/ModelRelationsTest.php`

**Interfaces:**
- Produces: `Customer::findByPhone(string $phone): ?Customer`

- [ ] **Step 1: Write the failing test**

Add to `tests/Unit/Models/ModelRelationsTest.php`:

```php
it('finds a registered customer by user phone', function () {
    $user = User::factory()->create(['phone' => '0999888777']);
    $user->assignRole('customer');
    $customer = Customer::create([
        'id' => $user->id,
        'whatsapp_number' => '0999888777',
        'wallet' => 0,
    ]);

    $found = Customer::findByPhone('0999888777');

    expect($found)->not->toBeNull()
        ->and($found->id)->toBe($customer->id)
        ->and($found->relationLoaded('user'))->toBeTrue()
        ->and($found->user->phone)->toBe('0999888777');
});

it('returns null when phone belongs to a non-customer user', function () {
    User::factory()->create(['phone' => '0888777666'])->assignRole('owner');

    expect(Customer::findByPhone('0888777666'))->toBeNull();
});
```

Ensure `RolesAndPermissionsSeeder` is seeded in this file's `beforeEach`.

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Unit/Models/ModelRelationsTest.php --filter="finds a registered customer by user phone|returns null when phone belongs to a non-customer user"`

Expected: FAIL — `Call to undefined method App\Models\Customer::findByPhone()`

- [ ] **Step 3: Write minimal implementation**

Add to `app/Models/Customer.php`:

```php
public static function findByPhone(string $phone): ?self
{
    return static::query()
        ->whereHas('user', fn ($query) => $query->where('phone', $phone))
        ->with('user')
        ->first();
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `php artisan test tests/Unit/Models/ModelRelationsTest.php --filter="finds a registered customer by user phone|returns null when phone belongs to a non-customer user"`

Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add app/Models/Customer.php tests/Unit/Models/ModelRelationsTest.php
git commit -m "feat: add Customer::findByPhone helper"
```

---

### Task 2: Owner booking verification cache service

**Files:**
- Create: `app/Services/OwnerBookingVerificationService.php`
- Test: `tests/Unit/Services/OwnerBookingVerificationServiceTest.php`

**Interfaces:**
- Produces:
  - `markVerified(string $phone): void`
  - `isVerified(string $phone): bool`
  - `consume(string $phone): bool` — returns true and clears cache if verified

- [ ] **Step 1: Write the failing test**

Create `tests/Unit/Services/OwnerBookingVerificationServiceTest.php`:

```php
<?php

use App\Services\OwnerBookingVerificationService;
use Illuminate\Support\Facades\Cache;

beforeEach(function () {
    Cache::flush();
    $this->service = new OwnerBookingVerificationService();
});

it('marks and checks a phone as verified', function () {
    $this->service->markVerified('5551234567');

    expect($this->service->isVerified('5551234567'))->toBeTrue()
        ->and($this->service->isVerified('5559999999'))->toBeFalse();
});

it('consumes verification once', function () {
    $this->service->markVerified('5551234567');

    expect($this->service->consume('5551234567'))->toBeTrue()
        ->and($this->service->isVerified('5551234567'))->toBeFalse()
        ->and($this->service->consume('5551234567'))->toBeFalse();
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Unit/Services/OwnerBookingVerificationServiceTest.php`

Expected: FAIL — class not found

- [ ] **Step 3: Write minimal implementation**

Create `app/Services/OwnerBookingVerificationService.php`:

```php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class OwnerBookingVerificationService
{
    private const int TTL_MINUTES = 30;

    public function markVerified(string $phone): void
    {
        Cache::put($this->cacheKey($phone), true, now()->addMinutes(self::TTL_MINUTES));
    }

    public function isVerified(string $phone): bool
    {
        return Cache::get($this->cacheKey($phone)) === true;
    }

    public function consume(string $phone): bool
    {
        if (! $this->isVerified($phone)) {
            return false;
        }

        Cache::forget($this->cacheKey($phone));

        return true;
    }

    private function cacheKey(string $phone): string
    {
        return "owner_booking_verified:{$phone}";
    }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `php artisan test tests/Unit/Services/OwnerBookingVerificationServiceTest.php`

Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add app/Services/OwnerBookingVerificationService.php tests/Unit/Services/OwnerBookingVerificationServiceTest.php
git commit -m "feat: add owner booking verification cache service"
```

---

### Task 3: Extend pending-customers intake (lookup + always OTP)

**Files:**
- Modify: `app/Http/Controllers/Api/PendingCustomerController.php`
- Modify: `app/Http/Requests/PendingCustomerRequest.php`
- Test: `tests/Feature/PendingCustomerFlowTest.php`

**Interfaces:**
- Consumes: `Customer::findByPhone()`, `OtpService::send()`
- Produces: intake JSON with `customer_type` of `'registered'` or `'pending'`, always includes OTP (dev mode)

- [ ] **Step 1: Write the failing tests**

Add to `tests/Feature/PendingCustomerFlowTest.php` setup a registered customer:

```php
$this->registeredCustomerUser = User::factory()->create([
    'name' => 'Registered Customer',
    'email' => 'registered@example.com',
    'phone' => '5551234567',
    'password' => bcrypt('Password123!'),
]);
$this->registeredCustomerUser->assignRole('customer');
$this->registeredCustomerUser->customer()->create([
    'whatsapp_number' => '5551234567',
    'wallet' => 0,
]);
```

Add tests:

```php
it('returns registered customer data and sends otp without creating pending customer', function () {
    $response = $this->withToken($this->ownerToken)->postJson('/api/pending-customers', [
        'phone' => '5551234567',
    ]);

    $response->assertOk()
        ->assertJsonPath('customer_type', 'registered')
        ->assertJsonPath('customer.user.phone', '5551234567')
        ->assertJsonPath('customer.user.name', 'Registered Customer')
        ->assertJsonStructure(['otp']);

    expect(PendingCustomer::byPhone('555123456789')->count())->toBe(0);
});

it('still requires name when phone is not registered', function () {
    $this->withToken($this->ownerToken)->postJson('/api/pending-customers', [
        'phone' => '9999999999',
    ])->assertStatus(422)
        ->assertJsonValidationErrors(['name']);
});

it('creates pending customer and sends otp for unknown phone', function () {
    $response = $this->withToken($this->ownerToken)->postJson('/api/pending-customers', [
        'name' => 'New Guest',
        'phone' => '9999999999',
        'whatsapp_number' => '9999999999',
    ]);

    $response->assertCreated()
        ->assertJsonPath('customer_type', 'pending')
        ->assertJsonPath('pending_customer.phone', '9999999999')
        ->assertJsonStructure(['otp']);
});
```

Fix the typo in first test — use `'5551234567'` in `PendingCustomer::byPhone(...)`.

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="returns registered customer data and sends otp|still requires name when phone is not registered|creates pending customer and sends otp for unknown phone"`

Expected: FAIL — response missing `customer_type: registered` or wrong status code

- [ ] **Step 3: Write minimal implementation**

Update `app/Http/Requests/PendingCustomerRequest.php`:

```php
public function rules(): array
{
    return [
        'name' => ['nullable', 'string', 'max:255'],
        'phone' => ['required', 'string', 'max:255'],
        'whatsapp_number' => ['nullable', 'string', 'max:255'],
    ];
}
```

Update `app/Http/Controllers/Api/PendingCustomerController.php` `store()`:

```php
use App\Http\Resources\CustomerResource;
use App\Models\Customer;

public function store(PendingCustomerRequest $request)
{
    $validated = $request->validated();
    $phone = $validated['phone'];

    $customer = Customer::findByPhone($phone);

    if ($customer) {
        $otp = $this->otpService->send($phone);

        return response()->json([
            'customer_type' => 'registered',
            'customer' => CustomerResource::make($customer),
            'message' => 'Registered customer found. OTP sent for consent.',
            'otp' => $otp, // TODO: remove when SMS/WhatsApp integration is added
        ]);
    }

    if (empty($validated['name'])) {
        return response()->json([
            'message' => 'The name field is required when no registered customer exists.',
            'errors' => ['name' => ['The name field is required when no registered customer exists.']],
        ], 422);
    }

    $pending = PendingCustomer::create([
        'name' => $validated['name'],
        'phone' => $phone,
        'whatsapp_number' => $validated['whatsapp_number'] ?? $phone,
        'is_verified' => false,
    ]);

    $otp = $this->otpService->send($pending->phone);

    return response()->json([
        'customer_type' => 'pending',
        'pending_customer' => $pending,
        'message' => 'OTP sent. Use /verify-otp to complete verification.',
        'otp' => $otp, // TODO: remove when SMS/WhatsApp integration is added
    ], 201);
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="returns registered customer data and sends otp|still requires name when phone is not registered|creates pending customer and sends otp for unknown phone"`

Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add app/Http/Controllers/Api/PendingCustomerController.php app/Http/Requests/PendingCustomerRequest.php tests/Feature/PendingCustomerFlowTest.php
git commit -m "feat: branch pending-customers intake by registered phone lookup"
```

---

### Task 4: Extend verify-otp for registered customers

**Files:**
- Modify: `app/Http/Controllers/Api/PendingCustomerController.php`
- Test: `tests/Feature/PendingCustomerFlowTest.php`

**Interfaces:**
- Consumes: `OwnerBookingVerificationService`, `Customer::findByPhone()`, `OtpService::verify()`
- Produces: OTP success sets cache for registered customers OR `PendingCustomer.is_verified = true`

- [ ] **Step 1: Write the failing test**

```php
it('marks registered customer phone as verified after otp', function () {
    $phone = '5551234567';

    $start = $this->withToken($this->ownerToken)->postJson('/api/pending-customers', [
        'phone' => $phone,
    ]);

    $otp = $start->json('otp');

    $this->postJson('/api/verify-otp', [
        'phone' => $phone,
        'otp' => $otp,
    ])->assertOk();

    expect(app(\App\Services\OwnerBookingVerificationService::class)->isVerified($phone))->toBeTrue();
});

it('still verifies pending customers after otp', function () {
    $phone = '5555555555';

    $start = $this->withToken($this->ownerToken)->postJson('/api/pending-customers', [
        'name' => 'Pending Customer',
        'phone' => $phone,
        'whatsapp_number' => $phone,
    ]);

    $otp = $start->json('otp');

    $this->postJson('/api/verify-otp', [
        'phone' => $phone,
        'otp' => $otp,
    ])->assertOk();

    expect(PendingCustomer::byPhone($phone)->first()->is_verified)->toBeTrue();
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="marks registered customer phone as verified after otp|still verifies pending customers after otp"`

Expected: FAIL — cache not set for registered customer

- [ ] **Step 3: Write minimal implementation**

Inject `OwnerBookingVerificationService` in `PendingCustomerController` constructor.

Update `verifyOtp()`:

```php
use App\Models\Customer;
use App\Services\OwnerBookingVerificationService;

public function __construct(
    private OtpService $otpService,
    private OwnerBookingVerificationService $ownerBookingVerification,
) {
}

public function verifyOtp(VerifyOtpRequest $request)
{
    $phone = $request->validated('phone');
    $otp = $request->validated('otp');

    if (! $this->otpService->verify($phone, $otp)) {
        return response()->json(['message' => 'Invalid or expired OTP'], 422);
    }

    if (Customer::findByPhone($phone)) {
        $this->ownerBookingVerification->markVerified($phone);

        return response()->json([
            'message' => 'OTP verified successfully.',
            'customer_type' => 'registered',
        ]);
    }

    PendingCustomer::byPhone($phone)->unverified()->first()?->update(['is_verified' => true]);

    return response()->json([
        'message' => 'OTP verified successfully.',
        'customer_type' => 'pending',
    ]);
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="marks registered customer phone as verified after otp|still verifies pending customers after otp"`

Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add app/Http/Controllers/Api/PendingCustomerController.php tests/Feature/PendingCustomerFlowTest.php
git commit -m "feat: verify otp for registered customers via booking consent cache"
```

---

### Task 5: ReservationService reservation_method support

**Files:**
- Modify: `app/Services/ReservationService.php`

**Interfaces:**
- Produces: `createReservationForCustomer(..., string $reservationMethod = 'system')`

- [ ] **Step 1: Update method signatures**

Change `createReservationForCustomer` and `persistReservation` to accept `string $reservationMethod = 'system'` and pass it into `Reservation::create()`:

```php
'reservation_method' => $reservationMethod,
```

- [ ] **Step 2: Run existing reservation tests**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php tests/Feature/ReservationFixesTest.php`

Expected: PASS (defaults unchanged)

- [ ] **Step 3: Commit**

```bash
git add app/Services/ReservationService.php
git commit -m "feat: allow reservation_method override in ReservationService"
```

---

### Task 6: Extend on-arrival store for registered customers (with OTP gate)

**Files:**
- Modify: `app/Http/Controllers/ReservationController.php`
- Modify: `app/Http/Requests/OnArrivalReservationRequest.php`
- Test: `tests/Feature/PendingCustomerFlowTest.php`

**Interfaces:**
- Consumes: `OwnerBookingVerificationService::consume()`, `Customer::findByPhone()`
- Produces: reservation with `Customer::class` + `'manual'` OR `PendingCustomer::class` + `'on_arrival'`

- [ ] **Step 1: Write the failing tests**

```php
function verifyRegisteredCustomerOtp($test, string $phone): void
{
    $start = $test->withToken($test->ownerToken)->postJson('/api/pending-customers', [
        'phone' => $phone,
    ]);

    $otp = $start->json('otp');

    $test->postJson('/api/verify-otp', [
        'phone' => $phone,
        'otp' => $otp,
    ])->assertOk();
}

it('rejects on-arrival reservation for registered customer without otp verification', function () {
    $photo = UploadedFile::fake()->image('id-card.jpg');

    $this->withToken($this->ownerToken)->postJson('/api/reservations/on-arrival', [
        'unit_id' => $this->unit->id,
        'check_in_date' => '2026-08-01',
        'check_out_date' => '2026-08-04',
        'adults_count' => 2,
        'children_count' => 1,
        'photo' => $photo,
        'name' => 'Registered Customer',
        'phone' => '5551234567',
        'whatsapp_number' => '5551234567',
    ])->assertStatus(422)
        ->assertJsonValidationErrors(['phone']);
});

it('creates on-arrival reservation for registered customer after otp verification', function () {
    verifyRegisteredCustomerOtp($this, '5551234567');

    $photo = UploadedFile::fake()->image('id-card.jpg');

    $response = $this->withToken($this->ownerToken)->postJson('/api/reservations/on-arrival', [
        'unit_id' => $this->unit->id,
        'check_in_date' => '2026-08-01',
        'check_out_date' => '2026-08-04',
        'adults_count' => 2,
        'children_count' => 0,
        'photo' => $photo,
        'name' => 'Registered Customer',
        'phone' => '5551234567',
        'whatsapp_number' => '5551234567',
    ]);

    $response->assertSuccessful()
        ->assertJsonPath('data.reservation_method', 'manual');

    $reservation = Reservation::findOrFail($response->json('data.id'));

    expect($reservation->customer_type)->toBe(Customer::class)
        ->and($reservation->customer_id)->toBe($this->registeredCustomerUser->customer->id);
});

it('still creates on-arrival reservation for verified pending customer', function () {
    $photo = UploadedFile::fake()->image('id-card.jpg');
    $phone = '5555555555';

    createVerifiedPendingCustomer($this, $phone);

    $response = $this->withToken($this->ownerToken)->postJson('/api/reservations/on-arrival', [
        'unit_id' => $this->unit->id,
        'check_in_date' => '2026-08-01',
        'check_out_date' => '2026-08-04',
        'adults_count' => 2,
        'children_count' => 1,
        'photo' => $photo,
        'name' => 'Pending Customer',
        'phone' => $phone,
        'whatsapp_number' => $phone,
    ]);

    $response->assertSuccessful()
        ->assertJsonPath('data.reservation_method', 'on_arrival');
});
```

Add `use App\Models\Customer;` if missing.

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="rejects on-arrival reservation for registered customer without otp|creates on-arrival reservation for registered customer after otp|still creates on-arrival reservation for verified pending customer"`

Expected: FAIL on registered-customer success path

- [ ] **Step 3: Update OnArrivalReservationRequest validation**

Replace the pending-only phone check in `withValidator()`:

```php
use App\Models\Customer;
use App\Services\OwnerBookingVerificationService;

$phone = $this->input('phone');
if ($phone && ! $validator->errors()->has('phone')) {
    $registeredCustomer = Customer::findByPhone($phone);

    if ($registeredCustomer) {
        if (! app(OwnerBookingVerificationService::class)->isVerified($phone)) {
            $validator->errors()->add(
                'phone',
                'Customer OTP consent is required before creating this reservation.'
            );
        }
    } else {
        $pending = PendingCustomer::byPhone($phone)->where('is_verified', true)->first();
        if (! $pending) {
            $validator->errors()->add(
                'phone',
                'No verified pending customer found for this phone number. Collect the data and verify the OTP first.'
            );
        }
    }
}

$user = $this->user();
$owner = $user?->owner ?? $user?->employee?->owner;
$unit = Unit::with('building')->find($unitId);

if ($owner && $unit && $unit->building->owner_id !== $owner->id) {
    $validator->errors()->add('unit_id', 'You do not manage this unit.');
}
```

- [ ] **Step 4: Update ReservationController::onArrivalStore**

Inject `OwnerBookingVerificationService`.

```php
public function onArrivalStore(OnArrivalReservationRequest $request)
{
    return DB::transaction(function () use ($request) {
        $data = $request->validated();
        $phone = $data['phone'];

        $customer = Customer::findByPhone($phone);

        if ($customer && $this->ownerBookingVerification->consume($phone)) {
            return ReservationService::createReservationForCustomer(
                $request,
                Customer::class,
                $customer->id,
                'manual'
            );
        }

        $pendingCustomer = PendingCustomer::byPhone($phone)
            ->where('is_verified', true)
            ->firstOrFail();

        return ReservationService::createReservationForCustomer(
            $request,
            PendingCustomer::class,
            $pendingCustomer->id,
            'on_arrival'
        );
    });
}
```

- [ ] **Step 5: Run test to verify it passes**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php`

Expected: PASS (all tests including existing guest flow)

- [ ] **Step 6: Commit**

```bash
git add app/Http/Controllers/ReservationController.php app/Http/Requests/OnArrivalReservationRequest.php tests/Feature/PendingCustomerFlowTest.php
git commit -m "feat: allow on-arrival booking for registered customers after otp consent"
```

---

### Task 7: Extend prepareOnArrival with same lookup branch

**Files:**
- Create: `app/Http/Requests/OnArrivalReservationPrepareRequest.php`
- Modify: `app/Http/Controllers/ReservationController.php:7,84`
- Test: `tests/Feature/PendingCustomerFlowTest.php`

**Interfaces:**
- Produces: prepare response mirrors `pending-customers` branching; always sends OTP; no photo required at prepare step

- [ ] **Step 1: Write the failing test**

Update existing prepare test to expect `customer_type`:

```php
it('prepares an on-arrival reservation for a registered customer and sends otp', function () {
    $response = $this->withToken($this->ownerToken)->postJson('/api/reservations/on-arrival/prepare', [
        'unit_id' => $this->unit->id,
        'check_in_date' => '2026-08-01',
        'check_out_date' => '2026-08-04',
        'adults_count' => 2,
        'children_count' => 1,
        'name' => 'Registered Customer',
        'phone' => '5551234567',
        'whatsapp_number' => '5551234567',
    ]);

    $response->assertOk()
        ->assertJsonPath('customer_type', 'registered')
        ->assertJsonPath('customer.user.phone', '5551234567')
        ->assertJsonStructure(['otp']);

    expect(PendingCustomer::byPhone('5551234567')->count())->toBe(0);
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php --filter="prepares an on-arrival reservation for a registered customer and sends otp"`

Expected: FAIL

- [ ] **Step 3: Create prepare request without photo**

Create `app/Http/Requests/OnArrivalReservationPrepareRequest.php`:

```php
<?php

namespace App\Http\Requests;

use App\Models\Unit;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;

class OnArrivalReservationPrepareRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['nullable', 'string', 'max:255'],
            'phone' => ['required', 'string', 'max:255'],
            'whatsapp_number' => ['nullable', 'string', 'max:255'],
            'unit_id' => ['required', 'exists:units,id'],
            'check_in_date' => ['required', 'date', 'after_or_equal:today'],
            'check_out_date' => ['required', 'date', 'after:check_in_date'],
            'adults_count' => ['required', 'integer', 'min:1'],
            'children_count' => ['required', 'integer', 'min:0'],
            'promo_code' => ['nullable', 'string'],
            'notes' => ['nullable', 'string'],
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $validator) {
            $unitId = $this->input('unit_id');

            if (! $unitId || $validator->errors()->has('unit_id')) {
                return;
            }

            $unit = Unit::find($unitId);

            if (! $unit) {
                return;
            }

            $adults = $this->input('adults_count');
            $children = $this->input('children_count');

            if ($adults !== null && $adults > $unit->max_adults) {
                $validator->errors()->add('adults_count', "The unit accepts a maximum of {$unit->max_adults} adults.");
            }

            if ($children !== null && $children > $unit->max_children) {
                $validator->errors()->add('children_count', "The unit accepts a maximum of {$unit->max_children} children.");
            }
        });
    }
}
```

- [ ] **Step 4: Update prepareOnArrival**

Change signature to `prepareOnArrival(OnArrivalReservationPrepareRequest $request)` and branch like `PendingCustomerController::store()`:

```php
use App\Http\Resources\CustomerResource;

public function prepareOnArrival(OnArrivalReservationPrepareRequest $request)
{
    $data = $request->validated();
    $phone = $data['phone'];

    $customer = Customer::findByPhone($phone);

    if ($customer) {
        $otp = $this->otpService->send($phone);

        return response()->json([
            'customer_type' => 'registered',
            'customer' => CustomerResource::make($customer),
            'message' => 'Registered customer found. OTP sent for consent.',
            'otp' => $otp, // TODO: remove when SMS/WhatsApp integration is added
        ]);
    }

    if (empty($data['name'])) {
        return response()->json([
            'message' => 'The name field is required when no registered customer exists.',
            'errors' => ['name' => ['The name field is required when no registered customer exists.']],
        ], 422);
    }

    $pendingCustomer = PendingCustomer::create([
        'name' => $data['name'],
        'phone' => $phone,
        'whatsapp_number' => $data['whatsapp_number'] ?? $phone,
        'is_verified' => false,
    ]);

    $otp = $this->otpService->send($pendingCustomer->phone);

    return response()->json([
        'customer_type' => 'pending',
        'pending_customer' => $pendingCustomer,
        'message' => 'OTP sent. Verify the OTP then confirm the reservation.',
        'otp' => $otp, // TODO: remove when SMS/WhatsApp integration is added
    ], 201);
}
```

- [ ] **Step 5: Run full pending customer test file**

Run: `php artisan test tests/Feature/PendingCustomerFlowTest.php`

Expected: PASS

- [ ] **Step 6: Commit**

```bash
git add app/Http/Requests/OnArrivalReservationPrepareRequest.php app/Http/Controllers/ReservationController.php tests/Feature/PendingCustomerFlowTest.php
git commit -m "feat: branch on-arrival prepare by registered customer lookup"
```

---

## Spec Coverage Self-Review

| Requirement | Task |
|-------------|------|
| Works inside existing route group (49–54) | All tasks — no new routes |
| Phone lookup returns registered customer | Task 3, 7 |
| Skip pending-customer creation when registered | Task 3, 7 |
| OTP always sent, even when found | Task 3, 7 |
| Owner cannot book without customer OTP consent | Task 4, 6 |
| Fall back to pending flow when not registered | Task 3 |
| Reservation linked to `Customer` when registered | Task 6 |
| `reservation_method` distinguishes manual vs on_arrival | Task 5, 6 |
| Unit ownership guard | Task 6 |

No placeholders remain. All method names consistent across tasks.

---

**Plan complete and saved to `docs/superpowers/plans/2026-06-18-owner-manual-reservation-registered-customer-plan.md`. Two execution options:**

**1. Subagent-Driven (recommended)** — fresh subagent per task, review between tasks, fast iteration

**2. Inline Execution** — execute tasks in this session using executing-plans, batch execution with checkpoints

**Which approach?**
