@wix/agent-skills 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +2 -1
  3. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +14 -0
  4. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +11 -6
  5. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +3 -17
  6. package/skills/wix-app/references/editor-react-component/SSR.md +119 -0
  7. package/skills/wix-design-system/SKILL.md +1 -1
  8. package/skills/wix-headless/references/BUILD-astro.md +5 -7
  9. package/skills/wix-headless/references/DESIGN_SYSTEM.md +6 -16
  10. package/skills/wix-headless/references/SETUP.md +1 -1
  11. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +41 -333
  12. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +77 -428
  13. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +2 -2
  14. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +7 -10
  15. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +2 -2
  16. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +3 -3
  17. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +2 -2
  18. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +7 -7
  19. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +4 -4
  20. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +2 -2
  21. package/skills/wix-headless/references/astro/templates/Footer.astro +4 -4
  22. package/skills/wix-headless/references/astro/templates/Navigation.astro +4 -4
  23. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +298 -142
  24. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +104 -162
  25. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +35 -14
  26. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +11 -2
  27. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +23 -85
  28. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +245 -0
  29. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +209 -198
  30. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +114 -19
  31. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +131 -15
  32. package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +3 -3
  33. package/skills/wix-headless/references/astro/templates/ecom/cart.astro +3 -3
  34. package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +17 -17
  35. package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +3 -3
  36. package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +4 -4
  37. package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +19 -18
  38. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +3 -3
  39. package/skills/wix-headless/references/astro/templates/global.css +17 -15
  40. package/skills/wix-headless/references/astro/templates/index.astro +4 -4
  41. package/skills/wix-headless/references/astro/templates/stores/components-stores.css +31 -31
  42. package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +4 -4
  43. package/skills/wix-headless/references/bookings/FLOW.md +246 -0
  44. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +65 -76
  45. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +60 -0
  46. package/skills/wix-headless/references/custom/bookings/WIRING.md +169 -60
  47. package/skills/wix-headless/references/shared/DESIGN_MD.md +2 -3
  48. package/skills/wix-headless/references/shared/IMPLEMENTER.md +4 -4
  49. package/skills/wix-headless/references/shared/STYLING.md +15 -18
  50. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +2 -2
  51. package/skills/wix-headless/references/verticals/bookings.md +9 -9
  52. package/skills/wix-headless/scripts/compose.mjs +4 -19
  53. package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -3
  54. package/skills/wix-manage/SKILL.md +12 -0
  55. package/skills/wix-manage/references/app-installation/install-wix-apps.md +3 -27
  56. package/skills/wix-manage/references/bookings/create-appointment-service.md +254 -0
  57. package/skills/wix-manage/references/bookings/create-booking-service-from-prompt.md +37 -0
  58. package/skills/wix-manage/references/bookings/create-class-service.md +233 -0
  59. package/skills/wix-manage/references/bookings/create-course-service.md +237 -0
  60. package/skills/wix-manage/references/cms/cms-data-items-crud.md +2 -1
  61. package/skills/wix-manage/references/cms/cms-schema-management.md +1 -1
  62. package/skills/wix-manage/references/stores/create-product-from-image.md +154 -2
  63. package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +0 -84
  64. package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +0 -34
  65. package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +0 -74
  66. package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +0 -76
