@wix/agent-skills 1.19.2 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
  3. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
  4. package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
  5. package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
  6. package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
  7. package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
  8. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
  9. package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
  10. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
  11. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
  12. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
  13. package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
  14. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
  15. package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
  16. package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
  17. package/skills/wix-headless/references/CAPABILITIES.md +2 -2
  18. package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
  19. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
  20. package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
  21. package/skills/wix-headless-fast/SKILL.md +200 -34
  22. package/skills/wix-headless-fast/entry/skill.md +10 -2
  23. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  24. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  25. package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
  26. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  27. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  28. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  29. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  30. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  32. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  33. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  34. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  35. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  36. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  37. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  38. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  39. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  40. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  41. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  42. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  43. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  44. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  48. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  49. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  50. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  51. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  52. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  53. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  54. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  55. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  56. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  57. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  59. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  60. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  61. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  62. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  63. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  64. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  66. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  67. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  68. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  69. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  71. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  74. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  75. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  76. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  77. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  78. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  79. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  80. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  85. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  86. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  87. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  88. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  100. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  104. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  107. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  112. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  113. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  114. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  115. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  116. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  117. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  118. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  119. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  120. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  122. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  126. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  127. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  129. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  130. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  131. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  132. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  133. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  134. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  135. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  136. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  137. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  142. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  143. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  144. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  145. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  146. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  147. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
  148. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  149. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  150. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  151. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  152. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
  153. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
  154. package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
  155. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  156. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
  157. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
  158. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
  159. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  160. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
  161. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  162. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
  163. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
  166. package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
  167. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  168. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  169. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
  170. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
  171. package/skills/wix-manage/SKILL.md +4 -1
  172. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  173. package/skills/wix-manage/references/events/create-wix-event.md +46 -0
  174. package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
  175. package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
  176. package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
  177. package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
  178. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
  179. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
  180. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  181. package/skills/wix-vibe-headless/platforms/base44.md +19 -4
  182. package/skills/wix-vibe-headless/platforms/generic.md +6 -0
  183. package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
  184. package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
  185. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
  186. package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
  187. package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
  188. package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
  189. package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
  190. package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
  191. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
  192. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
  193. package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
  194. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
  195. package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
  196. package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
  197. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
  198. package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
  199. package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
  200. package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
  201. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
  202. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
  203. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
  204. package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
  205. package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
  206. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
  207. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
  208. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
  209. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
  210. package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
  211. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
  212. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
  213. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  214. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  215. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,124 @@
