# Codex-ready implementation blueprint: MEWS for `brafa-laravel-livewire`

## Objective

Add **MEWS** as a first-class PMS provider in the existing BRAFA flow, using the repo’s current architecture:

1. Fetch bookings from MEWS.
2. Normalize them into the existing local `bookings` table flow.
3. Let the existing compare/reconcile flow create or update `booking_codes`.
4. Let the existing lock-send flow provision the code in Sciener.
5. After Sciener success, write the final passcode back to MEWS.

This blueprint is intentionally aligned with the current repo structure and existing service boundaries.

---

## 1. Current repo behavior to preserve

### 1.1 Provider switch for booking fetch

`ShowPropertyDetails::getBookings($PropertyID)` is the central provider switch. Right now it branches on `Property.API` and routes to:

- `beds24` / `godo` -> `HelperFunction::beds24FormattedBookings(...)`
- `total` -> `HelperFunction::getTotalBookings(...)`
- `fritimi` -> `HelperFunction::getFritimiBookings(...)`
- `bookingfactory` -> `HelperFunction::TBFFormattedBookings(...)`
- some providers currently return `[]`

MEWS must be added here as a new `if ($api == 'mews')` branch.

### 1.2 Booking sync path

`Auomate_beds24Service::getBookings($propertyId)` already acts as the orchestration layer:

- calls `ShowPropertyDetails->getBookings($propertyId)`
- handles provider-specific normalization into local DB
- runs `compareBookingsWithCodesTable()` for normal properties
- runs `compareGolfBookingsWithCodesTable()` for golf properties

Do **not** create a separate standalone MEWS sync pipeline if it can reuse this service.

### 1.3 Existing PMS write-back pattern

The repo already has provider-specific PMS write-back behavior:

- `Beds24APIService::write_beds24_passcode(...)`
- `TBF_API_SERVICE::write_tbf_keycode(...)`

But the current codebase mixes service-based write-back with property-specific automation commands. For MEWS, do **not** add another one-off console command pattern. Centralize the write-back after successful lock provisioning.

### 1.4 Best success checkpoint

`AssignedLocks::sendPendingPasscodes()` is the clean success point.

That method:

- finds pending `booking_codes`
- calculates effective timestamps with `calculateTimestamps(...)`
- sends passcodes via `ScienerApiService::sendPasscode_for_booking(...)`
- marks rows as `success` once `keyboardPwdId` is known
- also treats `errcode == -3007` as success after resolving existing keypad code ids

That is exactly where PMS write-back should happen.

### 1.5 Important mapping constraint

The repo explicitly documents an unconventional mapping in `AssignedLocks`:

- `assignedlocks.RoomName` == `rooms.RoomID`
- `assignedlocks.RoomID` == `rooms.UnitName`

MEWS resource mapping must respect that and not assume that labels alone are safe.

### 1.6 Current booking model quirk

`Booking::updateBooking(...)` currently updates `passcode` with `$startDate` instead of an actual passcode value.

That must be fixed before or during MEWS work, otherwise booking updates can corrupt passcodes.

---

## 2. High-level architecture for MEWS in this repo

Implement MEWS in the same style as existing PMS providers, but cleaner:

- **config model**: `MewsDetail`
- **API service**: `MewsApiService`
- **room/resource mapping model**: `MewsResourceMapping`
- **formatted booking fetch**: `HelperFunction::mewsFormattedBookings(...)`
- **provider branch**: add `mews` inside `ShowPropertyDetails::getBookings(...)`
- **PMS write-back service**: `PmsWritebackService`
- **webhook ingestion**: controller + queue job
- **polling fallback**: artisan command + scheduler

---

## 3. Database changes

### 3.1 Create `mews_details`

Create a new model:

- `app/Models/MewsDetail.php`

Create migration:

- `database/migrations/xxxx_xx_xx_xxxxxx_create_mews_details_table.php`

Suggested columns:

