@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2

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 (263) hide show
  1. package/README.md +321 -152
  2. package/VENDOR.md +69 -0
  3. package/assets/openspec/config.yaml +38 -0
  4. package/assets/templates/AGENTS.md +76 -0
  5. package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
  6. package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
  7. package/assets/templates/app/app/globals.css +5 -8
  8. package/assets/templates/app/app/layout.tsx +17 -7
  9. package/assets/templates/app/app/robots.ts +19 -0
  10. package/assets/templates/app/app/sitemap.ts +71 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
  13. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
  14. package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
  16. package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
  17. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
  18. package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
  20. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
  21. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
  22. package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
  23. package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
  24. package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
  25. package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
  26. package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
  27. package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
  28. package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
  29. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
  30. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
  31. package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
  32. package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
  33. package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
  34. package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
  35. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
  36. package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
  37. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
  38. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
  39. package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
  40. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
  41. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
  42. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
  43. package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
  44. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
  45. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
  46. package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
  47. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
  48. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
  49. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
  50. package/assets/templates/features/checkout/handlers.ts +159 -0
  51. package/assets/templates/features/checkout/state.ts +61 -0
  52. package/assets/templates/features/customer-auth/handlers.ts +59 -0
  53. package/assets/templates/features/customer-auth/state.ts +45 -0
  54. package/assets/templates/features/recommendations/state.ts +11 -0
  55. package/assets/templates/views/account/content.ts +35 -0
  56. package/assets/templates/views/account/index.tsx +114 -0
  57. package/assets/templates/views/account/registry.tsx +439 -0
  58. package/assets/templates/views/account/schema.ts +53 -0
  59. package/assets/templates/views/cart/content.ts +11 -0
  60. package/assets/templates/views/cart/data.ts +23 -0
  61. package/assets/templates/views/cart/index.tsx +53 -0
  62. package/assets/templates/views/cart/props.ts +12 -0
  63. package/assets/templates/views/cart/registry.tsx +81 -0
  64. package/assets/templates/views/cart/schema.ts +52 -0
  65. package/assets/templates/views/category/assembly/assembly.json +29 -0
  66. package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
  67. package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
  68. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
  69. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
  70. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
  71. package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
  72. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
  73. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
  74. package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
  75. package/assets/templates/views/category/content.ts +30 -0
  76. package/assets/templates/views/category/data.ts +12 -0
  77. package/assets/templates/views/category/index.tsx +299 -0
  78. package/assets/templates/views/category/props.ts +23 -0
  79. package/assets/templates/views/category/registry.tsx +122 -0
  80. package/assets/templates/views/category/schema.ts +91 -0
  81. package/assets/templates/views/checkout/content.ts +105 -0
  82. package/assets/templates/views/checkout/data.ts +23 -0
  83. package/assets/templates/views/checkout/index.tsx +111 -0
  84. package/assets/templates/views/checkout/props.ts +49 -0
  85. package/assets/templates/views/checkout/registry.tsx +217 -0
  86. package/assets/templates/views/checkout/schema.ts +229 -0
  87. package/assets/templates/views/home/assembly/assembly.json +33 -0
  88. package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
  89. package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
  90. package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
  91. package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
  92. package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
  93. package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
  94. package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
  95. package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
  96. package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
  97. package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
  98. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
  99. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
  100. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
  101. package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
  102. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
  103. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
  104. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
  105. package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
  106. package/assets/templates/views/home/content.ts +70 -0
  107. package/assets/templates/views/home/data.ts +24 -0
  108. package/assets/templates/views/home/index.tsx +57 -85
  109. package/assets/templates/views/home/props.ts +22 -0
  110. package/assets/templates/views/home/registry.tsx +38 -22
  111. package/assets/templates/views/home/schema.ts +93 -0
  112. package/assets/templates/views/product/content.ts +25 -0
  113. package/assets/templates/views/product/data.ts +50 -0
  114. package/assets/templates/views/product/index.tsx +148 -0
  115. package/assets/templates/views/product/props.ts +27 -0
  116. package/assets/templates/views/product/registry.tsx +103 -0
  117. package/assets/templates/views/product/schema.ts +121 -0
  118. package/assets/templates/views/static/content.ts +99 -0
  119. package/assets/templates/views/static/index.tsx +60 -0
  120. package/assets/templates/views/static/registry.tsx +66 -0
  121. package/assets/templates/views/static/schema.ts +45 -0
  122. package/bin/storefront.js +2424 -148
  123. package/examples/merchandising/category-led.json +37 -0
  124. package/examples/merchandising/collection-led.json +37 -0
  125. package/package.json +15 -9
  126. package/recipes/archetypes.json +28 -14
  127. package/recipes/catalog.json +400 -0
  128. package/recipes/catalog.mjs +128 -0
  129. package/recipes/catalog.schema.json +64 -0
  130. package/recipes/feature.schema.json +89 -0
  131. package/recipes/features.json +162 -0
  132. package/recipes/manifest.schema.json +69 -0
  133. package/recipes/operations.schema.json +43 -0
  134. package/recipes/recipe.schema.json +11 -2
  135. package/recipes/recipes.json +76 -19
  136. package/recipes/sections.json +34 -5
  137. package/recipes/validate.mjs +838 -32
  138. package/scripts/candidate.mjs +236 -0
  139. package/scripts/composers.mjs +160 -0
  140. package/scripts/env-file.mjs +86 -0
  141. package/scripts/file-classes.mjs +121 -0
  142. package/scripts/logical-controls.mjs +61 -0
  143. package/scripts/manifest.mjs +298 -0
  144. package/scripts/migrate-english-routes.mjs +276 -0
  145. package/scripts/operation-registry.mjs +256 -0
  146. package/scripts/operations.mjs +720 -0
  147. package/scripts/ownership-conflict.mjs +356 -0
  148. package/scripts/pinning.mjs +573 -0
  149. package/scripts/plan-apply.mjs +987 -0
  150. package/scripts/preflight.mjs +51 -0
  151. package/scripts/reconcile.mjs +549 -0
  152. package/scripts/render-templates.mjs +338 -68
  153. package/scripts/schema-validate.mjs +132 -0
  154. package/scripts/section-compat.mjs +300 -0
  155. package/scripts/section-config.mjs +253 -0
  156. package/scripts/section-operations.mjs +508 -0
  157. package/scripts/starter.mjs +17 -0
  158. package/scripts/sync-assets.mjs +22 -15
  159. package/scripts/theme/color.mjs +185 -0
  160. package/scripts/theme/css-theme.mjs +233 -0
  161. package/scripts/theme/fonts.mjs +123 -0
  162. package/scripts/theme/import-export.mjs +125 -0
  163. package/scripts/theme/preset.mjs +93 -0
  164. package/scripts/theme/theme-plan.mjs +292 -0
  165. package/scripts/theme/tokens.mjs +137 -0
  166. package/scripts/view-assembly.mjs +337 -0
  167. package/scripts/view-selection.mjs +211 -0
  168. package/skills/sf-feature/SKILL.md +80 -0
  169. package/skills/sf-setup/SKILL.md +79 -0
  170. package/skills/sf-theme-gen/SKILL.md +100 -30
  171. package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
  172. package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
  173. package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
  174. package/skills/sf-view-gen/SKILL.md +146 -93
  175. package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
  176. package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
  177. package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
  178. package/skills/sf-view-gen/assets/recipes/features.json +162 -0
  179. package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
  180. package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
  181. package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
  182. package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
  183. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
  184. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
  185. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
  186. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
  187. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
  188. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
  189. package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
  190. package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
  191. package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
  192. package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
  193. package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
  194. package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
  195. package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
  196. package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
  197. package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
  198. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
  199. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
  200. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
  201. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
  202. package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
  203. package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
  204. package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
  205. package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
  206. package/skills/sf-view-gen/references/views.md +58 -33
  207. package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
  208. package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
  209. package/skills/sf-view-gen/scripts/validate.mjs +838 -32
  210. package/skills/storefront-workflow/SKILL.md +179 -0
  211. package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
  212. package/CHANGELOG.md +0 -61
  213. package/commands/sf-add-view.md +0 -26
  214. package/commands/sf-theme.md +0 -20
  215. package/commands/sf-view.md +0 -25
  216. package/skills/building-storefronts/SKILL.md +0 -178
  217. package/skills/building-storefronts/references/frontend-integration.md +0 -229
  218. package/skills/json-render-remotion/SKILL.md +0 -111
  219. package/skills/json-render-solid/SKILL.md +0 -204
  220. package/skills/nextjs-shadcn/SKILL.md +0 -367
  221. package/skills/nextjs-shadcn/references/architecture.md +0 -499
  222. package/skills/nextjs-shadcn/references/project-setup.md +0 -162
  223. package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
  224. package/skills/nextjs-shadcn/references/sidebar.md +0 -274
  225. package/skills/nextjs-shadcn/references/styling.md +0 -555
  226. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
  227. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
  228. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
  229. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
  230. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
  231. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
  232. package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
  233. package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
  234. package/skills/shadcn-component-discovery/SKILL.md +0 -273
  235. package/skills/shadcn-component-discovery/references/registries.md +0 -226
  236. package/skills/shadcn-theming/SKILL.md +0 -104
  237. package/skills/storefront-best-practices/SKILL.md +0 -421
  238. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
  239. package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
  240. package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
  241. package/skills/storefront-best-practices/reference/components/footer.md +0 -112
  242. package/skills/storefront-best-practices/reference/components/hero.md +0 -241
  243. package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
  244. package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
  245. package/skills/storefront-best-practices/reference/components/popups.md +0 -221
  246. package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
  247. package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
  248. package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
  249. package/skills/storefront-best-practices/reference/components/search.md +0 -101
  250. package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
  251. package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
  252. package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
  253. package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
  254. package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
  255. package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
  256. package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
  257. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
  258. package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
  259. package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
  260. package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
  261. package/skills/storefront-best-practices/reference/medusa.md +0 -307
  262. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
  263. /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