1
+ // Online-ordering rules and DTO mapping — transport-agnostic, imported by both ./ordering.ts (SDK)
2
+ // and the REST twin in references/restaurants/rest/ordering.ts (fetch). The restaurant cart rides
3
+ // on the eCom current cart (Cart V2); what makes a line a RESTAURANT line is the catalogReference
4
+ // built here: the Orders app id plus options { operationId, menuId, sectionId } — all three, and
5
+ // no variantId. Raw entities carry `_id` (SDK) or `id` (REST). Has its own formatMoney (a copy of
6
+ // ../money.ts) so it stands alone when stripped. Imports are type-only.
7
+ import type { FulfillmentMethodInfo, OrderCart, OrderLine } from "./types";
8
+ import type { ImgSrc, Raw } from "./menu-core";
9
+
10
+ /** The Restaurants Orders app id — every restaurant cart line's catalogReference.appId. */
11
+ export const RESTAURANTS_ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60";
12
+
13
+ export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
14
+
15
+ /**
16
+ * Cart V2 money is ConvertedMoney { amount, convertedAmount } with NO formatted string, and the
17
+ * currency lives on the cart, not on the money. Never hardcode a symbol or assume USD.
18
+ */
19
+ export function formatMoney(money: Raw | null | undefined, currencyCode: string | null | undefined): string {
20
+ const value = money?.convertedAmount ?? money?.amount;
21
+ if (value == null || value === "") return "";
22
+ const currency = currencyCode || "USD";
23
+ try {
24
+ return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(Number(value));
25
+ } catch {
26
+ return `${value} ${currency}`;
27
+ }
28
+ }
29
+
30
+ export const cartCurrency = (raw: Raw | null | undefined): string =>
31
+ raw?.customerInfo?.currencyCode ?? raw?.businessInfo?.currencyCode ?? "";
32
+
33
+ /** The operation to order through: ENABLED, else the default, else the first; null when none exists. */
34
+ export function pickOperationId(operations: Raw[]): string | null {
35
+ const op = operations.find((o) => o.onlineOrderingStatus === "ENABLED") ?? operations.find((o) => o.default) ?? operations[0];
36
+ return op ? rawId(op) || null : null;
37
+ }
38
+
39
+ /** Enabled pickup/delivery methods, for display — fees and minimums as decimal strings. */
40
+ export function toFulfillmentMethods(raws: Raw[]): FulfillmentMethodInfo[] {
41
+ return raws
42
+ .filter((m) => m.enabled === true)
43
+ .map((m): FulfillmentMethodInfo => ({
44
+ id: rawId(m),
45
+ type: m.type === "DELIVERY" ? "DELIVERY" : "PICKUP",
46
+ name: m.name ?? "",
47
+ fee: m.fee ?? "0",
48
+ minOrderPrice: m.minOrderPrice ?? "0",
49
+ }))
50
+ .filter((m) => m.id);
51
+ }
52
+
53
+ export function toOrderLine(raw: Raw, currency: string, imgSrc: ImgSrc): OrderLine {
54
+ return {
55
+ lineItemId: rawId(raw), // the LINE id — what update/remove take, never the menu item id
56
+ itemName: raw.name?.original ?? "",
57
+ quantity: raw.quantityInfo?.confirmedQuantity ?? 0,
58
+ unitPrice: formatMoney(raw.pricing?.unitPrice, currency),
59
+ linePrice: formatMoney(raw.pricing?.totalPrice, currency),
60
+ imageUrl: imgSrc(raw.attributes?.image, 300, 300),
61
+ descriptionLines: ((raw.attributes?.descriptionLines ?? []) as Raw[])
62
+ .map((d) => {
63
+ const label = d.name?.original ?? "", value = d.plainText?.original ?? d.colorInfo?.original ?? "";
64
+ return label && value ? `${label}: ${value}` : value || label;
65
+ })
66
+ .filter(Boolean),
67
+ status: raw.status ?? "IN_STOCK", // not IN_STOCK → the line can't be checked out as-is
68
+ };
69
+ }
70
+
71
+ export function toOrderCart(raw: Raw | null, subtotal: string, imgSrc: ImgSrc): OrderCart {
72
+ const currency = cartCurrency(raw);
73
+ const lines = ((raw?.lineItems ?? []) as Raw[]).map((l) => toOrderLine(l, currency, imgSrc));
74
+ return { lines, itemCount: lines.reduce((n, l) => n + l.quantity, 0), subtotal, currency };
75
+ }
76
+
77
+ /** The after-discount subtotal from a cart estimate; "" when unknown. Fees, tax, delivery resolve at checkout. */
78
+ export function estimateSubtotal(estimate: Raw | null | undefined, raw: Raw): string {
79
+ return formatMoney(estimate?.summary?.priceSummary?.subtotal, cartCurrency(raw));
80
+ }
81
+
82
+ /** The line-refusal reasons an add can't recover from — checked before any call. */
83
+ export function assertOrderContext(itemId: string, menuId: string, sectionId: string): void {
84
+ if (!itemId || !menuId || !sectionId) throw new Error("addToOrder needs the item, menu, and section ids.");
85
+ }
86
+
87
+ /**
88
+ * The one catalogItems entry of an add: the Orders app id and the three context ids — the
89
+ * restaurant analog of the store's variantId. Modifier/variant selections are NOT sent (that
90
+ * options shape isn't documented for a client add); quantity only.
91
+ */
92
+ export function orderCatalogItem(itemId: string, ctx: { operationId: string; menuId: string; sectionId: string }, quantity: number): Raw {
93
+ return {
94
+ quantity,
95
+ catalogReference: {
96
+ catalogItemId: itemId,
97
+ appId: RESTAURANTS_ORDERS_APP_ID,
98
+ options: { operationId: ctx.operationId, menuId: ctx.menuId, sectionId: ctx.sectionId },
99
+ },
100
+ };
101
+ }
102
+
103
+ /** Read the add result — a refused add still returns 200: a line whose status isn't IN_STOCK, or no line, is a refusal. */
104
+ export function assertOrderAdded(cart: Raw | null | undefined, itemId: string): void {
105
+ // V2 nests the reference under `source` — a top-level lineItem.catalogReference no longer exists.
106
+ const line = ((cart?.lineItems ?? []) as Raw[]).find((l) => l.source?.catalogReference?.catalogItemId === itemId);
107
+ if (line?.status && line.status !== "IN_STOCK") {
108
+ throw new Error(`This dish isn't available right now (${String(line.status).toLowerCase().replace(/_/g, " ")}).`);
109
+ }
110
+ if (!line || line.quantityInfo?.confirmedQuantity === 0) {
111
+ throw new Error("The dish couldn't be added to the order — please try again.");
112
+ }
113
+ }
114
+
115
+ /** Refuse checkout for an empty order or any line not IN_STOCK — say which. */
116
+ export function assertOrderCheckoutable(raw: Raw | null): void {
117
+ const lines: Raw[] = raw?.lineItems ?? [];
118
+ if (!lines.length) throw new Error("Your order is empty.");
119
+ const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
120
+ if (unavailable.length) {
121
+ throw new Error(`Some dishes are no longer available: ${unavailable.map((l) => l.name?.original).filter(Boolean).join(", ")}.`);
122
+ }
123
+ if (!rawId(raw)) throw new Error("Checkout couldn't start: the order has no id.");
124
+ }
@@ -1,13 +1,13 @@
1
- // Online ordering (Restaurants Orders + Wix eCom Cart V2) — the only file that touches the
2
- // raw cart and operation entities. Copy as-is; extend by calling these exports, never by
3
- // editing them. The request shapes are exact and rewriting them is how carts break:
1
+ // Online ordering (Restaurants Orders + Wix eCom Cart V2) over the SDK — the only file that
2
+ // touches the raw cart and operation entities on this transport. Rules and mappers live in
3
+ // ./ordering-core (shared with the REST twin in references/restaurants/rest/); this file is the
4
+ // transport only. Copy as-is; extend by calling these exports, never by editing them. The request
5
+ // shapes are exact and rewriting them is how carts break:
4
6
  // - the catalogReference appId is the ORDERS app (9a5d83fd-…), never the Stores id;