- `id`
- `PropertyID` string/indexed
- `client_token` text
- `access_token` text
- `client_name` string nullable
- `enterprise_id` string nullable
- `service_id` string nullable
- `webhook_secret` string nullable
- `base_url` string nullable
- `use_service_order_notes` boolean default false
- `is_active` boolean default true
- timestamps

Suggested model setup:

- table name: `mews_details`
- fillable fields for all above
- cast booleans

### 3.2 Create `mews_resource_mappings`

Create a new model:

- `app/Models/MewsResourceMapping.php`

Create migration:

- `database/migrations/xxxx_xx_xx_xxxxxx_create_mews_resource_mappings_table.php`

Suggested columns:

- `id`
- `PropertyID` string/indexed
- `mews_resource_id` string/indexed
- `mews_resource_name` string nullable
- `mews_service_order_scope` string nullable
- `RoomID` string
- `UnitName` string
- `is_primary` boolean default true
- timestamps

This table is the bridge between MEWS resource identifiers and BRAFA’s `RoomID`/`UnitName` pair.

### 3.3 Add external refs to local bookings

Create a migration that adds nullable MEWS fields to `bookings`:

- `mews_reservation_id` string nullable index
- `mews_service_order_id` string nullable index
- `mews_resource_id` string nullable index
- `mews_enterprise_id` string nullable
- `mews_resource_access_token_id` string nullable

Alternative: create `booking_external_refs`, but for speed, adding nullable fields to `bookings` is simpler.

### 3.4 Create webhook log table

Create migration:

- `database/migrations/xxxx_xx_xx_xxxxxx_create_mews_webhook_logs_table.php`

Columns:

- `id`
- `PropertyID`
- `event_type` nullable
- `entity_id` nullable
- `payload` json
- `status` string default `received`
- `error_message` text nullable
- timestamps

### 3.5 Create write-back audit table

Create migration:

- `database/migrations/xxxx_xx_xx_xxxxxx_create_mews_writeback_logs_table.php`

Columns:

- `id`
- `PropertyID`
- `booking_id` nullable
- `mews_service_order_id` nullable
- `mews_resource_id` nullable
- `mews_resource_access_token_id` nullable
- `action` string
- `request_payload` json nullable
- `response_payload` json nullable
- `status` string
- `error_message` text nullable
- timestamps

---

## 4. Model changes

### 4.1 Fix `Booking` model bug

Update `app/Models/Booking.php`.

Current problem:

- `updateBooking(...)` sets `'passcode' => $startDate`

Fix options:

#### Preferred fix
Change signature to:

```php
public static function updateBooking(
    $id,
    $bookingId,
    $guestName,
    $roomId,
    $unitName,
    $passcode,
    $startDate,
    $endDate,
    $bookingStatus,
    $referenceNumber = null,
    $mewsReservationId = null,
    $mewsServiceOrderId = null,
    $mewsResourceId = null,
    $mewsEnterpriseId = null
)
```

Then update with:

```php
'passcode' => $passcode,
```

Also allow the new MEWS external id fields in `$fillable`.

### 4.2 Create `MewsDetail`

Minimal model:

```php
class MewsDetail extends Model
{
    protected $fillable = [
        'PropertyID',
        'client_token',
        'access_token',
        'client_name',
        'enterprise_id',
        'service_id',
        'webhook_secret',
        'base_url',
        'use_service_order_notes',
        'is_active',
    ];

    protected $casts = [
        'use_service_order_notes' => 'boolean',
        'is_active' => 'boolean',
    ];
}
```

### 4.3 Create `MewsResourceMapping`

Minimal model:

```php
class MewsResourceMapping extends Model
{
    protected $fillable = [
        'PropertyID',
        'mews_resource_id',
        'mews_resource_name',
        'mews_service_order_scope',
        'RoomID',
        'UnitName',
        'is_primary',
    ];

    protected $casts = [
        'is_primary' => 'boolean',
    ];
}
```

