Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion skills/wix-headless/references/bookings/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ If `global.css` ships a partial rule for any class above, flag it in your return
| Re-fetch the booking with `getBooking` from the confirmation page | Don't. Pass booking details (bookingId, startDate, service name) as URL query params during the client-side redirect after `createBooking` succeeds, and render from those. (A server re-fetch of someone else's booking is out of scope.) NB: this is **not** because `auth.elevate` is unavailable — see next row. |
| Assume `@wix/essentials` `auth.elevate` doesn't work in headless | It does. **Two different `elevate`s:** `@wix/essentials` `auth.elevate(fn)` works in `@wix/astro` SSR (it's how the listing/detail/CMS pages read data) — use it. What's unavailable is the **`wixClient.auth.elevate`** instance method on a hand-built `createClient({auth: OAuthStrategy})` (a Velo/Blocks API). Don't conflate them. |
| Mount `AvailabilityCalendar` without `client:only="react"` | Calendar must be fully client-side — SSR renders before timezone is known and breaks slot display. Always use `client:only="react"`. |
| Map a custom design's form into `contactDetails` only | Everything beyond name/email/phone is silently dropped. Pass the full form as **`formSubmission`** on `createBooking`, keyed by the booking form's field `target`s (Get Form Summary on `service.form.id`); Bookings derives the contact from it and stores the submission on the booking. If the design lacks a field the form requires (default form requires `email`), clone the default form, relax the requirement (PATCH `fields[]` `validation.required`, `mask.paths: ["fields"]`), and point `service.form.id` at the clone. Normalize phones to international format. Full recipe: `../custom/bookings/WIRING.md` § on-site booking flow. |
| Omit try/catch on `createBooking` | Booking creation can fail with 409 (slot taken) between slot fetch and submit. Catch and surface a friendly message; do not crash the page. |
| Use `service.id` to pass the service ID | The Wix SDK uses `_id` (with underscore) for all entity IDs. The service's ID is `service._id`. Using `service.id` will be `undefined`. |
| Read `resource._id` from `listAvailabilityTimeSlots` response | `availableResources` is EMPTY by default in list results. Omit `resource` from `createBooking` — Wix auto-assigns a resource during confirmation. |
| Omit `resource` from the APPOINTMENT `createBooking` slot and rely on auto-assign | **Specify `resource` explicitly** — Wix's [Book-an-appointment sample flow](https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/sample-flows) sets `startDate`, `endDate`, **`resource`**, and **`location`** in `bookedEntity.slot`. `availableResources` is empty in the `listAvailabilityTimeSlots` *list* response, but you don't need it: the service's staff resource id is **`service.staffMemberIds[0]`** (those values are resource ids). For a single-staff service pass `slot.resource = { _id: service.staffMemberIds[0] }` plus `slot.location = { locationType: "OWNER_BUSINESS" }` (no `location._id` needed — `OWNER_BUSINESS` resolves to the default business location). Auto-assign (omitting `resource`) works on fully dashboard-configured sites, but on **programmatically-provisioned / headless sites it fails** with `Resource settings conflict for resource type 1cd44cf8-…` — verified to persist across creating a business location, limiting staff to it, and using a freshly-created staff member. Specifying `resource` is the reliable default. |
| Use `timeSlot.slot.startDate` to read slot time | Slot fields are at the TOP LEVEL — there is no nested `slot` on the list response. APPOINTMENT slots: `timeSlot.localStartDate`, `timeSlot.localEndDate`, `timeSlot.scheduleId`. |
| Treat CLASS (event) slots like APPOINTMENT slots — guard clicks on `scheduleId`, book with `{scheduleId, startDate}` | CLASS event slots carry their session id at **`timeSlot.eventInfo.eventId`** and have **no `scheduleId`** (and no top-level `startDate`). Guarding the slot-click on `scheduleId` drops every CLASS click; the appointment-shaped `createBooking` payload doesn't apply. For CLASS: guard on `eventId`, and book with `bookedEntity.slot = { serviceId, eventId }` (Wix derives start/end/timezone/resource/location from the event). Keep `{ scheduleId, startDate, endDate, timezone, location }` for APPOINTMENT only. |
| Pass `hidden: true` services to the listing | Always filter with `{ hidden: false }` in your `queryServices` filter to exclude hidden services from the public listing. |
Expand Down
15 changes: 15 additions & 0 deletions skills/wix-headless/references/bookings/SERVICES_DATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ curl -sS -X POST \

