@wix/himalaya-cli 0.1527.0 → 0.1529.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +144 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/restaurants/src/extensions.d.ts +93 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +27 -5
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +19 -3
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +27 -5
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +58 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-restaurants.mjs +23 -0
  8. package/dist/cli.mjs +5844 -4606
  9. package/fixtures/wix-canned.json +83 -0
  10. package/guides/hhp.md +39 -0
  11. package/guides/himi-json-output.md +67 -1
  12. package/guides/himi-mcp.md +41 -0
  13. package/guides/iap-products.md +88 -23
  14. package/package.json +2 -2
  15. package/templates/beauty-salon/config.ts +12 -1
  16. package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +12 -5
  17. package/templates/beauty-salon/tier5-src/_shared/restaurant-ordering.ts +201 -0
  18. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  19. package/templates/branded-lite/config.ts +12 -1
  20. package/templates/branded-lite/dev/index.ts +1278 -180
  21. package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +12 -5
  22. package/templates/branded-lite/tier5-src/_shared/restaurant-ordering.ts +201 -0
  23. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  24. package/templates/branded-lite/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
  25. package/templates/branded-lite/tier5-src/himi-branded-lite-menus/index.ts +16 -5
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +1 -1
  27. package/templates/branded-lite/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
  28. package/templates/branded-lite/tier5-src/wix-spaces-food-orders/index.ts +807 -55
  29. package/templates/community-club/config.ts +12 -1
  30. package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +12 -5
  31. package/templates/events-meetups/config.ts +12 -1
  32. package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +12 -5
  33. package/templates/gym-fitness/config.ts +12 -1
  34. package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +12 -5
  35. package/templates/gym-fitness/tier5-src/_shared/restaurant-ordering.ts +201 -0
  36. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  37. package/templates/nonprofit-cause/config.ts +12 -1
  38. package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +12 -5
  39. package/templates/podcast-creator/config.ts +12 -1
  40. package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +12 -5
  41. package/templates/portfolio-creative/config.ts +12 -1
  42. package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +12 -5
  43. package/templates/portfolio-creative/tier5-src/_shared/restaurant-ordering.ts +201 -0
  44. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  45. package/templates/restaurant/README.md +1 -1
  46. package/templates/restaurant/config.ts +12 -1
  47. package/templates/restaurant/dev/index.ts +1373 -275
  48. package/templates/restaurant/himalaya.template.json +2 -1
  49. package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +12 -5
  50. package/templates/restaurant/tier5-src/_shared/member-orders.ts +8 -0
  51. package/templates/restaurant/tier5-src/_shared/restaurant-ordering.ts +201 -0
  52. package/templates/restaurant/tier5-src/himi-branded-lite-menu-item/index.ts +144 -77
  53. package/templates/restaurant/tier5-src/himi-branded-lite-menus/index.ts +16 -5
  54. package/templates/restaurant/tier5-src/wix-spaces-food-order-detail/index.ts +76 -14
  55. package/templates/restaurant/tier5-src/wix-spaces-food-orders/index.ts +807 -55
  56. package/templates/retail-store/config.ts +12 -1
  57. package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +12 -5
  58. package/templates/retail-store/tier5-src/_shared/restaurant-ordering.ts +201 -0
  59. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +6 -0
  60. package/templates/yoga-studio/config.ts +12 -1
  61. package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +12 -5
