diff --git a/skills/wix-headless/references/bookings/INSTRUCTIONS.md b/skills/wix-headless/references/bookings/INSTRUCTIONS.md index f3eb11fe..32ffc677 100644 --- a/skills/wix-headless/references/bookings/INSTRUCTIONS.md +++ b/skills/wix-headless/references/bookings/INSTRUCTIONS.md @@ -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. | diff --git a/skills/wix-headless/references/bookings/SERVICES_DATA.md b/skills/wix-headless/references/bookings/SERVICES_DATA.md index 6d7b6ecf..7643eb15 100644 --- a/skills/wix-headless/references/bookings/SERVICES_DATA.md +++ b/skills/wix-headless/references/bookings/SERVICES_DATA.md @@ -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: " -H "Content-Type: application/json" \ + -d '{"location":{"name":"","status":"ACTIVE","locationType":"BRANCH","default":true,"timeZone":"","address":{"country":"","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. @@ -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. | diff --git a/skills/wix-headless/references/custom/bookings/WIRING.md b/skills/wix-headless/references/custom/bookings/WIRING.md index 660718d1..b01ceadc 100644 --- a/skills/wix-headless/references/custom/bookings/WIRING.md +++ b/skills/wix-headless/references/custom/bookings/WIRING.md @@ -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.