@@ -0,0 +1,246 @@
1
+ # Bookings — the booking flow (framework-agnostic logic)
2
+
3
+ The booking logic — the step model, the shared state, and the exact `@wix` SDK
4
+ sequence — is the **same on every frontend** (astro, react/vite, vue, plain HTML).
5
+ This file is that shared logic. The per-framework wiring layers on top:
6
+
7
+ - **astro** (`frontendBuild: wix`): `../astro/bookings/SERVICES_PAGES.md` (SSR read pages) + `../astro/bookings/COMPONENTS.md` (the client islands).
8
+ - **own / static** (`frontendBuild: own`/`none`): `../custom/bookings/WIRING.md`.
9
+
10
+ **Code examples are React** (`../astro/templates/bookings/*.tsx` + `bookingDriver.ts`).
11
+ React is directly usable on astro islands and on react/vite. **On any other
12
+ framework, translate the examples** — the SDK calls and payloads are identical;
13
+ only the UI idiom (`useState`/`useEffect` → a store / `onMounted` / signals) and
14
+ the client acquisition differ (see "Client identity" below).
15
+
16
+ The flow covers **appointments and classes**. APPOINTMENT and CLASS share the
17
+ same booking sequence; they differ only in the availability call (`availabilityTimeSlots`
18
+ vs `eventTimeSlots`, noted inline). **Courses and add-ons are out of scope.**
19
+
20
+ ---
21
+
22
+ ## 1. The step model
23
+
24
+ A single customer journey, holding one shared selection. The catalog optionally
25
+ filters by **location** and **category**; the slots step optionally filters by
26
+ **staff**. All three auto-skip when there's nothing to choose (≤1 location, ≤1
27
+ category, ≤1 staff), so a plain single-location single-staff site sees the simple
28
+ five-step path:
29
+
30
+ ```
31
+ 0. Location (optional) pick a location — only shown when the site has >1 locations → scopes the catalog + availability
32
+ 1. Catalog list bookable services (optionally filtered by location/category) → pick a service
33
+ 2. Slots availability calendar (optionally filtered by staff) → pick a time slot
34
+ 3. Details the service's booking form (collect contact) → submit
35
+ 4. Book run the SDK sequence (§3)
36
+ 5a. paid → redirect to the Wix-hosted checkout → returns to the confirmation page
37
+ 5b. free/offline → place the order → confirmation page
38
+ ```
39
+
40
+ The location/category filters and the staff picker are covered in §§7–8.
41
+
42
+ **The catalog is the entry point but optional** — a visitor can land directly on a
43
+ service page and start at the Slots step. Every step rehydrates from the URL (§2),
44
+ so deep links work.
45
+
46
+ Mapping the steps onto pages/routes is the **target framework's choice** — translate
47
+ the step model to whatever router the framework uses. The skill recommends the routes
48
+ `/services`, `/services/[slug]`, `/booking-confirmation` (advertised in
49
+ `../verticals/bookings.md`). On astro specifically, SSR the catalog + detail pages
50
+ for SEO and run slots + form + book in a `client:only` island.
51
+
52
+ ## 2. Shared booking state
53
+
54
+ Steps 1–4 build up one selection (this is exactly `BookParams` + `SelectedSlot`
55
+ in `../astro/templates/bookings/bookingDriver.ts`):
56
+
57
+ - **`service`** — the chosen service object (the booking step reads `service.payment` + `service.bookingPolicy`; the slots step reads `service.staffMemberDetails.staffMembers` for the staff picker — §8).
58
+ - **`slot`** — `{ serviceType, serviceId, localStartDate, localEndDate, timezone, scheduleId?, eventId?, locationId?, locationType?, resource? }`. `resource = { _id, name }` is set only when a specific staff was chosen (§8); otherwise the driver emits the ANY_RESOURCE fallback. `locationId`/`locationType` come from the slot's own `location` (the booking books at that location).
59
+ - **`formSubmission`** — the booking-form values, **keyed by each field's `target`** (§4).
60
+
61
+ Hold it however the framework prefers (React state lifted to a coordinator, a
62
+ store, route loaders + a provider, query-params). **Two rules that bite:**
63
+
64
+ 1. **Persist the selection before navigating.** When a step lives on its own
65
+ route, write the selection into the shared state (or the URL) *before* moving
66
+ on, and let the next step **rehydrate from the slug/URL** on direct load /
67
+ refresh. A persistent provider that does not re-init on in-app navigation will
68
+ otherwise show an empty next step.
69
+ 2. **The slug is the carry-across key.** `serviceSlug` (the service's
70
+ `mainSlug.name`) identifies the service across the slots + details steps;
71
+ resolve the full service from it when state is lost.
72
+
73
+ ## 3. The booking SDK sequence — `bookingDriver.ts`
74
+
75
+ The whole booking — `createBooking → createCart → calculateCart → checkout-or-place`
76
+ — is in **`../astro/templates/bookings/bookingDriver.ts`**, in plain `@wix` SDK
77
+ calls (no React, no UI). **Use it as-is** (it is framework-agnostic); the booking
78
+ step calls `book(params)` then branches on the result. Do not re-author the
79
+ sequence — the payload shapes are exact and easy to get subtly wrong.
80
+
81
+ ```
82
+ book({ service, slot, formSubmission, timezone }) →
83
+ createBooking(...) // booking lands CREATED — the cart holds the seat
84
+ createCart(...) // one catalog item per bookingId, appId = BOOKING_APP_ID, channel WEB
85
+ calculateCart(cartId) // → { cart, summary }; totals on summary.priceSummary.total.amount
86
+ isCheckoutRequired(...) ?
87
+ → { CheckoutRequired, cartId } // paid → navigateToCheckout(cartId, postFlowUrl) → Wix-hosted checkout
88
+ : placeOrder(cartId) → { CheckoutSkipped, orderId } // free / pay-in-person → confirmation
89
+ ```
90
+
91
+ Key facts the driver encodes (do not deviate):
92
+ - **No `confirmBooking`** — `confirmBooking` is the classic Bookings server-side
93
+ confirm step (it moves a booking from `CREATED` to `CONFIRMED`). Here the **ecom
94
+ cart holds the seat** instead — `placeOrder` (free/offline) or the hosted checkout
95
+ (paid) drives confirmation — so a client-only site completes the whole flow with no
96
+ server elevation.
97
+ - **ANY_RESOURCE fallback (staff)** — when no staff is chosen (the default, and on
98
+ single-staff services), `createBooking` sends
99
+ `resourceSelections:[{ resourceTypeId:"1cd44cf8-756f-41c3-bd90-3e2ffcaf1155", selectionMethod:"ANY_RESOURCE" }]`
100
+ and Wix auto-assigns a bookable staff resource (appointment slots return
101
+ `availableResources:[]` yet book fine this way). A specific staff choice sets
102
+ `slot.resource` instead (§8).
103
+ - **Checkout decision** — `isCheckoutRequired`: if the service's booking policy charges
104
+ a **cancellation fee** (`service.bookingPolicy.cancellationFeePolicy.enabled`) →
105
+ checkout (a card must be on file); else total 0 → place; else `FULL_PAYMENT_OFFLINE`
106
+ → place; else checkout.
107
+ - **Redirect shape** — paid bookings hand the cart to the Wix-hosted **ecom**
108
+ checkout: `createRedirectSession({ ecomCheckout: { checkoutId: cartId }, callbacks:{ postFlowUrl } })`.
109
+
110
+ ### Browse + availability (the read calls the steps make)
111
+
112
+ Mirror what the Bookings SDK does (`services.queryServices` is the raw SDK, not a component):
113
+
114
+ ```js
115
+ // Catalog (list) — the filter MUST include appId; pass conditionalFields, then .find().
116
+ // Merge the optional category/location filters into the SAME filter object (§7):
117
+ services.queryServices({
118
+ query: { filter: { appId: BOOKING_APP_ID /* , "category.id": { $eq: catId },
119
+ "locations.business.id": { $hasSome: [locId] } */ }, paging: { limit: 100 } },
120
+ conditionalFields: ["STAFF_MEMBER_DETAILS"],
121
+ }).find(); // → result.items ; filter out s.hidden
122
+
123
+ // Single service by slug — the .eq() builder chain:
124
+ services.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] })
125
+ .eq("mainSlug.name", slug).eq("appId", BOOKING_APP_ID).limit(1).find();
126
+
127
+ // Availability (APPOINTMENT) — serviceId is a single GUID STRING. The staff (§8)
128
+ // and location filters are OPTIONAL params on the same call:
129
+ availabilityTimeSlots.listAvailabilityTimeSlots({
130
+ serviceId, fromLocalDate, toLocalDate, timeZone, bookable: true, cursorPaging: { limit: 100 },
131
+ // staff: resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [staffResourceId] }],
132
+ // includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID],
133
+ // location: locations: [{ _id: locationId, locationType: "BUSINESS" }],
134
+ }); // slots: result.timeSlots[] (localStartDate/localEndDate/scheduleId at top level)
135
+
136
+ // Availability (CLASS) — different namespace, PLURAL serviceIds; slots carry eventInfo.eventId, no scheduleId.
137
+ // CLASS filters staff + location via eventFilter instead:
138
+ eventTimeSlots.listEventTimeSlots({
139
+ serviceIds: [serviceId], fromLocalDate, toLocalDate, timeZone, includeNonBookable: false,
140
+ // eventFilter: { "resources.id": { $hasSome: [staffResourceId] }, "location.id": [locationId] },
141
+ });
142
+ ```
143
+
144
+ `fromLocalDate`/`toLocalDate` are **local** date strings `YYYY-MM-DDThh:mm:ss`
145
+ (no `Z`) with an explicit `timeZone`.
146
+
147
+ ## 4. The booking form — schema-driven (`@wix/forms`)
148
+
149
+ The booking form is a **`@wix/forms` form** attached to the service
150
+ (`service.form._id`, namespace `wix.bookings.v2.bookings`). Render it
151
+ **schema-driven** — read the field list and render inputs by field type — the
152
+ **same renderer the forms vertical uses** (`../astro/forms/CONTACT_FORM.md`):
153
+
154
+ 1. Fetch the form by `service.form._id` (`@wix/forms` `getForm`; server-side/elevated
155
+ where the framework allows). Read `form.formFields`.
156
+ 2. Keep `fieldType === "INPUT" && !hidden` **and** a recognized string
157
+ `componentType` (`TEXT_INPUT` / `PHONE_INPUT` / `DROPDOWN`). For each, take
158
+ `inputOptions.target`, `inputOptions.required`, `inputOptions.stringOptions.componentType`,
159
+ and the label from `inputOptions.stringOptions.{textInputOptions|dropdownOptions|phoneInputOptions}.label`.
160
+ **Skip complex, object-valued fields** — e.g. a multi-line `ADDRESS` — they carry
161
+ no string `componentType`; rendering them as a text input sends a string and
162
+ `createBooking` rejects it with **"must be object"**. The booking enforces only
163
+ the contact basics (`first_name`/`last_name`/`email`), so omitting an optional
164
+ complex field is safe (do **not** default unknown field types to a text input).
165
+ 3. Render generic inputs by `componentType`: `TEXT_INPUT` → text input,
166
+ `DROPDOWN` → select, `PHONE_INPUT` → `type=tel`; treat `identifier === "TEXT_AREA"`
167
+ (or `target` containing `message`) as a textarea (its `componentType` is still `TEXT_INPUT`).
168
+ 4. Collect the values into an object **keyed by each field's `target`** — that is
169
+ `formSubmission`. Pass it to `book()`. Only include fields the visitor filled;
170
+ never send a value for a field you didn't render.
171
+
172
+ **Do not** submit `contactDetails`, and **do not** hardcode field names — key by
173
+ `target`. The default booking form's targets are snake_case `first_name` /
174
+ `last_name` / `email` / `phone`. Reference example: `../astro/templates/bookings/BookingForm.tsx`.
175
+
176
+ ## 5. The availability calendar (slots step)
177
+
178
+ **Group slots by day — don't render a flat list of every slot with time-only
179
+ labels** (the visitor can't tell which day a slot is on; that flat grid was the
180
+ original usability failure). The calendar **shape is your / the brand's choice** —
181
+ week, month, or N-day. The reference example uses a **week view** (a 7-day strip
182
+ with week navigation → the picked day's times); fetch availability for the visible
183
+ window (`fromLocalDate`/`toLocalDate` = that window's bounds), group by calendar
184
+ day, and offer a **"check next availability"** action that probes forward when the
185
+ window is empty. Reference example:
186
+ `../astro/templates/bookings/AvailabilityCalendar.tsx`.
187
+
188
+ ## 6. Client identity (per framework)
189
+
190
+ The SDK calls are identical everywhere; only how you get the client differs:
191
+ - **astro** — SSR read pages use the ambient `@wix/essentials` client
192
+ (`auth.elevate(services.queryServices)(...)`); browser islands call the `@wix`
193
+ modules **ambiently** too (the `@wix/astro` visitor client, like the ecom
194
+ `CartView` island) — no `createClient`/`OAuthStrategy`.
195
+ - **own / static** — acquire a visitor client once:
196
+ `createClient({ modules, auth: OAuthStrategy({ clientId: appId }) })` and call
197
+ the same functions on it. CDN imports for `none`, bundled for `own`.
198
+
199
+ ## 7. Catalog filters — location & category (both auto-skip)
200
+
201
+ Both filter the **catalog query** and re-render the list. The cleanest
202
+ framework-agnostic shape is a re-query driven by a query param (`?locationId`,
203
+ `?category`) — links/SSR on a static catalog, or a store/router on a SPA. **Show
204
+ each filter only when there is more than one choice**, exactly as the SoT does.
205
+
206
+ **Location** (the SoT is location-first; here it auto-skips ≤1 location):
207
+ 1. `services.queryLocations()` → `{ businessLocations: { locations: [{ _id, type, business: { name } }] }, customLocations: { exists }, customerLocations: { exists } }`.
208
+ 2. Count = `businessLocations.locations.length + (customLocations.exists ? 1 : 0) + (customerLocations.exists ? 1 : 0)`. **Show the selector only when count > 1.**
209
+ 3. The chosen location filters the catalog query:
210
+ - a real business id → `filter["locations.business.id"] = { $hasSome: [id] }`;
211
+ - the synthetic `"custom"` / `"customer"` → `filter["locations.type"] = { $hasSome: ["CUSTOM"|"CUSTOMER"] }` (the SoT's synthetic-id mapping).
212
+ 4. **Scope availability to exactly ONE location — always, on a multi-location service.** `listAvailabilityTimeSlots` returns **one slot per location** per time ("if `locations` is not specified, returns time slots for all locations where the service is available"), so an unscoped call on a 2-location service shows every time **twice**. Pass a single-element filter: APPOINTMENT → `locations: [{ _id, locationType: "BUSINESS" }]`; CLASS → `eventFilter: { "location.id": [id] }`. (Staff does **not** multiply rows — one slot carries many `availableResources`; only location does.) The slots step builds its location list from **`queryLocations()`** (the site's real business locations, whose ids the availability engine recognizes) **intersected with the service's own location ids** — not from `service.locations` alone, whose entries can carry an id the availability engine doesn't recognize (scoping to it returns zero slots). A **location picker** defaults to the catalog-carried location (or the first) and scopes the call when there's a real location to scope to; when the site has no business locations the list is empty and the call stays unscoped (one location → no duplicates). The booked slot carries its own `location`, so the booking books at that location (the driver maps it).
213
+
214
+ **Category:**
215
+ 1. `categoriesV2.queryCategories().find()` → `result.items: [{ _id, name }]`. **Non-fatal** — if it fails, render the catalog without the bar.
216
+ 2. **Show the bar only when `items.length > 1`** (a fresh site has one default category).
217
+ 3. The chosen category filters the catalog query: `filter["category.id"] = { $eq: categoryId }`. A service carries its category at `service.category._id`.
218
+
219
+ Reference example: `../astro/templates/bookings/services/index.astro` (both filters, link-driven, auto-skipping).
220
+
221
+ ## 8. Staff / resource selection (auto-skips ≤1 staff)
222
+
223
+ A service carries its staff on the service object via the **`STAFF_MEMBER_DETAILS`
224
+ conditional field** (already requested in the catalog/detail query):
225
+ `service.staffMemberDetails.staffMembers: [{ staffMemberId, name }]`. The id used
226
+ for filtering and booking is **`staffMemberId`** — which, **despite the name, IS the
227
+ resource GUID** (it matches the service's `staffMemberIds` and the Staff Members API
228
+ `resourceId`). Below, `resourceId` is that value.
229
+
230
+ 1. Show a "Any staff member / pick one" control above the calendar **only when
231
+ there is more than one staff**.
232
+ 2. Selecting a staff re-fetches availability filtered to that resource:
233
+ - APPOINTMENT → `resourceTypes: [{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds: [resourceId] }]` (plus `includeResourceTypeIds: [STAFF_MEMBER_RESOURCE_TYPE_ID]`);
234
+ - CLASS → `eventFilter: { "resources.id": { $hasSome: [resourceId] } }`.
235
+ 3. When a slot is picked **with a specific staff selected**, set `slot.resource = { _id: resourceId, name }`. The driver books that resource. With **"any staff"**, leave `resource` unset — the driver emits the **ANY_RESOURCE fallback** and Wix auto-assigns a bookable resource (appointment slots can return `availableResources: []` yet book fine this way).
236
+
237
+ `STAFF_MEMBER_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155"` (exported from `bookingDriver.ts`). Reference example: `../astro/templates/bookings/AvailabilityCalendar.tsx` (the staff `<select>` + the filtered fetch).
238
+
239
+ ## 9. Out of scope
240
+
241
+ Waitlist, on-site manage/cancel, payment/deposit breakdown, and multi-service /
242
+ day-range are out of scope. (Waitlist and manage/cancel have **no headless SoT** —
243
+ neither the components nor the vibe plugin implement join/cancel logic, only
244
+ display-only policy flags — so they are deliberately not built here.) Show
245
+ bookable slots only; post-booking self-service is handled by the Wix-hosted flow /
246
+ member area.
@@ -1,106 +1,95 @@
1
1
  ---
2
2
  name: bookings-implementer
3
- description: "Implements Wix Bookings vertical — services catalog, service detail with availability calendar and booking flow, and booking confirmation. Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
3
+ description: "Implements the Wix Bookings vertical — services catalog (with optional location + category filters), a service detail page with a week-calendar availability picker (with optional staff selection), a schema-driven booking form, and confirmation, for appointments and classes. The booking runs client-side via the @wix SDK (ecom Cart V2 — no confirmBooking). Scopes: seed, components, pages. Extends references/shared/IMPLEMENTER.md."
4
4
  ---
5
5
 
6
6
  # Bookings Implementer
7
7
 
8
- Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups, site.json read pattern, return contract, style conventions, and common failure modes.
8
+ Extends `references/shared/IMPLEMENTER.md`. Read that file first for phase routing, REST auth + doc lookups, the `.wix/seeded.json` read pattern, the return contract, style conventions, and common failure modes.
9
+
10
+ ## The logic lives in one place
11
+
12
+ The booking **logic** — the step model, the shared selection state, the exact
13
+ `@wix` SDK sequence, and the schema-driven form — is framework-agnostic and lives
14
+ in **`./FLOW.md`**. Read it first. The astro guides below are the astro *wiring* of
15
+ that logic; the code examples under `<SKILL_ROOT>/references/astro/templates/bookings/`
16
+ are React (the astro islands use them directly). For an own/static build, the wiring guide is
17
+ `../custom/bookings/WIRING.md` (same logic, client-side `@wix/sdk`).
9
18
 
10
19
  ## Scope routing
11
20
 
12
21
  | Scope | Phase | Reference |
13
22
  |-------|-------|-----------|
14
- | `seed` | Seed (service creation via Wix Bookings REST API) | `./SERVICES_DATA.md` |
15
- | `components` | Components: `ServiceCard.astro`, `AvailabilityCalendar.tsx`, `BookingForm.tsx`, `ServiceBookingFlow.tsx`, `ManageBooking.tsx` (`SeoTags.astro` is pre-copied — see Templates) | `../astro/bookings/COMPONENTS.md` |
16
- | `pages` | Pages: `/services` listing, `/services/[slug]` detail, `/booking-confirmation`, `/manage-booking` | `../astro/bookings/SERVICES_PAGES.md` |
23
+ | `seed` | Seed — create services (and staff) via the Wix Bookings REST API | `./SERVICES_DATA.md` |
24
+ | `components` | Components — the client islands (week calendar, schema-driven form, the flow coordinator) | `./FLOW.md` + `../astro/bookings/COMPONENTS.md` |
25
+ | `pages` | Pages — `/services` listing, `/services/[slug]` detail, `/booking-confirmation`, + nav/home links | `./FLOW.md` + `../astro/bookings/SERVICES_PAGES.md` |
17
26
 
18
- ## Files this vertical creates / contributes
27
+ ## Templates — read and adapt (don't invent)
19
28
 
20
- See `<SKILL_ROOT>/references/verticals/bookings.md` frontmatter.
29
+ Canonical examples live at `<SKILL_ROOT>/references/astro/templates/bookings/`.
30
+ Your `components` and `pages` scopes **read these and adapt them** — adapt brand
31
+ copy, headings, and styling; keep the SDK calls, payload shapes, and the data
32
+ flow (re-authoring the SDK wiring from scratch is the main source of API-shape bugs).
21
33
 
22
- ## Pre-return file-existence assertion (pages scope)
34
+ Components (`components` scope — TSX only, no CSS):
35
+ - `<SKILL_ROOT>/references/astro/templates/bookings/AvailabilityCalendar.tsx` — the week calendar (week strip → the day's slots; APPOINTMENT via `availabilityTimeSlots`, CLASS via `eventTimeSlots`).
36
+ - `<SKILL_ROOT>/references/astro/templates/bookings/BookingForm.tsx` — the schema-driven form (renders the `@wix/forms` field list, keys values by `target`) that drives `bookingDriver.book()`.
37
+ - `<SKILL_ROOT>/references/astro/templates/bookings/ServiceBookingFlow.tsx` — the coordinator island (holds the selected slot; calendar → form → redirect).
23
38
 
24
- Before returning `status: "complete"` from the `pages` scope, verify all route files exist on disk:
25
- - `src/pages/services/index.astro`
26
- - `src/pages/services/[slug].astro`
27
- - `src/pages/booking-confirmation.astro`
28
- - `src/pages/manage-booking.astro`
39
+ Pages (`pages` scope):
40
+ - `<SKILL_ROOT>/references/astro/templates/bookings/ServiceCard.astro`
41
+ - `<SKILL_ROOT>/references/astro/templates/bookings/services/index.astro`, `<SKILL_ROOT>/references/astro/templates/bookings/services/[slug].astro`
42
+ - `<SKILL_ROOT>/references/astro/templates/bookings/booking-confirmation.astro`
29
43
 
30
- If any declared file is missing, return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<expected path>" }]` rather than claiming success.
44
+ ### Pre-copied by the orchestrator (do NOT write these yourself)
45
+ Mechanical, brand-agnostic — the orchestrator copies them before dispatch (BUILD-astro.md § build wave). Rely on them at the listed paths:
46
+ - `src/components/bookingDriver.ts` ← `<SKILL_ROOT>/references/astro/templates/bookings/bookingDriver.ts` — the booking SDK sequence (`book()`, `navigateToCheckout()`). The islands import it; never re-author it.
47
+ - `src/components/SeoTags.astro` ← `<SKILL_ROOT>/references/astro/templates/bookings/SeoTags.astro` — renders `service.seoData.tags`; imported by `services/[slug].astro`.
48
+ - `src/styles/components-bookings.css` ← `<SKILL_ROOT>/references/astro/templates/bookings/components-bookings.css` — the flow's component classes.
31
49
 
32
- ## Templates
50
+ If a pre-copied file is missing at runtime, that's an orchestrator-side bug — return `status: "partial"` with `errors: [{code: "UTILITY_TEMPLATE_NOT_PRECOPIED", path: "<missing>"}]`; do not author your own version.
33
51
 
34
- Canonical templates live at `<SKILL_ROOT>/references/astro/templates/bookings/`. Your `components` and `pages` scopes **read these and adapt them — don't invent markup or SDK wiring** (the booking/confirm/availability/waitlist wiring is subtle and was verified against a live site; re-authoring from scratch is a common source of API-shape bugs). Adapt brand copy, headings, and styling; keep the SDK calls and the data flow.
52
+ ## Pre-return file-existence assertion (pages scope)
35
53
 
36
- Components (`components` scope — `.tsx`/`.astro`, no CSS):
37
- - `…/templates/bookings/ServiceCard.astro`
38
- - `…/templates/bookings/AvailabilityCalendar.tsx` — branches on `serviceType` (APPOINTMENT → `availabilityTimeSlots`, CLASS → `eventTimeSlots`); capacity + instructor + full→waitlist
39
- - `…/templates/bookings/BookingForm.tsx` — `createBooking` → `/api/confirm-booking`; party size; waitlist on full
40
- - `…/templates/bookings/ServiceBookingFlow.tsx` — coordinator (threads `serviceType`)
41
- - `…/templates/bookings/ManageBooking.tsx` — cancel via anonymous token (used by `manage-booking.astro`)
54
+ Before returning `status: "complete"` from `pages`, verify on disk:
55
+ - `src/pages/services/index.astro`
56
+ - `src/pages/services/[slug].astro`
57
+ - `src/pages/booking-confirmation.astro`
42
58
 
43
- Pages (`pages` scope):
44
- - `…/templates/bookings/services/index.astro`, `…/services/[slug].astro`
45
- - `…/templates/bookings/booking-confirmation.astro`, `…/manage-booking.astro`
59
+ If any declared file is missing, return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<expected path>" }]`.
46
60
 
47
- ### Pre-copied by the orchestrator (do NOT write these yourself)
48
- These are mechanical (no brand content) — the orchestrator copies them before dispatch (BUILD-astro.md § Step 4.5). Just rely on them at the listed paths:
49
- - `src/styles/components-bookings.css` ← `…/templates/bookings/components-bookings.css`
50
- - `src/components/SeoTags.astro` ← `…/templates/bookings/SeoTags.astro` (renders `service.seoData.tags`; imported by `services/[slug].astro`)
51
- - `src/pages/api/confirm-booking.ts` ← `…/templates/bookings/api/confirm-booking.ts` (elevated `confirmBooking` — holds the seat)
52
- - `src/pages/api/waitlist.ts` ← `…/templates/bookings/api/waitlist.ts` (elevated native v1 waitlist register)
61
+ ## Dependencies (Setup installs these — see SETUP.md)
53
62
 
54
- If a pre-copied file is missing at runtime, that's an orchestrator-side bug — return `status: "partial"` with `errors: [{code: "UTILITY_TEMPLATE_NOT_PRECOPIED", path: "<missing>"}]`; do not author your own version.
63
+ `@wix/bookings @wix/essentials @wix/forms @wix/redirects @wix/auto_sdk_ecom_cart-v-2`.
64
+ `@wix/forms` renders the booking-form schema; `@wix/redirects` + `@wix/auto_sdk_ecom_cart-v-2` run the cart/checkout sequence.
55
65
 
56
66
  ## CSS ownership — bookings pack
57
67
 
58
- Bookings-specific component CSS lives in `src/styles/components-bookings.css` (pre-copied by the orchestrator from the skill template — see above), NOT in the designer's `global.css`. Classes the pack owns:
59
-
60
- - `.service-card`, `.service-card-image`, `.service-card-meta`, `.service-card-price` — the service card itself, including image containment, price badge, and duration tag.
61
- - `.service-grid` — the layout that lists service cards on `/services`. Include `display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: var(--spacing-xl);` plus View-Transitions opacity fade.
62
- - `.availability-calendar` — the date-picker shell wrapping the calendar widget.
63
- - `.time-slot`, `.time-slot--available`, `.time-slot--selected`, `.time-slot--full` (full CLASS session → waitlist), plus the `.time-slot-time` / `.time-slot-capacity` spans inside each slot button.
64
- - `.booking-form` (+ `.booking-form-note`) — the booking/waitlist form revealed after a slot is selected.
65
- - `.manage-booking`, `.manage-booking-summary`, `.manage-booking-status`, `.manage-booking-note`, `.manage-booking-link` — the `/manage-booking` view/cancel UI.
66
-
67
- If `global.css` ships a partial rule for any class above, flag it in your return JSON's `errors` array (`{code: "GLOBAL_CSS_LEAK", class: "<name>"}`) and override with the complete rule in `components-bookings.css`.
68
+ Bookings-specific CSS lives in `src/styles/components-bookings.css` (pre-copied — see above), NOT in `global.css`. Classes the pack owns: `.service-card*`, `.service-grid`, `.availability-*`, `.time-slot*`, `.booking-*`. If `global.css` ships a partial rule for any of these, flag it (`{code:"GLOBAL_CSS_LEAK", class:"<name>"}`) and override in `components-bookings.css`.
68
69
 
69
70
  ## Bookings-specific failure modes
70
71
 
71
72
  | Wrong | Right |
72
73
  |-------|-------|
73
- | Treat `createBooking` as the whole booking — stop after it returns | `createBooking` leaves the booking **`CREATED`**, which **does not hold a seat or block availability** (per the [lifecycle docs](https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/introduction): only `CONFIRMED` blocks availability). Without a follow-up **`confirmBooking`**, the class overbooks and capacity never drops. After createBooking returns `CREATED`, call `bookings.confirmBooking(bookingId, revision, { paymentStatus: "NOT_PAID", flowControlSettings: { checkAvailabilityValidation: true } })`. `confirmBooking` needs Manage Bookings → run it **server-side, elevated** (`auth.elevate`), not from the visitor client. |
74
- | Build the waitlist on `createBooking` (expecting a `WAITING_LIST` status) | `createBooking` has **no** waitlist option (verified: only `participantNotification`/`sendSmsReminder`); a full session returns `SESSION_CAPACITY_EXCEEDED`. The native waitlist is the **legacy v1** API `POST /bookings/v1/waitlist/register` (there is no v2/v3 waitlist). It needs the **v1-encoded session id** as `waitingResource` (surfaced in the `SESSION_CAPACITY_EXCEEDED` message: *"…on session `<id>`"*) **+ a Wix `contactId`** (resolve via Contacts API by email, create if missing), and the **Manage Bookings** scope → server-side elevated. Class/EVENT only. |
75
- | Call the v1 waitlist (or any Manage-Bookings REST) with `auth.elevate(async () => fetchWithAuth(...))` | `auth.elevate()` only grants permissions for a real **SDK function** (it reads the method's required-permission metadata); a plain closure isn't one, so it runs **un-elevated (visitor)** and 403s. For raw REST with no SDK wrapper, elevate the authenticated fetch itself: `const ef = auth.elevate(httpClient.fetchWithAuth); await ef(url, init)`. (For calls that *do* have an SDK method — `confirmBooking`, `getAnonymousActionToken` — elevate the method directly.) |
76
- | Pass the v3 `eventId` as the waitlist `waitingResource` | The v1 waitlist can't decode a v3 GUID (`"id … cannot be decoded"`). Use the **v1-encoded session id** (e.g. `2mmoW0vw…`) from the `SESSION_CAPACITY_EXCEEDED` message. |
77
- | Use Services V1 API endpoint (`/bookings/v1/catalog/services`) | Services V2 is at `POST https://www.wixapis.com/_api/bookings/v2/services`. V1 has a different (nested `info.*`) payload shape. |
78
- | Nest service fields under `info` (`info.name`, `info.description`) | V2 uses flat fields: `name`, `description`, `tagLine` at the top level of the `service` object. |
79
- | Use `payment.fixed.price.amount` | V2 uses `payment.fixed.price.value` (a string like `"75.00"`). `amount` is the V1 field name. |
80
- | Omit `defaultCapacity` when creating a service | Required in V2. Set to `1` for APPOINTMENT; use participant count for CLASS. |
81
- | Omit `onlineBooking` when creating a service | Required in V2. At minimum `{ "enabled": true }`. |
82
- | Omit `sessionDurations` for an APPOINTMENT service | Required for APPOINTMENT: `schedule.availabilityConstraints.sessionDurations: [<minutes as int>]`. Do NOT specify for CLASS. |
83
- | Use `availability.queryAvailability` or `availability` SDK namespace | Branch by service type: APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots()`; CLASS → **`eventTimeSlots.listEventTimeSlots()`** (a **different namespace** — `eventTimeSlots`, not `availabilityTimeSlots` — with plural `serviceIds` + `includeNonBookable`). The `availability` namespace does not exist in `@wix/bookings`. |
84
- | Pass UTC ISO strings to `listAvailabilityTimeSlots` | The method expects **local date strings** in `YYYY-MM-DDTHH:mm:ss` format (no `Z`), with a separate `timeZone` parameter. |
85
- | Read service slug from `service.slug` | V2 response puts slug at `service.mainSlug.name`. Fall back to `service.supportedSlugs[0].name`, then `service.id`. |
86
- | Call `queryServices({ query: { filter, paging } })` (object form) | In `@wix/bookings` the object form returns **0 items with no error**. Use the **query builder**: `await services.queryServices().limit(100).find()` → `result.items`. For a slug, fetch the (small) catalog and match `mainSlug.name` in JS, or `.eq("mainSlug.name", slug).find()`. |
87
- | Build a `createClient`/`OAuthStrategy` in `.astro` SSR frontmatter keyed off `import.meta.env.*_WIX_CLIENT_ID` | The `*_WIX_CLIENT_ID` env vars are client-only in `@wix/astro` and `undefined` at server render → `createClient` throws → HTTP 500 (only in the deployed runtime; dev has `.env.local`). SSR reads use `@wix/essentials` `auth.elevate(services.queryServices)()…` — the same ambient pattern as the CMS pages. |
88
- | Read the client ID in a browser island via `import.meta.env.*_WIX_CLIENT_ID` | Client islands import `{ WIX_CLIENT_ID } from "astro:env/client"`. `import.meta.env` is `undefined` in the browser bundle → `OAuthStrategy` sends an empty `client_id` → `oauth2/token` `400`. The var name is `WIX_CLIENT_ID` (no `PUBLIC_`). |
89
- | Read service name from `service.info.name` | V2 uses flat `service.name` (not `service.info.name`). Same for `description` and `tagLine`. |
90
- | Read duration from `service.schedule.sessions.duration` | V2 puts duration at `service.schedule.availabilityConstraints.sessionDurations[0]` (array of ints). |
91
- | Read media URL from `service.info.images[0].url` | V2 media: `service.media.mainMedia.image.url`. |
92
- | Use `import { availability } from '@wix/bookings'` | Use `import { availabilityTimeSlots } from '@wix/bookings'`. |
93
- | Call `availability.queryAvailability` server-side on every render | Availability is real-time and session-specific — always call from the client-side React island, NOT from `.astro` frontmatter SSR. |
94
- | Create a booking without `totalParticipants` | `createBooking` requires `totalParticipants: 1` (or `participantsChoices`). |
95
- | Read booking ID from `result.booking.id` | The booking ID field is `result.booking._id` (underscore prefix). |
96
- | Redirect to `/thank-you` (ecom route) after booking | Redirect to `/booking-confirmation?bookingId=<_id>`. The ecom thank-you page is unrelated to bookings. |
97
- | 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. |
98
- | 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. |
99
- | 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"`. |
100
- | 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. |
101
- | 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`. |
102
- | 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. |
103
- | 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`. |
104
- | 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. |
105
- | Pass `hidden: true` services to the listing | Always filter with `{ hidden: false }` in your `queryServices` filter to exclude hidden services from the public listing. |
106
- | Use `../../../` for imports in `[slug].astro` | `src/pages/services/[slug].astro` is two directories deep inside `src/`. Import Layout and components with `../../layouts/Layout.astro` and `../../components/…` — not three `../` which would escape `src/`. |
74
+ | Stop after `createBooking` returns | `createBooking` leaves the booking **`CREATED`**. The seat is held by the **ecom Cart V2**: continue `createCart → calculateCart → isCheckoutRequired ? hosted-checkout redirect : placeOrder`. The whole sequence is in `bookingDriver.ts` — use it. |
75
+ | Hardcode `selectedPaymentOption: "ONLINE"` on `createBooking` | **Derive** it from the service: `online && !inPerson → "ONLINE"`, `!online && inPerson → "OFFLINE"`, else `"ONLINE"`. A free / pay-in-person service is the only kind that reaches `placeOrder`, and booking it `ONLINE` makes the cart reject it with **`INSUFFICIENT_INVENTORY`** (`available_quantity: 0`). `bookingDriver.ts` derives this for you (don't pass `selectedPaymentOption` to `book()`). |
76
+ | Pass `contactDetails` to `createBooking` | Pass **`formSubmission`**, keyed by each field's **`target`** (default booking form: snake_case `first_name`/`last_name`/`email`/`phone`). `contactDetails` is what Wix derives back onto the response. |
77
+ | Hardcode the booking-form fields | Render the form **schema-driven**: read `service.form._id`'s fields via `@wix/forms`, render by `componentType`, key values by `target` (same renderer as `../astro/forms/CONTACT_FORM.md`). |
78
+ | Render every form field as a text input (default unknown types to `TEXT_INPUT`) | Render **only** fields with a recognized string `componentType` (`TEXT_INPUT`/`PHONE_INPUT`/`DROPDOWN`). **Skip complex, object-valued fields** (e.g. the default booking form's multi-line `ADDRESS`) — sending a string for them fails `createBooking` with **"must be object"**. Only `first_name`/`last_name`/`email` are enforced, so skipping optional complex fields is safe. |
79
+ | Call `queryServices` without an `appId` filter | Always include `appId` (`13d21c63-…`) in the filter. List: `queryServices({ query:{ filter:{ appId }, paging }, conditionalFields:["STAFF_MEMBER_DETAILS"] }).find()`. Single: the `.eq("mainSlug.name", slug).eq("appId", …).limit(1).find()` builder chain. |
80
+ | Build `createClient({ auth: OAuthStrategy({ clientId: import.meta.env.* }) })` in `.astro` SSR | The `*_WIX_CLIENT_ID` env var is client-only → `undefined` at server render → 500. SSR reads use the ambient `@wix/essentials`: `auth.elevate(services.queryServices)(...)`. |
81
+ | Build an `OAuthStrategy` client inside an astro browser island | Astro islands call the `@wix` modules **ambiently** (the `@wix/astro` visitor client, like the ecom `CartView`) — no `createClient`. (Own/static builds DO use `OAuthStrategy` — `../custom/bookings/WIRING.md`.) |
82
+ | Pass UTC ISO strings to `listAvailabilityTimeSlots` | Use **local** date strings `YYYY-MM-DDThh:mm:ss` (no `Z`) + a separate `timeZone`. |
83
+ | `availability.queryAvailability` / `import { availability }` | APPOINTMENT → `availabilityTimeSlots.listAvailabilityTimeSlots({ serviceId: <string> })`; CLASS → `eventTimeSlots.listEventTimeSlots({ serviceIds: [<string>] })`. The `availability` namespace does not exist. |
84
+ | Read slot time from `timeSlot.slot.startDate` | Slot fields are at the **top level**: `localStartDate`, `localEndDate`, `scheduleId` (APPOINTMENT). CLASS slots carry `eventInfo.eventId` and have **no** `scheduleId`. |
85
+ | Dump every slot in one flat, time-only grid | **Group slots by day** — a flat time-only grid hides which day a slot is on. The calendar shape (week / month / N-day) is your choice; the example uses a week strip → the picked day's times. |
86
+ | Read the staff id from `staffMember._id` / `.resourceId` on `staffMemberDetails.staffMembers` | The field is **`staffMemberId`** (on `service.staffMemberDetails.staffMembers[]`) — despite the name it IS the resource GUID (matches the service's `staffMemberIds`). Filter availability: APPOINTMENT `resourceTypes:[{ resourceTypeId: STAFF_MEMBER_RESOURCE_TYPE_ID, resourceIds:[staffMemberId] }]`; CLASS `eventFilter:{ "resources.id": { $hasSome:[staffMemberId] } }`. Record the choice as `slot.resource = { _id: staffMemberId, name }`; leave it unset for "any staff" (ANY_RESOURCE fallback). Show the picker only when >1 staff. |
87
+ | Always show the location selector / staff picker / category bar | **Auto-skip** each when there's nothing to choose: location count ≤1 (`businessLocations.length + custom?.exists + customer?.exists`), staff ≤1, categories ≤1. Single-location single-staff sites see the simple flow. |
88
+ | Filter the catalog by a synthetic location id, or hardcode the location filter | Real business id → `filter["locations.business.id"] = { $hasSome:[id] }`; synthetic `"custom"`/`"customer"` → `filter["locations.type"] = { $hasSome:["CUSTOM"|"CUSTOMER"] }`. Carry the choice to the detail page so availability is scoped too. |
89
+ | Call `listAvailabilityTimeSlots` **without** a `locations` filter on a multi-location service | It returns **one slot per location** per time → every time appears **N×** (duplicated rows). **Always scope availability to one business location** (`locations: [{ _id, locationType:"BUSINESS" }]`; CLASS: `eventFilter:{ "location.id":[id] }`). Build the calendar's location list from **`queryLocations()`** (the site's real business locations) intersected with the service's location ids — NOT from `service.locations` alone (its entries can carry an id the availability engine doesn't recognize → scoping to it returns 0 slots even though the service has availability). Default to the carried/first, picker when >1; no business locations → unscoped. Staff does **not** cause duplicates — one slot carries many `availableResources`; only location multiplies rows. |
90
+ | Let a `queryCategories` failure break the catalog | Categories are **non-fatal** — wrap in try/catch and render the catalog without the bar. Category filter: `filter["category.id"] = { $eq: categoryId }`; a service's category is `service.category._id`. |
91
+ | Read service slug from `service.slug` / name from `service.info.name` | V2 is flat: slug `service.mainSlug.name` (fallback `supportedSlugs[0].name`), `service.name`, `service.description`, `service.tagLine`; duration `service.schedule.availabilityConstraints.sessionDurations[0]`; price `service.payment.fixed.price.value` (string); media `service.media.mainMedia.image.url`. |
92
+ | Use `service.id` / `result.booking.id` | Entity ids are `_id` (underscore): `service._id`, `booking._id`. |
93
+ | Mount the calendar/form without `client:only="react"` | Availability + booking are timezone/session-specific — always `client:only="react"`. SSR only the read pages (catalog/detail) for SEO. |
94
+ | Omit try/catch on the booking | `createBooking` can reject (e.g. a slot taken between fetch and submit, or strict phone validation). Catch and surface a friendly message; don't crash. |
95
+ | Ship a payment/deposit breakdown, multi-service stacking, waitlist, or on-site manage/cancel | Out of scope. **Waitlist and manage/cancel have no headless SoT** (the components + vibe plugin expose only display-only policy flags — no join/cancel logic) — do not invent them on the REST/anonymous-token layer. Post-booking self-service is handled by the Wix-hosted flow / member area. |
@@ -93,6 +93,66 @@ curl -sS -X POST -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H
93
93
 
94
94
  ---
95
95
 
96
+ ## Step 3b — Create categories (OPTIONAL — only to surface the catalog category filter)
97
+
98
+ A fresh Bookings install has **one** default category ("Our Services"), so the
99
+ front-end's category filter bar stays hidden (it shows only when >1 category
100
+ exists). Create extra categories **only when the build wants a category filter**
101
+ (e.g. `intent.bookings.categories` is set, or several distinct service groups).
102
+ Skip this step otherwise — it's not needed for a basic seed.
103
+
104
+ ```bash
105
+ # Create a category. Returns { category: { id, ... } } — save the id.
106
+ curl -sS -X POST \
107
+ -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
108
+ -d '{ "category": { "name": "<Category name, e.g. Massage>" } }' \
109
+ "https://www.wixapis.com/bookings/v2/categories"
110
+ ```
111
+
112
+ Then **assign each service to a category** by including `category` in the Step-4
113
+ create payload: `"category": { "id": "<categoryId>" }`. (If a create rejects
114
+ `category`, assign it after creation via Update Service — `PATCH
115
+ /_api/bookings/v2/services/{id}` with `service.category.id` + the current
116
+ `revision` — per the docs' "create a category and connect a service" flow.) A
117
+ service can belong to **one** category. Distribute services across 2+ categories
118
+ so the filter is meaningful.
119
+
120
+ > The front-end reads categories via `categoriesV2.queryCategories().find()` and
121
+ > filters services with `filter["category.id"] = { $eq: id }`. No category data →
122
+ > no filter bar (graceful).
123
+
124
+ ---
125
+
126
+ ## Step 3c — Multiple business locations (OPTIONAL — only for a multi-location demo)
127
+
128
+ The front-end's location selector auto-skips when the site has ≤1 location, so a
129
+ single-location seed needs nothing here (and the booking flow is unaffected).
130
+ Create a second business location **only** for a multi-location build
131
+ (`intent.bookings.multiLocation`):
132
+
133
+ ```bash
134
+ # Create a business location. Returns { location: { id, ... } }.
135
+ curl -sS -X POST \
136
+ -H "Authorization: Bearer $TOKEN" -H "wix-site-id: <siteId>" -H "Content-Type: application/json" \
137
+ -d '{ "location": { "name": "<Branch name>", "status": "ACTIVE", "locationType": "BUSINESS",
138
+ "address": { "city": "<City>", "country": "<ISO country>" } } }' \
139
+ "https://www.wixapis.com/locations/v1/locations"
140
+ ```
141
+
142
+ Then attach locations to each service's `locations` array on create (Step 4):
143
+ `"locations": [ { "type": "BUSINESS", "business": { "id": "<locationId>" } }, … ]`.
144
+
145
+ > **Caveat (verified-behavior limitation, not a bug):** filtering *availability*
146
+ > by location also depends on each staff member's **working-hours location
147
+ > coverage** (the `STAFF_WORKING_HOURS_LOCATIONS` setting). Seeding that reliably
148
+ > is as fragile as custom working hours (see Step 3's warning), so for a seed,
149
+ > create the location + attach it to services to exercise the **catalog** location
150
+ > filter, and configure per-location staff hours from the Bookings dashboard if you
151
+ > need per-location availability to differ. The selector + service filtering work
152
+ > from the seed alone; per-location *slot* differences need the dashboard step.
153
+
154
+ ---
155
+
96
156
  ## Step 4 — Create services (Services V2 API)
97
157
 
98
158
  **Endpoint:** `POST https://www.wixapis.com/_api/bookings/v2/services`