5
- // - options MUST carry operationId + menuId + sectionId (all three) — the restaurant
6
- // analog of the store's variantId; there is NO variantId here;
7
- // - modifier/variant selections on the cart line are NOT sent — that options shape isn't
8
- // documented for client add-to-cart (display modifiers; send quantity only).
7
+ // - options MUST carry operationId + menuId + sectionId (all three) — no variantId;
8
+ // - modifier/variant selections are NOT sent on the line (undocumented for a client add).
9
9
  // Failures are loud: throws on a missing ordering operation, unavailable lines, and an empty
10
- // cart at checkout — surface the message, don't swallow it.
10
+ // order at checkout — surface the message, don't swallow it.
11
11
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/operations/list-operations.md
12
12
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/fulfillment-methods/list-fulfillment-methods.md
13
13
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/menus/items/items/list-items.md
@@ -20,8 +20,22 @@ import { currentCartV2 } from "@wix/ecom";
20
20
  import { redirects as redirectsModule } from "@wix/redirects";
21
21
  import { wixModule } from "../sdk";
22
22
  import { imgSrc } from "../media";
23
- import { formatMoney } from "../money";
24
- import type { FulfillmentMethodInfo, OrderCart, OrderLine } from "./types";
23
+ import {
24
+ RESTAURANTS_ORDERS_APP_ID,
25
+ assertOrderAdded,
26
+ assertOrderCheckoutable,
27
+ assertOrderContext,
28
+ estimateSubtotal,
29
+ orderCatalogItem,
30
+ pickOperationId,
31
+ rawId,
32
+ toFulfillmentMethods,
33
+ toOrderCart,
34
+ } from "./ordering-core";
35
+ import type { Raw } from "./menu-core";
36
+ import type { FulfillmentMethodInfo, OrderCart } from "./types";
37
+
38
+ export { RESTAURANTS_ORDERS_APP_ID };
25
39
 
26
40
  const operationsApi = wixModule(operationsModule);
27
41
  const fulfillmentApi = wixModule(fulfillmentModule);
@@ -29,11 +43,6 @@ const itemsApi = wixModule(itemsModule);
29
43
  const cartApi = wixModule(currentCartV2);
30
44
  const redirects = wixModule(redirectsModule);
31
45
 
32
- /** The Restaurants Orders app id — every restaurant cart line's catalogReference.appId. */
33
- export const RESTAURANTS_ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60";
34
-
35
- type Raw = Record<string, any>;
36
-
37
46
  // The ordering operation is site config — resolve once, reuse for every add.
38
47
  let operationIdPromise: Promise<string | null> | null = null;
39
48
 