@@ -0,0 +1,679 @@
1
+ // Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/commerce.ts)
2
+ //
3
+ // Host commerce boundary — the single seam between the generated storefront and
4
+ // `@thorprovider/runtime/commerce`. Every backend call the host makes goes
5
+ // through here so the contract is owned once, the layer boundary is explicit,
6
+ // and a future change to the runtime provider is absorbed in one file.
7
+ //
8
+ // Boundary rules (change B):
9
+ // - Server-only, JSX-free, Next-free. No `'use client'`, no React import.
10
+ // - Context-scoped: every cart/checkout call takes the validated single-store
11
+ // context, and the runtime verifies the backend cart belongs to it.
12
+ // - The backend is authoritative: totals are mapped from the backend cart and
13
+ // never computed locally; a missing amount is omitted, never invented.
14
+ // - Optional images are normalized (an absent image is omitted, never `""`).
15
+ // - Add uses the selected backend VARIANT id; update/remove use the backend
16
+ // cart LINE-ITEM id. The Navbar's DELTA is normalized to an absolute quantity
17
+ // against the authoritative line via `toAbsoluteLineUpdate`.
18
+ // - Completion is discriminated and never blind-retried; reconciliation reads
19
+ // the committed cart (`completedAt`) and completes once only when committed.
20
+
21
+ import type {
22
+ Cart,
23
+ Customer,
24
+ LoginCredentials,
25
+ Money,
26
+ Order,
27
+ PaymentMethod,
28
+ RegisterData,
29
+ ShippingMethod,
30
+ StoreContext,
31
+ } from '@thorprovider/types'
32
+ import {
33
+ addCartLines,
34
+ applyManualTestPayment,
35
+ APPROVED_MANUAL_TEST_PROVIDER_ID,
36
+ completeCart,
37
+ createCartForContext,
38
+ getCartForContext,
39
+ getCheckoutCountries,
40
+ getCurrentCustomer,
41
+ getCustomerOrders,
42
+ getCustomerOrdersPage,
43
+ getEligibleShippingMethods,
44
+ getPaymentMethodsByRegion,
45
+ getProductDetailById,
46
+ getProductDetailByHandle,
47
+ loginCustomer,
48
+ logoutCustomer,
49
+ reconcileCart,
50
+ registerCustomer,
51
+ removeCartLines,
52
+ selectFirstPurchasableVariant,
53
+ selectShippingMethod,
54
+ toAbsoluteLineUpdate,
55
+ updateCartLines,
56
+ updateShippingAddress,
57
+ validateCheckoutAddressForRegion,
58
+ type AuthOutcome,
59
+ type CartCompletionResult,
60
+ type CatalogProductDetail,
61
+ type CheckoutAddress,
62
+ type CheckoutFieldError,
63
+ type RegionCountry,
64
+ } from '@thorprovider/runtime/commerce'
65
+ import type {
66
+ AccountOrderItemView,
67
+ AccountOrderView,
68
+ CartLineState,
69
+ CartTotalsState,
70
+ CheckoutFieldErrors,
71
+ CheckoutShippingOption,
72
+ StorefrontOrder,
73
+ StorefrontOrderLineView,
74
+ } from './state.core'
75
+ import { assertBackendConfigured, getDataSource, getStoreContext, type StoreDataSource } from './store-context'
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Projections (serializable; the UI state holds these)
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /** The cart slice of the storefront state. */
82
+ export interface CartStatePatch {
83
+ cart: CartLineState[]
84
+ cartTotals: CartTotalsState | null
85
+ cartId: string | null
86
+ cartQuantity: number
87
+ cartCompleted: boolean
88
+ /** The shipping OPTION id the checkout UI selects (`so_`), not the applied
89
+ * shipping-method record id (`sm_`) used as payment proof. */
90
+ cartSelectedShippingOptionId: string | null
91
+ }
92
+
93
+ /** The empty cart slice, used when there is no active cart. */
94
+ export const EMPTY_CART_STATE: CartStatePatch = {
95
+ cart: [],
96
+ cartTotals: null,
97
+ cartId: null,
98
+ cartQuantity: 0,
99
+ cartCompleted: false,
100
+ cartSelectedShippingOptionId: null,
101
+ }
102
+
103
+ /** Result of a completion attempt. `unresolved` must never be blindly retried. */
104
+ export type PlaceOrderResult =
105
+ | { status: 'order'; order: StorefrontOrder; lines: StorefrontOrderLineView[] }
106
+ | { status: 'error'; message: string; code?: string }
107
+ | { status: 'cart-error'; message: string; code?: string }
108
+ | { status: 'unresolved'; message: string; code?: string }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Mapping helpers (pure; no I/O)
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /** Normalize an optional image URL: absent/blank is omitted, never `""`. */
115
+ export function normalizeImageUrl(url: string | null | undefined): string | undefined {
116
+ if (typeof url !== 'string') return undefined
117
+ const trimmed = url.trim()
118
+ return trimmed === '' ? undefined : trimmed
119
+ }
120
+
121
+ function lineVariantLabel(line: Cart['lines'][number]): string | undefined {
122
+ const options = line.merchandise.selectedOptions
123
+ .map((option) => option.value)
124
+ .filter((value) => value.trim() !== '')
125
+ if (options.length > 0) return options.join(' / ')
126
+ return line.merchandise.title.trim() === '' ? undefined : line.merchandise.title
127
+ }
128
+
129
+ /**
130
+ * A stable, unique line id. The backend line id is authoritative; when a
131
+ * provider omits it, a deterministic composite keeps list keys unique without
132
+ * pretending the synthetic id is a valid mutation identity.
133
+ */
134
+ export function stableLineId(line: Cart['lines'][number], index: number): string {
135
+ if (typeof line.id === 'string' && line.id.trim() !== '') return line.id
136
+ return `${line.merchandise.id}::${index}`
137
+ }
138
+
139
+ export function toCartLine(line: Cart['lines'][number], index: number): CartLineState {
140
+ return {
141
+ id: stableLineId(line, index),
142
+ merchandiseId: line.merchandise.id,
143
+ name: line.merchandise.product.title || line.merchandise.title,
144
+ variant: lineVariantLabel(line),
145
+ quantity: line.quantity,
146
+ lineTotal: line.cost.totalAmount,
147
+ // The backend unit price is carried through when supplied (including a
148
+ // legitimate zero). It is never derived from `total / quantity`; when the
149
+ // backend omits it, the field is omitted so the UI shows an unavailable
150
+ // unit price rather than an invented one.
151
+ unitPrice: line.cost.unitPrice,
152
+ image: normalizeImageUrl(line.merchandise.product.featuredImage?.url),
153
+ handle: line.merchandise.product.handle || undefined,
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Map the canonical cart to the host state slice. Totals come from the backend
159
+ * cart; only the fields the backend supplies are set, the rest are omitted.
160
+ */
161
+ export function toCartState(cart: Cart): CartStatePatch {
162
+ // The backend may expose a zero shipping amount as a placeholder before a
163
+ // method is selected. Showing it would render a false "free shipping"; omit
164
+ // shipping from the breakdown until the cart actually has a selected method.
165
+ const hasSelectedShipping = Boolean(
166
+ cart.selectedShippingMethodId || cart.selectedShippingOptionId,
167
+ )
168
+ return {
169
+ cart: cart.lines.map(toCartLine),
170
+ cartTotals: {
171
+ subtotal: cart.cost.subtotalAmount,
172
+ total: cart.cost.totalAmount,
173
+ tax: cart.cost.totalTaxAmount,
174
+ discount: cart.cost.discountAmount,
175
+ shipping: hasSelectedShipping ? cart.cost.shippingAmount : undefined,
176
+ },
177
+ cartId: cart.id ?? null,
178
+ cartQuantity: cart.totalQuantity,
179
+ cartCompleted: Boolean(cart.completedAt),
180
+ // The UI selects/highlights by the shipping OPTION id (`so_`). The applied
181
+ // shipping-method record id (`selectedShippingMethodId`, `sm_`) is the
182
+ // runtime's payment proof and is never exposed to the selection UI.
183
+ cartSelectedShippingOptionId: cart.selectedShippingOptionId ?? null,
184
+ }
185
+ }
186
+
187
+ export function toStorefrontOrder(order: Order): StorefrontOrder {
188
+ return {
189
+ number: order.orderNumber,
190
+ placedAt: order.createdAt,
191
+ email: order.email,
192
+ shipping: order.shippingAddress
193
+ ? {
194
+ name: `${order.shippingAddress.firstName} ${order.shippingAddress.lastName}`.trim(),
195
+ lines: [order.shippingAddress.address1, order.shippingAddress.city, order.shippingAddress.countryCode]
196
+ .filter((value): value is string => typeof value === 'string' && value.trim() !== ''),
197
+ }
198
+ : undefined,
199
+ totals: {
200
+ subtotal: order.subtotal,
201
+ discount: order.discount,
202
+ tax: order.tax,
203
+ shipping: order.shipping,
204
+ total: order.total,
205
+ },
206
+ }
207
+ }
208
+
209
+ export function toOrderLines(order: Order): StorefrontOrderLineView[] {
210
+ return order.items.map((item) => ({
211
+ id: item.id,
212
+ name: item.title,
213
+ variant: item.variantTitle,
214
+ quantity: item.quantity,
215
+ lineTotal: item.total,
216
+ unitPrice: item.price,
217
+ image: normalizeImageUrl(item.image?.url),
218
+ }))
219
+ }
220
+
221
+ export function toCheckoutShippingOptions(methods: ShippingMethod[]): CheckoutShippingOption[] {
222
+ return methods.map((method) => ({
223
+ id: method.id,
224
+ name: method.name,
225
+ price: method.price,
226
+ description: method.description,
227
+ estimatedDays: method.estimatedDays,
228
+ }))
229
+ }
230
+
231
+ /** Group the runtime's structured field errors into the block's error shape. */
232
+ export function toCheckoutFieldErrors(errors: CheckoutFieldError[]): CheckoutFieldErrors {
233
+ const grouped: CheckoutFieldErrors = {}
234
+ for (const error of errors) {
235
+ grouped[error.field] = [...(grouped[error.field] ?? []), { message: error.message }]
236
+ }
237
+ return grouped
238
+ }
239
+
240
+ /** Map the runtime's discriminated completion to the host result union. */
241
+ export function toPlaceOrderResult(result: CartCompletionResult): PlaceOrderResult {
242
+ if (result.type === 'order') {
243
+ return { status: 'order', order: toStorefrontOrder(result.order), lines: toOrderLines(result.order) }
244
+ }
245
+ if (result.type === 'cart') {
246
+ return { status: 'cart-error', message: result.error.message, code: result.error.code }
247
+ }
248
+ return { status: 'unresolved', message: result.error.message, code: result.error.code }
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Reads
253
+ // ---------------------------------------------------------------------------
254
+
255
+ export interface CatalogDetailResult {
256
+ detail: CatalogProductDetail | undefined
257
+ source: StoreDataSource
258
+ }
259
+
260
+ /** Directed product-detail lookup by handle, priced in the store context region. */
261
+ export async function loadProductDetail(handle: string): Promise<CatalogDetailResult> {
262
+ const context = getStoreContext()
263
+ const source = getDataSource()
264
+ assertBackendConfigured(source)
265
+ const detail = await getProductDetailByHandle(handle, context)
266
+ return { detail, source }
267
+ }
268
+
269
+ /** Directed product-detail lookup by backend id, priced in the store context region. */
270
+ export async function loadProductDetailById(
271
+ productId: string,
272
+ ): Promise<CatalogProductDetail | undefined> {
273
+ const context = getStoreContext()
274
+ assertBackendConfigured()
275
+ return getProductDetailById(productId, context)
276
+ }
277
+
278
+ /** The resolved add-to-cart target for a product-card shortcut. */
279
+ export type ProductCartTarget =
280
+ | { ok: true; variantId: string }
281
+ | { ok: false; error: string }
282
+
283
+ /**
284
+ * Resolve the product-card add-to-cart target: the product's FIRST purchasable
285
+ * variant (available for sale AND with a resolvable price), identified by its
286
+ * backend variant id. This is the explicit card exception to the product-detail
287
+ * rule that never falls back to a first variant (change
288
+ * storefront-card-add-to-cart). A product with no purchasable variant — or an
289
+ * unknown product — returns a truthful error and adds nothing.
290
+ */
291
+ export function resolveProductCartTarget(
292
+ detail: CatalogProductDetail | undefined,
293
+ ): ProductCartTarget {
294
+ const variant = detail ? selectFirstPurchasableVariant(detail) : undefined
295
+ if (!variant) {
296
+ return { ok: false, error: 'Este producto no tiene variantes disponibles.' }
297
+ }
298
+ return { ok: true, variantId: variant.id }
299
+ }
300
+
301
+ /** The context-scoped backend cart projected for the UI, or undefined when absent. */
302
+ export async function readCart(
303
+ cartId: string,
304
+ context: StoreContext = getStoreContext(),
305
+ ): Promise<CartStatePatch | undefined> {
306
+ assertBackendConfigured()
307
+ const cart = await getCartForContext(cartId, context)
308
+ return cart ? toCartState(cart) : undefined
309
+ }
310
+
311
+ export async function loadCheckoutCountries(
312
+ cartId: string,
313
+ context: StoreContext = getStoreContext(),
314
+ ): Promise<RegionCountry[]> {
315
+ assertBackendConfigured()
316
+ return getCheckoutCountries(cartId, context)
317
+ }
318
+
319
+ export async function loadShippingMethods(
320
+ cartId: string,
321
+ context: StoreContext = getStoreContext(),
322
+ ): Promise<ShippingMethod[]> {
323
+ assertBackendConfigured()
324
+ return getEligibleShippingMethods(cartId, context)
325
+ }
326
+
327
+ /**
328
+ * Label the approved manual TEST provider explicitly as a test provider rather
329
+ * than letting a legacy adapter name (for example "Cash on Delivery") imply a
330
+ * delivery/COD guarantee. Other methods pass through unchanged.
331
+ */
332
+ export function toDisplayPaymentMethods(methods: PaymentMethod[]): PaymentMethod[] {
333
+ return methods.map((method) =>
334
+ method.id === APPROVED_MANUAL_TEST_PROVIDER_ID
335
+ ? {
336
+ ...method,
337
+ name: 'Pago manual TEST',
338
+ description: 'Proveedor manual de prueba; no cobra fondos reales.',
339
+ }
340
+ : method,
341
+ )
342
+ }
343
+
344
+ export async function loadPaymentMethods(
345
+ context: StoreContext = getStoreContext(),
346
+ ): Promise<PaymentMethod[]> {
347
+ assertBackendConfigured()
348
+ const methods = await getPaymentMethodsByRegion(context.regionId)
349
+ return toDisplayPaymentMethods(methods)
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Cart mutations (context-scoped)
354
+ // ---------------------------------------------------------------------------
355
+
356
+ /** Create a fresh backend guest cart in the store context region. */
357
+ export async function createGuestCart(
358
+ context: StoreContext = getStoreContext(),
359
+ ): Promise<CartStatePatch> {
360
+ assertBackendConfigured()
361
+ const cart = await createCartForContext(context)
362
+ return toCartState(cart)
363
+ }
364
+
365
+ /** Add the explicitly selected backend variant at its requested quantity. */
366
+ export async function addVariant(
367
+ cartId: string,
368
+ variantId: string,
369
+ quantity: number,
370
+ context: StoreContext = getStoreContext(),
371
+ ): Promise<CartStatePatch> {
372
+ assertBackendConfigured()
373
+ const cart = await addCartLines(cartId, [{ merchandiseId: variantId, quantity }], context)
374
+ return toCartState(cart)
375
+ }
376
+
377
+ /** Set a line to an ABSOLUTE quantity by its backend line-item id. */
378
+ export async function setLineQuantity(
379
+ cartId: string,
380
+ lineId: string,
381
+ merchandiseId: string,
382
+ quantity: number,
383
+ context: StoreContext = getStoreContext(),
384
+ ): Promise<CartStatePatch> {
385
+ assertBackendConfigured()
386
+ const cart = await updateCartLines(cartId, [{ id: lineId, merchandiseId, quantity }], context)
387
+ return toCartState(cart)
388
+ }
389
+
390
+ /** Apply a relative DELTA by normalizing it against the authoritative line. */
391
+ export async function applyLineDelta(
392
+ cartId: string,
393
+ lineId: string,
394
+ delta: number,
395
+ context: StoreContext = getStoreContext(),
396
+ ): Promise<CartStatePatch> {
397
+ assertBackendConfigured()
398
+ const current = await getCartForContext(cartId, context)
399
+ if (!current) throw new Error(`Cart not found: ${cartId}`)
400
+ const update = toAbsoluteLineUpdate(current, lineId, delta)
401
+ const cart = await updateCartLines(cartId, [update], context)
402
+ return toCartState(cart)
403
+ }
404
+
405
+ /** Remove a line by its backend line-item id. */
406
+ export async function removeLine(
407
+ cartId: string,
408
+ lineId: string,
409
+ context: StoreContext = getStoreContext(),
410
+ ): Promise<CartStatePatch> {
411
+ assertBackendConfigured()
412
+ const cart = await removeCartLines(cartId, [lineId], context)
413
+ return toCartState(cart)
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Checkout mutations (context-scoped)
418
+ // ---------------------------------------------------------------------------
419
+
420
+ /** The validated checkout address shape (runtime contract). */
421
+ export type AddressInput = CheckoutAddress
422
+
423
+ /** Validate an untrusted address against the cart region's eligible countries. */
424
+ export function validateAddress(
425
+ input: unknown,
426
+ countries: RegionCountry[],
427
+ ): { success: true; address: CheckoutAddress } | { success: false; errors: CheckoutFieldError[] } {
428
+ return validateCheckoutAddressForRegion(input, countries)
429
+ }
430
+
431
+ /** True when the provider simply does not persist an address. */
432
+ function isUnsupportedAddressUpdate(error: unknown): boolean {
433
+ return error instanceof Error && /does not support updateShippingAddress/i.test(error.message)
434
+ }
435
+
436
+ /**
437
+ * Persist the cart's address when the provider supports it; the backend then
438
+ * recalculates totals on the next read. A provider that does not persist an
439
+ * address (for example the mock fixture) is tolerated: the address has already
440
+ * been validated and shipping eligibility is resolved from the cart region.
441
+ */
442
+ export async function saveShippingAddress(
443
+ cartId: string,
444
+ address: AddressInput,
445
+ context: StoreContext = getStoreContext(),
446
+ ): Promise<CartStatePatch> {
447
+ assertBackendConfigured()
448
+ const current = await getCartForContext(cartId, context)
449
+ if (!current) throw new Error(`Cart not found before address update: ${cartId}`)
450
+ try {
451
+ // The runtime address validator treats province/phone as optional; the
452
+ // address-setter input still declares them, so pass an empty string when the
453
+ // shopper did not supply one rather than failing the type contract.
454
+ await updateShippingAddress(cartId, {
455
+ email: address.email,
456
+ firstName: address.firstName,
457
+ lastName: address.lastName,
458
+ address1: address.address1,
459
+ address2: address.address2,
460
+ company: address.company,
461
+ city: address.city,
462
+ province: address.province ?? '',
463
+ postalCode: address.postalCode,
464
+ countryCode: address.countryCode,
465
+ phone: address.phone ?? '',
466
+ })
467
+ } catch (error) {
468
+ if (isUnsupportedAddressUpdate(error)) return toCartState(current)
469
+ throw error
470
+ }
471
+ const cart = await getCartForContext(cartId, context)
472
+ return toCartState(cart ?? current)
473
+ }
474
+
475
+ /** Apply an eligible shipping method and return the recalculated cart. */
476
+ export async function selectShipping(
477
+ cartId: string,
478
+ methodId: string,
479
+ context: StoreContext = getStoreContext(),
480
+ ): Promise<CartStatePatch> {
481
+ assertBackendConfigured()
482
+ const cart = await selectShippingMethod(cartId, methodId, context)
483
+ return toCartState(cart)
484
+ }
485
+
486
+ /** Apply the single approved manual TEST payment provider (requires shipping). */
487
+ export async function applyPayment(
488
+ cartId: string,
489
+ context: StoreContext = getStoreContext(),
490
+ ): Promise<void> {
491
+ assertBackendConfigured()
492
+ await applyManualTestPayment(cartId, context)
493
+ }
494
+
495
+ /** Complete the cart exactly once, preserving the native discriminator. */
496
+ export async function completeOrder(
497
+ cartId: string,
498
+ context: StoreContext = getStoreContext(),
499
+ ): Promise<PlaceOrderResult> {
500
+ assertBackendConfigured()
501
+ return toPlaceOrderResult(await completeCart(cartId, context))
502
+ }
503
+
504
+ /**
505
+ * Reconcile a committed cart into its order. The caller MUST have authorized
506
+ * this cart through the verified session cookie; this never accepts a bare
507
+ * order/cart id as authorization. Reads `completedAt` first and completes once
508
+ * only when the backend committed.
509
+ */
510
+ export async function reconcileOrder(
511
+ cartId: string,
512
+ context: StoreContext = getStoreContext(),
513
+ ): Promise<PlaceOrderResult> {
514
+ assertBackendConfigured()
515
+ return toPlaceOrderResult(await reconcileCart(cartId, context))
516
+ }
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // Customer auth boundary (change: storefront-customer-auth, task 2.2)
520
+ // ---------------------------------------------------------------------------
521
+ //
522
+ // The host never talks to the backend SDK directly. These helpers delegate to
523
+ // `@thorprovider/runtime/commerce`, which builds a request-scoped provider
524
+ // carrying exactly the caller's token. No token is logged or returned to the
525
+ // client; the runtime outcome is passed through unchanged.
526
+
527
+ /** Authenticate a shopper; the caller persists the returned token in its cookie. */
528
+ export async function authenticate(credentials: LoginCredentials): Promise<AuthOutcome> {
529
+ return loginCustomer(credentials)
530
+ }
531
+
532
+ /** Register a shopper; the caller persists the returned token in its cookie. */
533
+ export async function registerShopper(data: RegisterData): Promise<AuthOutcome> {
534
+ return registerCustomer(data)
535
+ }
536
+
537
+ /** Best-effort backend logout; the authoritative clear is the host cookie. */
538
+ export async function endSession(): Promise<void> {
539
+ return logoutCustomer()
540
+ }
541
+
542
+ /** Resolve the current customer for a request token, or null when absent. */
543
+ export async function loadCustomer(token: string | null): Promise<Customer | null> {
544
+ return getCurrentCustomer(token)
545
+ }
546
+
547
+ // ---------------------------------------------------------------------------
548
+ // Customer orders boundary (change: storefront-customer-orders, task 2.1)
549
+ // ---------------------------------------------------------------------------
550
+ //
551
+ // The host never talks to the backend SDK directly. `getCustomerOrders`
552
+ // delegates to `@thorprovider/runtime/commerce`, which builds a request-scoped
553
+ // provider carrying exactly the caller's token. The boundary maps the backend
554
+ // orders to a serializable display shape: Money is formatted from the order's
555
+ // own `currencyCode` (never hardcoded, never divided by 100) and a field the
556
+ // backend omits is omitted, never invented.
557
+
558
+ /**
559
+ * Format backend Money for display from its own currency, or undefined when the
560
+ * amount is absent/blank/non-finite. The decimal-string amount is already in
561
+ * major units, so it is never divided by 100.
562
+ */
563
+ function formatMoney(money?: Money): string | undefined {
564
+ if (!money) return undefined
565
+ const amount = money.amount
566
+ if (typeof amount !== 'string' || amount.trim() === '') return undefined
567
+ const value = Number(amount)
568
+ if (!Number.isFinite(value)) return undefined
569
+ return new Intl.NumberFormat(undefined, {
570
+ style: 'currency',
571
+ currency: money.currencyCode,
572
+ }).format(value)
573
+ }
574
+
575
+ /** Format an ISO timestamp as a locale date, or return the raw value unchanged. */
576
+ function formatOrderDate(value: string): string {
577
+ const parsed = new Date(value)
578
+ if (Number.isNaN(parsed.getTime())) return value
579
+ return parsed.toLocaleDateString()
580
+ }
581
+
582
+ /** Join the present address parts, filtering blanks; undefined when none apply. */
583
+ function formatAddress(address: Order['shippingAddress']): string | undefined {
584
+ if (!address) return undefined
585
+ const name = `${address.firstName ?? ''} ${address.lastName ?? ''}`.trim()
586
+ const parts = [name, address.address1, address.city, address.countryCode].filter(
587
+ (value): value is string => typeof value === 'string' && value.trim() !== '',
588
+ )
589
+ if (parts.length === 0) return undefined
590
+ return parts.join(', ')
591
+ }
592
+
593
+ /** Map one backend order line to its display view, omitting absent optional fields. */
594
+ export function toAccountOrderItemView(item: Order['items'][number]): AccountOrderItemView {
595
+ const view: AccountOrderItemView = {
596
+ id: item.id,
597
+ name: item.title,
598
+ quantity: item.quantity,
599
+ lineTotal: formatMoney(item.total) ?? '',
600
+ }
601
+ if (typeof item.variantTitle === 'string' && item.variantTitle.trim() !== '') {
602
+ view.variant = item.variantTitle
603
+ }
604
+ const image = normalizeImageUrl(item.image?.url)
605
+ if (image !== undefined) view.image = image
606
+ return view
607
+ }
608
+
609
+ /** Map a backend order to the account view row, omitting absent optional fields. */
610
+ export function toAccountOrderView(order: Order): AccountOrderView {
611
+ const view: AccountOrderView = {
612
+ id: order.id,
613
+ number: order.orderNumber,
614
+ date: formatOrderDate(order.createdAt),
615
+ status: order.status,
616
+ total: formatMoney(order.total) ?? '',
617
+ items: order.items.map(toAccountOrderItemView),
618
+ }
619
+ const subtotal = formatMoney(order.subtotal)
620
+ if (subtotal !== undefined) view.subtotal = subtotal
621
+ const tax = formatMoney(order.tax)
622
+ if (tax !== undefined) view.tax = tax
623
+ const shipping = formatMoney(order.shipping)
624
+ if (shipping !== undefined) view.shipping = shipping
625
+ const address = formatAddress(order.shippingAddress)
626
+ if (address !== undefined) view.address = address
627
+ return view
628
+ }
629
+
630
+ /**
631
+ * Load the authenticated customer's order history for a request token, mapped
632
+ * to the account view rows. A missing token yields an empty list (the runtime
633
+ * builds no provider); it never fabricates an order.
634
+ */
635
+ export async function loadOrders(
636
+ token: string | null,
637
+ context: StoreContext = getStoreContext(),
638
+ ): Promise<AccountOrderView[]> {
639
+ const orders = await getCustomerOrders(token, { salesChannelId: context.salesChannelId })
640
+ return orders.map(toAccountOrderView)
641
+ }
642
+
643
+ /**
644
+ * The additive paginated history window projected for the account view.
645
+ * `count` is the backend's real total for the authenticated customer on the
646
+ * store's channel; it is never the page length.
647
+ */
648
+ export interface AccountOrdersPage {
649
+ orders: AccountOrderView[]
650
+ count: number
651
+ limit: number
652
+ offset: number
653
+ }
654
+
655
+ /**
656
+ * Load one bounded page of the authenticated customer's channel-scoped history
657
+ * through the additive paginated contract. A missing token yields an empty page
658
+ * with `count: 0` (the runtime builds no provider); it never fabricates an order.
659
+ */
660
+ export async function loadOrdersPage(
661
+ token: string | null,
662
+ options: { page?: number; pageSize?: number } = {},
663
+ context: StoreContext = getStoreContext(),
664
+ ): Promise<AccountOrdersPage> {
665
+ const page = await getCustomerOrdersPage({
666
+ token,
667
+ page: options.page,
668
+ pageSize: options.pageSize,
669
+ salesChannelId: context.salesChannelId,
670
+ })
671
+ return {
672
+ orders: page.orders.map(toAccountOrderView),
673
+ count: page.count,
674
+ limit: page.limit,
675
+ offset: page.offset,
676
+ }
677
+ }
678
+
679
+ export type { Money }
@@ -0,0 +1,26 @@
1
+ // Generated by Thor Commerce storefront (composition composer: features).
2
+ // Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
3
+ // Installed manifest catalog features, in stable order, with their persisted
4
+ // config. This is the generation seam the catalog URL boundary consumes to
5
+ // reject parameters owned by a feature that is not installed, and the seam the
6
+ // single controlled CategoryFilter2 reads to select its control
7
+ // visibility/config from the installed manifest only.
8
+
9
+ import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'
10
+
11
+ export const mountedFeatures: readonly CatalogFeatureName[] = []
12
+
13
+ // Every installed feature name, unfiltered by the catalog URL boundary. The
14
+ // general presence seam cross-cutting features read to gate their own
15
+ // contributions (e.g. the navigability feature's shell chrome).
16
+ export const mountedFeatureNames: readonly string[] = []
17
+
18
+ export const mountedFeatureConfigs: Partial<Record<string, Record<string, unknown>>> = {
19
+ }
20
+
21
+ // Typed route/action availability derived from the installed manifest. The
22
+ // generated host uses these to hide links to routes that are not installed,
23
+ // so a removed view never leaves an active dead-end navigation target.
24
+ export const cartViewAvailable = false
25
+
26
+ export const checkoutAvailable = false