---

## 5. New service: `MewsApiService`

Create:

- `app/Services/MewsApiService.php`

### 5.1 Responsibilities

This service should:

- build authenticated MEWS requests
- fetch reservations in date windows
- fetch services/resources if needed
- fetch and upsert resource access tokens
- optionally write service order notes
- verify/retry/log MEWS requests

### 5.2 Constructor

Use:

- `baseUrl` from `MewsDetail->base_url` if present, else config fallback
- Laravel `Http`
- centralized request method

### 5.3 Required public methods

Implement these methods:

```php
public function getConfigForProperty(string $propertyId): MewsDetail
public function postForProperty(string $propertyId, string $path, array $payload = []): array
public function getReservationsByIds(string $propertyId, array $reservationIds): array
public function getReservationsByDateWindow(string $propertyId, string $startUtc, string $endUtc, ?string $cursor = null): array
public function getResources(string $propertyId): array
public function getServices(string $propertyId): array
public function getResourceAccessTokens(string $propertyId, array $serviceOrderIds = [], array $tokenIds = []): array
public function addResourceAccessToken(string $propertyId, array $token): array
public function updateResourceAccessToken(string $propertyId, array $token): array
public function deleteResourceAccessToken(string $propertyId, array $tokenIds): array
public function getServiceOrderNotes(string $propertyId, array $serviceOrderIds): array
public function addServiceOrderNote(string $propertyId, string $serviceOrderId, string $text, ?string $type = null): array
public function updateServiceOrderNote(string $propertyId, string $noteId, string $text): array
```

### 5.4 Internal request helper

Suggested helper:

```php
private function buildHeaders(MewsDetail $config): array
private function normalizeResponse(Response $response): array
private function logWriteback(string $propertyId, string $action, array $request, array $response, string $status, ?string $error = null): void
```

### 5.5 Error handling

- timeout + retry
- log 4xx/5xx failures into `mews_writeback_logs`
- throw a domain exception or return normalized error arrays
- never silently swallow write-back failures

---

## 6. Add MEWS formatted bookings into helper layer

Modify:

- `app/Services/HelperFunction.php`

### 6.1 Add method

Add:

```php
public function mewsFormattedBookings($PropertyID)
```

### 6.2 Responsibilities

This method should:

1. load `MewsDetail` for the property
2. fetch reservations from MEWS for a sensible window
3. resolve MEWS assigned resource ids
4. map them via `mews_resource_mappings`
5. derive BRAFA-compatible booking arrays
6. return a list shaped like other provider methods

### 6.3 Output shape

Each returned booking array must match what the rest of BRAFA expects:

```php
[
    'propertyId' => $PropertyID,
    'bookId' => <stable reservation/service order id>,
    'roomId' => <BRAFA RoomID>,
    'unitName' => <BRAFA UnitName>,
    'guestName' => <guest name>,
    'firstNight' => <Y-m-d string>,
    'lastNight' => <Y-m-d string>,
    'status' => 'confirmed',
    'bookingStatus' => 'confirmed' | 'cancelled',
    'passcode' => <existing active mews/sciener code or derived fallback>,
    'referenceNumber' => <nullable external confirmation>,
    'infoItems' => [
        ['code' => 'BRAFA_KEY', 'text' => <passcode or null>],
    ],
    'mewsReservationId' => <nullable>,
    'mewsServiceOrderId' => <nullable>,
    'mewsResourceId' => <nullable>,
    'mewsEnterpriseId' => <nullable>,
]
```

### 6.4 Booking id choice

Use a stable local `bookId` strategy.

Recommended:

- if the MEWS stay/service order is the true unit of access, use `mews_service_order_id`
- if not, use reservation id, but only if it is guaranteed to map 1:1 to the actual room/unit entry

Because BRAFA’s downstream code assumes `bookId` is the stable key for booking-code reconciliation, **prefer service-order id** if that is what aligns with access-token write-back.

