# Entrify (Golfsvítan) Booking Integration — Design

Status: **DRAFT for review** (no code yet)
Modeled on: `golfbokanir` (`GolfReservationApiService` + `compareGolfBookingsWithCodesTable`)

## 1. Summary

Poll Entrify's bookings-codes API, ingest bookings, and behave like the existing
golf integrations: only **today's** codes go to the locks, and when a booking is
**modified/moved** we adjust the lock code accordingly — **modifying the passcode
in place** (TTLock `v3/keyboardPwd/change`) rather than deleting and re-adding.

Endpoint (per-tenant):
```
GET {base}/api/tenants/{tenant}/bookings/codes
Header: X-Entry-Access-Key: <key>
```

## 2. Decisions locked in
- **Property model:** one property **Golfsvítan**, with two **units** — `Egilshöll`
  (propId 100001) and `Hafnarfjörður` (propId 100000) — each mapped to its own lock
  via `AssignedLock` (PropertyID + RoomID/unit → lockId).
- **Modify in place:** on time/code change, call `ScienerApiService::modify_passcode`
  (keeps the same `keyboardPwdId`, no gap with no code). Delete is reserved for
  cancellations and cross-unit moves only.

## 3. Field mapping (Entrify → our model)

| Entrify | Our model | Notes |
|---|---|---|
| `bookId` | `bookings.bookingId` | scoped by our PropertyID (composite unique) |
| `propId` | unit → `AssignedLock` lock | 100001 = Egilshöll, 100000 = Hafnarfjörður |
| `unitName` | `bookings.unitName` / `Room` | matches the AssignedLock RoomID/unit |
| `accessKey` | `bookings.passcode` | provider-supplied; **can be empty** |
| `startTime` / `endTime` | `startDate` / `endDate` | lock validity window |
| `modifiedDate` | change hint only | unreliable here (see §6) |
| `guestName` | `guestName` | |
| `tenantId` / `tenantName` | tenant scope / api key | |

## 4. Components
- **`entrify_details` table:** `tenant`, `propId`, `PropertyID`, `unit_name`,
  `api_key`, `base_url`. One row per (tenant, propId) → unit mapping.
- **`EntrifyApiService`:** `syncTenantBookings($tenant)` — fetch, validate, upsert
  (mirrors `GolfReservationApiService::syncPropertyBookings`).
- **API type** `entrify` on the Golfsvítan property; `Auomate_beds24Service::getBookings`
  routes it to the Entrify sync + push.
- **Per-property automation (like every other system):** each Entrify property gets the
  standard generated `app:automate-{name}` command + its own `scheduled_tasks` row,
  toggleable from the dashboard. The generated command calls `getBookings($PropertyID)`,
  which routes to Entrify — so there is **no** global/all-properties runner.
- **UI onboarding (like every other API):** added to the "New property" modal
  (`SetApiKey`) as the **Entrify** option. The handler calls
  `EntrifyApiService::onboard()` (creates the property, **auto-discovers the units from
  the feed**, seeds `entrify_details`) then `manageScheduledTasks` +
  `GenerateAutomationScripts` — exactly like Total/golfbokanir. No terminal command.
- Reuse: `BookingCode`, `modify_passcode`, `delete_Passcode`, `AssignedLock`.

## 5. Sync & code lifecycle (per poll)

1. **Fetch.** On any non-200 or non-array/garbled payload → **abort the whole sync,
   change nothing** (this is the guard against false cancellations — see §6).
2. **Upsert all bookings** into `bookings` (keep the full forward schedule), but only
   **act on the lock for bookings whose `startTime` date is today** (the golfbokanir
   "today only" rule). This is the "only today's codes → prevent same codes" control:
   the lock only ever holds today's small set, so 4-digit collisions are rare.