---

## Step 2b — Ensure a business location exists (APPOINTMENT)

APPOINTMENT services are booked against a business **location**, and a freshly app-installed site often has **none** (`POST /locations/v1/locations/query` → `{ "locations": [] }`). Without one the service is created against the placeholder location `123e4567-e89b-12d3-a456-426614174000`, which can't resolve at booking time. This is **Step 1 of the official [Set Up a Service](https://dev.wix.com/docs/api-reference/business-solutions/bookings/flow-set-up-a-service) flow** — do it before creating services. Query existing locations; if none, create a default one:

```bash
curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
-d '{"location":{"name":"<brand>","status":"ACTIVE","locationType":"BRANCH","default":true,"timeZone":"<IANA tz, e.g. Asia/Jerusalem>","address":{"country":"<ISO>","city":"…"}}}' \
"https://www.wixapis.com/locations/v1/locations"
```

`timeZone` is **required** (omitting it 400s with `timeZone must not be empty`). Bookings then resolve against `location.locationType: "OWNER_BUSINESS"` at booking time (see `INSTRUCTIONS.md`). If the dashboard/CLI already created a location, the query returns it — skip the create.

---

## Step 3 — Create staff members (when `intent.bookings.hasStaff` is `true`)

Execute this step BEFORE Step 4 when staff are required. APPOINTMENT services need at least one resource to exist before they can be created.
Expand Down Expand Up @@ -287,6 +301,7 @@ Verify by fetching slots the way the front-end does (`eventTimeSlots.listEventTi
| 400 `"defaultCapacity is required"` | Add `"defaultCapacity": 1` to the payload (required in V2, not obvious from V1 docs). |
| 400 `"onlineBooking is required"` | Add `"onlineBooking": { "enabled": true }` — required in V2. |
| 400 on `payment.options` | At least one of `online` or `inPerson` must be `true`. Set `"inPerson": true` as fallback. |
| 400 `payment.options.online ... applicable only to ... FIXED or VARIED` | The service is **free** (`payment.rateType: "NO_FEE"`). With `NO_FEE`, `online` must be `false` — set `"inPerson": true` (and `"online": false`) instead. `online: true` is only allowed for `FIXED`/`VARIED` rate types. |
| 400 `"sessionDurations is required"` | Add `"schedule": { "availabilityConstraints": { "sessionDurations": [60] } }` for APPOINTMENT types. |
| 400 `MISSING_APPOINTMENT_RESOURCES` on service create | An APPOINTMENT service's `staffMemberIds` must be **non-empty**. Pass the created staff `resourceId`s (Step 3), or — when `hasStaff` is false — the default **Business Owner** `resourceId` (query `/bookings/v1/staff-members/query` with `{"query":{}}`). An empty `[]` always 400s here. |
| 400 enum error on `locations.type` | Use `"BUSINESS"`, not `"OWNER_BUSINESS"`. The services endpoint accepts `UNKNOWN_LOCATION_TYPE`, `CUSTOM`, `BUSINESS`, `CUSTOMER`. `OWNER_BUSINESS` is valid on `createBooking.bookedEntity.slot.location.locationType` only. |
Expand Down
16 changes: 14 additions & 2 deletions skills/wix-headless/references/custom/bookings/WIRING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ The hosted flow collects contact details, takes payment when the service is paid

> **CLASS sessions:** the verified `slotAvailability.slot` shape above is the APPOINTMENT one. For a CLASS service, slots come from `eventTimeSlots` with `eventInfo.eventId` and no `scheduleId` — verify the class slot→redirect mapping against the redirects SDK docs at wiring time before wiring a CLASS service; don't guess the shape.

## Deferred (needs the server runtime — `@wix/astro`)
## On-site booking flow (server runtime — `@wix/astro`)

On-site booking flow (calendar + form + elevated `confirmBooking` seat-hold), manage/cancel via anonymous action tokens, and the native waitlist (v1, Manage Bookings scope) — all implemented in the astro vertical (`references/astro/bookings/`); out of scope for client-only integration.
When the project **has** a server runtime (any `@wix/astro` project, however it was set up), wire the full on-site flow instead of the hosted-checkout redirect. The complete component implementation lives in the astro vertical (`references/astro/bookings/`); when the site's existing UI must stay intact (a brought-in design you should not rewrite), the minimal verified wiring is two server routes the existing front-end fetches:

1. **Render only real availability — replace the mock's displayed data, not just its submit.** A brought-in design's date/time pickers are mocks: hardcoded times, seeded "taken" flags, day lists pinned to the date the design was generated. Add `GET /api/availability` — elevated (`@wix/essentials` `auth.elevate`) `availabilityTimeSlots.listAvailabilityTimeSlots` over the next ~14 days, grouped into `{ days: { "YYYY-MM-DD": ["HH:mm", …] } }` — and drive the picker **only** from it, so everything selectable is actually bookable. Re-validate the exact slot server-side at submit time (it can be taken between fetch and submit; return a friendly "slot taken" rather than booking a different time).

2. **Create + confirm server-side** (`POST /api/book`): elevated `bookings.createBooking` → `bookings.confirmBooking(bookingId, revision, { paymentStatus: "NOT_PAID", flowControlSettings: { checkAvailabilityValidation: true } })`. `CREATED` holds no seat — without the confirm, nothing reaches the dashboard and capacity never drops. In the slot, pass `resource: { _id: service.staffMemberIds[0] }` and `location: { locationType: "OWNER_BUSINESS" }` explicitly — on a minimally-provisioned site the omit-resource auto-assign fails with "Resource settings conflict" (see `../../bookings/INSTRUCTIONS.md`).

3. **Send the whole form via `formSubmission` — not `contactDetails`.** Mapping only name/phone into `contactDetails` silently drops every other field the design collects. Pass `formSubmission` on `createBooking`, keyed by the booking form's field `target`s (Form Schemas *Get Form Summary*: `GET /form-schema-service/v4/forms/{service.form.id}/summary`; the default form's targets: `first_name`, `last_name`, `email`, `phone`, `address`, `add_your_message`). Bookings derives the booking's contact from it **and** stores the submission on the booking, dashboard-visible. Fold custom design fields (child's name, age, topic, notes) into `add_your_message` as labeled lines.
- **Audit the form's `required` flags against what the design can actually guarantee — for every field, not just the obvious one.** The default booking form requires `first_name`, `last_name`, `email`, **and** `address`. A brought-in design typically can't guarantee them: no email input → `email` fails; a single "full name" input → one-word names have **no `last_name`** (fails at booking time, in production, for real visitors); no address input → `address`. Clone the default form (`POST /form-schema-service/v4/forms/00000000-0000-0000-0000-000000000000/clone`), PATCH the clone's **`fields[]`** setting `validation.required: false` on every field the design can't guarantee, with `mask.paths: ["fields"]` (the legacy `formFields[]` shape is rejected with a misleading `fieldType cannot be provided…` error), then point the service at the clone (PATCH the service with `form.id` + current `revision`). In the submission, **omit** empty optional fields rather than sending `""`.
- **Phone must be international.** The form validates country codes — normalize local input before submitting (e.g. IL `05X-XXXXXXX` → `+9725XXXXXXXX`).

4. **Handle the confirm revision race.** With `formSubmission`, Bookings processes the submission/contact **asynchronously after create** and bumps the booking's revision — so `confirmBooking` with the create-time revision can fail with `INVALID_REVISION` ("Outdated revision for entity id: …", the entity being the booking itself). Do **not** re-create (that orphans `CREATED` bookings): loop the confirm — on `INVALID_REVISION`, re-read the booking's current `revision` (query `POST /_api/bookings-service/v2/bookings/query` filtered by id, via `auth.elevate(httpClient.fetchWithAuth)`) and confirm again, a few attempts with small backoff.

Still client-only-deferred: manage/cancel via anonymous action tokens and the native waitlist (v1, Manage Bookings scope) — implemented in the astro vertical.
Loading