### 6.5 Status mapping

Map MEWS states to BRAFA states:

- active / confirmed / started -> `confirmed`
- canceled / voided -> `cancelled`
- unknown -> `unknown`

### 6.6 Existing passcode logic

If an active token already exists in MEWS for the same service order/resource, reuse that as `passcode` so repeated imports stay idempotent.

If nothing exists, derive the same fallback passcode style BRAFA uses now, for example:

```php
$custom_passcode = substr((string) $bookId, -4);
```

But the authoritative passcode still becomes the one actually written to Sciener and confirmed in `booking_codes`.

---

## 7. Add MEWS branch in `ShowPropertyDetails`

Modify:

- `app/Http/Livewire/Dashboard/ShowPropertyDetails.php`

### 7.1 Update `getBookings($PropertyID)`

Add:

```php
if ($api == 'mews') {
    $mewsBookings = $helper->mewsFormattedBookings($PropertyID);
    $sortedbookings = $this->sortBookingsByStartDate($mewsBookings);
    Log::info('====================================MEWS BOOKINGS FORMATTED =======================================');
    return $sortedbookings;
}
```

### 7.2 Keep `getBookings_fromDB(...)` unchanged initially

The current DB-display method already reads from the `bookings` table and formats for display. That is fine for MEWS as long as the imported records are saved into the same local table shape.

---

## 8. Persist MEWS external ids during booking insert/update

### 8.1 Update insert logic

Find where non-TBF bookings are inserted/updated in `HelperFunction::handleNonTBFBookings(...)` and related flows.

Extend that logic so that when the provider is `mews`, it also saves:

- `mews_reservation_id`
- `mews_service_order_id`
- `mews_resource_id`
- `mews_enterprise_id`

### 8.2 Update `Booking::insertBooking(...)`

Expand the method signature and insert payload to accept the new MEWS fields.

### 8.3 Update `Booking::updateBooking(...)`

Same as above, plus the passcode bug fix.

### 8.4 Acceptance rule

Every MEWS-backed booking row in `bookings` should be traceable back to the exact external reservation/stay/resource.

---

## 9. Reuse the existing compare flow

Do **not** create a new compare engine.

Use:

- `Auomate_beds24Service::getBookings($propertyId)`
- `HelperFunction::compareBookingsWithCodesTable($propertyId)`

### 9.1 What Codex should verify

Make sure `mewsFormattedBookings(...)` produces everything the compare flow expects:

- `bookId`
- `roomId`
- `unitName`
- `guestName`
- `firstNight`
- `lastNight`
- `passcode`
- `bookingStatus`

### 9.2 What the compare flow already handles

The current compare flow already handles:

- new bookings -> create `booking_codes`
- date changes -> clear/reset keypad ids and mark `unsent`
- room changes -> delete old codes and reinsert for new lock assignments
- cancelled bookings -> skipped or cleaned up depending on path

That is exactly why MEWS should plug into the existing local booking normalization rather than inventing a parallel pipeline.

---

## 10. Add generic PMS write-back service

Create:

- `app/Services/PmsWritebackService.php`

### 10.1 Public API

```php
class PmsWritebackService
{
    public function writePasscode(BookingCode $code): void
}
```

### 10.2 Routing logic

Inside `writePasscode(...)`:

1. resolve property API using `Property::where('PropertyID', $code->PropertyID)->value('API')`
2. route to:
   - Beds24/GoDo -> existing `Beds24APIService::write_beds24_passcode(...)`
   - BookingFactory -> existing `TBF_API_SERVICE::write_tbf_keycode(...)`
   - MEWS -> new MEWS write-back implementation
3. log unsupported providers and return

### 10.3 Why

This removes the need to keep PMS write-back scattered across property-specific commands like the current TBF hotel automation commands.

---