@@ -44,12 +53,7 @@ let operationIdPromise: Promise<string | null> | null = null;
44
53
  export function resolveOperationId(): Promise<string | null> {
45
54
  operationIdPromise ??= operationsApi
46
55
  .listOperations()
47
- .then((res: Raw) => {
48
- const ops: Raw[] = res.operations ?? [];
49
- const op =
50
- ops.find((o: Raw) => o.onlineOrderingStatus === "ENABLED") ?? ops.find((o: Raw) => o.default) ?? ops[0];
51
- return op?._id ?? null;
52
- })
56
+ .then((res: Raw) => pickOperationId(res.operations ?? []))
53
57
  .catch(() => {
54
58
  operationIdPromise = null; // transient failure — allow a retry on the next call
55
59
  return null;
@@ -61,34 +65,24 @@ export function resolveOperationId(): Promise<string | null> {
61
65
  export async function fetchFulfillmentMethods(): Promise<FulfillmentMethodInfo[]> {
62
66
  try {
63
67
  const res: Raw = await fulfillmentApi.listFulfillmentMethods();
64
- return (res.fulfillmentMethods ?? [])
65
- .filter((m: Raw) => m.enabled === true)
66
- .map((m: Raw): FulfillmentMethodInfo => ({
67
- id: m._id ?? "",
68
- type: m.type === "DELIVERY" ? "DELIVERY" : "PICKUP",
69
- name: m.name ?? "",
70
- fee: m.fee ?? "0",
71
- minOrderPrice: m.minOrderPrice ?? "0",
72
- }))
73
- .filter((m: FulfillmentMethodInfo) => m.id);
68
+ return toFulfillmentMethods(res.fulfillmentMethods ?? []);
74
69
  } catch {
75
70
  return []; // display nicety — ordering still works without the list
76
71
  }
77
72
  }
78
73
 
79
- // The V2 cart doesn't reliably return line-item images — join them from the menu items by
80
- // the line's catalogItemId and cache for the session.
74
+ // A line's attributes.image may come back absent — join the menu items' images by the line's
75
+ // catalogItemId and cache for the session.
81
76
  const lineImageCache = new Map<string, string>();
82
-
83
- async function fillLineImages(lines: OrderLine[], raws: Raw[]): Promise<void> {
77
+ async function fillLineImages(cart: OrderCart, raws: Raw[]): Promise<void> {
84
78
  const wanted = new Map<string, number[]>(); // itemId -> line indexes
85
- raws.forEach((raw: Raw, i: number) => {
86
- if (lines[i].imageUrl) return;
79
+ raws.forEach((raw, i) => {
80
+ if (cart.lines[i].imageUrl) return;
87
81
  const itemId = raw.source?.catalogReference?.catalogItemId;
88
82
  if (!itemId) return;
89
83
  const cached = lineImageCache.get(itemId);
90
84
  if (cached !== undefined) {
91
- lines[i].imageUrl = cached;
85
+ cart.lines[i].imageUrl = cached;
92
86
  return;
93
87
  }
94
88
  wanted.set(itemId, [...(wanted.get(itemId) ?? []), i]);
@@ -98,62 +92,27 @@ async function fillLineImages(lines: OrderLine[], raws: Raw[]): Promise<void> {
98
92
  const res: Raw = await itemsApi.listItems({ itemIds: [...wanted.keys()] });
99
93
  for (const item of (res.items ?? []) as Raw[]) {
100
94
  const url = imgSrc(item.image, 300, 300);
101
- lineImageCache.set(item._id, url);
102
- for (const i of wanted.get(item._id) ?? []) lines[i].imageUrl = url;
95
+ lineImageCache.set(rawId(item), url);
96
+ for (const i of wanted.get(rawId(item)) ?? []) cart.lines[i].imageUrl = url;
103
97
  }
104
98
  } catch {
105
99
  /* images are a nicety — the cart stays correct without them */
106
100
  }
107
101
  }
108
102
 
109
- function cartCurrency(raw: Raw | null): string {
110
- return raw?.customerInfo?.currencyCode ?? raw?.businessInfo?.currencyCode ?? "";
111
- }
112
-
113
- function toLine(raw: Raw, currency: string): OrderLine {
114
- const descriptionLines: string[] = (raw.attributes?.descriptionLines ?? [])
115
- .map((d: Raw) => {
116
- const label = d.name?.original ?? "";
117
- const value = d.plainText?.original ?? d.colorInfo?.original ?? "";
118
- return label && value ? `${label}: ${value}` : value || label;
119
- })
120
- .filter(Boolean);
121
- return {
122
- lineItemId: raw._id ?? "",
123
- itemName: raw.name?.original ?? "",
124
- quantity: raw.quantityInfo?.confirmedQuantity ?? 0,
125
- unitPrice: formatMoney(raw.pricing?.unitPrice, currency),
126
- linePrice: formatMoney(raw.pricing?.totalPrice, currency),
127
- imageUrl: imgSrc(raw.attributes?.image, 300, 300),
128
- descriptionLines,
129
- status: raw.status ?? "IN_STOCK",
130
- };
131
- }
132
-
133
- function toCart(raw: Raw | null, subtotal: string): OrderCart {
134
- const currency = cartCurrency(raw);
135
- const lines = (raw?.lineItems ?? []).map((l: Raw) => toLine(l, currency));
136
- return {
137
- lines,
138
- itemCount: lines.reduce((sum: number, l: OrderLine) => sum + l.quantity, 0),
139
- subtotal,
140
- currency,
141
- };
142
- }
143
-
144
103
  async function readCartWithSubtotal(raw: Raw | null): Promise<OrderCart> {
145
- if (!raw?.lineItems?.length) return toCart(raw, "");
104
+ // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.
105
+ if (!raw?.lineItems?.length) return toOrderCart(raw, "", imgSrc);
146
106
  // The authoritative after-discount subtotal comes from the cart estimate, never from
147
107
  // hand-summing line items (fees, tax, and delivery resolve at checkout).
148
108
  let subtotal = "";
149
109
  try {
150
- const estimate: Raw = await cartApi.estimateCurrentCart();
151
- subtotal = formatMoney(estimate?.summary?.priceSummary?.subtotal, cartCurrency(raw));
110
+ subtotal = estimateSubtotal(await cartApi.estimateCurrentCart(), raw);
152
111
  } catch {
153
112
  /* estimate is a display nicety — the cart itself is still valid */
154
113
  }
155
- const cart = toCart(raw, subtotal);
156
- await fillLineImages(cart.lines, raw.lineItems as Raw[]);
114
+ const cart = toOrderCart(raw, subtotal, imgSrc);
115
+ await fillLineImages(cart, raw.lineItems as Raw[]);
157
116
  return cart;
158
117
  }
159
118
 
@@ -163,7 +122,7 @@ export async function fetchOrderCart(): Promise<OrderCart> {
163
122
  const { cart } = await cartApi.getCurrentCart();
164
123
  return readCartWithSubtotal(cart as Raw);
165
124
  } catch {
166
- return toCart(null, "");
125
+ return toOrderCart(null, "", imgSrc);
167
126
  }
168
127
  }
169
128
 
@@ -179,31 +138,11 @@ export async function addToOrder(
179
138
  ): Promise<OrderCart> {
180
139
  const operationId = await resolveOperationId();
181
140
  if (!operationId) throw new Error("Online ordering isn't available right now.");
182
- if (!itemId || !menuId || !sectionId) throw new Error("addToOrder needs the item, menu, and section ids.");
183
-
141
+ assertOrderContext(itemId, menuId, sectionId);
184
142
  const { cart } = await cartApi.addLineItemsToCurrentCart({
185
- catalogItems: [
186
- {
187
- quantity,
188
- catalogReference: {
189
- catalogItemId: itemId,
190
- appId: RESTAURANTS_ORDERS_APP_ID,
191
- options: { operationId, menuId, sectionId },
192
- },
193
- },
194
- ],
143
+ catalogItems: [orderCatalogItem(itemId, { operationId, menuId, sectionId }, quantity)],
195
144
  });
196
-
197
- // V2 nests the reference under `source` — a top-level lineItem.catalogReference no longer exists.
198
- const line = ((cart as Raw)?.lineItems ?? []).find(
199
- (l: Raw) => l.source?.catalogReference?.catalogItemId === itemId,
200
- );
201
- if (line?.status && line.status !== "IN_STOCK") {
202
- throw new Error(`This dish isn't available right now (${line.status.toLowerCase().replace(/_/g, " ")}).`);
203
- }
204
- if (!line || line.quantityInfo?.confirmedQuantity === 0) {
205
- throw new Error("The dish couldn't be added to the order — please try again.");
206
- }
145
+ assertOrderAdded(cart as Raw, itemId);
207
146
  return readCartWithSubtotal(cart as Raw);
208
147
  }
209
148
 
@@ -231,18 +170,10 @@ export async function removeOrderLine(lineItemId: string): Promise<OrderCart> {
231
170
  export async function orderCheckoutUrl(): Promise<string> {
232
171
  const { cart } = await cartApi.getCurrentCart();
233
172
  const raw = cart as Raw;
234
- const lines: Raw[] = raw?.lineItems ?? [];
235
- if (!lines.length) throw new Error("Your order is empty.");
236
- const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
237
- if (unavailable.length) {
238
- const names = unavailable.map((l) => l.name?.original).filter(Boolean).join(", ");
239
- throw new Error(`Some dishes are no longer available: ${names}.`);
240
- }
241
- if (!raw?._id) throw new Error("Checkout couldn't start: the order has no id.");
242
-
173
+ assertOrderCheckoutable(raw);
243
174
  const origin = typeof window !== "undefined" ? window.location.origin : "";
244
175
  const session = await redirects.createRedirectSession({
245
- ecomCheckout: { checkoutId: raw._id },
176
+ ecomCheckout: { checkoutId: rawId(raw) },
246
177
  callbacks: {
247
178
  postFlowUrl: origin ? `${origin}/` : undefined,
248
179
  thankYouPageUrl: origin ? `${origin}/` : undefined,
@@ -0,0 +1,193 @@
1
+ // The table-reservation state machine as a framework-free store — the logic behind useReservation,
2
+ // usable from React (useReservation wraps it), from a static page or Vue/Svelte, or as the
3
+ // specification for a port. Location, date + party size, AVAILABLE slots, the 10-minute hold, the
4
+ // details form, and confirm. All correctness (AVAILABLE-only slots, hold → reserve with the
5
+ // revision, firstName + E.164 phone, RESERVED vs REQUESTED) lives in the data layer; this store
6
+ // orchestrates. Browser-only: availability is timezone-specific. One store per mounted surface:
7
+ // createReservationStore(), `start()` when mounted.
8
+ import { completeReservation, fetchReservationLocations, fetchReservationSlots, holdReservation } from "./reservations";
9
+ import type { ReservationConfirmation, ReservationHold, ReservationLocationInfo, ReservationReservee, ReservationSlot } from "./types";
10
+
11
+ const todayKey = (): string => {
12
+ const d = new Date();
13
+ const pad = (n: number) => String(n).padStart(2, "0");
14
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
15
+ };
16
+
17
+ /** Everything a reservation surface renders from. Read it with getState() or through a subscription. */
18
+ export interface ReservationState {
19
+ /** null while loading; [] when Table Reservations isn't set up (honest empty state). */
20
+ locations: ReservationLocationInfo[] | null;
21
+ /** The active location (default first). Render a picker only when locations.length > 1. */
22
+ location: ReservationLocationInfo | null;
23
+ /** "YYYY-MM-DD" for a date input. */
24
+ date: string;
25
+ /** "HH:mm" anchor for the slot fan-out. */
26
+ time: string;
27
+ /** Clamped to the location's partySizeMin/Max. */
28
+ partySize: number;
29
+ /** AVAILABLE slots for the current query; null until findSlots ran. */
30
+ slots: ReservationSlot[] | null;
31
+ /** Set after holdSlot — the visitor has 10 minutes to confirm. */
32
+ held: ReservationHold | null;
33
+ reservee: ReservationReservee;
34
+ /** True when a slot is held and firstName + phone are filled — gate the confirm CTA. */
35
+ canConfirm: boolean;
36
+ /** Set on success. REQUESTED → tell the visitor approval is pending, not confirmed. */
37
+ confirmed: ReservationConfirmation | null;
38
+ loading: boolean;
39
+ error: string | null;
40
+ }
41
+
42
+ export interface ReservationStore {
43
+ getState(): ReservationState;
44
+ subscribe(listener: () => void): () => void;
45
+ /** Load the locations. Call once when mounted (a browser). */
46
+ start(): void;
47
+ stop(): void;
48
+ setLocationId(id: string): void;
49
+ setDate(date: string): void;
50
+ setTime(time: string): void;
51
+ setPartySize(size: number): void;
52
+ findSlots(): Promise<void>;
53
+ holdSlot(slot: ReservationSlot): Promise<void>;
54
+ setReserveeField(field: keyof ReservationReservee, value: string): void;
55
+ confirm(): Promise<void>;
56
+ /** Back to slot picking (keeps date/party). */
57
+ reset(): void;
58
+ }
59
+
60
+ export function createReservationStore(): ReservationStore {
61
+ let locations: ReservationLocationInfo[] | null = null;
62
+ let locationId: string | null = null;
63
+ let date = todayKey();
64
+ let time = "19:00";
65
+ let partySize = 2;
66
+ let slots: ReservationSlot[] | null = null;
67
+ let held: ReservationHold | null = null;
68
+ let reservee: ReservationReservee = { firstName: "", phone: "" };
69
+ let confirmed: ReservationConfirmation | null = null;
70
+ let loading = false;
71
+ let error: string | null = null;
72
+ let started = false;
73
+ const listeners = new Set<() => void>();
74
+ let snapshot: ReservationState | null = null;
75
+ const emit = () => {
76
+ snapshot = null;
77
+ for (const fn of listeners) fn();
78
+ };
79
+
80
+ const currentLocation = (): ReservationLocationInfo | null => locations?.find((l) => l.id === locationId) ?? locations?.[0] ?? null;
81
+
82
+ function getState(): ReservationState {
83
+ if (snapshot) return snapshot;
84
+ snapshot = {
85
+ locations,
86
+ location: currentLocation(),
87
+ date,
88
+ time,
89
+ partySize,
90
+ slots,
91
+ held,
92
+ reservee,
93
+ canConfirm: !!held && reservee.firstName.trim().length > 0 && reservee.phone.trim().length > 0 && !loading,
94
+ confirmed,
95
+ loading,
96
+ error,
97
+ };
98
+ return snapshot;
99
+ }
100
+
101
+ const fail = (e: unknown) => (e instanceof Error ? e.message : String(e));
102
+
103
+ return {
104
+ getState,
105
+ subscribe(fn) {
106
+ listeners.add(fn);
107
+ return () => listeners.delete(fn);
108
+ },
109
+ start() {
110
+ if (started) return;
111
+ started = true;
112
+ fetchReservationLocations()
113
+ .then((locs) => {
114
+ if (!started) return;
115
+ locations = locs;
116
+ locationId ??= locs[0]?.id ?? null;
117
+ emit();
118
+ })
119
+ .catch((e) => {
120
+ if (!started) return;
121
+ locations = [];
122
+ error = fail(e);
123
+ emit();
124
+ });
125
+ },
126
+ stop() {
127
+ started = false;
128
+ },
129
+ setLocationId(id) { locationId = id; emit(); },
130
+ setDate(next) { date = next; emit(); },
131
+ setTime(next) { time = next; emit(); },
132
+ setPartySize(size) {
133
+ const loc = currentLocation();
134
+ const min = loc?.partySizeMin ?? 1;
135
+ const max = loc?.partySizeMax ?? 20;
136
+ partySize = Math.min(Math.max(size, min), max);
137
+ emit();
138
+ },
139
+ async findSlots() {
140
+ const loc = currentLocation();
141
+ if (!loc) return;
142
+ error = null; slots = null; held = null; confirmed = null; loading = true;
143
+ emit();
144
+ try {
145
+ const aroundIso = new Date(`${date}T${time}:00`).toISOString();
146
+ slots = await fetchReservationSlots(loc.id, aroundIso, partySize);
147
+ } catch (e) {
148
+ slots = [];
149
+ error = fail(e);
150
+ } finally {
151
+ loading = false;
152
+ emit();
153
+ }
154
+ },
155
+ async holdSlot(slot) {
156
+ const loc = currentLocation();
157
+ if (!loc) return;
158
+ error = null; loading = true;
159
+ emit();
160
+ try {
161
+ held = await holdReservation(loc.id, slot.startIso, partySize);
162
+ } catch (e) {
163
+ error = fail(e);
164
+ } finally {
165
+ loading = false;
166
+ emit();
167
+ }
168
+ },
169
+ setReserveeField(field, value) {
170
+ reservee = { ...reservee, [field]: value };
171
+ emit();
172
+ },
173
+ async confirm() {
174
+ if (!held) return;
175
+ error = null; loading = true;
176
+ emit();
177
+ try {
178
+ confirmed = await completeReservation(held, reservee);
179
+ } catch (e) {
180
+ // An expired hold can't be reserved — send the visitor back to slot picking.
181
+ error = fail(e);
182
+ held = null;
183
+ } finally {
184
+ loading = false;
185
+ emit();
186
+ }
187
+ },
188
+ reset() {
189
+ held = null; confirmed = null; slots = null; error = null;
190
+ emit();
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,94 @@
1
+ // Table-reservation rules and DTO mapping — transport-agnostic, imported by both
2
+ // ./reservations.ts (SDK) and the REST twin in references/restaurants/rest/reservations.ts (fetch).
3
+ // The flow is location → AVAILABLE slots → hold (10-minute temporary reservation) → reserve with
4
+ // the visitor's details; a reservation is a hold, not a purchase. Raw entities carry `_id` (SDK)
5
+ // or `id` (REST); slot dates arrive as Date (SDK) or ISO string (REST). Imports are type-only.
6
+ import type {
7
+ ReservationConfirmation,
8
+ ReservationHold,
9
+ ReservationLocationInfo,
10
+ ReservationReservee,
11
+ ReservationSlot,
12
+ } from "./types";
13
+ import type { Raw } from "./menu-core";
14
+
15
+ export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
16
+
17
+ export function toLocation(raw: Raw): ReservationLocationInfo {
18
+ const online = raw.configuration?.onlineReservations ?? {};
19
+ return {
20
+ id: rawId(raw),
21
+ default: raw.default === true,
22
+ partySizeMin: online.partySize?.min ?? 1,
23
+ partySizeMax: online.partySize?.max ?? 20,
24
+ approvalMode: online.approval?.mode ?? "AUTOMATIC",
25
+ onlineReservationsEnabled: online.onlineReservationsEnabled === true,
26
+ };
27
+ }
28
+
29
+ /** Active (non-archived) locations, default first. `onlineReservationsEnabled: false` → the premium-gated toggle is off. */
30
+ export function toLocations(raws: Raw[]): ReservationLocationInfo[] {
31
+ return raws
32
+ .filter((l) => l.archived !== true)
33
+ .map(toLocation)
34
+ .filter((l) => l.id)
35
+ .sort((a, b) => Number(b.default) - Number(a.default));
36
+ }
37
+
38
+ export function toSlot(raw: Raw): ReservationSlot {
39
+ const start: Date = raw.startDate instanceof Date ? raw.startDate : new Date(raw.startDate);
40
+ let label = "";
41
+ let dayKey = "";
42
+ try {
43
+ label = start.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
44
+ const pad = (n: number) => String(n).padStart(2, "0");
45
+ dayKey = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;
46
+ } catch {
47
+ /* label/dayKey stay "" — startIso still books */
48
+ }
49
+ return {
50
+ startIso: start.toISOString(),
51
+ label,
52
+ dayKey,
53
+ durationMinutes: raw.duration ?? 0,
54
+ manualApproval: raw.manualApproval === true,
55
+ };
56
+ }
57
+
58
+ /** AVAILABLE slots only — UNAVAILABLE and NON_WORKING_HOURS are dropped (offering them makes the hold fail). */
59
+ export function availableSlots(raws: Raw[]): ReservationSlot[] {
60
+ return raws.filter((s) => s.status === "AVAILABLE").map(toSlot);
61
+ }
62
+
63
+ /** The hold's { reservationId, revision } that completeReservation NEEDS; throws when the slot wasn't held. */
64
+ export function toHold(res: Raw | null | undefined, startIso: string, partySize: number): ReservationHold {
65
+ const reservationId = rawId(res?.reservation);
66
+ const revision = res?.reservation?.revision;
67
+ if (!reservationId || !revision) {
68
+ throw new Error("That time couldn't be held — it may have just been taken. Pick another slot.");
69
+ }
70
+ return { reservationId, revision: String(revision), startIso, partySize };
71
+ }
72
+
73
+ /** The reservee body of a reserve: firstName + phone (E.164) REQUIRED, trimmed; optional fields only when filled. */
74
+ export function reserveeBody(reservee: ReservationReservee): Raw {
75
+ if (!reservee.firstName?.trim() || !reservee.phone?.trim()) {
76
+ throw new Error("First name and phone number are required.");
77
+ }
78
+ return {
79
+ firstName: reservee.firstName.trim(),
80
+ phone: reservee.phone.trim(),
81
+ ...(reservee.lastName?.trim() ? { lastName: reservee.lastName.trim() } : {}),
82
+ ...(reservee.email?.trim() ? { email: reservee.email.trim() } : {}),
83
+ };
84
+ }
85
+
86
+ /** RESERVED = confirmed; REQUESTED = pending approval. Throws when the hold expired (no reservation back). */
87
+ export function toConfirmation(res: Raw | null | undefined): ReservationConfirmation {
88
+ const reservation = res?.reservation;
89
+ const reservationId = rawId(reservation);
90
+ if (!reservationId) {
91
+ throw new Error("The reservation couldn't be completed — the hold may have expired. Start over.");
92
+ }
93
+ return { reservationId, status: reservation.status === "REQUESTED" ? "REQUESTED" : "RESERVED" };
94
+ }