Setting the file. One moment.
Seed Restaurants · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 16.730
Position 730 of 828
Type JavaScript
Size 28 KB
Lines 579 restaurants/seed/ seed-restaurants.mjs
JavaScript · 579 lines · 28 KB
14
// "items": [{ "name", "description"?, "price"? | "variants"?: [{ "name", "price" }],
15 // "modifierGroups"?: [{ "name", "required"?, "min"?, "max"?,
16 // "modifiers": [{ "name", "price"?, "preSelected"?, "inStock"? }] }],
17 // "acceptSpecialRequests"?, "imageMediaId"? | "imageUrl"? | "imagePath"? | "imagePrompt"? }] }] }],
18 // "ordering"?: true | { "address"? }, // menu-first add-on; address is STEP 0
19 // "reservations"?: true | { "partySize"? { "min","max" }, "address"? } }
20 //
21 // Seeding is ADDITIVE — nothing on the site is ever deleted, the Menus install's own sample
22 // "Dinner Menu" included: it is reported (`preexistingMenus`) and the owner removes it in the
23 // dashboard if they want to. Unexpected
24 // shapes → read the live API reference; every call below
25 // carries a docs: line with its reference page.
26 import { setSiteCurrency } from "../../shared/seed/site.mjs" ;
27 import { readFileSync } from "node:fs" ;
28 import { resolveItemImages } from "../../shared/seed/images.mjs" ;
29 import { seedSiteId } from "../../shared/seed/site-context.mjs" ;
30 import { wixToken } from "../../shared/seed/wix-cli.mjs" ;
31 import { isMain } from "../../shared/seed/main.mjs" ;
32
33 const API = "https://www.wixapis.com" ;
34 const MENUS_APP_ID = "b278a256-2757-4f19-9313-c05c783bec92" ;
35 const ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60" ;
36 const TABLE_RESERVATIONS_APP_ID = "f9c07de2-5341-40c6-b096-8eb39de391fb" ;
37
38 export function makeCtx ({ cwd = process. cwd () } = {}) {
39 // The content site: the config's site, or the parent on a migration preview (site-context.mjs stops
40 // a seed there unless --allow-parent is passed after the user confirmed).
41 const siteId = seedSiteId ({ cwd, argv: process.argv });
42 const token = wixToken (siteId, cwd);
43 return { token, siteId };
44 }
45
46 async function req ( ctx , path , { method = "POST" , body } = {}) {
47 const res = await fetch ( API + path, {
48 method,
49 headers: {
50 Authorization: `Bearer ${ ctx . token }` ,
51 "wix-site-id" : ctx.siteId,
52 "Content-Type" : "application/json" ,
53 },
54 body: body ? JSON . stringify (body) : undefined ,
55 });
56 const json = await res. json (). catch (() => ({}));
57 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
58 return json;
59 }
60
61 const sleep = ( ms ) => new Promise (( r ) => setTimeout (r, ms));
62 const money = ( n ) => String (n ?? 0 );
63
64 // ---- app installs --------------------------------------------------------------------------------
65
66 // docs: https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-apps-created-by-wix.md
67 async function installApp ( ctx , appDefId ) {
68 try {
69 await req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
70 tenant: { tenantType: "SITE" , id: ctx.siteId },
71 appInstance: { appDefId, enabled: true },
72 } });
73 } catch {
74 /* already installed is fine */
75 }
76 }
77 export async function installMenusApp ( ctx ) { return installApp (ctx, MENUS_APP_ID ); }
78 export async function installOrdersApp ( ctx ) { return installApp (ctx, ORDERS_APP_ID ); }
79 export async function installTableReservationsApp ( ctx ) { return installApp (ctx, TABLE_RESERVATIONS_APP_ID ); }
80
81 /** True when the Menus API answers — i.e. the app is already on the site. */
82 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/menus/list-menus.md
83 export async function menusAppPresent ( ctx ) {
84 try {
85 await req (ctx, "/restaurants/menus/v1/menus" , { method: "GET" });
86 return true ;
87 } catch {
88 return false ;
89 }
90 }
91
92 // ---- menu (setup-restaurants.md) -----------------------------------------------------------------
93 // Everything is Restaurants Menus V1 on /restaurants/menus/v1/... . REST flattens the
94 // protobuf wrappers: plain values ("visible": true), never {"value": …}.
95
96 /**
97 * What the Menus app already holds before this seed writes anything. A FRESH install ships a
98 * populated sample "Dinner Menu" (~4 sections, ~21 items) that renders next to the seeded menus;
99 * on an existing site these are the owner's menus. Reported, never touched: this seed deletes
100 * nothing on a site, ever. Polls briefly on a fresh install (the sample provisions async).
101 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/menus/list-menus.md
102 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/sections/list-sections.md
103 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/list-items.md
104 */
105 export async function readPreexistingMenus ( ctx , { tries = 1 , delayMs = 2000 } = {}) {
106 let menus = [];
107 for ( let i = 0 ; i < tries; i ++ ) {
108 const r = await req (ctx, "/restaurants/menus/v1/menus" , { method: "GET" });
109 menus = r.menus ?? [];
110 if (menus. length ) break ;
111 if (i < tries - 1 ) await sleep (delayMs);
112 }
113 if ( ! menus. length ) return [];
114 const itemsRes = await req (ctx, "/restaurants/menus/v1/items" , { method: "GET" });
115 const sectionsRes = await req (ctx, "/restaurants/menus/v1/sections" , { method: "GET" });
116 return menus. map (( m ) => ({
117 id: m.id,
118 name: m.name,
119 sections: (sectionsRes.sections ?? []). filter (( sec ) => (m.sectionIds ?? []). includes (sec.id)). length ,
120 items: (itemsRes.items ?? []). length ,
121 }));
122 }
123
124 // A bulk create's created entities, in input order (results[].item; the per-entry flag is results[].itemMetadata.success).
125 const bulkEntities = ( res ) => (res.results ?? []). map (( r ) => r.item ?? r.variant ?? r.modifier ?? r.modifierGroup ?? null );
126
127 /**
128 * Bulk-create the modifiers of the plan's groups (the reusable "Extra cheese" entities; the
129 * up-charge and pre-selection live on the group's reference, not here). Returns ids in input order.
130 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/item-modifiers/bulk-create-modifiers.md
131 */
132 export async function createModifiers ( ctx , modifiers ) {
133 if ( ! modifiers. length ) return [];
134 const r = await req (ctx, "/restaurants/menus/v1/bulk/modifiers/create" , {
135 body: { modifiers: modifiers. map (( m ) => ({ name: m.name, inStock: m.inStock !== false })), returnEntity: true },
136 });
137 return bulkEntities (r). map (( m ) => m?.id);
138 }
139
140 /**
141 * Bulk-create modifier groups: each carries its modifier references (id, preSelected, the
142 * up-charge as additionalChargeInfo.additionalCharge — a decimal string in the site currency) and
143 * its rule { required, minSelections, maxSelections }. Returns ids in input order.
144 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/item-modifier-groups/bulk-create-modifier-groups.md
145 */
146 export async function createModifierGroups ( ctx , groups ) {
147 if ( ! groups. length ) return [];
148 const r = await req (ctx, "/restaurants/menus/v1/bulk/modifier-groups/create" , {
149 body: {
150 modifierGroups: groups. map (( g ) => ({
151 name: g.name,
152 modifiers: g.modifiers. map (( m ) => ({
153 id: m.id,
154 preSelected: m.preSelected === true ,
155 additionalChargeInfo: { additionalCharge: money (m.price) },
156 })),
157 rule: {
158 required: g.required === true ,
159 minSelections: g.min ?? (g.required ? 1 : 0 ),
160 ... (g.max != null ? { maxSelections: g.max } : {}),
161 },
162 })),
163 returnEntity: true ,
164 },
165 });
166 return bulkEntities (r). map (( g ) => g?.id);
167 }
168
169 /**
170 * Bulk-create price variants (the reusable "Small" / "Large" names; the price per item lives on
171 * the item's priceVariants reference). Returns ids in input order.
172 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/item-variants/bulk-create-variants.md
173 */
174 export async function createVariants ( ctx , variants ) {
175 if ( ! variants. length ) return [];
176 const r = await req (ctx, "/restaurants/menus/v1/bulk/variants/create" , {
177 body: { variants: variants. map (( v ) => ({ name: v.name })), returnEntity: true },
178 });
179 return bulkEntities (r). map (( v ) => v?.id);
180 }
181
182 /**
183 * The writable fields of one item from its plan entry and the ids of its created variants/groups:
184 * flat `priceInfo` OR `priceVariants` (never both), the group references, and the order settings.
185 * Used by the create AND echoed by the image pass (Update Item is a full replace).
186 */
187 export function itemBody ( it , { variantIds = [], groupIds = [] } = {}) {
188 const pricing = it.variants?. length
189 ? { priceVariants: { variants: it.variants. map (( v , i ) => ({ variantId: variantIds[i], priceInfo: { price: money (v.price) } })). filter (( v ) => v.variantId) } }
190 : { priceInfo: { price: money (it.price) } };
191 return {
192 name: it.name,
193 ... (it.description ? { description: it.description } : {}),
194 ... pricing,
195 ... (groupIds. length ? { modifierGroups: groupIds. filter (Boolean). map (( id ) => ({ id })) } : {}),
196 orderSettings: { inStock: it.inStock !== false , acceptSpecialRequests: it.acceptSpecialRequests === true },
197 visible: true ,
198 };
199 }
200
201 /**
202 * Build ONE menu BOTTOM-UP in bulk phases: modifiers → modifier groups → variants → items →
203 * sections → menu. Prices are decimal STRINGS in the site currency (send none). visible:true is
204 * baked in at every level — required to render on the live site.
205 * Returns { menuId, name, sectionIds, itemIds, items: [{ id, revision, body }] } — the per-item
206 * revision and body feed the image pass (Update Item is a full replace).
207 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/bulk-create-items.md
208 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/sections/bulk-create-sections.md
209 * docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/menus/create-menu.md
210 */
211 export async function createMenu ( ctx , menu ) {
212 const flat = [];
213 menu.sections. forEach (( sec , si ) => (sec.items || []). forEach (( it ) => flat. push ({ ... it, _section: si })));
214
215 // STEP 1 — every modifier of every group of every item, ONE bulk call; then the groups.
216 const modifierPlan = [];
217 const groupPlan = [];
218 flat. forEach (( it , ii ) => (it.modifierGroups || []). forEach (( g , gi ) => {
219 groupPlan. push ({ ... g, _item: ii, _group: gi, _modifierIdx: (g.modifiers || []). map (( m ) => modifierPlan. push (m) - 1 ) });
220 }));
221 const modifierIds = await createModifiers (ctx, modifierPlan);
222 const groupIds = await createModifierGroups (ctx, groupPlan. map (( g ) => ({
223 ... g,
224 modifiers: g._modifierIdx. map (( mi ) => ({ ... modifierPlan[mi], id: modifierIds[mi] })). filter (( m ) => m.id),
225 })));
226 const groupIdsByItem = flat. map (() => []);
227 groupPlan. forEach (( g , i ) => groupIdsByItem[g._item]. push (groupIds[i]));
228
229 // STEP 2 — every variant of every variant-priced item, ONE bulk call.
230 const variantPlan = [];
231 const variantIdxByItem = flat. map (( it ) => (it.variants || []). map (( v ) => variantPlan. push (v) - 1 ));
232 const variantIds = await createVariants (ctx, variantPlan);
233
234 // STEP 3 — bulk-create every item across all sections in ONE request.
235 const bodies = flat. map (( it , i ) => itemBody (it, { variantIds: variantIdxByItem[i]. map (( vi ) => variantIds[vi]), groupIds: groupIdsByItem[i] }));
236 const itemRes = await req (ctx, "/restaurants/menus/v1/bulk/items/create" , { body: { items: bodies, returnEntity: true } });
237 const createdItems = bulkEntities (itemRes);
238 const itemIdsBySection = menu.sections. map (() => []);
239 flat. forEach (( it , i ) => {
240 const id = createdItems[i]?.id;
241 if (id) itemIdsBySection[it._section]. push (id);
242 });
243
244 // STEP 4 — bulk-create sections, each carrying the itemIds of its items in display order.
245 const secRes = await req (ctx, "/restaurants/menus/v1/bulk/sections/create" , {
246 body: {
247 sections: menu.sections. map (( sec , si ) => ({
248 name: sec.name,
249 ... (sec.description ? { description: sec.description } : {}),
250 visible: true ,
251 itemIds: itemIdsBySection[si],
252 })),
253 returnEntity: true ,
254 },
255 });
256 const sectionIds = bulkEntities (secRes). map (( s ) => s?.id);
257
258 // STEP 5 — create the menu (single create wraps in `menu`), carrying its sectionIds.
259 // businessLocationId omitted -> binds to the site's default (main) location.
260 const menuRes = await req (ctx, "/restaurants/menus/v1/menus" , {
261 body: {
262 menu: {
263 name: menu.name,
264 ... (menu.description ? { description: menu.description } : {}),
265 visible: true ,
266 sectionIds,
267 },
268 },
269 });
270 return {
271 menuId: menuRes.menu?.id,
272 name: menu.name,
273 sectionIds,
274 itemIds: createdItems. map (( it ) => it?.id),
275 items: createdItems. map (( it , i ) => ({ id: it?.id, revision: it?.revision, body: bodies[i] })),
276 };
277 }
278
279 // Restaurants binds an item image by Wix Media file ID — an external url must be imported
280 // first; a plan `imagePrompt` is generated (Wix AI, 1 credit) then imported. Both live in the
281 // shared util (parallel, resilient, never blocks the seed).
282 export { importImage } from "../../shared/seed/images.mjs" ;
283
284 // Image pass. Update Item is a FULL-ENTITY REPLACE with NO field mask — each entry MUST echo
285 // the item's current `revision` AND every field it was created with (its `body`: pricing, modifier
286 // groups, order settings), or the pricing fails 428 MISSING_ITEM_PRICING / the groups are wiped.
287 // `image` is an OBJECT { id, url, height, width } (never a bare string); the binding field is the
288 // Wix Media file `id`.
289 // items: [{ id, revision, body, image: { id, url, height, width } }]
290 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/bulk-update-item.md
291 // The bulk update returns 200 on PARTIAL failure: each item's outcome is in
292 // results[].itemMetadata (success, error, originalIndex). Returns { attached: [id], failures: [{ id, error }] }.
293 export async function attachItemImages ( ctx , items ) {
294 const r = await req (ctx, "/restaurants/menus/v1/bulk/items/update" , {
295 body: {
296 items: items. map (( it ) => ({
297 item: { ... (it.body ?? { priceInfo: { price: money (it.price) } }), id: it.id, revision: it.revision, image: it.image },
298 })),
299 },
300 });
301 return bulkOutcome (items, r.results);
302 }
303
304 // Pair a bulk response's results[] to the inputs by originalIndex; an input with no result did not persist.
305 export function bulkOutcome ( items , results ) {
306 const attached = [];
307 const failures = [];
308 for ( const x of results ?? []) {
309 const src = items[x.itemMetadata?.originalIndex];
310 if ( ! src) continue ;
311 if (x.itemMetadata?.success) attached. push (src.id);
312 else failures. push ({ id: src.id, error: x.itemMetadata?.error?.description ?? x.itemMetadata?.error?.code ?? "unknown" });
313 }
314 for ( const it of items) if ( ! attached. includes (it.id) && ! failures. some (( f ) => f.id === it.id)) failures. push ({ id: it.id, error: "no result for item" });
315 return { attached, failures };
316 }
317
318 // ---- business location (shared STEP 0 for ordering + reservations) ------------------------------
319 // Update Location is a FULL OVERRIDE — send the WHOLE `location` object (omitted fields are
320 // wiped), echo `default:true` (omitting it 400s CHANGE_DEFAULT_FORBIDDEN) and the current
321 // `revision`. address.country is a 2-letter ISO code. Without a real address, ordering is
322 // "testing only" and checkout breaks — a placeholder must be flagged to the owner.
323 // location: { name, timeZone, email?, phone?, address: { country, subdivision, city,
324 // postalCode, streetAddress: { number, name }, formattedAddress } }
325 // docs: https://dev.wix.com/docs/api-reference/business-management/locations/list-locations.md
326 // docs: https://dev.wix.com/docs/api-reference/business-management/locations/create-location.md
327 // docs: https://dev.wix.com/docs/api-reference/business-management/locations/update-location.md
328 export async function setBusinessLocation ( ctx , location ) {
329 const list = await req (ctx, "/locations/v1/locations" , { method: "GET" });
330 const def = (list.locations ?? []). find (( l ) => l.default);
331 if ( ! def) {
332 // No default location at all (a bare site can have none) -> CREATE one; operations
333 // auto-bind on first-location-add.
334 const r = await req (ctx, "/locations/v1/locations" , { body: { location: { ... location, default: true } } });
335 return r.location;
336 }
337 const r = await req (ctx, `/locations/v1/locations/${ def . id }` , {
338 method: "PUT" ,
339 body: { location: { ... location, id: def.id, revision: def.revision, default: true } },
340 });
341 return r.location;
342 }
343
344 // ---- online ordering (setup-restaurant-orders.md) ------------------------------------------------
345 // The Orders-app install AUTO-provisions a working setup (an ENABLED operation with Pickup +
346 // Delivery attached, every menu ordering-enabled) — these helpers VERIFY it; they never POST
347 // an operation. Each micro-service is on its OWN host prefix — do not normalize.
348
349 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/list-operations.md
350 export async function listOperations ( ctx ) {
351 const r = await req (ctx, "/restaurants-operations/v1/operations" , { method: "GET" });
352 return r.operations ?? [];
353 }
354
355 // A fresh install provisions the operation ASYNC — poll until it lands.
356 export async function listOperationsWithRetry ( ctx , { tries = 15 , delayMs = 2000 } = {}) {
357 for ( let i = 0 ; i < tries; i ++ ) {
358 const ops = await listOperations (ctx). catch (() => []);
359 if (ops. length ) return ops;
360 if (i < tries - 1 ) await sleep (delayMs);
361 }
362 return [];
363 }
364
365 // Normally already ENABLED — only PATCH when DISABLED/PAUSED_UNTIL. revision mandatory + current.
366 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/update-operation.md
367 export async function enableOperation ( ctx , operationId , revision ) {
368 return req (ctx, `/restaurants-operations/v1/operations/${ operationId }` , {
369 method: "PATCH" ,
370 body: { operation: { revision, onlineOrderingStatus: "ENABLED" } },
371 });
372 }
373
374 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/menu-ordering-settings/query-menu-ordering-settings.md
375 export async function queryMenuOrderingSettings ( ctx ) {
376 const r = await req (ctx, "/menu-ordering-settings/v1/menu-ordering-settings/query" , { body: { query: {} } });
377 return r.menuOrderingSettings ?? [];
378 }
379
380 // Only when an entry shows onlineOrderingEnabled:false / operationId:"none".
381 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/menu-ordering-settings/update-menu-ordering-settings.md
382 export async function updateMenuOrderingSettings ( ctx , settingsId , patch ) {
383 return req (ctx, `/menu-ordering-settings/v1/menu-ordering-settings/${ settingsId }` , {
384 method: "PATCH" ,
385 body: { menuOrderingSettings: patch },
386 });
387 }
388
389 // ---- table reservations (setup-restaurant-reservations.md) ---------------------------------------
390 // The install AUTO-provisions one default reservation location with a complete config; the
391 // one thing OFF is onlineReservationsEnabled (premium-gated). A reservation location cannot
392 // be created via this API — discover, configure, enable. Post-Jan-2026 field names:
393 // partySize (not partiesSize), approval (not manualApproval).
394
395 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservation-locations/list-reservation-locations.md
396 export async function listReservationLocations ( ctx ) {
397 const r = await req (ctx, "/table-reservations/reservation-locations/v1/reservation-locations" , { method: "GET" });
398 return r.reservationLocations ?? [];
399 }
400
401 export async function listReservationLocationsWithRetry ( ctx , { tries = 15 , delayMs = 2000 } = {}) {
402 for ( let i = 0 ; i < tries; i ++ ) {
403 const locs = await listReservationLocations (ctx). catch (() => []);
404 if (locs. length ) return locs;
405 if (i < tries - 1 ) await sleep (delayMs);
406 }
407 return [];
408 }
409
410 // Partial PATCH; revision mandatory; works on a non-premium site. The `location` object
411 // (address/name) is IMMUTABLE here — only touch `configuration`.
412 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservation-locations/update-reservation-location.md
413 export async function updateReservationLocation ( ctx , reservationLocationId , revision , configuration ) {
414 return req (ctx, `/table-reservations/reservation-locations/v1/reservation-locations/${ reservationLocationId }` , {
415 method: "PATCH" ,
416 body: { reservationLocation: { id: reservationLocationId, revision, configuration } },
417 });
418 }
419
420 // PREMIUM-ONLY: on a non-premium site this THROWS `428 PREMIUM_ONLY` — expected and
421 // non-fatal; the caller records it and continues (never retry-spiral, never fail the seed).
422 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservation-locations/update-reservation-location.md
423 export async function enableOnlineReservations ( ctx , reservationLocationId , revision ) {
424 return req (ctx, `/table-reservations/reservation-locations/v1/reservation-locations/${ reservationLocationId }` , {
425 method: "PATCH" ,
426 body: {
427 reservationLocation: {
428 id: reservationLocationId,
429 revision,
430 configuration: { onlineReservations: { onlineReservationsEnabled: true } },
431 },
432 },
433 });
434 }
435
436 /**
437 * ONE-CALL seed: Menus install (+ a report of what the app already holds) → menus bottom-up → images →
438 * ordering add-on → reservations add-on, ids threaded in memory. The default path.
439 */
440 export async function setupRestaurants ( ctx , plan ) {
441 // Before any item exists: an item's price is stored in the site currency.
442 if (plan.currency) await setSiteCurrency (ctx, plan.currency);
443 const menusPlan = plan.menus ?? [];
444 const wasPresent = await menusAppPresent (ctx);
445 await installMenusApp (ctx);
446 // Menus the plan did not create: the install's sample "Dinner Menu" on a fresh site (polled, it
447 // provisions async), the owner's menus on an existing one. Reported for the closing message,
448 // never deleted.
449 const preexistingMenus = await readPreexistingMenus (ctx, { tries: wasPresent ? 1 : 8 }). catch (() => []);
450
451 const createdMenus = [];
452 for ( const m of menusPlan) createdMenus. push ( await createMenu (ctx, m));
453
454 // image pass — resolve each item's image (import by url / generate by prompt) in ONE
455 // parallel wave (restaurants binds by file id), then bulk full-replace with revision +
456 // the item's created body echoed. Never block on image failure.
457 let imagesAttached = 0 ;
458 const imageItems = [];
459 menusPlan. forEach (( m , mi ) => {
460 const flat = m.sections. flatMap (( s ) => s.items || []);
461 flat. forEach (( it , i ) => {
462 const created = createdMenus[mi]?.items?.[i];
463 if ((it.imageMediaId || it.imageUrl || it.imagePath || it.imagePrompt) && created?.id) {
464 imageItems. push ({ ... created, imageMediaId: it.imageMediaId, imageUrl: it.imageUrl, imagePath: it.imagePath, imagePrompt: it.imagePrompt, name: it.name });
465 }
466 });
467 });
468 const files = await resolveItemImages (ctx, imageItems. map (( it ) => ({
469 mediaId: it.imageMediaId,
470 url: it.imageUrl,
471 path: it.imagePath,
472 prompt: it.imagePrompt,
473 displayName: `${ it . name || "item"}.png` ,
474 })));
475 const toAttach = imageItems
476 . map (( it , i ) => (files[i]
477 ? { id: it.id, revision: it.revision ?? "1" , body: it.body,
478 image: { id: files[i].id, url: files[i].url, width: 1024 , height: 1024 } }
479 : null ))
480 . filter (Boolean);
481 const imageFailures = [];
482 if (toAttach. length ) {
483 try {
484 const r = await attachItemImages (ctx, toAttach);
485 imagesAttached = r.attached. length ;
486 for ( const f of r.failures) imageFailures. push (f);
487 } catch (e) {
488 for ( const it of toAttach) imageFailures. push ({ id: it.id, error: e?.message ?? String (e) });
489 }
490 }
491
492 // shared STEP 0 address — set once across both add-ons.
493 let locationSet = false ;
494 const ensureLocation = async ( address ) => {
495 if (address && ! locationSet) {
496 await setBusinessLocation (ctx, address);
497 locationSet = true ;
498 }
499 };
500
501 const ordering = { enabled: false , addressSet: false };
502 if (plan.ordering) {
503 const cfg = typeof plan.ordering === "object" ? plan.ordering : {};
504 await installOrdersApp (ctx); // auto-provisions operation + methods + per-menu settings
505 await ensureLocation (cfg.address);
506 ordering.addressSet = locationSet;
507 const ops = await listOperationsWithRetry (ctx);
508 const op = ops. find (( o ) => o.default) ?? ops[ 0 ];
509 if ( ! op) throw new Error ( "No ordering operation appeared — the Orders app install may not have completed; re-run the seed." );
510 if (op.onlineOrderingStatus !== "ENABLED" ) await enableOperation (ctx, op.id, op.revision);
511 // confirm each menu is orderable (auto-created + auto-enabled per menu; PATCH only when off)
512 const settings = await queryMenuOrderingSettings (ctx);
513 for ( const s of settings) {
514 if (s.onlineOrderingEnabled === false || s.operationId === "none" ) {
515 await updateMenuOrderingSettings (ctx, s.id, {
516 revision: s.revision,
517 operationId: op.id,
518 onlineOrderingEnabled: true ,
519 availability: { type: "ALWAYS_AVAILABLE" , timeZone: cfg.address?.timeZone ?? "America/New_York" },
520 }). catch (() => {});
521 }
522 }
523 ordering.enabled = true ;
524 ordering.operationId = op.id;
525 if ( ! ordering.addressSet) ordering.note = "No address in the plan — ordering is 'testing only' until the owner sets the real business address." ;
526 }
527
528 const reservations = { enabled: false };
529 if (plan.reservations) {
530 const cfg = typeof plan.reservations === "object" ? plan.reservations : {};
531 await installTableReservationsApp (ctx); // auto-provisions the default reservation location
532 await ensureLocation (cfg.address);
533 let [loc] = await listReservationLocationsWithRetry (ctx);
534 if ( ! loc) throw new Error ( "No reservation location appeared — the Table Reservations install may not have completed; re-run the seed." );
535 const configuration = cfg.configuration ?? (cfg.partySize ? { onlineReservations: { partySize: cfg.partySize } } : null );
536 if (configuration) {
537 await updateReservationLocation (ctx, loc.id, loc.revision, configuration);
538 [loc] = await listReservationLocations (ctx); // re-read for the bumped revision
539 }
540 reservations.reservationLocationId = loc.id;
541 try {
542 await enableOnlineReservations (ctx, loc.id, loc.revision);
543 reservations.enabled = true ;
544 } catch {
545 // 428 PREMIUM_ONLY on a free site — expected; record, don't fail.
546 reservations.premiumRequired = true ;
547 }
548 }
549
550 return {
551 menus: createdMenus. map (({ items , ... m }) => m),
552 imagesAttached,
553 imageFailures,
554 preexistingMenus,
555 // the Menus app's slug (route read from the app's registered dashboard pages); `restaurants/menus` is no route
556 dashboardMenusUrl: `https://manage.wix.com/dashboard/${ ctx . siteId }/wix-restaurants-menus-new` ,
557 ordering,
558 reservations,
559 };
560 }
561
562 // ---- CLI entry ----------------------------------------------------------------------------------
563
564 const invokedDirectly = isMain ( import . meta .url);
565 if (invokedDirectly) {
566 const planPath = process.argv[ 2 ];
567 if ( ! planPath) {
568 console. error ( "usage: node seed-restaurants.mjs <plan.json> (run from the project root)" );
569 process. exit ( 1 );
570 }
571 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
572 const ctx = makeCtx ();
573 setupRestaurants (ctx, plan)
574 . then (( result ) => console. log ( JSON . stringify (result, null , 2 )))
575 . catch (( e ) => {
576 console. error (e.message);
577 process. exit ( 1 );
578 });
579 }