## 11. Hook PMS write-back into `AssignedLocks::sendPendingPasscodes()`

Modify:

- `app/Http/Livewire/Dashboard/AssignedLocks.php`

### 11.1 Inject or resolve `PmsWritebackService`

Either inject it, or simply resolve it inside the method:

```php
$pmsWriteback = app(\App\Services\PmsWritebackService::class);
```

### 11.2 Call write-back only on success

Inside `sendPendingPasscodes()`, after this block:

```php
if (isset($response['keyboardPwdId'])) {
    $code->update([... 'Status' => 'success']);
}
```

add:

```php
$pmsWriteback->writePasscode($code->fresh());
```

Also do the same in the `errcode == -3007` block, because that path also marks the code as `success`.

### 11.3 Never write back on failure

Do not call PMS write-back in the `failed` branch.

---

## 12. Implement MEWS write-back inside `PmsWritebackService`

### 12.1 Resolve booking + external refs

When provider is `mews`:

1. load the local `Booking` row by:
   - `propertyId == $code->PropertyID`
   - `bookingId == $code->BookingID`
2. read:
   - `mews_service_order_id`
   - `mews_resource_id`
   - `mews_resource_access_token_id` if already known
3. if the booking row does not exist or lacks external refs, log and abort safely

### 12.2 Upsert token

Use `MewsApiService` to:

1. fetch existing resource access tokens for the booking’s `mews_service_order_id`
2. find a matching token by:
   - `Type = PinCode`
   - `ResourceId = mews_resource_id` when available
3. if found -> update it
4. if not found -> add a new one

### 12.3 Token payload

Build from the actual Sciener-effective window, not raw booking dates.

Use the same timestamps that `AssignedLocks::calculateTimestamps(...)` uses for sending to Sciener.

Suggested payload shape:

```php
[
    'ServiceOrderId' => $booking->mews_service_order_id,
    'ResourceId' => $booking->mews_resource_id,
    'Type' => 'PinCode',
    'Value' => $code->Passcode,
    'ValidityStartUtc' => <converted from milliseconds to ISO UTC>,
    'ValidityEndUtc' => <converted from milliseconds to ISO UTC>,
]
```

### 12.4 Optional note mirror

If `MewsDetail->use_service_order_notes` is true:

- upsert a readable note on the service order
- do **not** treat the note as source of truth

Suggested note text:

```text
BRAFA PIN 4821 | Valid 2026-04-25T15:00:00Z -> 2026-04-27T11:00:00Z | Resource 204
```

### 12.5 Persist returned token id

If MEWS returns a token id on create/update:

- store it on the local `bookings.mews_resource_access_token_id`

---

## 13. Add webhook ingestion

Create:

- `app/Http/Controllers/Webhooks/MewsWebhookController.php`

Add route:

- `POST /webhooks/mews/{propertyId}`

### 13.1 Controller behavior

```php
public function __invoke(Request $request, $propertyId)
```

Steps:

1. resolve `MewsDetail` by `PropertyID`
2. validate secret/header/path token
3. store raw payload into `mews_webhook_logs`
4. dispatch sync job
5. return `response()->json(['ok' => true], 202)`

### 13.2 Do not sync inline

Do not fetch reservations or write bookings inside the controller.

---

## 14. Add queued reservation sync job

Create:

- `app/Jobs/SyncMewsReservationJob.php`

### 14.1 Constructor args

```php
public function __construct(string $propertyId, array $eventPayload)
```

### 14.2 Handle flow

Inside `handle(...)`:

1. parse reservation/service order ids from webhook payload
2. refetch full MEWS reservation data via `MewsApiService`
3. normalize each booking row with the same logic as `mewsFormattedBookings(...)`
4. upsert into local `bookings`
5. call compare flow:

```php
$helper = new HelperFunction();
$helper->compareBookingsWithCodesTable($propertyId);
```

Alternative:

```php
app(\App\Services\Auomate_beds24Service::class)->getBookings($propertyId);
```