3. **Per today booking, resolve the unit's lock** via `AssignedLock`. Then:
   - **No code yet on lock** (no `BookingCode`/`keyboardPwdId`) → **ADD**
     (`sendPasscode_for_booking`), record `keyboardPwdId`.
   - **Code exists + something changed** (time and/or `accessKey`, same unit/lock) →
     **MODIFY in place** (`modify_passcode` → `keyboardPwd/change` with new
     `newKeyboardPwd`/`startDate`/`endDate`), update `BookingCode`. No delete.
   - **No change** → no-op (idempotent).
4. **Change detection** = diff stored vs incoming `startTime`/`endTime`/`accessKey`/
   `propId(unit)`. Do **not** rely on `modifiedDate` (§6).

## 6. The payload gaps (and how the design absorbs them)

1. **No status / cancellation field.** golfbokanir has `status`; Entrify doesn't.
   → Until Entrify confirms otherwise, infer cancellation from **absence** from the
   feed, but only after the booking is **missing on two consecutive *successful*
   polls** (absorbs API hiccups). On confirmed cancellation: `delete_Passcode` +
   mark `bookings.bookingStatus = cancelled`. **(Vendor question.)**
2. **`accessKey` can be empty** (e.g. bookId 359). A today booking with no key → we
   can't grant access → **flag** (skip, log, surface); re-check next poll.
3. **`modifiedDate` unreliable** — every row shares `2026-06-15 22:29:55` despite
   different `bookDate`s → looks bulk-/poll-stamped. Use field-diffing, not this.
4. **Timezone** — assume Atlantic/Reykjavík (no DST); confirm. Drives "today" + windows.
5. **Completeness/pagination** — confirm the feed is the full current list (required
   for absence-based cancellation) and whether a `from`/`to` filter exists.
6. **Write-back?** GET-only shown. If Entrify has no PUT/POST, `accessKey` is
   **read-only** (like Total): we can't fix empty or too-simple codes — only flag for
   a source-side fix. **(Vendor question.)**

## 7. Contingencies
- **Modified/moved within today** (time and/or code) → modify in place (§5).
- **Moved to a different day** → it drops out of "today"; delete today's lock code,
  keep the booking; it re-adds on its real day.
- **Moved to a different unit/propId** → different lock; `keyboardPwd/change` can't
  cross locks → **delete on old lock + add on new** (the one delete+add case).
- **Cancelled** → §6.1.
- **Too-simple `accessKey`** → Sciener `-2032`. Same read-only problem as the shelved
  code-health work; flag for source-side fix. (Note: golf compare path doesn't handle
  `-2032` yet either.)
- **Duplicate `accessKey`, same lock, same day** → `-3007` on the 2nd add; with
  modify-in-place we key by `keyboardPwdId` so the tracked record is unambiguous; flag
  the collision.
- **Slot already ended at poll time** → don't add; rely on passcode `endDate` expiry +
  the delete script.
- **Lock offline / send fails** → retried next poll (idempotent).
- **`modify_passcode` token expiry** → it currently lacks `10003/10004` refresh; add
  the same refresh-and-retry the add path uses.
- **Iceland: no DST** → window math stays simple.

## 8. Reused / new code
- New: `entrify_details` table + model, `EntrifyApiService`, scheduled command,
  routing of `api == 'entrify'` in `Auomate_beds24Service::getBookings`.
- Reuse: `compareGolfBookingsWithCodesTable`, `modify_passcode`, `delete_Passcode`,
  `getDoorTimings`, `BookingCode`, `AssignedLock`.
- Add `-2032` awareness when the code-health work lands.

## 9. Open questions for Entrify / you
1. **Cancellations** — does a cancelled/deleted booking disappear from the feed, or
   stay with a status? (Determines §6.1.)
2. **Write-back** — any endpoint to set `accessKey`? (Determines empty/`-2032` handling.)
3. **Timezone** of the timestamps.
4. **Completeness** — full list vs paginated; is there a `from`/`to` filter?
5. **Empty `accessKey`** — when does it populate? Is it ever permanent?
6. **`modifiedDate`** — is it per-booking or bulk/poll-stamped? (We'll field-diff regardless.)
7. Door-timing buffer for golf slots — exact slot, or small pre/post pad?
