Setting the file. One moment.
Ordering · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page — line 222
This file
Number 16.727
Position 727 of 828
Type TypeScript
Size 16 KB
Lines 299 restaurants/rest/ ordering.ts
TypeScript · 299 lines · 16 KB
13 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/menu-ordering-settings/query-menu-ordering-settings.md
14 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/fulfillment-methods/list-fulfillment-methods.md
15 // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md
16 // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
17 // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/set-delivery-method-for-current-cart.md
18 // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/update-current-cart.md
19 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/calculate-available-time-slots-for-date.md
20 // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/calculate-first-available-time-slot-per-fulfillment-type.md
21 import { WixApiError, wixRequest } from "../../shared/rest/client.js" ;
22 import { imgSrc } from "../../shared/rest/media.js" ;
23 import { fetchSiteMoney } from "./menu.js" ;
24 import { initialSelection, nextCursor, validateSelection, type Raw } from "../app/wix/restaurants/menu-core.js" ;
25 import {
26 NOT_DELIVERABLE,
27 RESTAURANTS_ORDERS_APP_ID,
28 assertOrderAdded,
29 assertOrderCheckoutable,
30 assertOrderContext,
31 assertOrderable,
32 estimateSubtotal,
33 fulfillmentAddress,
34 fulfillmentCode,
35 orderCatalogItem,
36 rawId,
37 resolveOperation,
38 restaurantToday,
39 slotsAddressQuery,
40 slotsDateQuery,
41 toFulfillmentMethods,
42 toFulfillmentTimes,
43 toMenuOrderingMap,
44 toOrderCart,
45 toRestaurantRegion,
46 } from "../app/wix/restaurants/ordering-core.js" ;
47 import type { DeliveryAddress, FulfillmentChoice, FulfillmentMethodInfo, FulfillmentTime, MenuItem, MenuOrderingInfo, OrderCart, OrderSelection, OrderingStatus } from "../app/wix/restaurants/types.js" ;
48
49 export { RESTAURANTS_ORDERS_APP_ID };
50
51 const CART = "/ecom/v2/carts/current" ;
52
53 // The ordering operation is site config — resolve once, reuse for every add.
54 // GET /restaurants-operations/v1/operations
55 let orderingPromise : Promise < OrderingStatus > | null = null ;
56
57 /**
58 * The operation to order through and its state (ENABLED, else the default, else the first).
59 * `status` "NONE" when the site has no online ordering; DISABLED / PAUSED_UNTIL refuse every add.
60 */
61 export function resolveOrdering () : Promise < OrderingStatus > {
62 orderingPromise ??= wixRequest < Raw >( "/restaurants-operations/v1/operations" , { method: "GET" })
63 . then (( res ) => resolveOperation (res?.operations ?? []))
64 . catch (() => {
65 orderingPromise = null ; // transient failure — allow a retry on the next call
66 return resolveOperation ([]);
67 });
68 return orderingPromise;
69 }
70
71 /** The id of the operation to order through — null when the site has none. */
72 export function resolveOperationId () : Promise < string | null > {
73 return resolveOrdering (). then (( s ) => s.operationId);
74 }
75
76 /**
77 * Every menu's ordering settings under the operation, keyed by menuId (which menus take online
78 * orders and when). A menu absent from the map is not orderable. {} when there is no operation.
79 * POST /menu-ordering-settings/v1/menu-ordering-settings/query { query: { filter: { operationId }, cursorPaging: { cursor? } } }
80 */
81 export async function fetchMenuOrdering () : Promise < Record < string , MenuOrderingInfo >> {
82 const { operationId } = await resolveOrdering ();
83 if ( ! operationId) return {};
84 const raws : Raw [] = [];
85 let cursor : string | null = null ;
86 do {
87 const res : Raw = await wixRequest < Raw >( "/menu-ordering-settings/v1/menu-ordering-settings/query" , {
88 body: { query: { filter: { operationId }, ... (cursor ? { cursorPaging: { cursor } } : {}) } },
89 });
90 raws. push ( ... ((res?.menuOrderingSettings ?? []) as Raw []));
91 cursor = nextCursor (res);
92 } while (cursor);
93 return toMenuOrderingMap (raws);
94 }
95
96 /**
97 * The operation's enabled pickup/delivery methods (operation.fulfillmentIds only), fees and
98 * minimums formatted in the site currency — for display.
99 * GET /fulfillment-methods/v1/fulfillment-methods[?cursorPaging.cursor=…]
100 */
101 export async function fetchFulfillmentMethods () : Promise < FulfillmentMethodInfo []> {
102 try {
103 const [ status , money , raws ] = await Promise . all ([ resolveOrdering (), fetchSiteMoney (), listFulfillmentRaws ()]);
104 return toFulfillmentMethods (raws, status.fulfillmentIds, money);
105 } catch {
106 return []; // ordering still works without the list (pickup ASAP)
107 }
108 }
109
110 // The operation's raw fulfillment methods (operation.fulfillmentIds only), read once per session.
111 let fulfillmentRawsPromise : Promise < Raw []> | null = null ;
112 function listFulfillmentRaws () : Promise < Raw []> {
113 fulfillmentRawsPromise ??= ( async () => {
114 const status = await resolveOrdering ();
115 if ( ! status.fulfillmentIds. length ) return [];
116 const wanted = new Set (status.fulfillmentIds);
117 const raws : Raw [] = [];
118 let cursor : string | null = null ;
119 do {
120 const res : Raw = await wixRequest < Raw >( "/fulfillment-methods/v1/fulfillment-methods" , { method: "GET" , query: cursor ? { "cursorPaging.cursor" : cursor } : {} });
121 raws. push ( ... ((res?.fulfillmentMethods ?? []) as Raw []));
122 cursor = nextCursor (res);
123 } while (cursor);
124 return raws. filter (( m ) => wanted. has ( rawId (m)));
125 })(). catch (( e ) => {
126 fulfillmentRawsPromise = null ; // transient failure — allow a retry on the next call
127 throw e;
128 });
129 return fulfillmentRawsPromise;
130 }
131
132 // A line's attributes.image may come back absent — join the menu items' images by the line's
133 // catalogItemId, once per item, cached for the session. GET /restaurants/menus-item/v1/items?itemIds=…
134 const lineImageCache = new Map < string , string >();
135 async function fillLineImages ( cart : OrderCart , raws : Raw []) : Promise < void > {
136 const wanted = new Map < string , number []>();
137 raws. forEach (( raw , i ) => {
138 if (cart.lines[i].imageUrl) return ;
139 const itemId = raw.source?.catalogReference?.catalogItemId;
140 if ( ! itemId) return ;
141 const cached = lineImageCache. get (itemId);
142 if (cached !== undefined ) { cart.lines[i].imageUrl = cached; return ; }
143 wanted. set (itemId, [ ... (wanted. get (itemId) ?? []), i]);
144 });
145 if ( ! wanted.size) return ;
146 try {
147 const res = await wixRequest < Raw >( "/restaurants/menus-item/v1/items" , { method: "GET" , query: { itemIds: [ ... wanted. keys ()] } });
148 for ( const item of (res?.items ?? []) as Raw []) {
149 const url = imgSrc (item.image, 300 , 300 );
150 lineImageCache. set ( rawId (item), url);
151 for ( const i of wanted. get ( rawId (item)) ?? []) cart.lines[i].imageUrl = url;
152 }
153 } catch {
154 /* images are a nicety — the cart stays correct without them */
155 }
156 }
157
158 async function readCartWithSubtotal ( raw : Raw | null ) : Promise < OrderCart > {
159 // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.
160 if ( ! raw?.lineItems?. length ) return toOrderCart (raw, "" , imgSrc);
161 // The after-discount subtotal comes from the estimate, never from hand-summing lines; fees, tax,
162 // and delivery resolve at checkout. POST /ecom/v2/carts/current/estimate {}
163 let subtotal = "" ;
164 try {
165 subtotal = estimateSubtotal ( await wixRequest < Raw >( `${ CART }/estimate` , { body: {} }), raw);
166 } catch {
167 /* the estimate is a display nicety — the cart itself is still valid */
168 }
169 const cart = toOrderCart (raw, subtotal, imgSrc);
170 await fillLineImages (cart, raw.lineItems as Raw []);
171 return cart;
172 }
173
174 // GET /ecom/v2/carts/current — a 404 means no cart yet.
175 async function getCurrentCartRaw () : Promise < Raw | null > {
176 try {
177 return ( await wixRequest < Raw >( CART , { method: "GET" }))?.cart ?? null ;
178 } catch (e) {
179 if (e instanceof WixApiError && e.status === 404 ) return null ;
180 throw e;
181 }
182 }
183
184 /** The visitor's current order cart. An empty cart (not an error) when none exists yet. */
185 export async function fetchOrderCart () : Promise < OrderCart > {
186 return readCartWithSubtotal ( await getCurrentCartRaw ());
187 }
188
189 /**
190 * Add a dish to the current order. `item` is the MenuItem DTO as rendered; `menuId` and `sectionId`
191 * are the ids of the menu and section it is RENDERED UNDER (the fetchMenus tree); `selection` is
192 * what the visitor chose (start from initialSelection(item)). Throws when ordering isn't accepting,
193 * the dish can't be ordered, a modifier rule fails, or the line is refused (a refused add still returns 200).
194 * POST /ecom/v2/carts/current/add-line-items
195 * { catalogItems: [{ quantity, catalogReference: { catalogItemId, appId: <Orders app>, options: {
196 * operationId, menuId, menuSectionId, sectionId, priceVariant?: { id, formattedPrice },
197 * modifierGroups?: [{ id, modifiers: [{ id, price, formattedPrice? }] }], specialRequests? } } }] }
198 */
199 export async function addToOrder (
200 item : MenuItem ,
201 { menuId , sectionId } : { menuId : string ; sectionId : string },
202 quantity = 1 ,
203 selection : OrderSelection = initialSelection (item),
204 ) : Promise < OrderCart > {
205 const { operationId , status } = await resolveOrdering ();
206 if ( ! operationId || status !== "ENABLED" ) throw new Error ( "Online ordering isn't available right now." );
207 assertOrderContext (item.id, menuId, sectionId);
208 assertOrderable (item);
209 const check = validateSelection (item, selection);
210 if ( ! check.ok) throw new Error (Object. values (check.errors)[ 0 ]);
211 const res = await wixRequest < Raw >( `${ CART }/add-line-items` , {
212 body: { catalogItems: [ orderCatalogItem (item, { operationId, menuId, sectionId }, quantity, selection)] },
213 });
214 assertOrderAdded (res?.cart, item.id);
215 return readCartWithSubtotal (res?.cart ?? null );
216 }
217
218 /**
219 * Change a line's quantity. `lineItemId` is OrderLine.lineItemId, never the menu item id.
220 * POST /ecom/v2/carts/current/update-line-items { lineItems: [{ lineItemId, quantity: { newQuantity } }] }
221 */
222 export async function updateOrderQuantity ( lineItemId : string , quantity : number ) : Promise < OrderCart > {
223 const res = await wixRequest < Raw >( `${ CART }/update-line-items` , { body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] } });
224 return readCartWithSubtotal (res?.cart ?? null );
225 }
226
227 /** Remove a line by OrderLine.lineItemId. POST /ecom/v2/carts/current/remove-line-items { lineItemIds } */
228 export async function removeOrderLine ( lineItemId : string ) : Promise < OrderCart > {
229 const res = await wixRequest < Raw >( `${ CART }/remove-line-items` , { body: { lineItemIds: [lineItemId] } });
230 return readCartWithSubtotal (res?.cart ?? null );
231 }
232
233 /**
234 * The times the order can be fulfilled at: ASAP for each type, then the scheduled slots of `dayKey`
235 * ("YYYY-MM-DD" at the restaurant; default today), ASAP first. Pass the delivery `address` once the
236 * visitor has entered it: the delivery times are then those of the methods that deliver there (none
237 * when no method does). [] when there is no operation. Pass the chosen entry to setOrderFulfillment as `time`.
238 * GET /restaurants-operations/v1/operations/{id}/first-available-time-slot-per-fulfillment-type[?deliveryAddress.*]
239 * GET /restaurants-operations/v1/operations/{id}/available-time-slots-per-date?date.day=&date.month=&date.year=[&deliveryAddress.*]
240 * deliveryAddress.addressLine / .city / .postalCode / .country / .subdivision
241 */
242 export async function fetchFulfillmentTimes ( dayKey ?: string , address ?: DeliveryAddress ) : Promise < FulfillmentTime []> {
243 const { operationId , timeZone } = await resolveOrdering ();
244 if ( ! operationId) return [];
245 const base = `/restaurants-operations/v1/operations/${ operationId }` ;
246 const where = slotsAddressQuery (address, address ? toRestaurantRegion ( await listFulfillmentRaws ()) : { country: "" , subdivision: "" });
247 const [ asap , slots ] = await Promise . all ([
248 wixRequest < Raw >( `${ base }/first-available-time-slot-per-fulfillment-type` , { method: "GET" , query: where }),
249 wixRequest < Raw >( `${ base }/available-time-slots-per-date` , { method: "GET" , query: { ... slotsDateQuery (dayKey ?? restaurantToday (timeZone)), ... where } }),
250 ]);
251 return toFulfillmentTimes (asap, slots);
252 }
253
254 /**
255 * Set how and when the order is fulfilled: pickup or delivery, a time from fetchFulfillmentTimes
256 * (omitted → ASAP), and for delivery the address (street, city; country and subdivision default to
257 * the restaurant's). Pickup puts the restaurant's country on the cart, which Wix requires before
258 * any delivery method. Throws NOT_DELIVERABLE when the address is outside every delivery area.
259 * PATCH /ecom/v2/carts/current { cart: { deliveryInfo: { address: { addressLine, city, postalCode, country, subdivision } } } }
260 * (the street field is addressLine; addressLine1 is dropped)
261 * POST /ecom/v2/carts/current/set-delivery-method { deliveryMethod: { code: "PICKUP|ASAP" | "<TYPE>|<startMs>|<endMs>" } }
262 */
263 export async function setOrderFulfillment ( choice : FulfillmentChoice ) : Promise < OrderCart > {
264 const address = fulfillmentAddress (choice, toRestaurantRegion ( await listFulfillmentRaws ()));
265 await wixRequest < Raw >( CART , { method: "PATCH" , body: { cart: { deliveryInfo: { address } } } });
266 try {
267 const res = await wixRequest < Raw >( `${ CART }/set-delivery-method` , { body: { deliveryMethod: { code: fulfillmentCode (choice) } } });
268 return readCartWithSubtotal (res?.cart ?? null );
269 } catch (e) {
270 if (e instanceof WixApiError && e.code === "DELIVERY_METHOD_NOT_FOUND" ) {
271 throw new Error (choice.type === "DELIVERY" ? NOT_DELIVERABLE : "This pickup time isn't available. Choose another time." );
272 }
273 throw e;
274 }
275 }
276
277 /**
278 * Start the Wix-hosted checkout for the current order and return the URL to navigate the FULL
279 * document to. The cart's id IS the checkout id. The fulfillment must be on the cart first
280 * (setOrderFulfillment); a cart with none gets pickup ASAP. Payment is collected on the hosted
281 * page. `origin` must be the site's real https origin as registered on the OAuth app's allowed
282 * domains (browser: window.location.origin) — an unlisted or http origin 403s on return.
283 * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } }
284 */
285 export async function orderCheckoutUrl ( origin : string = typeof window !== "undefined" ? window.location.origin : "" ) : Promise < string > {
286 const raw = await getCurrentCartRaw ();
287 assertOrderCheckoutable (raw);
288 if ( ! raw?.deliveryInfo?.method?.code) {
289 await setOrderFulfillment ({ type: "PICKUP" }). catch (() => {
290 throw new Error ( "Choose pickup or delivery before checkout." );
291 });
292 }
293 const res = await wixRequest < Raw >( "/headless/v1/redirect-session" , {
294 body: { ecomCheckout: { checkoutId: rawId (raw) }, callbacks: origin ? { postFlowUrl: `${ origin }/` , thankYouPageUrl: `${ origin }/` } : {} },
295 });
296 const url = res?.redirectSession?.fullUrl;
297 if ( ! url) throw new Error ( "Checkout couldn't start: no redirect URL returned." );
298 return url;
299 }