But prefer not to trigger extra external fetches if the job already has the exact reservation.

### 14.3 Idempotency

The job must be safe to retry.

Use stable external ids when updating/inserting.

---

## 15. Add polling fallback command

Create:

- `app/Console/Commands/SyncMewsBookings.php`

### 15.1 Signature

```php
protected $signature = 'app:sync-mews-bookings {propertyId}'
```

### 15.2 Flow

1. fetch the property’s MEWS config
2. fetch reservations for a bounded window, e.g.:
   - start = now - 7 days
   - end = now + 30 days
3. page through results if needed
4. upsert local bookings
5. run compare flow

### 15.3 Scheduler

Later add to scheduler for MEWS properties:

- every 5 or 10 minutes
- nightly wider backfill if desired

---

## 16. Add MEWS room/resource sync

The current `AssignedLocks::updateRooms()` is Beds24-specific.

For MEWS, add a provider-specific room sync path.

### 16.1 Create helper/service method

Add to `HelperFunction` or `MewsApiService`:

```php
public function syncMewsRooms(string $propertyId): array
```

### 16.2 Behavior

1. fetch MEWS resources
2. map resource ids/names into local `rooms`
3. create/update `mews_resource_mappings`
4. preserve existing `AssignedLock` rows where `UnitName` or normalized names can be matched

### 16.3 Update `AssignedLocks`

Add a method like:

```php
public function fetchMewsRooms()
```

Behavior should mirror `fetchFritimiRooms()` and `fetchTotalRooms()`, but call `syncMewsRooms()`.

### 16.4 Optional UI hook

If `propertyApi === 'mews'`, expose the MEWS room sync button in the existing assigned locks UI.

---

## 17. Clean up BookingFactory special-case automation

The repo currently has hotel-specific commands that:

- call `Auomate_beds24Service->getBookings(...)`
- then separately fetch TBF bookings and call `write_tbf_keycode(...)`

Do **not** replicate that for MEWS.

### 17.1 Short-term

Leave those commands in place for now, but do not build a MEWS equivalent.

### 17.2 Medium-term cleanup

Refactor those commands later to:

- rely on normal sync/reconcile/send flow
- rely on centralized `PmsWritebackService`

That way all providers share the same success-triggered PMS write-back pattern.

---

## 18. Implementation checklist for Codex

Use this order exactly.

### Phase A: foundations

1. Create `MewsDetail` model + migration.
2. Create `MewsResourceMapping` model + migration.
3. Add MEWS external id columns to `bookings`.
4. Fix `Booking::updateBooking(...)` so it no longer writes `passcode = startDate`.
5. Create `MewsApiService` with authenticated request helper.

### Phase B: booking ingest

6. Add `HelperFunction::mewsFormattedBookings($PropertyID)`.
7. Add `if ($api == 'mews')` branch in `ShowPropertyDetails::getBookings(...)`.
8. Update booking insert/update paths to persist MEWS external ids.
9. Test that `Auomate_beds24Service::getBookings($propertyId)` works unchanged for a MEWS property.

### Phase C: write-back

10. Create `PmsWritebackService`.
11. Add provider routing for Beds24, BookingFactory, and Mews.
12. Implement MEWS token upsert/delete inside `PmsWritebackService` using `MewsApiService`.
13. Hook `PmsWritebackService::writePasscode(...)` into `AssignedLocks::sendPendingPasscodes()` after success.
14. Also hook the `-3007` success path.

### Phase D: events and reconciliation

15. Create `MewsWebhookController` + route.
16. Create `mews_webhook_logs` migration.
17. Create `SyncMewsReservationJob`.
18. Create `SyncMewsBookings` artisan command.
19. Add scheduler entry.

### Phase E: room sync and polish

20. Add `syncMewsRooms(...)`.
21. Add `fetchMewsRooms()` in `AssignedLocks`.
22. Add `mews_writeback_logs` migration.
23. Add tests.
24. Add feature flag or property-level toggle for rollout.

