@wix/agent-skills 1.18.2 → 1.19.1
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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +42 -40
- package/skills/wix-app/references/BACKEND_API.md +151 -152
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +12 -8
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
- package/skills/wix-app/scripts/pkg-root.cjs +73 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
- package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
- package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
- package/skills/wix-base44-connector/SKILL.md +24 -22
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless-fast/SKILL.md +25 -7
- package/skills/wix-headless-fast/entry/skill.md +23 -14
- package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
- package/skills/wix-manage/SKILL.md +14 -8
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +59 -33
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +45 -22
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +70 -12
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +53 -17
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +17 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +11 -3
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +5 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +5 -4
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +33 -5
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
|
@@ -133,33 +133,108 @@ function toDetail(raw: RawProduct): ProductDetail {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
// Search Products applies sort/filter/search server-side, across the WHOLE catalog, before
|
|
137
|
+
// cursor paging — the only correct place for them. Sorting or filtering an already-fetched
|
|
138
|
+
// page only rearranges the slice you happen to hold.
|
|
139
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md
|
|
140
|
+
export const CATALOG_SORTS = {
|
|
141
|
+
featured: { label: "Default order" }, // the catalog's own order; no merchant-curated promise
|
|
142
|
+
priceAsc: { label: "Price: low to high" },
|
|
143
|
+
priceHigh: { label: "Price: high to low" },
|
|
144
|
+
name: { label: "Name: A\u2013Z" },
|
|
145
|
+
newest: { label: "Newest" },
|
|
146
|
+
} as const;
|
|
147
|
+
export type CatalogSort = keyof typeof CATALOG_SORTS;
|
|
148
|
+
|
|
149
|
+
const SORT_FIELDS: Partial<Record<CatalogSort, { fieldName: string; order: "ASC" | "DESC" }[]>> = {
|
|
150
|
+
priceAsc: [{ fieldName: "actualPriceRange.minValue.amount", order: "ASC" }, { fieldName: "name", order: "ASC" }],
|
|
151
|
+
priceHigh: [{ fieldName: "actualPriceRange.minValue.amount", order: "DESC" }, { fieldName: "name", order: "ASC" }],
|
|
152
|
+
name: [{ fieldName: "name", order: "ASC" }],
|
|
153
|
+
newest: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "name", order: "ASC" }],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function priceBound(value: number | string | undefined, name: string): number | undefined {
|
|
157
|
+
if (value == null || value === "") return undefined;
|
|
158
|
+
const parsed = Number(value);
|
|
159
|
+
if (!String(value).trim() || !Number.isFinite(parsed) || parsed < 0) {
|
|
160
|
+
throw new Error(`${name} must be a non-negative number.`);
|
|
161
|
+
}
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface CatalogSearchOptions {
|
|
166
|
+
limit?: number;
|
|
167
|
+
/** Continues the ORIGINAL query — pass alone; new sort/filter values start without one. */
|
|
168
|
+
cursor?: string;
|
|
169
|
+
categoryId?: string | null;
|
|
170
|
+
sort?: CatalogSort;
|
|
171
|
+
/** Bounds compare the product's minimum actual variant price, in site currency. */
|
|
172
|
+
minPrice?: number | string;
|
|
173
|
+
maxPrice?: number | string;
|
|
174
|
+
inStockOnly?: boolean;
|
|
175
|
+
/** Name search, max 100 chars. */
|
|
176
|
+
search?: string;
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* reconstruct it.
|
|
180
|
+
* Search visible catalog products — sorted, filtered, and searched by Wix across the whole
|
|
181
|
+
* catalog, then cursor-paged. `nextCursor` continues the same query; start over (no cursor)
|
|
182
|
+
* whenever any selection changes.
|
|
150
183
|
*/
|
|
184
|
+
export async function searchCatalog({
|
|
185
|
+
limit = 24,
|
|
186
|
+
cursor,
|
|
187
|
+
categoryId,
|
|
188
|
+
sort = "featured",
|
|
189
|
+
minPrice,
|
|
190
|
+
maxPrice,
|
|
191
|
+
inStockOnly = false,
|
|
192
|
+
search = "",
|
|
193
|
+
}: CatalogSearchOptions = {}): Promise<{ products: ProductSummary[]; nextCursor: string | null }> {
|
|
194
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
195
|
+
throw new Error("limit must be between 1 and 100.");
|
|
196
|
+
let query: RawProduct = { cursorPaging: { limit, ...(cursor ? { cursor } : {}) } };
|
|
197
|
+
if (!cursor) {
|
|
198
|
+
if (!(sort in CATALOG_SORTS)) throw new Error("Unsupported catalog sort.");
|
|
199
|
+
const min = priceBound(minPrice, "minPrice");
|
|
200
|
+
const max = priceBound(maxPrice, "maxPrice");
|
|
201
|
+
if (min !== undefined && max !== undefined && min > max)
|
|
202
|
+
throw new Error("minPrice must not exceed maxPrice.");
|
|
203
|
+
if (search.trim().length > 100) throw new Error("Search must be at most 100 characters.");
|
|
204
|
+
const conditions: RawProduct[] = [{ visible: true }];
|
|
205
|
+
if (categoryId)
|
|
206
|
+
conditions.push({ "allCategoriesInfo.categories": { $matchItems: [{ id: categoryId }] } });
|
|
207
|
+
// Search rejects two operators in one field object — separate bounds, joined by $and.
|
|
208
|
+
if (min !== undefined)
|
|
209
|
+
conditions.push({ "actualPriceRange.minValue.amount": { $gte: String(min) } });
|
|
210
|
+
if (max !== undefined)
|
|
211
|
+
conditions.push({ "actualPriceRange.minValue.amount": { $lte: String(max) } });
|
|
212
|
+
if (inStockOnly) conditions.push({ "inventory.availabilityStatus": { $eq: "IN_STOCK" } });
|
|
213
|
+
query = {
|
|
214
|
+
...query,
|
|
215
|
+
filter: { $and: conditions },
|
|
216
|
+
...(SORT_FIELDS[sort] ? { sort: SORT_FIELDS[sort] } : {}),
|
|
217
|
+
...(search.trim() ? { search: { expression: search.trim(), fields: ["name"] } } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const res: RawProduct = await products.searchProducts(query, { fields: LIST_FIELDS as any });
|
|
221
|
+
return {
|
|
222
|
+
products: (res.products ?? []).map((p: RawProduct) => toSummary(p)),
|
|
223
|
+
nextCursor: res.pagingMetadata?.cursors?.next ?? null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** First page of visible products in the default order — a thin wrap of searchCatalog. */
|
|
228
|
+
export async function fetchProducts({ limit = 24 } = {}): Promise<ProductSummary[]> {
|
|
229
|
+
return (await searchCatalog({ limit })).products;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** First page of a category — same wrap; category filtering only works through search. */
|
|
151
233
|
export async function fetchProductsByCategory(
|
|
152
234
|
categoryId: string,
|
|
153
|
-
{ limit =
|
|
235
|
+
{ limit = 24 } = {},
|
|
154
236
|
): Promise<ProductSummary[]> {
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
filter: { "allCategoriesInfo.categories": { $matchItems: [{ id: categoryId }] } },
|
|
158
|
-
cursorPaging: { limit },
|
|
159
|
-
},
|
|
160
|
-
{ fields: LIST_FIELDS as any },
|
|
161
|
-
);
|
|
162
|
-
return (res.products ?? []).map((p: RawProduct) => toSummary(p));
|
|
237
|
+
return (await searchCatalog({ limit, categoryId })).products;
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
/** Fetch one product by its URL slug, with options/modifiers/variants. Null when not found. */
|
|
@@ -31,7 +31,8 @@ node <SKILL_ROOT>/references/storefront/seed/seed-store.mjs plan.json
|
|
|
31
31
|
{ "name": "Backstage Guide", "description": "…", "price": 12,
|
|
32
32
|
"digitalFilePath": "/Users/me/guide.pdf" }
|
|
33
33
|
],
|
|
34
|
-
"categories": { "Legends": ["The Glam Rocker"], "Rising Stars": [] }
|
|
34
|
+
"categories": { "Legends": ["The Glam Rocker"], "Rising Stars": [] },
|
|
35
|
+
"currency": "EUR"
|
|
35
36
|
}
|
|
36
37
|
```
|
|
37
38
|
|
|
@@ -53,8 +54,20 @@ node <SKILL_ROOT>/references/storefront/seed/seed-store.mjs plan.json
|
|
|
53
54
|
- `digitalFilePath` (a file on this machine) or `digitalFileUrl` — makes the product a digital
|
|
54
55
|
download, uploaded and created with both the file and stock (`quantity` is ignored). It's also the
|
|
55
56
|
only way in: a file-less digital product is created successfully, reads back healthy, and is then
|
|
56
|
-
rejected at add-to-cart as `ITEM_NOT_FOUND_IN_CATALOG`.
|
|
57
|
+
rejected at add-to-cart as `ITEM_NOT_FOUND_IN_CATALOG`. **No real file in hand?** Don't invent a
|
|
58
|
+
URL and don't ship the product as digital — seed it physical with stock (drop
|
|
59
|
+
`digitalFilePath`/`digitalFileUrl`, add `inStock` or a `quantity`) and tell the user the
|
|
60
|
+
download needs a real file before it can be sold.
|
|
57
61
|
- `categories` — category name → product NAMES. Omit when the brief names none.
|
|
62
|
+
- `quantity` — tracked stock, a non-negative integer. For stock that isn't counted (made to
|
|
63
|
+
order, print on demand, unlimited) use `"inStock": true` **instead** of `quantity`; sending
|
|
64
|
+
both is rejected.
|
|
65
|
+
- `currency` — 3-letter ISO code. Set it **only when the brief names one** ("prices in euros",
|
|
66
|
+
"a German store charging EUR"). Do **not** infer it from a language, a country, or an address
|
|
67
|
+
— an unrequested switch silently reprices the whole catalog. The seed applies it before
|
|
68
|
+
creating anything, because a product's price is stored in the site currency at create time.
|
|
69
|
+
For a few seconds afterwards product reads can still report the old currency; that lag is
|
|
70
|
+
expected and self-resolves, so don't re-verify it or retry.
|
|
58
71
|
|
|
59
72
|
**Default to 3 products** unless the brief asks for a specific catalog — the seed shows the
|
|
60
73
|
shape, not a full inventory; the owner adds the rest in the dashboard. **Make those 3 exercise
|
|
@@ -64,6 +77,12 @@ truthfully to the business (a ceramics studio has glaze colors; a bakery doesn't
|
|
|
64
77
|
**Seeding is additive — never delete or overwrite existing content.** No cleanup, no removing
|
|
65
78
|
"sample" data, no resets. If a cleanup genuinely seems needed, ask the user first.
|
|
66
79
|
|
|
80
|
+
**A bulk create can partially succeed.** The result carries `failures: [{ name, error }]` next
|
|
81
|
+
to `products` — read it. A non-empty `failures` means those products are genuinely absent, not
|
|
82
|
+
mis-mapped, so the rest of the catalog is fine to build on. To retry, re-run the **same** plan:
|
|
83
|
+
creation is idempotent by name, so products that already exist are skipped rather than
|
|
84
|
+
duplicated. Never hand-patch ids to "fill the gap".
|
|
85
|
+
|
|
67
86
|
Two things this module does not seed (dashboard-only — tell the merchant):
|
|
68
87
|
**ribbons** ("New", "Best Seller") and **per-choice linked media** (color choice → gallery photo).
|
|
69
88
|
|
|
@@ -162,7 +162,7 @@ function buildOptions(options = []) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// Full Cartesian product, each variant priced/stocked from the product; visible:true baked in.
|
|
165
|
-
function expandVariants(options = [], { price, compareAtPrice, quantity }, digitalFileId) {
|
|
165
|
+
function expandVariants(options = [], { price, compareAtPrice, quantity, inStock }, digitalFileId) {
|
|
166
166
|
const base = {
|
|
167
167
|
price: {
|
|
168
168
|
actualPrice: { amount: String(price) },
|
|
@@ -171,7 +171,10 @@ function expandVariants(options = [], { price, compareAtPrice, quantity }, digit
|
|
|
171
171
|
visible: true,
|
|
172
172
|
...(digitalFileId
|
|
173
173
|
? { digitalProperties: { digitalFile: { id: digitalFileId } }, inventoryItem: { inStock: true } }
|
|
174
|
-
|
|
174
|
+
// inStock:true == untracked stock — always buyable, no count. Otherwise track a quantity.
|
|
175
|
+
: { physicalProperties: {}, inventoryItem: inStock === true
|
|
176
|
+
? { inStock: true }
|
|
177
|
+
: { quantity: quantity ?? 0, preorderInfo: { enabled: false } } }),
|
|
175
178
|
};
|
|
176
179
|
if (!options.length) return [base];
|
|
177
180
|
let combos = [[]];
|
|
@@ -235,7 +238,10 @@ async function uploadDigitalFile(ctx, { digitalFileUrl, digitalFilePath, digital
|
|
|
235
238
|
const bytes = digitalFilePath
|
|
236
239
|
? readFileSync(digitalFilePath)
|
|
237
240
|
: await fetch(digitalFileUrl).then((r) => {
|
|
238
|
-
|
|
241
|
+
// Don't invent a file and don't ship an unbuyable DIGITAL product: seed it PHYSICAL
|
|
242
|
+
// with stock (drop digitalFileUrl/digitalFilePath, set inStock or a quantity) and
|
|
243
|
+
// tell the user the download still needs a real file.
|
|
244
|
+
if (!r.ok) throw new Error(`digitalFileUrl ${digitalFileUrl} -> ${r.status}. No fetchable file: re-seed this product as PHYSICAL with stock and tell the user it needs a real file before it can be sold as a download.`);
|
|
239
245
|
return r.arrayBuffer();
|
|
240
246
|
});
|
|
241
247
|
const { uploadUrl } = await req(ctx, "/site-media/v1/files/generate-upload-url", { body: { mimeType, fileName } });
|
|
@@ -266,15 +272,36 @@ export async function bulkCreateProducts(ctx, products) {
|
|
|
266
272
|
};
|
|
267
273
|
const r = await req(ctx, "/stores/v3/bulk/products-with-inventory/create", { body });
|
|
268
274
|
// NB: results nest under productResults.results[].item — NOT a top-level `results`.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
// The bulk returns 200 even on PARTIAL failure, so never map results by array position:
|
|
276
|
+
// pair each result to its input via itemMetadata.originalIndex and drop the ones that
|
|
277
|
+
// didn't persist. Positional mapping shifts every id after a failure onto the wrong
|
|
278
|
+
// product — which then mislabels categories and attaches images to the wrong items.
|
|
279
|
+
const created = [];
|
|
280
|
+
const failures = [];
|
|
281
|
+
for (const x of r.productResults?.results ?? []) {
|
|
282
|
+
const i = x.itemMetadata?.originalIndex;
|
|
283
|
+
const src = typeof i === "number" ? products[i] : undefined;
|
|
284
|
+
if (!x.itemMetadata?.success || !x.item?.id) {
|
|
285
|
+
failures.push({
|
|
286
|
+
name: src?.name,
|
|
287
|
+
error: x.itemMetadata?.error?.description ?? x.itemMetadata?.error?.code ?? "unknown",
|
|
288
|
+
});
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
created.push({
|
|
292
|
+
id: x.item.id, slug: x.item.slug, revision: x.item.revision, name: src?.name,
|
|
293
|
+
variantId: x.item.variantsInfo?.variants?.[0]?.id,
|
|
294
|
+
hasOptions: (src?.options?.length ?? 0) > 0,
|
|
295
|
+
isDigital: !!fileIds[i],
|
|
296
|
+
quantity: src?.quantity ?? 0,
|
|
297
|
+
inStock: src?.inStock,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
276
300
|
await stockOptionlessProducts(ctx, created);
|
|
277
|
-
return
|
|
301
|
+
return {
|
|
302
|
+
created: created.map((p) => ({ id: p.id, slug: p.slug, revision: p.revision, name: p.name })),
|
|
303
|
+
failures,
|
|
304
|
+
};
|
|
278
305
|
}
|
|
279
306
|
|
|
280
307
|
// The bulk create stocks a variant via its choices; an OPTION-LESS product's single default
|
|
@@ -290,9 +317,15 @@ async function stockOptionlessProducts(ctx, created) {
|
|
|
290
317
|
const vById = new Map((q.products ?? []).map((p) => [p.id, p.variantsInfo?.variants?.[0]?.id]));
|
|
291
318
|
need.forEach((p) => { if (!p.variantId) p.variantId = vById.get(p.id); });
|
|
292
319
|
}
|
|
320
|
+
// inStock:true == untracked stock (always buyable, no count). Only send a quantity when the
|
|
321
|
+
// product actually tracks one, or Wix rejects the pair.
|
|
293
322
|
const inventoryItems = need
|
|
294
323
|
.filter((p) => p.variantId)
|
|
295
|
-
.map((p) => ({
|
|
324
|
+
.map((p) => ({
|
|
325
|
+
productId: p.id,
|
|
326
|
+
variantId: p.variantId,
|
|
327
|
+
...(p.inStock === true ? { inStock: true } : { quantity: p.quantity }),
|
|
328
|
+
}));
|
|
296
329
|
if (inventoryItems.length) {
|
|
297
330
|
await req(ctx, "/stores/v3/bulk/inventory-items/create", { body: { inventoryItems } });
|
|
298
331
|
}
|
|
@@ -342,20 +375,68 @@ export async function attachProductImages(ctx, items) {
|
|
|
342
375
|
});
|
|
343
376
|
}
|
|
344
377
|
|
|
378
|
+
// Reject plans the API would reject halfway through, while nothing has been created yet —
|
|
379
|
+
// a mid-batch 400 leaves a half-seeded store that the agent then has to reason about.
|
|
380
|
+
export function validateProducts(products) {
|
|
381
|
+
const problems = [];
|
|
382
|
+
const colorByName = new Map();
|
|
383
|
+
products.forEach((p, i) => {
|
|
384
|
+
const where = p.name ? `"${p.name}"` : `product #${i + 1}`;
|
|
385
|
+
if (!p.name) problems.push(`${where}: name is required`);
|
|
386
|
+
if (p.quantity != null && (!Number.isInteger(p.quantity) || p.quantity < 0)) {
|
|
387
|
+
problems.push(`${where}: quantity must be a non-negative integer (got ${p.quantity}) — omit it and set inStock:true for untracked stock`);
|
|
388
|
+
}
|
|
389
|
+
for (const opt of p.options ?? []) {
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
for (const c of opt.choices ?? []) {
|
|
392
|
+
const key = typeof c === "string" ? c : c?.name;
|
|
393
|
+
if (seen.has(key)) problems.push(`${where}: option "${opt.name}" repeats the choice "${key}"`);
|
|
394
|
+
seen.add(key);
|
|
395
|
+
// Wix keys a color choice by name, so the same name with two codes collides.
|
|
396
|
+
const code = typeof c === "object" ? c?.colorCode : undefined;
|
|
397
|
+
if (code) {
|
|
398
|
+
const prev = colorByName.get(key);
|
|
399
|
+
if (prev && prev !== code) problems.push(`color "${key}" is ${prev} on one product and ${code} on another — pick one`);
|
|
400
|
+
colorByName.set(key, code);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (problems.length) throw new Error(`invalid seed plan:\n - ${problems.join("\n - ")}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Site currency, set BEFORE any product exists (see setupStore). Existing product reads can
|
|
409
|
+
// keep reporting the old currency for a short while after this returns — that lag is expected
|
|
410
|
+
// and self-resolves, so don't re-verify or retry on it.
|
|
411
|
+
// docs: https://dev.wix.com/docs/rest/business-management/site-properties/properties/update-site-properties
|
|
412
|
+
async function setSiteCurrency(ctx, currency) {
|
|
413
|
+
await req(ctx, "/site-properties/v4/properties", {
|
|
414
|
+
method: "PATCH",
|
|
415
|
+
body: { properties: { paymentCurrency: currency }, fields: ["paymentCurrency"] },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
345
419
|
/**
|
|
346
|
-
* ONE-CALL seed: install → create products → categories → attach images, ids
|
|
347
|
-
* memory. This is the default path — call it once instead of the individual
|
|
420
|
+
* ONE-CALL seed: install → currency → create products → categories → attach images, ids
|
|
421
|
+
* threaded in memory. This is the default path — call it once instead of the individual
|
|
422
|
+
* functions.
|
|
348
423
|
*/
|
|
349
|
-
export async function setupStore(ctx, { products = [], categories = {} } = {}) {
|
|
424
|
+
export async function setupStore(ctx, { products = [], categories = {}, currency } = {}) {
|
|
425
|
+
validateProducts(products);
|
|
350
426
|
await installStoresApp(ctx);
|
|
427
|
+
// Before any product exists: a product's price is stored in the site currency at create time,
|
|
428
|
+
// so switching afterwards leaves the catalog priced in the old one.
|
|
429
|
+
if (currency) await setSiteCurrency(ctx, currency);
|
|
351
430
|
|
|
352
431
|
// Idempotent by name: an errored bulk create (429/5xx) may still have applied server-side,
|
|
353
432
|
// and SKILL.md tells the agent to re-run a failed seed — creating only the names that don't
|
|
354
433
|
// exist yet makes that rerun safe instead of a duplicator.
|
|
355
434
|
const existing = await queryProductsByNames(ctx, products.map((p) => p.name));
|
|
356
435
|
const toCreate = products.filter((p) => !existing.has(p.name));
|
|
357
|
-
const created = toCreate.length
|
|
358
|
-
|
|
436
|
+
const { created, failures } = toCreate.length
|
|
437
|
+
? await bulkCreateProducts(ctx, toCreate)
|
|
438
|
+
: { created: [], failures: [] };
|
|
439
|
+
const createdByName = new Map(created.map((p) => [p.name, p]));
|
|
359
440
|
const withNames = products.map((p) => {
|
|
360
441
|
const hit = createdByName.get(p.name) ?? existing.get(p.name);
|
|
361
442
|
return { ...(hit ?? {}), name: p.name };
|
|
@@ -381,6 +462,8 @@ export async function setupStore(ctx, { products = [], categories = {} } = {}) {
|
|
|
381
462
|
prompt: products[i]?.imagePrompt,
|
|
382
463
|
displayName: `${p.slug || "product"}.png`,
|
|
383
464
|
})));
|
|
465
|
+
// `p.id` guards this: a product that failed to create has no id, and bulk-updating an
|
|
466
|
+
// undefined id would 400 the whole batch and cost every other product its image.
|
|
384
467
|
const imageItems = withNames
|
|
385
468
|
.map((p, i) => (files[i] && p.id ? { id: p.id, url: files[i].url, altText: products[i]?.altText ?? p.slug } : null))
|
|
386
469
|
.filter(Boolean);
|
|
@@ -392,7 +475,10 @@ export async function setupStore(ctx, { products = [], categories = {} } = {}) {
|
|
|
392
475
|
/* never block on image failure — the products stay text-only */
|
|
393
476
|
}
|
|
394
477
|
|
|
395
|
-
|
|
478
|
+
// failures is part of the result, not an exception: a partial seed still leaves a usable
|
|
479
|
+
// store, and the agent needs the names to report rather than silently shipping a short
|
|
480
|
+
// catalog. Re-run the seed to retry them — existing names are skipped, not duplicated.
|
|
481
|
+
return { products: withNames, categories: cats, imagesAttached, failures };
|
|
396
482
|
}
|
|
397
483
|
|
|
398
484
|
// ---- CLI entry ----------------------------------------------------------------------------------
|
|
@@ -45,6 +45,9 @@ Read and update SEO titles and tags at the right level. For "change my site's SE
|
|
|
45
45
|
### [Generate and Read a Wix Site's Content Plan](references/seo/manage-content-plan.md)
|
|
46
46
|
Generate an SEO content plan and read its blog post topics, or troubleshoot an existing content plan flow stuck at KEYWORD_RESEARCH while polling GetContentPlanFlow. Use this recipe for both generation and stalled-flow questions: it explains the intentional pause, the Create Content Plan release request, missing flow IDs, and the exact public API paths and response fields.
|
|
47
47
|
|
|
48
|
+
### [Manage Google Search Console for a Wix Site](references/seo/manage-google-search-console.md)
|
|
49
|
+
"Connect a Wix site to Google Search Console and drive its setup through the public GSC Connection and Site Readiness APIs: check connection and readiness, start the Google authorization, verify ownership, add the property, submit the sitemap, request indexing, read search performance, run URL inspection, recover a stale connection, or disconnect. The site owner authorizes in their own browser via a single-use connect URL."
|
|
50
|
+
|
|
48
51
|
---
|
|
49
52
|
|
|
50
53
|
## Accessibility
|
|
@@ -77,7 +80,7 @@ Creates and publishes blog posts using Blog Posts API. Covers resolving the requ
|
|
|
77
80
|
## Bookings
|
|
78
81
|
|
|
79
82
|
### [Booking Service Policy Setup](references/bookings/booking-service-policy-setup.md)
|
|
80
|
-
Sets up booking policies, cancellation rules, and waitlist configuration using the
|
|
83
|
+
Sets up booking policies, cancellation rules, and waitlist configuration using the Booking Policies API — query for the (default) bookingPolicy entity, then PATCH it with its revision. Covers cancellationPolicy, reschedulePolicy, booking-notice limits, waitlistPolicy, and participants limits — e.g. "customers can cancel up to 24 hours before".
|
|
81
84
|
|
|
82
85
|
### [Booking System Integration Gaps](references/bookings/booking-system-integration-gaps.md)
|
|
83
86
|
Documents undocumented API patterns for booking payments. Covers Bookings→Ecommerce integration, booking ID transformation to catalog items, and async payment confirmation flows.
|
|
@@ -283,7 +286,7 @@ Creates payment links for unpaid bookings using Payment Links API. Links booking
|
|
|
283
286
|
|
|
284
287
|
## Google Ads
|
|
285
288
|
|
|
286
|
-
**Routing — Google paid-advertising campaigns for a site (Smart & Performance Max).** All flows require a Google Ads account, created once via the setup recipe. Budgets are in micros (1,000,000 = 1 currency unit). REST base: `https://www.wixapis.com/google
|
|
289
|
+
**Routing — Google paid-advertising campaigns for a site (Smart & Performance Max).** All flows require a Google Ads account, created once via the setup recipe. Budgets are in micros (1,000,000 = 1 currency unit). REST base: `https://www.wixapis.com/_serverless/pa-google/v1`.
|
|
287
290
|
- **First-time setup / "connect Google Ads" / `ACCOUNT_NOT_FOUND`** → [Install and Create an Account](references/google-ads/install-and-create-account.md) (do this before anything else).
|
|
288
291
|
- **Suggested keywords / geo / budget / ad copy / images** → [Get AI Campaign Suggestions](references/google-ads/get-campaign-suggestions.md).
|
|
289
292
|
- **Offer a Success Guide after creating a PMAX Leads campaign (a proactive offer requires approval) / improve it / success guide / says they completed or fixed a guide recommendation / mark or reopen a recommendation** → [Manage a Campaign Success Guide](references/google-ads/manage-campaign-success-guide.md) (a direct improvement/guide request is already approval; works while `LEARNING` and before metrics exist).
|
|
@@ -293,25 +296,25 @@ Creates payment links for unpaid bookings using Payment Links API. Links booking
|
|
|
293
296
|
- **Ad spend, fees, upcoming charges, credit balance** → [Retrieve Billing and Payment Details](references/google-ads/billing-and-payment.md).
|
|
294
297
|
|
|
295
298
|
### [Install Google Ads and Create an Account](references/google-ads/install-and-create-account.md)
|
|
296
|
-
"One-time setup for running Google paid ads on a Wix site: install the Wix Google Ads app, then create the Google Ads account that every campaign, suggestion, and analytics call depends on. Covers checking whether an account already exists, choosing a currency, optionally attaching a promotional incentive (credit offer), linking a Google Merchant Center account, and deleting an account. Use when the user wants to 'set up Google Ads', 'connect Google Ads', 'start advertising on Google', 'create a Google Ads account', or hits an ACCOUNT_NOT_FOUND / app-not-installed error before creating a campaign. Google Ads REST API, base https://www.wixapis.com/google
|
|
299
|
+
"One-time setup for running Google paid ads on a Wix site: install the Wix Google Ads app, then create the Google Ads account that every campaign, suggestion, and analytics call depends on. Covers checking whether an account already exists, choosing a currency, optionally attaching a promotional incentive (credit offer), linking a Google Merchant Center account, and deleting an account. Use when the user wants to 'set up Google Ads', 'connect Google Ads', 'start advertising on Google', 'create a Google Ads account', or hits an ACCOUNT_NOT_FOUND / app-not-installed error before creating a campaign. Google Ads REST API, base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
297
300
|
|
|
298
301
|
### [Get AI Campaign Suggestions for Google Ads](references/google-ads/get-campaign-suggestions.md)
|
|
299
|
-
"Reference for the Google Ads Suggestions API on a Wix site: AI/Google-generated inputs that help build effective campaigns — keyword themes (from a URL or autocomplete), geo-target options, low/recommended/high daily-budget tiers with estimated clicks, PMAX budget recommendations, text assets (headlines/descriptions), AI image assets (auto-uploaded to Wix Media), search themes, promotional incentive offers, and complete AI-generated campaign configurations from a campaign brief. Use when the user asks 'suggest keywords for my ads', 'what budget should I use', 'where should I target', 'generate ad copy/headlines', 'generate ad images', 'suggest a whole campaign', or when a create-campaign flow needs suggested values. REST base https://www.wixapis.com/google
|
|
302
|
+
"Reference for the Google Ads Suggestions API on a Wix site: AI/Google-generated inputs that help build effective campaigns — keyword themes (from a URL or autocomplete), geo-target options, low/recommended/high daily-budget tiers with estimated clicks, PMAX budget recommendations, text assets (headlines/descriptions), AI image assets (auto-uploaded to Wix Media), search themes, promotional incentive offers, and complete AI-generated campaign configurations from a campaign brief. Use when the user asks 'suggest keywords for my ads', 'what budget should I use', 'where should I target', 'generate ad copy/headlines', 'generate ad images', 'suggest a whole campaign', or when a create-campaign flow needs suggested values. REST base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
300
303
|
|
|
301
304
|
### [Create and Launch a Performance Max Campaign](references/google-ads/create-performance-max-campaign.md)
|
|
302
|
-
"Creates and launches a Google Ads Performance Max (PMAX) campaign for a Wix site — a goal-based campaign that runs across all Google channels (Search, Display, YouTube, Gmail, Discover, Maps) from an asset group of headlines, descriptions, images, and (for PMAX Leads) search-theme signals. Covers generating AI text and image assets, generating search themes, getting a Google budget recommendation, assembling the asset group with the required minimum assets, choosing PERFORMANCE_MAX vs PERFORMANCE_MAX_LEADS (leads: phone/form goals, negative keywords, 28-day learning) vs retail/Shopping (Merchant Center feed), creating in PAUSED, and launching. Use for 'create a Performance Max campaign', 'PMAX', 'run ads across all of Google', 'lead-gen Google campaign', or 'Google Shopping ads'. Requires an existing Google Ads account. REST base https://www.wixapis.com/google
|
|
305
|
+
"Creates and launches a Google Ads Performance Max (PMAX) campaign for a Wix site — a goal-based campaign that runs across all Google channels (Search, Display, YouTube, Gmail, Discover, Maps) from an asset group of headlines, descriptions, images, and (for PMAX Leads) search-theme signals. Covers generating AI text and image assets, generating search themes, getting a Google budget recommendation, assembling the asset group with the required minimum assets, choosing PERFORMANCE_MAX vs PERFORMANCE_MAX_LEADS (leads: phone/form goals, negative keywords, 28-day learning) vs retail/Shopping (Merchant Center feed), creating in PAUSED, and launching. Use for 'create a Performance Max campaign', 'PMAX', 'run ads across all of Google', 'lead-gen Google campaign', or 'Google Shopping ads'. Requires an existing Google Ads account. REST base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
303
306
|
|
|
304
307
|
### [Manage Campaign Lifecycle](references/google-ads/manage-campaign-lifecycle.md)
|
|
305
|
-
"Manages existing Google Ads campaigns on a Wix site: list/get, launch (first activation) vs resume (reactivate after a pause), pause a running campaign — optionally with a scheduled auto-resume date — update name/budget/targeting, change the daily budget, delete permanently, and read status history / change log. Use when the user wants to 'pause my Google ad', 'resume my campaign', 'stop the campaign', 'change my daily budget', 'rename the campaign', 'delete this campaign', 'list my Google Ads campaigns', or 'why did my campaign status change'. Requires an existing Google Ads account and campaign. REST base https://www.wixapis.com/google
|
|
308
|
+
"Manages existing Google Ads campaigns on a Wix site: list/get, launch (first activation) vs resume (reactivate after a pause), pause a running campaign — optionally with a scheduled auto-resume date — update name/budget/targeting, change the daily budget, delete permanently, and read status history / change log. Use when the user wants to 'pause my Google ad', 'resume my campaign', 'stop the campaign', 'change my daily budget', 'rename the campaign', 'delete this campaign', 'list my Google Ads campaigns', or 'why did my campaign status change'. Requires an existing Google Ads account and campaign. REST base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
306
309
|
|
|
307
310
|
### [Manage a Campaign Success Guide](references/google-ads/manage-campaign-success-guide.md)
|
|
308
311
|
"Retrieves and manages the campaign success guide for an existing Wix Google Ads Performance Max Leads campaign. Offer it after creating a supported campaign, even while it is learning or has no metrics; that proactive offer requires approval before retrieval. A direct request to improve a campaign, see what to fix next, or show the guide is already approval to retrieve it. Also use when a user reports completing or wants to reopen a guide item. Treat a clear completion report as an actionable update request even when phrased as a statement. Covers campaign selection, prioritized guide retrieval, and suggestion status updates without redundant confirmation. REST base https://www.wixapis.com/pa-platform/suggestions/v1."
|
|
309
312
|
|
|
310
313
|
### [Query Campaign Performance Analytics](references/google-ads/query-campaign-analytics.md)
|
|
311
|
-
"Reads performance analytics for a Google Ads campaign on a Wix site: daily performance metrics (impressions, clicks, CTR, cost, leads, phone calls) with optional previous-period comparison and trends; conversion metrics from Wix Analytics (orders, revenue, leads, CPL, ROAS); the search terms that triggered a campaign's ads; per-product shopping performance for retail campaigns; and per-asset performance (headlines, descriptions, images) for PMAX Leads. Explains when to use campaignResourceName vs the Wix campaignId, the dateRange shape, field enums, sorting, and paging. Use when the user asks 'how is my campaign doing', 'show ad performance', 'what search terms triggered my ads', 'which products/assets perform best', 'campaign ROI/ROAS', or 'conversions from my Google ads'. REST base https://www.wixapis.com/google
|
|
314
|
+
"Reads performance analytics for a Google Ads campaign on a Wix site: daily performance metrics (impressions, clicks, CTR, cost, leads, phone calls) with optional previous-period comparison and trends; conversion metrics from Wix Analytics (orders, revenue, leads, CPL, ROAS); the search terms that triggered a campaign's ads; per-product shopping performance for retail campaigns; and per-asset performance (headlines, descriptions, images) for PMAX Leads. Explains when to use campaignResourceName vs the Wix campaignId, the dateRange shape, field enums, sorting, and paging. Use when the user asks 'how is my campaign doing', 'show ad performance', 'what search terms triggered my ads', 'which products/assets perform best', 'campaign ROI/ROAS', or 'conversions from my Google ads'. REST base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
312
315
|
|
|
313
316
|
### [Retrieve Google Ads Billing and Payment Details](references/google-ads/billing-and-payment.md)
|
|
314
|
-
"Retrieves billing and payment details for a Wix site's Google Ads account: the current billing period's ad spend (usage), the Wix service fee, the total charge, any promotional coupon adjustment, the billing period dates, and the account's credit balance (positive = available credits, negative = outstanding debt not yet charged). Also explains reading current vs remaining budget from the account object. Use when the user asks 'how much have I spent on Google Ads', 'what's my next Google Ads charge', 'show my ad billing', 'do I have ad credits left', 'why was I charged', or 'upcoming Google Ads payment'. Requires an existing Google Ads account. REST base https://www.wixapis.com/google
|
|
317
|
+
"Retrieves billing and payment details for a Wix site's Google Ads account: the current billing period's ad spend (usage), the Wix service fee, the total charge, any promotional coupon adjustment, the billing period dates, and the account's credit balance (positive = available credits, negative = outstanding debt not yet charged). Also explains reading current vs remaining budget from the account object. Use when the user asks 'how much have I spent on Google Ads', 'what's my next Google Ads charge', 'show my ad billing', 'do I have ad credits left', 'why was I charged', or 'upcoming Google Ads payment'. Requires an existing Google Ads account. REST base https://www.wixapis.com/_serverless/pa-google/v1."
|
|
315
318
|
|
|
316
319
|
### [Google Ads Dashboard Navigation](references/google-ads/google-ads-dashboard-navigation.md)
|
|
317
320
|
"Builds a direct link to the Wix Google Ads dashboard page on manage.wix.com, where campaigns created via the Google Ads API recipes are managed. Use when the user asks where something is in the Wix dashboard, wants a direct link to a dashboard page, or you need a dashboard URL to include with the result of an API operation."
|
|
@@ -421,6 +424,9 @@ Drive the Wix Site Import agent to migrate an existing store or site from anothe
|
|
|
421
424
|
### [Sites Dashboard Navigation](references/sites/sites-dashboard-navigation.md)
|
|
422
425
|
"Builds direct links to the account-level sites pages on manage.wix.com — the My Sites list (all sites in the account) and each site's own dashboard. Pairs the site list with the Query Sites read API. Use when the user asks where something is in the Wix dashboard, wants a direct link to a dashboard page, or you need a dashboard URL to include with the result of an API operation."
|
|
423
426
|
|
|
427
|
+
### [Upload a Website or HTML Files](references/sites/upload-static-site.md)
|
|
428
|
+
Get a user's ready-made website live on Wix when they already have the site files — an index.html, a zip, or a static build exported from an AI builder or any other tool — by sending them to the Wix Headless drop page, where they upload the files and instantly get a live hosted site they can claim into their account. Use whenever the user wants to upload, publish, or host their own HTML/CSS/JS files as a NEW site. Not for migrating a live store/site from another platform by URL or from CSV exports (use Site Import), not for adding HTML or custom code into an existing Wix site, and not for uploading images or documents to a site's media files.
|
|
429
|
+
|
|
424
430
|
---
|
|
425
431
|
|
|
426
432
|
## Stores
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Booking Service Policy Setup"
|
|
3
|
-
description: Sets up booking policies, cancellation rules, and waitlist configuration using the
|
|
3
|
+
description: Sets up booking policies, cancellation rules, and waitlist configuration using the Booking Policies API (query, then PATCH the bookingPolicy entity with its revision). Covers cancellationPolicy, reschedulePolicy, booking-notice limits, waitlistPolicy, and participants limits — e.g. "customers can cancel up to 24 hours before".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Technical Step-by-Step Instructions: Configuring Wix Bookings Service Policies (Real-World, API-First)
|
|
@@ -20,72 +20,98 @@ Wix Bookings policy configuration allows businesses to set rules for:
|
|
|
20
20
|
- **Waitlist policies**: When waitlists are enabled, capacity handling
|
|
21
21
|
- **Group booking policies**: Maximum participants per booking
|
|
22
22
|
|
|
23
|
-
Policies
|
|
23
|
+
Policies are **standalone entities** managed by the Booking Policies API (`/bookings/v1/booking-policies`), not fields nested on the service:
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
25
|
+
- Installing Wix Bookings creates a **"Default policy"** (`default: true`) that every new service references.
|
|
26
|
+
- Each service points at exactly one policy via its `bookingPolicy` field; many services can share a policy.
|
|
27
|
+
- To change rules site-wide, update the default policy. To give one service different rules, create a separate policy and reference it from that service.
|
|
27
28
|
|
|
28
29
|
### IMPORTANT NOTES
|
|
29
30
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- Policy options
|
|
33
|
-
- All policy configurations support the same core features across service types
|
|
31
|
+
- A default policy exists on any site with Wix Bookings installed — **even before any service exists**. "Set a cancellation window" is always actionable by updating the default policy.
|
|
32
|
+
- Policy updates are PATCH-style: send only the sub-policies you want to change, plus the current `revision` (fetched via query/get) inside the `bookingPolicy` object.
|
|
33
|
+
- Policy options are the same across service types (APPOINTMENT, CLASS, COURSE).
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
## Steps
|
|
38
38
|
|
|
39
|
-
### 1.
|
|
39
|
+
### 1. Find the Policy to Change
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Query existing policies (`POST https://www.wixapis.com/bookings/v1/booking-policies/query`):
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
```json
|
|
44
|
+
{ "query": { "cursorPaging": { "limit": 100 } } }
|
|
45
|
+
```
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
For a site-wide change, pick the policy with `"default": true`. For a specific service, read the service first — its embedded `bookingPolicy.id` names the policy it uses. Save the policy's `id` and `revision`.
|
|
46
48
|
|
|
47
|
-
###
|
|
49
|
+
### 2. Update the Policy
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
`PATCH https://www.wixapis.com/bookings/v1/booking-policies/<POLICY_ID>` with the current `revision` inside the `bookingPolicy` object and only the sub-policies you're changing. Example — cancellations allowed up to 24 hours before the session:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"bookingPolicy": {
|
|
56
|
+
"id": "<POLICY_ID>",
|
|
57
|
+
"revision": "<CURRENT_REVISION>",
|
|
58
|
+
"cancellationPolicy": {
|
|
59
|
+
"enabled": true,
|
|
60
|
+
"limitLatestCancellation": true,
|
|
61
|
+
"latestCancellationInMinutes": 1440
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Field map (business rule → policy field):**
|
|
68
|
+
|
|
69
|
+
| Business rule | Sub-policy fields |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Cancellation deadline | `cancellationPolicy.{enabled, limitLatestCancellation, latestCancellationInMinutes}` |
|
|
72
|
+
| Reschedule deadline | `reschedulePolicy.{enabled, limitLatestReschedule, latestRescheduleInMinutes}` |
|
|
73
|
+
| How far ahead customers can book | `limitEarlyBookingPolicy.{enabled, earliestBookingInMinutes}` |
|
|
74
|
+
| Booking notice / last-minute cutoff | `limitLateBookingPolicy.{enabled, latestBookingInMinutes}` |
|
|
75
|
+
| Booking after session start (courses/classes) | `bookAfterStartPolicy.enabled` |
|
|
76
|
+
| Waitlist | `waitlistPolicy.{enabled, capacity, reservationTimeInMinutes}` |
|
|
77
|
+
| Participants per booking | `participantsPolicy.{enabled, maxParticipantsPerBooking}` |
|
|
78
|
+
|
|
79
|
+
### 3. Per-Service Policies (Optional)
|
|
80
|
+
|
|
81
|
+
To give one service different rules, create a new policy (`POST https://www.wixapis.com/bookings/v1/booking-policies`) and point the service at it by updating the service's `bookingPolicy` reference (see [Update Service](https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/update-service)).
|
|
50
82
|
|
|
51
83
|
### 4. Verify Policy Application
|
|
52
84
|
|
|
53
|
-
|
|
85
|
+
Read the service (`GET /bookings/v2/services/<SERVICE_ID>`) — the response embeds the full resolved `bookingPolicy`, so you can confirm the effective rules exactly as customers will experience them.
|
|
54
86
|
|
|
55
87
|
### IMPORTANT NOTES
|
|
56
88
|
|
|
57
|
-
- **
|
|
58
|
-
- **Partial updates**:
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **Time calculations**: Policy deadlines are calculated from booking start time in business timezone
|
|
62
|
-
- **Course flexibility**: Course services support additional policy options like `bookUntilXMinutesAfterStart`
|
|
89
|
+
- **Revision required**: Every policy update needs the current `revision` inside `bookingPolicy`; fetch it first, or the PATCH fails.
|
|
90
|
+
- **Partial updates**: Send only the sub-policies you're changing; the rest keep their values.
|
|
91
|
+
- **Enabled flags matter**: Each sub-policy has an `enabled` (and often a `limit*`) boolean — setting only the minutes value without enabling the limit has no effect.
|
|
92
|
+
- **Time calculations**: Policy deadlines are calculated from the booking's start time, in minutes (24 hours = 1440).
|
|
63
93
|
|
|
64
94
|
### Troubleshooting Common Issues
|
|
65
95
|
|
|
66
96
|
**Policies not applying:**
|
|
67
97
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
98
|
+
- Policies are separate entities — verify you PATCHed `/bookings/v1/booking-policies/<id>`, not fields on the service object (there is no `service.policy` field)
|
|
99
|
+
- Confirm the service actually references the policy you changed (`service.bookingPolicy.id`)
|
|
100
|
+
- Check the `revision` was current — a stale revision fails the update
|
|
71
101
|
|
|
72
102
|
**Waitlist not working:**
|
|
73
103
|
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
- Check that `waitingListPolicy.capacity` is set if you want limited waitlist size
|
|
104
|
+
- Verify `waitlistPolicy.enabled` is `true` and `waitlistPolicy.capacity` is set
|
|
105
|
+
- Confirm the service has capacity configured (`defaultCapacity` for CLASS/COURSE)
|
|
77
106
|
|
|
78
107
|
**Cancellation policies not enforced:**
|
|
79
108
|
|
|
80
|
-
-
|
|
81
|
-
- Verify `cancelationAllowed` is `true` if cancellations should be permitted
|
|
82
|
-
- Check that payment and booking flow supports the configured policy
|
|
109
|
+
- Set all three fields together: `cancellationPolicy.enabled: true`, `limitLatestCancellation: true`, and `latestCancellationInMinutes` — the minutes value alone has no effect without the booleans
|
|
83
110
|
|
|
84
111
|
**Group booking limits not working:**
|
|
85
112
|
|
|
86
|
-
- Confirm `
|
|
87
|
-
-
|
|
88
|
-
- Check that booking UI respects the participant limit
|
|
113
|
+
- Confirm `participantsPolicy.enabled` is `true` and `participantsPolicy.maxParticipantsPerBooking` is set
|
|
114
|
+
- Check that the booking UI respects the participant limit
|
|
89
115
|
|
|
90
116
|
## API Documentation References
|
|
91
117
|
|