Setting the file. One moment.
Seed Restaurants · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page 22.10
Post Detail
This file
Number 22.119
Position 119 of 145
Type JavaScript
Size 25 KB
Lines 452 references/restaurants/seed/ seed-restaurants.cjs
JavaScript · 452 lines · 25 KB
14 // const menu = await seed.createMenu(ctx, { // builds items → sections → menu bottom-up
15 // name: "Dinner", description: "Evening menu",
16 // sections: [
17 // { name: "Antipasti", description: "To start", items: [
18 // { name: "Bruschetta al Pomodoro", description: "Grilled sourdough, San Marzano tomatoes, basil.", price: 9.50 },
19 // ] },
20 // ],
21 // }); // → { menuId, sectionIds, itemIds }
22 //
23 // // --- ONLINE ORDERING (add-on, on demand; MENU-FIRST — seed the menu above first) ---
24 // await seed.installOrdersApp(ctx); // auto-provisions a working ordering setup
25 // await seed.setBusinessLocation(ctx, address); // STEP 0 — REQUIRED (see preconditions below)
26 // const ops = await seed.listOperations(ctx); // verify the auto-created operation (never create one)
27 // const methods = await seed.listFulfillmentMethods(ctx); // reshape only what the request names
28 // const mos = await seed.queryMenuOrderingSettings(ctx); // confirm each menu is onlineOrderingEnabled
29 //
30 // // --- TABLE RESERVATIONS (add-on, on demand; INDEPENDENT of menu/ordering) ---
31 // await seed.installTableReservationsApp(ctx); // auto-provisions a default reservation location
32 // await seed.setBusinessLocation(ctx, address); // STEP 0 — shared with ordering (do it once if both)
33 // const [loc] = await seed.listReservationLocations(ctx); // discover the default (never create one)
34 // await seed.enableOnlineReservations(ctx, loc.id, loc.revision);// PREMIUM-GATED — 428 on a free site (record, don't fail)
35 //
36 // // --- EXPERIENCES (add-on WITHIN Table Reservations, on demand) ---
37 // await seed.createExperiences(ctx, loc.id, experiences); // one per named dining occasion
38 //
39 // **NOT yet live-verified — transcribed from setup-restaurants.md / setup-restaurant-orders.md /
40 // setup-restaurant-reservations.md / setup-restaurant-experiences.md.** If any call fails with a
41 // shape the caller didn't expect, fall back to the documentation skill available in your environment (search + read the live Wix API
42 // reference) — never guess.
43 //
44 // ── PRECONDITIONS the recipes flag (record in the handoff, do NOT fail the seed) ─────────────────
45 // • Ordering STEP 0 address is REQUIRED: without a real business-location address Wix limits
46 // ordering to "testing only" and checkout breaks. If the brief names no address, set a clearly
47 // marked placeholder and flag the owner to fix it.
48 // • Completing a PAID order needs a premium plan + a configured payment method (dashboard/premium
49 // provisioning the skill can't do headlessly).
50 // • enableOnlineReservations is PREMIUM-ONLY: on a non-premium site it throws `428 PREMIUM_ONLY`.
51 // That is EXPECTED and non-fatal — record it as a precondition and continue; do not retry-spiral.
52 // • Booking an Experience is premium-gated the same way (create works on a free site; booking needs
53 // premium + online reservations enabled).
54
55 const API = "https://www.wixapis.com" ;
56
57 // App definition ids (SETUP.md §2). Menus is the seedable core; Orders + Table Reservations are
58 // separate optional apps. Experiences are a FEATURE of Table Reservations — no separate install.
59 const MENUS_APP_ID = "b278a256-2757-4f19-9313-c05c783bec92" ;
60 const ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60" ;
61 const TABLE_RESERVATIONS_APP_ID = "f9c07de2-5341-40c6-b096-8eb39de391fb" ;
62
63 async function req ( ctx , path , { method = "POST" , body } = {}) {
64 const res = await fetch ( API + path, {
65 method,
66 headers: {
67 Authorization: `Bearer ${ ctx . token }` ,
68 "wix-site-id" : ctx.siteId,
69 "Content-Type" : "application/json" ,
70 },
71 body: body ? JSON . stringify (body) : undefined ,
72 });
73 const json = await res. json (). catch (() => ({}));
74 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
75 return json;
76 }
77
78 // ── app install ─────────────────────────────────────────────────────────────────────────────────
79 // Body shape per SETUP.md §2 (tenant + appInstance). One install call per app.
80 async function installApp ( ctx , appDefId ) {
81 return req (ctx, "/apps-installer-service/v1/app-instance/install" , {
82 body: {
83 tenant: { tenantType: "SITE" , id: ctx.siteId },
84 appInstance: { appDefId, enabled: true },
85 },
86 });
87 }
88 async function installMenusApp ( ctx ) { return installApp (ctx, MENUS_APP_ID ); }
89 async function installOrdersApp ( ctx ) { return installApp (ctx, ORDERS_APP_ID ); }
90 async function installTableReservationsApp ( ctx ) { return installApp (ctx, TABLE_RESERVATIONS_APP_ID ); }
91
92 // ══ MENU (setup-restaurants.md) ═══════════════════════════════════════════════════════════════════
93 // Everything is the Restaurants Menus V1 API on /restaurants/menus/v1/... — one service.
94 // REST flattens the protobuf wrappers: send plain values (`"visible": true`), never `{"value": …}`.
95
96 async function listMenuItems ( ctx ) {
97 const r = await req (ctx, "/restaurants/menus/v1/items" , { method: "GET" });
98 return (r.items ?? []). map (( it ) => ({ id: it.id, name: it.name }));
99 }
100 async function listMenuSections ( ctx ) {
101 const r = await req (ctx, "/restaurants/menus/v1/sections" , { method: "GET" });
102 return (r.sections ?? []). map (( s ) => ({ id: s.id, name: s.name }));
103 }
104 async function listMenus ( ctx ) {
105 const r = await req (ctx, "/restaurants/menus/v1/menus" , { method: "GET" });
106 return (r.menus ?? []). map (( m ) => ({ id: m.id, name: m.name }));
107 }
108
109 /**
110 * Build one menu BOTTOM-UP (items → sections → menu) in three bulk phases.
111 * @param menu {
112 * name, description?,
113 * sections: [{ name, description?, items: [{ name, description?, price }] }]
114 * }
115 * price -> priceInfo.price as a decimal STRING; currency is the site's (send none).
116 * description is a plain string (NOT rich-text nodes); omit for a name-only item/section.
117 * visible:true is baked in at every level (item, section, menu) — required to render on the live site.
118 * @returns { menuId, sectionIds:[…], itemIds:[…] } (revisions available via listMenuItems for image pass)
119 */
120 async function createMenu ( ctx , menu ) {
121 // STEP 1 — bulk-create every item across all sections in ONE request; track which section each belongs to.
122 const flat = [];
123 menu.sections. forEach (( sec , si ) => (sec.items || []). forEach (( it ) => flat. push ({ ... it, _section: si })));
124 const itemRes = await req (ctx, "/restaurants/menus/v1/bulk/items/create" , {
125 body: {
126 items: flat. map (( it ) => ({
127 name: it.name,
128 ... (it.description ? { description: it.description } : {}),
129 priceInfo: { price: String (it.price) },
130 visible: true ,
131 })),
132 returnEntity: true ,
133 },
134 });
135 // created items are under results[].item (results[].itemMetadata.success is the per-item flag)
136 const createdItems = (itemRes.results ?? []). map (( r ) => r.item);
137 const itemIdsBySection = menu.sections. map (() => []);
138 flat. forEach (( it , i ) => {
139 const id = createdItems[i]?.id;
140 if (id) itemIdsBySection[it._section]. push (id);
141 });
142
143 // STEP 2 — bulk-create sections, each carrying the itemIds of its items (real ids from STEP 1).
144 const secRes = await req (ctx, "/restaurants/menus/v1/bulk/sections/create" , {
145 body: {
146 sections: menu.sections. map (( sec , si ) => ({
147 name: sec.name,
148 ... (sec.description ? { description: sec.description } : {}),
149 visible: true ,
150 itemIds: itemIdsBySection[si],
151 })),
152 returnEntity: true ,
153 },
154 });
155 const sectionIds = (secRes.results ?? []). map (( r ) => r.item?.id);
156
157 // STEP 3 — create the menu (single create wraps in `menu`), carrying its sectionIds.
158 // businessLocationId omitted -> binds to the site's default (main) location.
159 const menuRes = await req (ctx, "/restaurants/menus/v1/menus" , {
160 body: {
161 menu: {
162 name: menu.name,
163 ... (menu.description ? { description: menu.description } : {}),
164 visible: true ,
165 sectionIds,
166 },
167 },
168 });
169 return { menuId: menuRes.menu?.id, sectionIds, itemIds: createdItems. map (( it ) => it?.id) };
170 }
171
172 // Import an external image URL into Wix Media → { id, url }. Restaurants binds a menu-item image by
173 // the Wix Media file **id**, NOT a url — an external url (e.g. a base44 generate_image result) MUST
174 // be imported first; the raw url as the id stores (200) but renders nothing. id = wixstatic file id,
175 // url = the permanent wixstatic url.
176 async function importImage ( ctx , url , displayName = "image.png" ) {
177 const r = await req (ctx, "/site-media/v1/files/import" , { body: { url, mimeType: "image/png" , displayName } });
178 const f = r.file || r;
179 if ( ! f?.id) throw new Error ( `import-file returned no file id: ${ JSON . stringify ( r ). slice ( 0 , 200 ) }` );
180 return { id: f.id, url: f.url };
181 }
182
183 // Optional (pass-2). The ITEM is the image-bearing entity. Update Item is a FULL-ENTITY REPLACE
184 // with NO field mask — you MUST echo each item's current `revision` AND `priceInfo`, or it fails
185 // `428 MISSING_ITEM_PRICING` and the image does NOT apply. `image` is an OBJECT { id, url, height, width }
186 // (never a bare string); the binding field is the Wix Media file `id` (from importImage). Never block on image failure.
187 // items: [{ id, revision, price, image: { id, url, height, width } }]
188 async function attachItemImages ( ctx , items ) {
189 return req (ctx, "/restaurants/menus/v1/bulk/items/update" , {
190 body: {
191 items: items. map (( it ) => ({
192 item: {
193 id: it.id,
194 revision: it.revision,
195 priceInfo: { price: String (it.price) },
196 image: it.image,
197 },
198 })),
199 },
200 });
201 }
202
203 // ══ BUSINESS LOCATION (shared STEP 0 for ordering + reservations) ══════════════════════════════════
204 // Update Location is a FULL OVERRIDE — send the WHOLE `location` object (omitted fields are wiped),
205 // echo `default:true` (omitting it 400s CHANGE_DEFAULT_FORBIDDEN) and the current `revision`.
206 // address.country is a 2-letter ISO-3166 code. The address propagates to Site Properties.
207 // `location`: { name, timeZone, email?, phone?, address:{ country, subdivision, city, postalCode,
208 // streetAddress:{ number, name }, formattedAddress } }
209 async function setBusinessLocation ( ctx , location ) {
210 const list = await req (ctx, "/locations/v1/locations" , { method: "GET" });
211 const def = (list.locations ?? []). find (( l ) => l.default);
212 // No default location at all (a bare Orders-only site can have none) -> CREATE one; operations
213 // auto-bind on first-location-add. Otherwise overwrite the existing default (full override).
214 if ( ! def) {
215 const r = await req (ctx, "/locations/v1/locations" , { body: { location: { ... location, default: true } } });
216 return r.location;
217 }
218 const r = await req (ctx, `/locations/v1/locations/${ def . id }` , {
219 method: "PUT" ,
220 body: { location: { ... location, id: def.id, revision: def.revision, default: true } },
221 });
222 return r.location;
223 }
224
225 // ══ ONLINE ORDERING (setup-restaurant-orders.md) ═══════════════════════════════════════════════════
226 // MENU-FIRST: a menu must exist (createMenu above) before ordering; each menu binds to the operation
227 // via a menu-ordering-settings object. The Orders-app install AUTO-provisions a working setup (an
228 // ENABLED operation with Pickup + Delivery attached, every menu ordering-enabled) — these helpers
229 // VERIFY and RESHAPE it; they do NOT build ordering from scratch. Each micro-service is on its OWN
230 // host prefix (restaurants-operations / fulfillment-methods / menu-ordering-settings) — do not normalize.
231
232 // STEP 1 — verify the auto-created operation (never POST to create one). If empty, the install hasn't
233 // finished provisioning: wait briefly and retry the GET once (caller's concern), then fail loud.
234 async function listOperations ( ctx ) {
235 const r = await req (ctx, "/restaurants-operations/v1/operations" , { method: "GET" });
236 return r.operations ?? [];
237 }
238 // Normally already "ENABLED" — only PATCH if you see DISABLED/PAUSED_UNTIL. revision mandatory + current.
239 async function enableOperation ( ctx , operationId , revision ) {
240 return req (ctx, `/restaurants-operations/v1/operations/${ operationId }` , {
241 method: "PATCH" ,
242 body: { operation: { revision, onlineOrderingStatus: "ENABLED" } },
243 });
244 }
245 // A newly created fulfillment method is NOT auto-attached — PATCH the operation's fulfillmentIds with
246 // the FULL array (existing ids + the new one) or it won't be offered at checkout.
247 async function setOperationFulfillmentIds ( ctx , operationId , revision , fulfillmentIds ) {
248 return req (ctx, `/restaurants-operations/v1/operations/${ operationId }` , {
249 method: "PATCH" ,
250 body: { operation: { revision, fulfillmentIds } },
251 });
252 }
253
254 // STEP 2 — reconcile fulfillment methods to the request. Install ships three (Pickup enabled,
255 // "Delivery Area #1" enabled fee "0", "DoorDash Drive" disabled), all with a San Francisco placeholder
256 // address. Wrap bodies in camelCase `fulfillmentMethod`; fee/minOrderPrice are decimal STRINGS.
257 async function listFulfillmentMethods ( ctx ) {
258 const r = await req (ctx, "/fulfillment-methods/v1/fulfillment-methods" , { method: "GET" });
259 return r.fulfillmentMethods ?? [];
260 }
261 // patch: partial { revision, name?, fee?, minOrderPrice?, enabled?, availability?, pickupOptions?/deliveryOptions? }
262 async function updateFulfillmentMethod ( ctx , methodId , patch ) {
263 return req (ctx, `/fulfillment-methods/v1/fulfillment-methods/${ methodId }` , {
264 method: "PATCH" ,
265 body: { fulfillmentMethod: patch },
266 });
267 }
268 // Create a method beyond the defaults, THEN setOperationFulfillmentIds to attach it (create does NOT attach).
269 // pickupOptions for type PICKUP, deliveryOptions (with a deliveryArea) for DELIVERY — send the one matching type.
270 async function createFulfillmentMethod ( ctx , fulfillmentMethod ) {
271 const r = await req (ctx, "/fulfillment-methods/v1/fulfillment-methods" , { body: { fulfillmentMethod } });
272 return r.fulfillmentMethod;
273 }
274
275 // STEP 3 — verify each menu is orderable. Auto-created + auto-enabled per menu, so normally a confirmation.
276 async function queryMenuOrderingSettings ( ctx ) {
277 const r = await req (ctx, "/menu-ordering-settings/v1/menu-ordering-settings/query" , { body: { query: {} } });
278 return r.menuOrderingSettings ?? [];
279 }
280 // Only if an entry shows onlineOrderingEnabled:false / operationId:"none" (or a menu should be display-only).
281 // patch: { revision, operationId, onlineOrderingEnabled, availability:{ type:"ALWAYS_AVAILABLE", timeZone } }
282 async function updateMenuOrderingSettings ( ctx , settingsId , patch ) {
283 return req (ctx, `/menu-ordering-settings/v1/menu-ordering-settings/${ settingsId }` , {
284 method: "PATCH" ,
285 body: { menuOrderingSettings: patch },
286 });
287 }
288
289 // ══ TABLE RESERVATIONS (setup-restaurant-reservations.md) ══════════════════════════════════════════
290 // INDEPENDENT of the menu (reservations bind to a LOCATION, not a menu — no menu-first rule) and there
291 // is NOTHING to bulk-seed (visitors create reservations at runtime). The install AUTO-provisions one
292 // default reservation location with a complete config; the one thing OFF is onlineReservationsEnabled.
293 // A reservation location CANNOT be created via this API — discover, configure, and enable it.
294 // Use the post-Jan-2026 field names: partySize (not partiesSize), approval (not manualApproval),
295 // tables.ids (not tableIds), ignoreConflicts.
296
297 // STEP 1 — discover the default location (never create one). If empty, wait + retry the GET once, else fail loud.
298 async function listReservationLocations ( ctx ) {
299 const r = await req (ctx, "/table-reservations/reservation-locations/v1/reservation-locations" , { method: "GET" });
300 return r.reservationLocations ?? [];
301 }
302 // STEP 2 — customize config (only what the request names). Partial PATCH; revision mandatory. Works on a
303 // non-premium site. The `location` object (address/name) is IMMUTABLE here — only touch `configuration`.
304 // configuration: { onlineReservations: { partySize?:{min,max}, minimumReservationNotice?:{number,unit},
305 // defaultTurnoverTime?, businessSchedule?, approval?:{mode:"AUTOMATIC"}, ... } }
306 async function updateReservationLocation ( ctx , reservationLocationId , revision , configuration ) {
307 return req (ctx, `/table-reservations/reservation-locations/v1/reservation-locations/${ reservationLocationId }` , {
308 method: "PATCH" ,
309 body: { reservationLocation: { id: reservationLocationId, revision, configuration } },
310 });
311 }
312 // STEP 3 — turn on online reservations. PREMIUM-ONLY: on a non-premium site this THROWS `428 PREMIUM_ONLY`
313 // ("Can't turn on online reservation for a non-premium website"). That is EXPECTED and non-fatal — the
314 // caller records it as a premium precondition and continues; do NOT retry-spiral or fail the seed.
315 async function enableOnlineReservations ( ctx , reservationLocationId , revision ) {
316 return req (ctx, `/table-reservations/reservation-locations/v1/reservation-locations/${ reservationLocationId }` , {
317 method: "PATCH" ,
318 body: {
319 reservationLocation: {
320 id: reservationLocationId,
321 revision,
322 configuration: { onlineReservations: { onlineReservationsEnabled: true } },
323 },
324 },
325 });
326 }
327
328 // ══ EXPERIENCES (setup-restaurant-experiences.md) ══════════════════════════════════════════════════
329 // An experience is a reservation that IS a curated dining occasion (wine tasting, chef's table). Feature
330 // of the Table Reservations app — no separate install. Created against a reservationLocationId (from
331 // listReservationLocations). The full create payload lives in the live docs (fields evolve) — build each
332 // `experience` from the Create-Experience doc; this only wires the loop. Set configuration.visible:true.
333 // GOTCHAS the docs won't state:
334 // • Notice fields are FLAT under onlineReservations (minimumReservationNotice / maximumReservationNotice),
335 // NOT wrapped in `noticePeriod` (the doc example's wrapper is stale).
336 // • paymentPolicyType: PER_GUEST (needs perGuestOptions.price, a decimal string) or FREE.
337 // • businessSchedule.entries[] carries the recurrence (WEEKLY + weeklyOptions.startDaysAndTimes[{day,time}],
338 // or ONE_TIME); durationInMinutes sits on businessSchedule, not per entry.
339 // • Creating works on a free site; BOOKING is premium-gated (record, don't fail).
340 // experiences: [{ configuration: { displayInfo:{name,shortDescription}, paymentPolicy, onlineReservations, visible } }]
341 async function createExperiences ( ctx , reservationLocationId , experiences ) {
342 const out = [];
343 for ( const exp of experiences) {
344 const r = await req (ctx, "/table-reservations/experiences/v1/experiences" , {
345 body: { experience: { reservationLocationId, ... exp } },
346 });
347 out. push ({ id: r.experience?.id, name: r.experience?.configuration?.displayInfo?.name });
348 }
349 return out;
350 }
351
352 // ══ ONE-CALL ORCHESTRATOR ══════════════════════════════════════════════════════════════════════════
353 /**
354 * DEFAULT one-call path — seed a whole Wix Restaurants site from a plain plan; the caller threads NO
355 * ids across exec calls. Installs the Menus app and builds the menu BOTTOM-UP (items → sections → menu):
356 * `createMenu` bulk-creates every item first, then the sections carrying their item ids, then the menu
357 * carrying its section ids — every child exists before its parent. In-memory name→id maps (item, section)
358 * wire the tree and map each plan item to its created id for the image pass (full-replace, echo revision
359 * + price; freshly created items are at revision "1"). Online ordering and table reservations are
360 * on-demand add-ons — touched ONLY when the plan asks, each installing its own app and following the
361 * module's own fns. Ordering needs a business-location address (`setBusinessLocation` STEP 0); if the
362 * plan names none the recipe's precondition applies (record + flag). `enableOnlineReservations` is
363 * premium-gated (428 on a free site) — expected and non-fatal, recorded not thrown.
364 *
365 * @param plan {
366 * menu: { name, description?, sections: [{ name, description?,
367 * items: [{ name, description?, price, imageUrl? }] }] }, // imageUrl = a plain url; imported to Wix Media here
368 * ordering?: boolean | { address? },
369 * reservations?: boolean | { address?, configuration? },
370 * }
371 * @returns { menuId, sectionIds, itemIds, orderingEnabled, reservationsEnabled, imagesAttached }
372 */
373 async function setupRestaurants ( ctx , plan ) {
374 await installMenusApp (ctx);
375
376 const { menuId , sectionIds , itemIds } = await createMenu (ctx, plan.menu);
377
378 // name→id maps: createMenu flattens sections→items in order, so itemIds/sectionIds line up 1:1.
379 const flatItems = plan.menu.sections. flatMap (( s ) => s.items || []);
380 const itemNameToId = new Map (flatItems. map (( it , i ) => [it.name, itemIds[i]]));
381
382 // image pass — import each item's url to Wix Media (restaurants binds by file id), then full-replace
383 // update its created id (revision "1", fresh + priceInfo). A failed import just skips that image.
384 const imageItems = [];
385 for ( const it of flatItems) {
386 if ( ! it.imageUrl) continue ;
387 try {
388 const file = await importImage (ctx, it.imageUrl, `${ it . name || "item"}.png` );
389 imageItems. push ({ id: itemNameToId. get (it.name), revision: "1" , price: it.price,
390 image: { id: file.id, url: file.url, height: 1024 , width: 1024 } });
391 } catch { /* skip this item's image */ }
392 }
393 let imagesAttached = 0 ;
394 if (imageItems. length ) {
395 try { await attachItemImages (ctx, imageItems); imagesAttached = imageItems. length ; }
396 catch { /* never block on image failure */ }
397 }
398
399 // shared STEP 0 address — set once across both add-ons (SEED.md: do it once if both run).
400 let locationSet = false ;
401 const ensureLocation = async ( address ) => {
402 if (address && ! locationSet) { await setBusinessLocation (ctx, address); locationSet = true ; }
403 };
404
405 let orderingEnabled = false ;
406 if (plan.ordering) {
407 const cfg = typeof plan.ordering === "object" ? plan.ordering : {};
408 await installOrdersApp (ctx); // auto-provisions operation + methods + per-menu settings
409 await ensureLocation (cfg.address);
410 await listOperations (ctx); // verify the auto-created operation
411 await queryMenuOrderingSettings (ctx); // confirm each menu is orderable
412 orderingEnabled = true ;
413 }
414
415 let reservationsEnabled = false ;
416 if (plan.reservations) {
417 const cfg = typeof plan.reservations === "object" ? plan.reservations : {};
418 await installTableReservationsApp (ctx); // auto-provisions the default reservation location
419 await ensureLocation (cfg.address); // independent of menu; address optional for reservations
420 let [loc] = await listReservationLocations (ctx);
421 if (loc && cfg.configuration) {
422 await updateReservationLocation (ctx, loc.id, loc.revision, cfg.configuration);
423 [loc] = await listReservationLocations (ctx); // re-read for the bumped revision
424 }
425 if (loc) {
426 try { await enableOnlineReservations (ctx, loc.id, loc.revision); reservationsEnabled = true ; }
427 catch { /* 428 PREMIUM_ONLY on a free site is expected — record, don't fail */ }
428 }
429 }
430
431 return { menuId, sectionIds, itemIds, orderingEnabled, reservationsEnabled, imagesAttached };
432 }
433
434 module . exports = {
435 // DEFAULT one-call orchestrator
436 setupRestaurants,
437 // app install
438 installMenusApp, installOrdersApp, installTableReservationsApp,
439 // menu
440 listMenuItems, listMenuSections, listMenus,
441 createMenu, importImage, attachItemImages,
442 // shared business location (ordering + reservations STEP 0)
443 setBusinessLocation,
444 // ordering add-on
445 listOperations, enableOperation, setOperationFulfillmentIds,
446 listFulfillmentMethods, updateFulfillmentMethod, createFulfillmentMethod,
447 queryMenuOrderingSettings, updateMenuOrderingSettings,
448 // reservations add-on
449 listReservationLocations, updateReservationLocation, enableOnlineReservations,
450 // experiences add-on
451 createExperiences,
452 };