@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.
- package/package.json +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- 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
|
|
2
|
-
// raw cart and operation entities
|
|
3
|
-
//
|
|
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) —
|
|
6
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
24
|
-
|
|
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
|
-
//
|
|
80
|
-
//
|
|
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
|
|
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
|
|
102
|
-
for (const i of wanted.get(item
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
156
|
-
await fillLineImages(cart
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
176
|
+
ecomCheckout: { checkoutId: rawId(raw) },
|
|
246
177
|
callbacks: {
|
|
247
178
|
postFlowUrl: origin ? `${origin}/` : undefined,
|
|
248
179
|
thankYouPageUrl: origin ? `${origin}/` : undefined,
|
package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts
ADDED
|
@@ -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
|
+
}
|
package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts
ADDED
|
@@ -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
|
+
}
|