---

## 19. Code skeletons for Codex

### 19.1 `PmsWritebackService`

```php
namespace App\Services;

use App\Models\Booking;
use App\Models\BookingCode;
use App\Models\Property;

class PmsWritebackService
{
    public function writePasscode(BookingCode $code): void
    {
        $api = Property::where('PropertyID', $code->PropertyID)->value('API');

        if (in_array($api, ['beds24', 'godo'], true)) {
            app(Beds24APIService::class)->write_beds24_passcode($code->BookingID, $code->PropertyID);
            return;
        }

        if ($api === 'bookingfactory') {
            $booking = Booking::where('propertyId', $code->PropertyID)
                ->where('bookingId', $code->BookingID)
                ->first();

            if (!$booking) {
                return;
            }

            app(TBF_API_SERVICE::class)->write_tbf_keycode(
                $code->BookingID,
                $code->BookingID,
                $booking->roomId,
                $code->PropertyID
            );
            return;
        }

        if ($api === 'mews') {
            $this->writeMewsPasscode($code);
            return;
        }
    }

    protected function writeMewsPasscode(BookingCode $code): void
    {
        // implement using Booking + MewsApiService
    }
}
```

### 19.2 `MewsWebhookController`

```php
namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use App\Jobs\SyncMewsReservationJob;
use App\Models\MewsDetail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class MewsWebhookController extends Controller
{
    public function __invoke(Request $request, $propertyId)
    {
        $config = MewsDetail::where('PropertyID', $propertyId)->firstOrFail();

        // TODO: validate webhook secret/header

        DB::table('mews_webhook_logs')->insert([
            'PropertyID' => $propertyId,
            'event_type' => data_get($request->all(), 'EventType'),
            'entity_id' => data_get($request->all(), 'EntityId'),
            'payload' => json_encode($request->all()),
            'status' => 'received',
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        SyncMewsReservationJob::dispatch((string) $propertyId, $request->all());

        return response()->json(['ok' => true], 202);
    }
}
```

### 19.3 Hook in `AssignedLocks::sendPendingPasscodes()`

After each success update, add:

```php
app(\App\Services\PmsWritebackService::class)->writePasscode($code->fresh());
```

Place it in both success branches.

---

## 20. Test cases Codex should implement

### 20.1 Booking ingest

- MEWS property returns reservations -> `bookings` rows are created with external ids.
- Reservation change updates local booking rather than duplicating it.

### 20.2 Compare flow reuse

- New MEWS booking produces `booking_codes` rows for every assigned lock.
- Date change marks existing code rows `unsent` and clears keypad ids.
- Room change deletes/rebuilds code rows for the new assignment.

### 20.3 PMS write-back

- Successful Sciener response with `keyboardPwdId` -> MEWS token add/update is called.
- `errcode == -3007` path also triggers MEWS write-back.
- Failed Sciener response does not trigger MEWS write-back.

### 20.4 Webhooks

- webhook request logs payload
- job is dispatched
- repeated webhook for same reservation is idempotent

### 20.5 Resource mapping

- missing MEWS resource mapping logs a clear error and skips write-back
- valid mapping writes token against correct MEWS resource

---

## 21. Final implementation rules

1. **MEWS must be just another provider at the booking-fetch layer, not a completely separate automation stack.**
2. **The existing local `bookings` -> `booking_codes` -> Sciener flow should remain the core orchestration path.**
3. **MEWS write-back must happen only after Sciener success inside `AssignedLocks::sendPendingPasscodes()`.**
4. **Do not build a MEWS-specific version of the current hotel-specific TBF commands.**
5. **Fix the `Booking::updateBooking()` passcode bug before relying on booking updates for MEWS.**
6. **Use stable MEWS external ids on the local `bookings` row so write-back is deterministic.**