@@ -345,7 +345,7 @@ export default defineApp({
345
345
  // Your on-device database. It must declare every entity a worker reads or writes,
346
346
  // or those calls fail closed — keep it in step with `local_database` above.
347
347
  dataSchema: {
348
- "version": 1,
348
+ "version": 2,
349
349
  "entities": [
350
350
  {
351
351
  "name": "products",
@@ -470,6 +470,17 @@ export default defineApp({
470
470
  }
471
471
  ]
472
472
  },
473
+ {
474
+ "name": "food_orders",
475
+ "encrypted": true,
476
+ "fields": [
477
+ {
478
+ "name": "id",
479
+ "type": "string",
480
+ "required": true
481
+ }
482
+ ]
483
+ },
473
484
  {
474
485
  "name": "member_bookings",
475
486
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }
@@ -317,7 +317,7 @@ export default defineApp({
317
317
  // Your on-device database. It must declare every entity a worker reads or writes,
318
318
  // or those calls fail closed — keep it in step with `local_database` above.
319
319
  dataSchema: {
320
- "version": 1,
320
+ "version": 2,
321
321
  "entities": [
322
322
  {
323
323
  "name": "products",
@@ -442,6 +442,17 @@ export default defineApp({
442
442
  }
443
443
  ]
444
444
  },
445
+ {
446
+ "name": "food_orders",
447
+ "encrypted": true,
448
+ "fields": [
449
+ {
450
+ "name": "id",
451
+ "type": "string",
452
+ "required": true
453
+ }
454
+ ]
455
+ },
445
456
  {
446
457
  "name": "member_bookings",
447
458
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }
@@ -475,7 +475,7 @@ export default defineApp({
475
475
  // Your on-device database. It must declare every entity a worker reads or writes,
476
476
  // or those calls fail closed — keep it in step with `local_database` above.
477
477
  dataSchema: {
478
- "version": 1,
478
+ "version": 2,
479
479
  "entities": [
480
480
  {
481
481
  "name": "products",
@@ -600,6 +600,17 @@ export default defineApp({
600
600
  }
601
601
  ]
602
602
  },
603
+ {
604
+ "name": "food_orders",
605
+ "encrypted": true,
606
+ "fields": [
607
+ {
608
+ "name": "id",
609
+ "type": "string",
610
+ "required": true
611
+ }
612
+ ]
613
+ },
603
614
  {
604
615
  "name": "member_bookings",
605
616
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }
@@ -0,0 +1,201 @@
1
+ // Restaurants online ordering — Wix's visitor "Place an order from a custom storefront" flow,
2
+ // shared by the dish detail (himi-branded-lite-menu-item: add a dish to the Wix cart) and the
3
+ // order screen (wix-spaces-food-orders: cart → pickup/delivery → hosted checkout → history).
4
+ //
5
+ // The cart is the site's eCom CURRENT cart (Cart V2). It is the same cart the Stores cart screen
6
+ // reads (V1 and V2 answer the same cart id), so a restaurant line is told apart by its catalog
7
+ // app — Restaurants Orders — never assumed. Every call here is visitor-scope; the member's placed
8
+ // orders are read through the eCom orders search the member Orders screen already uses, never
9
+ // through the owner-capable Restaurants Orders API.
10
+ import {
11
+ getFirstAvailableTimeSlotPerFulfillmentType,
12
+ listFulfillmentMethods,
13
+ listOperations,
14
+ queryMenus,
15
+ querySections,
16
+ type WixFulfillmentMethod,
17
+ type WixFulfillmentTimeSlot,
18
+ type WixMenu,
19
+ type WixOperation,
20
+ type WixRestaurantsAddress,
21
+ type WixSection,
22
+ } from "@himalaya/wix-restaurants";
23
+ import type { WixCartV2, WixCartV2LineItem, WixCatalogReference } from "@himalaya/wix-ecom";
24
+ import { formatCurrency } from "../_shared/engine-intl.js";
25
+
26
+ /** Wix Restaurants Orders — the catalog app a restaurant cart line belongs to. */
27
+ export const RESTAURANTS_ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60";
28
+
29
+ export type FulfillmentType = "PICKUP" | "DELIVERY";
30
+
31
+ /** The operation a visitor orders from: an `ENABLED` one, the site's default first. None means
32
+ * online ordering is closed (`DISABLED`, `PAUSED_UNTIL`, or no operation at all). */
33
+ export function orderingOperation(operations: WixOperation[] | undefined, selection: { businessLocationId?: string; operationId?: string } = {}): WixOperation | undefined {
34
+ const open = (operations ?? []).filter((operation) => !!operation?.id && operation.onlineOrderingStatus === "ENABLED");
35
+ if (selection.operationId) return open.find((operation) => operation.id === selection.operationId);
36
+ if (selection.businessLocationId) return open.find((operation) => operation.businessLocationId === selection.businessLocationId);
37
+ return open.find((operation) => operation.default === true) ?? open[0];
38
+ }
39
+
40
+ export async function loadOrderingOperation(selection: { businessLocationId?: string; operationId?: string } = {}): Promise<WixOperation | undefined> {
41
+ return orderingOperation((await listOperations()).operations, selection);
42
+ }
43
+
44
+ /** The enabled pickup/delivery methods [operation] offers: the ones its `fulfillmentIds` name. An
45
+ * operation that names none falls back only to methods at its own business location — never to
46
+ * every method on the site, which on a multi-location site would offer another location's pickup. */
47
+ export function offeredMethods(operation: WixOperation, methods: WixFulfillmentMethod[] | undefined): WixFulfillmentMethod[] {
48
+ const ids = new Set(operation.fulfillmentIds ?? []);
49
+ const belongs = (method: WixFulfillmentMethod) => ids.size > 0
50
+ ? ids.has(method.id)
51
+ : !!operation.businessLocationId && method.businessLocationId === operation.businessLocationId;
52
+ return (methods ?? []).filter((method) =>
53
+ !!method?.id && method.enabled === true && (method.type === "PICKUP" || method.type === "DELIVERY") && belongs(method));
54
+ }
55
+
56
+ export async function loadOfferedMethods(operation: WixOperation): Promise<WixFulfillmentMethod[]> {
57
+ const all: WixFulfillmentMethod[] = [];
58
+ let cursor = "";
59
+ // A site has a handful of methods; five pages of 100 is a bound, not an expectation.
60
+ for (let page = 0; page < 5; page++) {
61
+ const response = await listFulfillmentMethods({ limit: 100, ...(cursor ? { cursor } : {}) });
62
+ all.push(...(response.fulfillmentMethods ?? []));
63
+ cursor = response.pagingMetadata?.hasNext ? response.pagingMetadata.cursors?.next ?? "" : "";
64
+ if (!cursor) break;
65
+ }
66
+ return offeredMethods(operation, all);
67
+ }
68
+
69
+ /** A decimal string from Wix ("6.99", "0") as a number; anything else is 0 (a missing fee is free). */
70
+ export function decimal(value: unknown): number {
71
+ const n = typeof value === "number" ? value : Number(typeof value === "string" ? value.trim() : NaN);
72
+ return Number.isFinite(n) ? n : 0;
73
+ }
74
+
75
+ /** Cart V2 money carries no formatted string: format the decimal with the cart's currency. */
76
+ export function money(amount: unknown, currency: string | undefined): string {
77
+ return formatCurrency(decimal(amount), (currency ?? "").trim() || "USD", "en");
78
+ }
79
+
80
+ export function cartCurrency(cart: WixCartV2 | undefined): string {
81
+ return cart?.businessInfo?.currencyCode || cart?.customerInfo?.currencyCode || "";
82
+ }
83
+
84
+ /** Where a dish is ordered from. Checkout validates the line against `menuSectionId`, so it must
85
+ * be a visible section that lists the dish, inside a visible menu that lists the section. */
86
+ export interface DishPlacement {
87
+ menuId: string;
88
+ sectionId: string;
89
+ businessLocationId?: string;
90
+ }
91
+
92
+ /** The section the dish was shown under wins when it qualifies; otherwise the first visible
93
+ * menu → section that lists it. None → the dish is on no orderable menu. */
94
+ export function pickPlacement(
95
+ dishId: string,
96
+ menus: WixMenu[],
97
+ sections: WixSection[],
98
+ hint: { menuId?: string; sectionId?: string } = {},
99
+ ): DishPlacement | undefined {
100
+ const listing = new Map(sections
101
+ .filter((section) => !!section?.id && section.visible !== false && (section.itemIds ?? []).includes(dishId))
102
+ .map((section) => [section.id, section]));
103
+ const candidates: DishPlacement[] = [];
104
+ for (const menu of menus) {
105
+ if (!menu?.id || menu.visible === false) continue;
106
+ for (const sectionId of menu.sectionIds ?? []) {
107
+ if (listing.has(sectionId)) candidates.push({ menuId: menu.id, sectionId, businessLocationId: menu.businessLocationId });
108
+ }
109
+ }
110
+ return candidates.find((c) => c.sectionId === hint.sectionId && (!hint.menuId || c.menuId === hint.menuId)) ?? candidates[0];
111
+ }
112
+
113
+ export async function loadDishPlacement(dishId: string, hint: { menuId?: string; sectionId?: string }): Promise<DishPlacement | undefined> {
114
+ const [sections, menus] = await Promise.all([
115
+ querySections({ paging: { limit: 100 } }),
116
+ queryMenus({ paging: { limit: 50 } }),
117
+ ]);
118
+ return pickPlacement(dishId, menus.menus ?? [], sections.sections ?? [], hint);
119
+ }
120
+
121
+ /** The catalog reference for one dish line. There is no `variantId` key in `options`. */
122
+ export function dishReference(dishId: string, operationId: string, placement: DishPlacement): WixCatalogReference {
123
+ return {
124
+ appId: RESTAURANTS_ORDERS_APP_ID,
125
+ catalogItemId: dishId,
126
+ options: {
127
+ operationId,
128
+ menuId: placement.menuId,
129
+ menuSectionId: placement.sectionId,
130
+ sectionId: placement.sectionId,
131
+ },
132
+ };
133
+ }
134
+
135
+ export function isRestaurantLine(line: WixCartV2LineItem | undefined): boolean {
136
+ return line?.source?.catalogReference?.appId === RESTAURANTS_ORDERS_APP_ID;
137
+ }
138
+
139
+ function lineVariantId(line: WixCartV2LineItem): string {
140
+ const variant = (line.source?.catalogReference?.options as { priceVariant?: { id?: unknown } } | undefined)?.priceVariant;
141
+ return typeof variant?.id === "string" ? variant.id : "";
142
+ }
143
+
144
+ /** The cart line Wix kept for [reference] — same dish, operation, menu, section and variant — or
145
+ * undefined when Wix dropped it. Wix can refuse an item and still answer 200, so the returned cart
146
+ * is the proof. The operation and menu matter: the same dish added from two restaurant locations
147
+ * is two lines, and only the one just added proves this add. */
148
+ export function addedLine(cart: WixCartV2 | undefined, reference: WixCatalogReference): WixCartV2LineItem | undefined {
149
+ type PlacementOptions = { operationId?: unknown; menuId?: unknown; menuSectionId?: unknown };
150
+ const options = (reference.options ?? {}) as PlacementOptions & { priceVariant?: { id?: unknown } };
151
+ const variantId = typeof options.priceVariant?.id === "string" ? options.priceVariant.id : "";
152
+ return (cart?.lineItems ?? []).find((line) => {
153
+ const ref = line.source?.catalogReference;
154
+ if (!ref || ref.appId !== reference.appId || ref.catalogItemId !== reference.catalogItemId) return false;
155
+ const lineOptions = (ref.options ?? {}) as PlacementOptions;
156
+ if (lineOptions.operationId !== options.operationId || lineOptions.menuId !== options.menuId) return false;
157
+ if (lineOptions.menuSectionId !== options.menuSectionId) return false;
158
+ if (lineVariantId(line) !== variantId) return false;
159
+ if (line.status === "OUT_OF_STOCK" || line.status === "REMOVED_FROM_CATALOG") return false;
160
+ const confirmed = line.quantityInfo?.confirmedQuantity;
161
+ return confirmed === undefined || confirmed > 0;
162
+ });
163
+ }
164
+
165
+ export function lineQuantity(line: WixCartV2LineItem): number {
166
+ const q = line.quantityInfo?.confirmedQuantity ?? line.quantityInfo?.requestedQuantity ?? 1;
167
+ return Number.isFinite(q) && q >= 0 ? Math.floor(q) : 0;
168
+ }
169
+
170
+ /** How many dishes the cart holds (restaurant lines only). */
171
+ export function restaurantItemCount(cart: WixCartV2 | undefined): number {
172
+ return (cart?.lineItems ?? []).filter(isRestaurantLine).reduce((total, line) => total + lineQuantity(line), 0);
173
+ }
174
+
175
+ export function slotFor(slots: WixFulfillmentTimeSlot[] | undefined, type: FulfillmentType): WixFulfillmentTimeSlot | undefined {
176
+ return (slots ?? []).find((slot) => slot?.fulfilmentType === type);
177
+ }
178
+
179
+ /** The `deliveryMethod.code` for [type]: `<TYPE>|ASAP`, or the slot's own window for a pre-order.
180
+ * An ASAP slot must be sent as ASAP — its start/end window is refused as `DELIVERY_METHOD_NOT_FOUND`. */
181
+ export function deliveryCode(type: FulfillmentType, slot?: WixFulfillmentTimeSlot): string {
182
+ const window = slot?.timeSlot;
183
+ if (window?.orderSchedulingType === "PREORDER") {
184
+ const start = Date.parse(window.startTime ?? ""), end = Date.parse(window.endTime ?? "");
185
+ if (Number.isFinite(start) && Number.isFinite(end)) return `${type}|${start}|${end}`;
186
+ }
187
+ return `${type}|ASAP`;
188
+ }
189
+
190
+ export async function firstSlots(operationId: string, deliveryAddress?: WixRestaurantsAddress): Promise<WixFulfillmentTimeSlot[]> {
191
+ return (await getFirstAvailableTimeSlotPerFulfillmentType(operationId, deliveryAddress)).timeslotsPerFulfillmentType ?? [];
192
+ }
193
+
194
+ /** The pickup address the cart needs before a pickup method can be set: the restaurant's own
195
+ * country (and region when Wix gives one) — Wix requires an address even for pickup. */
196
+ export function pickupCartAddress(method: WixFulfillmentMethod | undefined, fallbackCountry = ""): WixRestaurantsAddress | undefined {
197
+ const address = method?.pickupOptions?.address;
198
+ const country = (address?.country || fallbackCountry).trim();
199
+ if (!country) return undefined;
200
+ return { country, ...(address?.subdivision ? { subdivision: address.subdivision } : {}) };
201
+ }
@@ -1,6 +1,7 @@
1
1
  import { withOnlineWrites } from "../_shared/online-actions.js";
2
2
  import { defineService as registerState } from "@himalaya/state";
3
3
  import { getStoreProduct } from "../_shared/himi-store.js";
4
+ import { RESTAURANTS_ORDERS_APP_ID } from "../_shared/restaurant-ordering.js";
4
5
  import { iapPriceCopy } from "../_shared/iap-worker.js";
5
6
  import { withIapPresentation } from "../_shared/iap-presentation.js";
6
7
  import { type IapCommerce, classifyCart } from "@himalaya/iap-checkout";
@@ -523,6 +524,11 @@ const svc = defineService<State, {
523
524
  const commerce = cartPricing;
524
525
  const items = lines.map(line => (line as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference);
525
526
  if (items.some(item => !item?.catalogItemId || !item.appId)) return derive({ ...state, checkoutHint: iapPriceCopy("cart").purchaseUnavailable });
527
+ // Restaurant dishes share this Wix cart but check out on the food order screen; this checkout
528
+ // takes Stores products only, so say which lines are in the way rather than "unavailable".
529
+ if (items.some(item => item!.appId === RESTAURANTS_ORDERS_APP_ID)) {
530
+ return derive({ ...state, checkoutHint: copyParam("cart.restaurantItemsHint", "This cart also has restaurant dishes. Remove them here to check out your store items.") });
531
+ }
526
532
  await commerce.loadPrices(items.map((item, index) => ({ reference: { kind: "storesProduct", id: item!.catalogItemId }, commerceDisplayPrice: lines[index].price?.formattedAmount })));
527
533
  const classification = classifyCart(items.map((item, index) => ({ route: commerce.pricing.view({ kind: "storesProduct", id: item!.catalogItemId }).route, quantity: lines[index].quantity ?? 1 })));
528
534
  if (classification !== "regular") return derive({ ...state, checkoutHint: classification === "mixedRefused" ? copyParam("cart.iapSingleItemOnly", "Buy digital items individually, with quantity 1 and no options.") : iapPriceCopy("cart").purchaseUnavailable });
@@ -362,7 +362,7 @@ export default defineApp({
362
362
  // Your on-device database. It must declare every entity a worker reads or writes,
363
363
  // or those calls fail closed — keep it in step with `local_database` above.
364
364
  dataSchema: {
365
- "version": 1,
365
+ "version": 2,
366
366
  "entities": [
367
367
  {
368
368
  "name": "products",
@@ -487,6 +487,17 @@ export default defineApp({
487
487
  }
488
488
  ]
489
489
  },
490
+ {
491
+ "name": "food_orders",
492
+ "encrypted": true,
493
+ "fields": [
494
+ {
495
+ "name": "id",
496
+ "type": "string",
497
+ "required": true
498
+ }
499
+ ]
500
+ },
490
501
  {
491
502
  "name": "member_bookings",
492
503
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }
@@ -368,7 +368,7 @@ export default defineApp({
368
368
  // Your on-device database. It must declare every entity a worker reads or writes,
369
369
  // or those calls fail closed — keep it in step with `local_database` above.
370
370
  dataSchema: {
371
- "version": 1,
371
+ "version": 2,
372
372
  "entities": [
373
373
  {
374
374
  "name": "products",
@@ -493,6 +493,17 @@ export default defineApp({
493
493
  }
494
494
  ]
495
495
  },
496
+ {
497
+ "name": "food_orders",
498
+ "encrypted": true,
499
+ "fields": [
500
+ {
501
+ "name": "id",
502
+ "type": "string",
503
+ "required": true
504
+ }
505
+ ]
506
+ },
496
507
  {
497
508
  "name": "member_bookings",
498
509
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }
@@ -401,7 +401,7 @@ export default defineApp({
401
401
  // Your on-device database. It must declare every entity a worker reads or writes,
402
402
  // or those calls fail closed — keep it in step with `local_database` above.
403
403
  dataSchema: {
404
- "version": 1,
404
+ "version": 2,
405
405
  "entities": [
406
406
  {
407
407
  "name": "products",
@@ -526,6 +526,17 @@ export default defineApp({
526
526
  }
527
527
  ]
528
528
  },
529
+ {
530
+ "name": "food_orders",
531
+ "encrypted": true,
532
+ "fields": [
533
+ {
534
+ "name": "id",
535
+ "type": "string",
536
+ "required": true
537
+ }
538
+ ]
539
+ },
529
540
  {
530
541
  "name": "member_bookings",
531
542
  "encrypted": true,
@@ -133,15 +133,18 @@ export function itemLabels(value: unknown, limit = 3): string[] {
133
133
  }
134
134
 
135
135
  /** Price-variant rows ({ name, priceLine }) from the item's loose
136
- * `priceVariants.variants` payload — [] when the item has a single price. */
137
- export function itemPriceVariants(item: WixItem): Array<{ id: string; name: string; priceLine: string }> {
136
+ * `priceVariants.variants` payload — [] when the item has a single price.
137
+ * `variantId` is Wix's own id for the variant (live rows carry `variantId`; older
138
+ * fixtures `id`), "" when the payload has none — a synthetic `variant-<n>` row id is
139
+ * only for the UI and must never be sent to Wix. `price` is the decimal price. */
140
+ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> {
138
141
  const raw = item as unknown as { priceVariants?: { variants?: unknown[] } };
139
142
  const variants = raw.priceVariants?.variants;
140
143
  if (!Array.isArray(variants)) return [];
141
- const rows: Array<{ id: string; name: string; priceLine: string }> = [];
144
+ const rows: Array<{ id: string; variantId: string; name: string; priceLine: string; price: string }> = [];
142
145
  for (const [index, entry] of variants.entries()) {
143
146
  if (!entry || typeof entry !== "object") continue;
144
- const node = entry as { id?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown } };
147
+ const node = entry as { id?: unknown; variantId?: unknown; name?: unknown; priceInfo?: { price?: unknown; currency?: unknown; formattedPrice?: unknown } };
145
148
  const name = typeof node.name === "string" ? cleanTitle(node.name) : "";
146
149
  const price = node.priceInfo?.price;
147
150
  if (price == null || price === "" || Number.isNaN(Number(price))) continue;
@@ -152,7 +155,11 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; name: stri
152
155
  priceLine = formatCurrency(Number(price), currency, "en");
153
156
  } catch { /* keep the bare amount */ }
154
157
  }
155
- rows.push({ id: typeof node.id === "string" && node.id ? node.id : `variant-${index}`, name: name || "Option", priceLine });
158
+ const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
159
+ if (formatted) priceLine = normalizeFormattedPrice(formatted);
160
+ const variantId = typeof node.variantId === "string" && node.variantId ? node.variantId
161
+ : typeof node.id === "string" && node.id ? node.id : "";
162
+ rows.push({ id: variantId || `variant-${index}`, variantId, name: name || "Option", priceLine, price: `${price}` });
156
163
  }
157
164
  return rows;
158
165
  }