@revenexx/cover 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/app/api/account.ts +8 -0
  4. package/app/api/categories.ts +3 -0
  5. package/app/api/checkout.ts +6 -0
  6. package/app/api/images.ts +4 -0
  7. package/app/api/markets.ts +3 -0
  8. package/app/api/product.ts +3 -0
  9. package/app/api/products.ts +4 -0
  10. package/app/api/search.ts +4 -0
  11. package/app/app.config.ts +29 -0
  12. package/app/assets/css/cover.css +25 -0
  13. package/app/components/account/AccountMenu.vue +30 -0
  14. package/app/components/account/AccountShell.vue +18 -0
  15. package/app/components/account/AccountSidebar.vue +90 -0
  16. package/app/components/account/address/AccountAddressCard.vue +287 -0
  17. package/app/components/account/carts/AccountCartsTable.vue +284 -0
  18. package/app/components/account/dashboard/AccountDashboard.vue +351 -0
  19. package/app/components/account/directorder/AccountDirectOrder.vue +512 -0
  20. package/app/components/account/governance/AccountApprovalLimitsTable.vue +221 -0
  21. package/app/components/account/governance/AccountCostCenterDetail.vue +276 -0
  22. package/app/components/account/governance/AccountCostCentersTable.vue +252 -0
  23. package/app/components/account/governance/AccountRequisitionDetail.vue +295 -0
  24. package/app/components/account/governance/AccountRequisitionsTable.vue +255 -0
  25. package/app/components/account/governance/AccountWorkflowDetail.vue +215 -0
  26. package/app/components/account/governance/AccountWorkflowsTable.vue +168 -0
  27. package/app/components/account/governance/GovernanceApprovalLimitModal.vue +183 -0
  28. package/app/components/account/governance/GovernanceCostCenterModal.vue +188 -0
  29. package/app/components/account/governance/GovernanceWorkflowModal.vue +191 -0
  30. package/app/components/account/orderlists/AccountOrderListDetail.vue +349 -0
  31. package/app/components/account/orderlists/AccountOrderListsTable.vue +352 -0
  32. package/app/components/account/orders/AccountOrderDetail.vue +376 -0
  33. package/app/components/account/orders/AccountOrdersTable.vue +281 -0
  34. package/app/components/account/preferences/AccountPreferences.vue +50 -0
  35. package/app/components/auth/AuthForgotPasswordPanel.vue +88 -0
  36. package/app/components/auth/AuthLoginPanel.vue +103 -0
  37. package/app/components/auth/AuthLoginTeaser.vue +24 -0
  38. package/app/components/auth/AuthPageHeader.vue +21 -0
  39. package/app/components/auth/AuthRegisterPanel.vue +431 -0
  40. package/app/components/auth/AuthRegisterTeaser.vue +24 -0
  41. package/app/components/auth/AuthResetPasswordPanel.vue +115 -0
  42. package/app/components/cart/CartButton.vue +42 -0
  43. package/app/components/cart/actions/CartCostCenterModal.vue +110 -0
  44. package/app/components/cart/actions/CartExportModal.vue +82 -0
  45. package/app/components/cart/actions/CartPositionTextsModal.vue +92 -0
  46. package/app/components/cart/actions/CartSaveToListModal.vue +177 -0
  47. package/app/components/cart/actions/CartWorkflowPickerModal.vue +64 -0
  48. package/app/components/cart/drawer/CartDrawer.vue +49 -0
  49. package/app/components/cart/item/CartItem.vue +318 -0
  50. package/app/components/cart/manage/CartNameModal.vue +68 -0
  51. package/app/components/cart/position/CartPosition.vue +369 -0
  52. package/app/components/cart/quickadd/CartQuickAdd.vue +145 -0
  53. package/app/components/cart/recommend/CartCrossSell.vue +102 -0
  54. package/app/components/cart/recommend/CartRecommendCard.vue +59 -0
  55. package/app/components/cart/recommend/CartReorderStrip.vue +108 -0
  56. package/app/components/cart/requisition/CartRequisitionGroup.vue +74 -0
  57. package/app/components/cart/skeleton/CartSkeleton.vue +18 -0
  58. package/app/components/cart/summary/CartSummary.vue +25 -0
  59. package/app/components/cart/summary/CartSummaryBreakdown.vue +35 -0
  60. package/app/components/cart/switcher/CartSwitcher.vue +147 -0
  61. package/app/components/cart/target/CartTargetModal.vue +129 -0
  62. package/app/components/cart/view/CartApprovalsPanel.vue +89 -0
  63. package/app/components/cart/view/CartListHeader.vue +86 -0
  64. package/app/components/cart/view/CartPositionList.vue +94 -0
  65. package/app/components/cart/view/CartSummaryPanel.vue +254 -0
  66. package/app/components/cart/view/CartView.vue +310 -0
  67. package/app/components/category/info/CategoryPrice.vue +47 -0
  68. package/app/components/category/info/CategorySku.vue +15 -0
  69. package/app/components/category/info/CategoryStock.vue +25 -0
  70. package/app/components/category/list/CategoryItemCount.vue +27 -0
  71. package/app/components/category/list/CategoryListPagination.vue +81 -0
  72. package/app/components/category/skeleton/CategoryDetailSkeleton.vue +12 -0
  73. package/app/components/category/skeleton/CategoryListSkeleton.vue +65 -0
  74. package/app/components/category/view/CategoryViewToggle.vue +33 -0
  75. package/app/components/category/view/GridView.vue +78 -0
  76. package/app/components/category/view/ListView.vue +80 -0
  77. package/app/components/checkout/confirmation/CheckoutConfirmationView.vue +289 -0
  78. package/app/components/checkout/form/CheckoutSchemaForm.vue +72 -0
  79. package/app/components/checkout/onepage/CheckoutAddressPickerModal.vue +260 -0
  80. package/app/components/checkout/onepage/CheckoutAddressSection.vue +186 -0
  81. package/app/components/checkout/onepage/CheckoutDeliverySection.vue +158 -0
  82. package/app/components/checkout/onepage/CheckoutGate.vue +67 -0
  83. package/app/components/checkout/onepage/CheckoutGuestDetailsSection.vue +154 -0
  84. package/app/components/checkout/onepage/CheckoutNotesSection.vue +62 -0
  85. package/app/components/checkout/onepage/CheckoutOnePageView.vue +77 -0
  86. package/app/components/checkout/onepage/CheckoutPaymentSection.vue +181 -0
  87. package/app/components/checkout/onepage/CheckoutSectionTitle.vue +18 -0
  88. package/app/components/checkout/onepage/CheckoutSummarySection.vue +211 -0
  89. package/app/components/checkout/onepage/CheckoutWorkflowPanel.vue +38 -0
  90. package/app/components/layout/AppLogo.vue +21 -0
  91. package/app/components/layout/footer/Footer.vue +148 -0
  92. package/app/components/layout/header/AppBar.vue +75 -0
  93. package/app/components/layout/header/CategoryNav.vue +93 -0
  94. package/app/components/layout/header/HeaderActionItem.vue +81 -0
  95. package/app/components/layout/header/LocaleSwitcher.vue +123 -0
  96. package/app/components/layout/header/MainNav.vue +61 -0
  97. package/app/components/layout/header/MobileMenu.vue +81 -0
  98. package/app/components/layout/header/TopBar.vue +63 -0
  99. package/app/components/listing/ListingView.vue +227 -0
  100. package/app/components/product/detail/ProductDetailAddToCart.vue +188 -0
  101. package/app/components/product/detail/ProductDetailDescription.vue +20 -0
  102. package/app/components/product/detail/ProductDetailImageGallery.vue +58 -0
  103. package/app/components/product/detail/ProductDetailPricing.vue +108 -0
  104. package/app/components/product/detail/ProductDetailSaveToList.vue +291 -0
  105. package/app/components/product/detail/ProductDetailSpecifications.vue +33 -0
  106. package/app/components/product/detail/ProductDetailStock.vue +36 -0
  107. package/app/components/search/filter/SearchFacets.vue +256 -0
  108. package/app/components/search/filter/SearchFacetsDrawer.vue +58 -0
  109. package/app/components/ui/AppShell.vue +7 -0
  110. package/app/components/ui/debug/PiniaStateCard.vue +106 -0
  111. package/app/components/ui/feedback/EmptyState.vue +30 -0
  112. package/app/components/ui/feedback/ErrorBoundary.vue +44 -0
  113. package/app/components/ui/feedback/NotFound.vue +34 -0
  114. package/app/components/ui/forms/CountrySelection.vue +14 -0
  115. package/app/components/ui/forms/FormKitDatePicker.vue +124 -0
  116. package/app/components/ui/forms/PasswordInput.vue +136 -0
  117. package/app/components/ui/forms/SearchInput.vue +213 -0
  118. package/app/components/ui/table/TableSortButton.vue +51 -0
  119. package/app/components/ui/table/TableToolbar.vue +66 -0
  120. package/app/composables/useAccount.ts +27 -0
  121. package/app/composables/useAccountAddresses.ts +116 -0
  122. package/app/composables/useAccountOrders.ts +18 -0
  123. package/app/composables/useAppUrl.ts +38 -0
  124. package/app/composables/useAuthStore.ts +202 -0
  125. package/app/composables/useB2BContext.ts +23 -0
  126. package/app/composables/useCartActions.ts +25 -0
  127. package/app/composables/useCartCalculation.ts +90 -0
  128. package/app/composables/useCartSelection.ts +46 -0
  129. package/app/composables/useCartStore.ts +837 -0
  130. package/app/composables/useCartSummaryFormatting.ts +28 -0
  131. package/app/composables/useCategories.ts +29 -0
  132. package/app/composables/useCheckoutOnePage.ts +438 -0
  133. package/app/composables/useCheckoutProfileSource.ts +8 -0
  134. package/app/composables/useCheckoutSchema.ts +28 -0
  135. package/app/composables/useDataTable.ts +14 -0
  136. package/app/composables/useFormValidation.ts +29 -0
  137. package/app/composables/useIcons.ts +17 -0
  138. package/app/composables/useLocalePreferences.ts +45 -0
  139. package/app/composables/useMarkets.ts +15 -0
  140. package/app/composables/useProduct.ts +89 -0
  141. package/app/composables/useProductCard.ts +8 -0
  142. package/app/composables/useProductCardActions.ts +106 -0
  143. package/app/composables/useProductImage.ts +12 -0
  144. package/app/composables/useProductListing.ts +210 -0
  145. package/app/composables/useProductUrl.ts +17 -0
  146. package/app/composables/useProducts.ts +130 -0
  147. package/app/composables/useSearch.ts +12 -0
  148. package/app/composables/useThemeStore.ts +81 -0
  149. package/app/composables/useViewMode.ts +18 -0
  150. package/app/config/countries.ts +74 -0
  151. package/app/config/icons.ts +283 -0
  152. package/app/config/navigation.ts +191 -0
  153. package/app/config/palette.ts +13 -0
  154. package/app/config/themes.ts +20 -0
  155. package/app/formkit.config.ts +50 -0
  156. package/app/interfaces/account/address-list.ts +34 -0
  157. package/app/interfaces/account/order-list.ts +47 -0
  158. package/app/interfaces/account/order-lists.ts +35 -0
  159. package/app/interfaces/account/profile.ts +17 -0
  160. package/app/interfaces/account.ts +3 -0
  161. package/app/interfaces/address.ts +9 -0
  162. package/app/interfaces/auth.ts +104 -0
  163. package/app/interfaces/b2b.ts +234 -0
  164. package/app/interfaces/cart-calculation.ts +131 -0
  165. package/app/interfaces/cart-item.ts +45 -0
  166. package/app/interfaces/checkout-draft.ts +4 -0
  167. package/app/interfaces/checkout.ts +43 -0
  168. package/app/interfaces/delivery.ts +13 -0
  169. package/app/interfaces/market.ts +24 -0
  170. package/app/interfaces/payment.ts +19 -0
  171. package/app/interfaces/persisted-cart.ts +10 -0
  172. package/app/interfaces/product-detail.ts +54 -0
  173. package/app/interfaces/product-list.ts +33 -0
  174. package/app/interfaces/search-facets.ts +14 -0
  175. package/app/interfaces/validation.ts +14 -0
  176. package/app/layouts/default.vue +78 -0
  177. package/app/layouts/focus.vue +80 -0
  178. package/app/plugins/formkit-locale.client.ts +23 -0
  179. package/app/shared/constants.ts +1 -0
  180. package/app/validations/companyName.ts +18 -0
  181. package/app/validations/emailFormat.ts +10 -0
  182. package/app/validations/formValidationConfig.ts +50 -0
  183. package/app/validations/maxLength.ts +12 -0
  184. package/app/validations/minLength.ts +9 -0
  185. package/app/validations/optionalCompanyName.ts +23 -0
  186. package/app/validations/passwordsMatch.ts +5 -0
  187. package/app/validations/phoneNumber.ts +19 -0
  188. package/app/validations/required.ts +11 -0
  189. package/app/validations/termsRequired.ts +4 -0
  190. package/app/validations/types.ts +10 -0
  191. package/app/validations/zipCode.ts +15 -0
  192. package/i18n/locales/de/account.json +458 -0
  193. package/i18n/locales/de/auth.json +155 -0
  194. package/i18n/locales/de/cart.json +263 -0
  195. package/i18n/locales/de/checkout.json +217 -0
  196. package/i18n/locales/de/common.json +80 -0
  197. package/i18n/locales/de/cover.json +33 -0
  198. package/i18n/locales/de/order.json +1 -0
  199. package/i18n/locales/de/product.json +71 -0
  200. package/i18n/locales/de/search.json +54 -0
  201. package/i18n/locales/de/validation.json +12 -0
  202. package/i18n/locales/en/account.json +458 -0
  203. package/i18n/locales/en/auth.json +155 -0
  204. package/i18n/locales/en/cart.json +263 -0
  205. package/i18n/locales/en/checkout.json +217 -0
  206. package/i18n/locales/en/common.json +80 -0
  207. package/i18n/locales/en/cover.json +33 -0
  208. package/i18n/locales/en/order.json +1 -0
  209. package/i18n/locales/en/product.json +71 -0
  210. package/i18n/locales/en/search.json +54 -0
  211. package/i18n/locales/en/validation.json +12 -0
  212. package/nuxt.config.ts +109 -0
  213. package/package.json +65 -0
  214. package/public/img/product-placeholder.svg +8 -0
  215. package/public/templates/direct-order-template.csv +3 -0
  216. package/public/templates/direct-order-template.xlsx +0 -0
  217. package/server/api/account/address/[id].delete.ts +51 -0
  218. package/server/api/account/address/[id].put.ts +83 -0
  219. package/server/api/account/address/index.post.ts +60 -0
  220. package/server/api/account/addresses.get.ts +25 -0
  221. package/server/api/account/context.get.ts +16 -0
  222. package/server/api/account/governance/[domain]/[id].put.ts +57 -0
  223. package/server/api/account/governance/[domain].post.ts +111 -0
  224. package/server/api/account/order-lists/[id].delete.ts +22 -0
  225. package/server/api/account/order-lists/[id].get.ts +17 -0
  226. package/server/api/account/order-lists/[id].put.ts +46 -0
  227. package/server/api/account/order-lists/index.get.ts +14 -0
  228. package/server/api/account/order-lists/index.post.ts +38 -0
  229. package/server/api/account/orders/[id].get.ts +35 -0
  230. package/server/api/account/orders.get.ts +35 -0
  231. package/server/api/account/profile.get.ts +56 -0
  232. package/server/api/account/profile.put.ts +128 -0
  233. package/server/api/account/requisitions/[id].get.ts +15 -0
  234. package/server/api/account/requisitions.get.ts +23 -0
  235. package/server/api/auth/login.post.ts +37 -0
  236. package/server/api/auth/logout.post.ts +4 -0
  237. package/server/api/auth/me.get.ts +3 -0
  238. package/server/api/auth/personas.get.ts +7 -0
  239. package/server/api/auth/recovery.post.ts +32 -0
  240. package/server/api/auth/recovery.put.ts +37 -0
  241. package/server/api/auth/register.post.ts +126 -0
  242. package/server/api/cart/calculate.post.ts +25 -0
  243. package/server/api/cart/export.post.ts +81 -0
  244. package/server/api/cart/index.delete.ts +10 -0
  245. package/server/api/cart/index.get.ts +10 -0
  246. package/server/api/cart/sync.post.ts +14 -0
  247. package/server/api/carts/[id]/activate.post.ts +18 -0
  248. package/server/api/carts/[id]/index.delete.ts +18 -0
  249. package/server/api/carts/[id]/index.put.ts +19 -0
  250. package/server/api/carts/[id]/items.post.ts +21 -0
  251. package/server/api/carts/index.get.ts +14 -0
  252. package/server/api/carts/index.post.ts +14 -0
  253. package/server/api/categories/[slug].get.ts +22 -0
  254. package/server/api/categories/index.get.ts +11 -0
  255. package/server/api/checkout/profile.get.ts +21 -0
  256. package/server/api/checkout/schema/[key].get.ts +19 -0
  257. package/server/api/checkout/session.post.ts +8 -0
  258. package/server/api/images/detail/[filename].get.ts +18 -0
  259. package/server/api/images/list/[filename].get.ts +17 -0
  260. package/server/api/markets.get.ts +9 -0
  261. package/server/api/orders/index.post.ts +376 -0
  262. package/server/api/payment/methods.post.ts +65 -0
  263. package/server/api/product/[id].get.ts +29 -0
  264. package/server/api/products/[id].get.ts +30 -0
  265. package/server/api/products/index.get.ts +21 -0
  266. package/server/api/requisitions/index.post.ts +67 -0
  267. package/server/api/shipping/rates.post.ts +70 -0
  268. package/server/api/themes/index.get.ts +9 -0
  269. package/server/api/typesense/drop.get.ts +22 -0
  270. package/server/api/typesense/health.get.ts +9 -0
  271. package/server/api/typesense/search.post.ts +199 -0
  272. package/server/api/typesense/seed.get.ts +112 -0
  273. package/server/api/typesense/suggest.post.ts +69 -0
  274. package/server/config/account/organization.json +146 -0
  275. package/server/config/account/personas.json +169 -0
  276. package/server/config/account/user.json +8 -0
  277. package/server/config/account/workflows.json +19 -0
  278. package/server/data/account/address-list.json +103 -0
  279. package/server/data/account/order-list.json +491 -0
  280. package/server/data/account/order-lists.json +149 -0
  281. package/server/data/account/profile.json +9 -0
  282. package/server/data/account/registration-requests.json +3 -0
  283. package/server/data/account/requisitions.json +686 -0
  284. package/server/data/categories.json +186 -0
  285. package/server/data/forms/checkout/add-address.json +24 -0
  286. package/server/data/list/5137-1.png +0 -0
  287. package/server/data/list/5498-1.png +0 -0
  288. package/server/data/list/5498-2.png +0 -0
  289. package/server/data/list/5498-3.png +0 -0
  290. package/server/data/list/5498-4.png +0 -0
  291. package/server/data/list/5498-5.png +0 -0
  292. package/server/data/list/5498-6.png +0 -0
  293. package/server/data/list/5519-1.png +0 -0
  294. package/server/data/list/5713-1.png +0 -0
  295. package/server/data/list/5789-1.png +0 -0
  296. package/server/data/list/5930-1.png +0 -0
  297. package/server/data/list/6127-1.png +0 -0
  298. package/server/data/list/6234-1.png +0 -0
  299. package/server/data/list/6238-1.png +0 -0
  300. package/server/data/list/6246-1.png +0 -0
  301. package/server/data/list/6270-1.png +0 -0
  302. package/server/data/list/6330-1.png +0 -0
  303. package/server/data/list/6336-1.png +0 -0
  304. package/server/data/list/6360-1.png +0 -0
  305. package/server/data/list/6363-1.png +0 -0
  306. package/server/data/list/6375-1.png +0 -0
  307. package/server/data/list/6385-1.png +0 -0
  308. package/server/data/list/6413-1.png +0 -0
  309. package/server/data/list/6418-1.png +0 -0
  310. package/server/data/list/6465-1.png +0 -0
  311. package/server/data/list/6477-1.png +0 -0
  312. package/server/data/list/6509-1.png +0 -0
  313. package/server/data/list/6545-1.png +0 -0
  314. package/server/data/list/6548-1.png +0 -0
  315. package/server/data/list/6566-1.png +0 -0
  316. package/server/data/list/6581-1.png +0 -0
  317. package/server/data/list/6609-1.png +0 -0
  318. package/server/data/list/6611-1.png +0 -0
  319. package/server/data/list/6641-1.png +0 -0
  320. package/server/data/list/6659-1.png +0 -0
  321. package/server/data/list/6662-1.png +0 -0
  322. package/server/data/list/6689-1.png +0 -0
  323. package/server/data/list/6698-1.png +0 -0
  324. package/server/data/list/6701-1.png +0 -0
  325. package/server/data/list/6752-1.png +0 -0
  326. package/server/data/list/6755-1.png +0 -0
  327. package/server/data/list/6837-1.png +0 -0
  328. package/server/data/list/6841-1.png +0 -0
  329. package/server/data/list/6844-1.png +0 -0
  330. package/server/data/list/6846-1.png +0 -0
  331. package/server/data/list/6886-1.png +0 -0
  332. package/server/data/list/6895-1.png +0 -0
  333. package/server/data/list/6897-1.png +0 -0
  334. package/server/data/list/6919-1.png +0 -0
  335. package/server/data/list/6977-1.png +0 -0
  336. package/server/data/list/6983-1.png +0 -0
  337. package/server/data/list/6984-1.png +0 -0
  338. package/server/data/list/6985-1.png +0 -0
  339. package/server/data/list/6986-1.png +0 -0
  340. package/server/data/list/6989-1.png +0 -0
  341. package/server/data/list/6995-1.png +0 -0
  342. package/server/data/list/6998-1.png +0 -0
  343. package/server/data/markets.json +24 -0
  344. package/server/data/product-detail.json +2450 -0
  345. package/server/data/product-list.json +2450 -0
  346. package/server/data/themes.json +8 -0
  347. package/server/interfaces/account.ts +20 -0
  348. package/server/interfaces/auth.ts +32 -0
  349. package/server/interfaces/b2bContext.ts +23 -0
  350. package/server/interfaces/cart.ts +46 -0
  351. package/server/interfaces/cartCalculation.ts +17 -0
  352. package/server/interfaces/category.ts +12 -0
  353. package/server/interfaces/checkoutProfile.ts +5 -0
  354. package/server/interfaces/log.ts +3 -0
  355. package/server/interfaces/market.ts +10 -0
  356. package/server/interfaces/product.ts +21 -0
  357. package/server/interfaces/schema.ts +20 -0
  358. package/server/interfaces/theme.ts +10 -0
  359. package/server/services/ApiAuthService.ts +138 -0
  360. package/server/services/ApiCartService.ts +254 -0
  361. package/server/services/ApiCategoryService.ts +25 -0
  362. package/server/services/ApiMarketService.ts +71 -0
  363. package/server/services/ApiProductService.ts +53 -0
  364. package/server/services/LocalFileCategoryService.ts +23 -0
  365. package/server/services/LocalFileCheckoutProfileService.ts +117 -0
  366. package/server/services/LocalFileProductService.ts +128 -0
  367. package/server/services/LocalFileSchemaService.ts +70 -0
  368. package/server/services/LocalFileThemeService.ts +18 -0
  369. package/server/services/LogService.ts +28 -0
  370. package/server/services/MockAccountService.ts +58 -0
  371. package/server/services/MockAuthService.ts +105 -0
  372. package/server/services/MockB2BContextService.ts +149 -0
  373. package/server/services/MockCartCalculationService.ts +395 -0
  374. package/server/services/MockMarketService.ts +18 -0
  375. package/server/services/SdkAccountService.ts +56 -0
  376. package/server/services/SdkAuthService.ts +83 -0
  377. package/server/services/SessionCartService.ts +31 -0
  378. package/server/utils/accountService.ts +30 -0
  379. package/server/utils/authCookie.ts +13 -0
  380. package/server/utils/authService.ts +43 -0
  381. package/server/utils/b2bService.ts +49 -0
  382. package/server/utils/cartService.ts +59 -0
  383. package/server/utils/categoryService.ts +19 -0
  384. package/server/utils/checkoutProfileService.ts +9 -0
  385. package/server/utils/checkoutSession.ts +38 -0
  386. package/server/utils/coverData.ts +84 -0
  387. package/server/utils/governanceStore.ts +38 -0
  388. package/server/utils/i18n.ts +76 -0
  389. package/server/utils/inventoryService.ts +14 -0
  390. package/server/utils/liveCatalog.ts +234 -0
  391. package/server/utils/liveInventories.ts +76 -0
  392. package/server/utils/liveOrders.ts +139 -0
  393. package/server/utils/livePrices.ts +93 -0
  394. package/server/utils/locale.ts +39 -0
  395. package/server/utils/logService.ts +19 -0
  396. package/server/utils/marketService.ts +24 -0
  397. package/server/utils/orderService.ts +14 -0
  398. package/server/utils/paymentService.ts +14 -0
  399. package/server/utils/priceService.ts +14 -0
  400. package/server/utils/productService.ts +28 -0
  401. package/server/utils/productsSchema.ts +30 -0
  402. package/server/utils/revenexxApi.ts +136 -0
  403. package/server/utils/schemaService.ts +25 -0
  404. package/server/utils/serviceMode.ts +70 -0
  405. package/server/utils/shippingService.ts +14 -0
  406. package/server/utils/shopSdk.ts +88 -0
  407. package/server/utils/themeService.ts +16 -0
  408. package/server/utils/typesense.ts +27 -0
@@ -0,0 +1,71 @@
1
+ import type { ShopMarket, ShopMarketLocale } from "../../app/interfaces/market";
2
+ import type { IMarketService } from "../interfaces/market";
3
+
4
+ interface ApiMarketRow {
5
+ id: string;
6
+ code: string;
7
+ name: string;
8
+ currency: string;
9
+ status: string;
10
+ is_default: boolean;
11
+ position: number;
12
+ }
13
+
14
+ interface ApiLocaleRow {
15
+ code: string;
16
+ language: string;
17
+ country: string;
18
+ is_default: boolean;
19
+ position: number;
20
+ }
21
+
22
+ interface ApiListPage<T> {
23
+ items: T[];
24
+ }
25
+
26
+ const MARKET_CACHE_TTL_MS = 5 * 60 * 1000;
27
+
28
+ let cache: { markets: ShopMarket[]; loadedAt: number } | null = null;
29
+
30
+ /**
31
+ * Live markets via the public revenexx API (markets app):
32
+ * GET /v1/markets + each market's locales. Cached briefly — the selector
33
+ * renders on every page and markets change rarely.
34
+ */
35
+ export class ApiMarketService implements IMarketService {
36
+ async listMarkets(): Promise<ShopMarket[]> {
37
+ if (cache && Date.now() - cache.loadedAt < MARKET_CACHE_TTL_MS) {
38
+ return cache.markets;
39
+ }
40
+
41
+ const api = useRevenexxApi();
42
+ const { items } = await api.get<ApiListPage<ApiMarketRow>>("/v1/markets");
43
+ const active = items
44
+ .filter(m => m.status === "active")
45
+ .sort((a, b) => a.position - b.position || a.code.localeCompare(b.code));
46
+
47
+ const markets = await Promise.all(active.map(async (market): Promise<ShopMarket> => {
48
+ const { items: locales } = await api.get<ApiListPage<ApiLocaleRow>>(
49
+ `/v1/markets/${encodeURIComponent(market.id)}/locales`,
50
+ );
51
+ return {
52
+ id: market.id,
53
+ code: market.code,
54
+ name: market.name,
55
+ currency: market.currency,
56
+ isDefault: market.is_default,
57
+ locales: locales
58
+ .sort((a, b) => a.position - b.position)
59
+ .map((locale): ShopMarketLocale => ({
60
+ code: locale.code,
61
+ language: locale.language,
62
+ country: locale.country,
63
+ isDefault: locale.is_default,
64
+ })),
65
+ };
66
+ }));
67
+
68
+ cache = { markets, loadedAt: Date.now() };
69
+ return markets;
70
+ }
71
+ }
@@ -0,0 +1,53 @@
1
+ import type { Product } from "../../app/composables/useProducts";
2
+ import type { ProductDetail } from "../../app/interfaces/product-detail";
3
+ import type { Locale } from "../utils/locale";
4
+
5
+ import type { IProductService, ProductListFilter } from "../interfaces/product";
6
+
7
+ /** Listing cap for the stub — the live index holds tens of thousands of products. */
8
+ const LIST_PAGE_SIZE = 48;
9
+
10
+ /**
11
+ * Live product reads via the public revenexx API: the products app's real
12
+ * Typesense collection, scoped per locale (document id = `{entity_id}:{locale}`).
13
+ * Prices, stocks and images are not in the index yet — the mapping serves
14
+ * neutral defaults (see liveCatalog.ts).
15
+ */
16
+ export class ApiProductService implements IProductService {
17
+ async listProducts(filter?: ProductListFilter): Promise<Product[]> {
18
+ const locale: Locale = filter?.locale ?? "de";
19
+ const filterParts = [`locale:=${tsFilterValue(locale)}`, "enabled:=true"];
20
+
21
+ // Category facets carry the category CODE — our category slugs.
22
+ // Products are indexed with their LEAF category only, so a top-level
23
+ // category filters across all of its subcategory codes.
24
+ if (filter?.subcategory) {
25
+ filterParts.push(`categories:=${tsFilterValue(filter.subcategory)}`);
26
+ }
27
+ else if (filter?.category) {
28
+ const tree = await getLiveCategories(locale);
29
+ const node = tree.find(c => c.slug === filter.category);
30
+ const codes = [filter.category, ...(node?.subcategories.map(s => s.slug) ?? [])];
31
+ filterParts.push(`categories:=[${codes.map(tsFilterValue).join(",")}]`);
32
+ }
33
+
34
+ const result = await searchLiveProducts({
35
+ q: "*",
36
+ queryBy: "name,sku",
37
+ filterBy: filterParts.join(" && "),
38
+ perPage: LIST_PAGE_SIZE,
39
+ });
40
+
41
+ return (result.hits ?? []).map(hit => mapLiveDocToProduct(hit.document));
42
+ }
43
+
44
+ async getProductById(id: string, locale: Locale = "de"): Promise<Product | null> {
45
+ const doc = await getLiveProductDoc(id, locale);
46
+ return doc ? mapLiveDocToProduct(doc) : null;
47
+ }
48
+
49
+ async getProductDetail(id: string, locale: Locale = "de"): Promise<ProductDetail | null> {
50
+ const doc = await getLiveProductDoc(id, locale);
51
+ return doc ? mapLiveDocToDetail(doc) : null;
52
+ }
53
+ }
@@ -0,0 +1,23 @@
1
+ import type { ProductCategory } from "../../app/config/navigation";
2
+ import type { ICategoryService } from "../interfaces/category";
3
+
4
+ /**
5
+ * Reads categories from a local JSON file.
6
+ * Replace with a database-backed implementation once a data store is available.
7
+ */
8
+ export class LocalFileCategoryService implements ICategoryService {
9
+ private readonly dataKey: string;
10
+
11
+ constructor(dataKey: string) {
12
+ this.dataKey = dataKey;
13
+ }
14
+
15
+ async listCategories(): Promise<ProductCategory[]> {
16
+ return readCoverDataJson<ProductCategory[]>(this.dataKey);
17
+ }
18
+
19
+ async getCategoryBySlug(slug: string): Promise<ProductCategory | null> {
20
+ const categories = await this.listCategories();
21
+ return categories.find(c => c.slug === slug) ?? null;
22
+ }
23
+ }
@@ -0,0 +1,117 @@
1
+ import type { Address } from "../../app/interfaces/address";
2
+ import type { CheckoutProfile } from "../../app/interfaces/checkout";
3
+
4
+ import type { ICheckoutProfileService } from "../interfaces/checkoutProfile";
5
+
6
+ /** Shape of the account address book (account/address-list.json). */
7
+ interface AccountAddressEntry {
8
+ id: string;
9
+ company: string;
10
+ label: string;
11
+ firstName: string;
12
+ lastName: string;
13
+ street: string;
14
+ streetNumber: string;
15
+ city: string;
16
+ zip: string;
17
+ country: string;
18
+ isDefault: boolean;
19
+ type: "billing" | "shipping";
20
+ }
21
+
22
+ const MOCK_PROFILE: CheckoutProfile = {
23
+ addresses: [
24
+ {
25
+ id: "addr-1",
26
+ companyName: "Demo GmbH",
27
+ contactName: "Max Mustermann",
28
+ street: "Musterstraße 1",
29
+ city: "Berlin",
30
+ postalCode: "10115",
31
+ country: "DE",
32
+ },
33
+ {
34
+ id: "addr-2",
35
+ companyName: "Demo GmbH",
36
+ contactName: "Erika Musterfrau",
37
+ street: "Rechnungsweg 5",
38
+ city: "Hamburg",
39
+ postalCode: "20095",
40
+ country: "DE",
41
+ },
42
+ ],
43
+ defaultShippingAddressId: "addr-1",
44
+ defaultBillingAddressId: "addr-1",
45
+ availablePaymentMethods: [
46
+ { method: "invoice", label: "Rechnung", description: "30 Tage netto" },
47
+ { method: "po_number", label: "Bestellschein", description: "Zahlung gemäß Bestellschein" },
48
+ { method: "cost_center", label: "Kostenstelle", description: "Interne Verrechnung" },
49
+ ],
50
+ defaultPaymentMethod: "invoice",
51
+ costCenters: [
52
+ { id: "cc-1", code: "CC-100", label: "Marketing" },
53
+ { id: "cc-2", code: "CC-200", label: "Engineering" },
54
+ { id: "cc-3", code: "CC-300", label: "Operations" },
55
+ ],
56
+ availableDeliveryMethods: [
57
+ { method: "standard", label: "Standard", price: 0, eta: "3–5" },
58
+ { method: "express", label: "Express", price: 9.90, eta: "1–2" },
59
+ ],
60
+ defaultDeliveryMethod: "standard",
61
+ };
62
+
63
+ /** Checkout options for guests: no address book, invoice only. */
64
+ const GUEST_PROFILE: CheckoutProfile = {
65
+ addresses: [],
66
+ defaultShippingAddressId: "",
67
+ defaultBillingAddressId: "",
68
+ availablePaymentMethods: [
69
+ { method: "invoice", label: "Rechnung", description: "Zahlbar nach Erhalt" },
70
+ ],
71
+ defaultPaymentMethod: "invoice",
72
+ costCenters: [],
73
+ availableDeliveryMethods: MOCK_PROFILE.availableDeliveryMethods,
74
+ defaultDeliveryMethod: "standard",
75
+ };
76
+
77
+ /**
78
+ * Checkout profile backed by the account address book: the checkout and
79
+ * /account/addresses read and write the same mutable store, so addresses
80
+ * created in either place show up in both.
81
+ */
82
+ export class LocalFileCheckoutProfileService implements ICheckoutProfileService {
83
+ async getProfile(userId: string | null): Promise<CheckoutProfile> {
84
+ if (!userId) {
85
+ return GUEST_PROFILE;
86
+ }
87
+ try {
88
+ const data = await readCoverMutableJson<{ addresses: AccountAddressEntry[] }>(
89
+ "account/address-list.json",
90
+ );
91
+ const addresses: Address[] = data.addresses.map(entry => ({
92
+ id: entry.id,
93
+ companyName: entry.company || `${entry.firstName} ${entry.lastName}`.trim(),
94
+ contactName: `${entry.firstName} ${entry.lastName}`.trim(),
95
+ street: `${entry.street} ${entry.streetNumber}`.trim(),
96
+ city: entry.city,
97
+ postalCode: entry.zip,
98
+ country: entry.country,
99
+ }));
100
+ const defaultOf = (type: "shipping" | "billing"): string =>
101
+ data.addresses.find(entry => entry.type === type && entry.isDefault)?.id
102
+ ?? data.addresses.find(entry => entry.type === type)?.id
103
+ ?? addresses[0]?.id
104
+ ?? "";
105
+ return {
106
+ ...MOCK_PROFILE,
107
+ addresses,
108
+ defaultShippingAddressId: defaultOf("shipping"),
109
+ defaultBillingAddressId: defaultOf("billing"),
110
+ };
111
+ }
112
+ catch {
113
+ // Address book unavailable — fall back to the bundled profile.
114
+ return MOCK_PROFILE;
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,128 @@
1
+ import type { Product } from "../../app/composables/useProducts";
2
+ import type { ProductDetail } from "../../app/interfaces/product-detail";
3
+
4
+ import type { IProductService, ProductListFilter } from "../interfaces/product";
5
+
6
+ interface SourceProduct {
7
+ id: number;
8
+ name: string;
9
+ sku: string;
10
+ available: boolean;
11
+ }
12
+
13
+ interface SourceCategory {
14
+ type: string;
15
+ name: string;
16
+ }
17
+
18
+ interface SourceImage {
19
+ type: string;
20
+ priority: number;
21
+ url: string;
22
+ }
23
+
24
+ interface SourceStock {
25
+ quantity: number;
26
+ maxOrderQuantity: number;
27
+ }
28
+
29
+ interface SourceTax {
30
+ rate?: number;
31
+ includedInPrice?: boolean;
32
+ }
33
+
34
+ interface SourceItem {
35
+ product: SourceProduct[];
36
+ categories: SourceCategory[];
37
+ images: SourceImage[];
38
+ stocks: SourceStock[];
39
+ prices: { [key: string]: number }[];
40
+ tax?: SourceTax;
41
+ }
42
+
43
+ function mapToProduct(source: SourceItem): Product | null {
44
+ const mainProduct = source.product[0];
45
+ if (!mainProduct) {
46
+ return null;
47
+ }
48
+
49
+ const slugPath = source.categories.find(c => c.type === "slug")?.name ?? "";
50
+ const slugSegments = slugPath.split("/").filter(Boolean);
51
+ const categorySlug = slugSegments[0] ?? "";
52
+ const candidateSubcategory = slugSegments[1];
53
+ const subcategorySlug = candidateSubcategory && candidateSubcategory !== "product" && candidateSubcategory !== "products"
54
+ ? candidateSubcategory
55
+ : undefined;
56
+
57
+ const categoryName = source.categories.find(c => c.type === "main")?.name ?? categorySlug;
58
+ const subcategoryName = source.categories.find(c => c.type === "subcategory")?.name;
59
+
60
+ const primaryImage = [...(source.images ?? [])]
61
+ .sort((a, b) => a.priority - b.priority)
62
+ .find(img => img.type === "main")?.url
63
+ ?? source.images?.[0]?.url
64
+ ?? "";
65
+
66
+ const maxOrderQuantity = source.stocks?.[0]?.maxOrderQuantity;
67
+ const taxRate = typeof source.tax?.rate === "number" && Number.isFinite(source.tax.rate)
68
+ ? source.tax.rate
69
+ : 0;
70
+
71
+ return {
72
+ // Demo JSON carries numeric ids — the contract is string (UUID live).
73
+ id: String(mainProduct.id),
74
+ name: mainProduct.name,
75
+ sku: mainProduct.sku,
76
+ available: mainProduct.available,
77
+ prices: source.prices ?? [],
78
+ tax: { rate: taxRate, includedInPrice: source.tax?.includedInPrice },
79
+ stocks: source.stocks ?? [],
80
+ ...(typeof maxOrderQuantity === "number" ? { maxOrderQuantity } : {}),
81
+ image: primaryImage,
82
+ category: categoryName,
83
+ categorySlug,
84
+ ...(subcategoryName ? { subcategory: subcategoryName } : {}),
85
+ ...(subcategorySlug ? { subcategorySlug } : {}),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Reads product list and product detail from local JSON files.
91
+ * Mirrors the lookup pattern of a real database-backed product service.
92
+ * Replace with a database-backed implementation once a data store is available.
93
+ */
94
+ export class LocalFileProductService implements IProductService {
95
+ private readonly listKey: string;
96
+ private readonly detailKey: string;
97
+
98
+ constructor(listKey: string, detailKey: string) {
99
+ this.listKey = listKey;
100
+ this.detailKey = detailKey;
101
+ }
102
+
103
+ async listProducts(filter?: ProductListFilter): Promise<Product[]> {
104
+ const all = (await readCoverDataJson<SourceItem[]>(this.listKey))
105
+ .map(mapToProduct)
106
+ .filter((p): p is Product => p !== null);
107
+
108
+ return all.filter((product) => {
109
+ if (filter?.category && product.categorySlug !== filter.category) {
110
+ return false;
111
+ }
112
+ if (filter?.subcategory && product.subcategorySlug !== filter.subcategory) {
113
+ return false;
114
+ }
115
+ return true;
116
+ });
117
+ }
118
+
119
+ async getProductById(id: string): Promise<Product | null> {
120
+ const products = await this.listProducts();
121
+ return products.find(p => p.id === id) ?? null;
122
+ }
123
+
124
+ async getProductDetail(id: string): Promise<ProductDetail | null> {
125
+ const all = await readCoverDataJson<Record<string, ProductDetail>>(this.detailKey);
126
+ return all[id] ?? null;
127
+ }
128
+ }
@@ -0,0 +1,70 @@
1
+ import type { ISchemaService, ISchemaTranslator, SchemaObject, SchemaValue } from "../interfaces/schema";
2
+
3
+ /**
4
+ * Loads form schemas from local JSON files and translates i18n keys server-side.
5
+ * getSchema() returns a fully translated, ready-to-serve schema.
6
+ * This is the default implementation; swap it out via app.config → schemaService
7
+ * when schemas are served from a remote CMS or API.
8
+ */
9
+ export class LocalFileSchemaService implements ISchemaService, ISchemaTranslator {
10
+ private readonly basePrefix: string;
11
+ private readonly rawCache = new Map<string, SchemaObject>();
12
+
13
+ constructor(basePrefix: string) {
14
+ this.basePrefix = basePrefix;
15
+ }
16
+
17
+ private async loadRaw(key: string): Promise<SchemaObject> {
18
+ if (this.rawCache.has(key)) {
19
+ return this.rawCache.get(key)!;
20
+ }
21
+ const schema = await readCoverDataJson<SchemaObject>(`${this.basePrefix}/${key}.json`);
22
+ this.rawCache.set(key, schema);
23
+ return schema;
24
+ }
25
+
26
+ translateSchema(schema: SchemaValue, locale: string): SchemaValue {
27
+ if (Array.isArray(schema)) {
28
+ return schema.map(item => this.translateSchema(item, locale));
29
+ }
30
+ if (schema !== null && typeof schema === "object") {
31
+ const obj = schema as SchemaObject;
32
+ const result: SchemaObject = {};
33
+ for (const [key, value] of Object.entries(obj)) {
34
+ if ((key === "title" || key === "label") && typeof value === "string" && !value.includes(" ")) {
35
+ result[key] = serverT(value, locale);
36
+ }
37
+ else if (key === "message" && typeof value === "string" && !value.includes(" ")) {
38
+ result[key] = serverT(value.split(".").pop()!, locale, "validation");
39
+ }
40
+ else if (key === "validationMessages" && value !== null && typeof value === "object") {
41
+ // FormKit: { rule: "validation.key" } — values are i18n keys.
42
+ result[key] = Object.fromEntries(
43
+ Object.entries(value as Record<string, string>).map(([rule, msg]) => [
44
+ rule,
45
+ typeof msg === "string" && !msg.includes(" ")
46
+ ? serverT(msg.split(".").pop()!, locale, "validation")
47
+ : msg,
48
+ ]),
49
+ );
50
+ }
51
+ else if (key === "children" && typeof value === "string" && !value.includes(" ") && value.includes(".")) {
52
+ // FormKit $el nodes: a key-like string child is an i18n key
53
+ // (e.g. section headings); literal texts contain spaces.
54
+ result[key] = serverT(value, locale);
55
+ }
56
+ else {
57
+ result[key] = this.translateSchema(value, locale);
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ return schema;
63
+ }
64
+
65
+ async getSchema(key: string, locale: string): Promise<SchemaObject> {
66
+ const raw = await this.loadRaw(key);
67
+ await preloadLocaleNamespaces(locale, ["checkout", "validation"]);
68
+ return this.translateSchema(raw, locale) as SchemaObject;
69
+ }
70
+ }
@@ -0,0 +1,18 @@
1
+ import type { Theme } from "../../app/config/themes";
2
+ import type { IThemeService } from "../interfaces/theme";
3
+
4
+ /**
5
+ * Reads themes from a local JSON file.
6
+ * Replace with a database-backed implementation once a data store is available.
7
+ */
8
+ export class LocalFileThemeService implements IThemeService {
9
+ private readonly dataKey: string;
10
+
11
+ constructor(dataKey: string) {
12
+ this.dataKey = dataKey;
13
+ }
14
+
15
+ async listThemes(): Promise<Theme[]> {
16
+ return readCoverDataJson<Theme[]>(this.dataKey);
17
+ }
18
+ }
@@ -0,0 +1,28 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ import type { ILogService } from "../interfaces/log";
5
+
6
+ export class LogService implements ILogService {
7
+ private readonly logPath: string;
8
+
9
+ constructor(logPath: string) {
10
+ this.logPath = logPath;
11
+ mkdirSync(dirname(logPath), { recursive: true });
12
+ }
13
+
14
+ error(message: string, context?: Record<string, unknown>): void {
15
+ const entry = JSON.stringify({
16
+ level: "error",
17
+ timestamp: new Date().toISOString(),
18
+ message,
19
+ ...context,
20
+ });
21
+ try {
22
+ appendFileSync(this.logPath, entry + "\n");
23
+ }
24
+ catch {
25
+ console.error("[LogService] Failed to write log entry:", entry);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,58 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { StoredSession } from "../../app/interfaces/auth";
4
+ import type { AuthPersona } from "../interfaces/auth";
5
+ import type { AccountUser, IAccountService } from "../interfaces/account";
6
+
7
+ /**
8
+ * Stub implementation reading users from a local JSON file and querying by ID.
9
+ * Mirrors the lookup pattern of a real database-backed identity service.
10
+ * Until auth is available, always resolves the user with id=1.
11
+ * Replace with a real identity service implementation once auth is available.
12
+ */
13
+ export class MockAccountService implements IAccountService {
14
+ private readonly configKey: string;
15
+
16
+ constructor(configKey: string) {
17
+ this.configKey = configKey;
18
+ }
19
+
20
+ async getUser(event?: H3Event): Promise<AccountUser> {
21
+ // When a mock persona is signed in, the identity is that persona —
22
+ // this keeps account pages consistent with the auth simulation.
23
+ const persona = event ? await this.personaFromSession(event) : null;
24
+ if (persona) {
25
+ return {
26
+ id: persona.id,
27
+ name: persona.name,
28
+ email: persona.email,
29
+ initials: persona.name.split(/\s+/).slice(0, 2).map(p => p[0]?.toUpperCase() ?? "").join(""),
30
+ };
31
+ }
32
+
33
+ const users = await readCoverConfigJson<AccountUser[]>(this.configKey);
34
+ const user = users.find(u => u.id === 1);
35
+ if (!user) {
36
+ throw new Error("MockAccountService: user with id=1 not found in config");
37
+ }
38
+ return user;
39
+ }
40
+
41
+ private async personaFromSession(event: H3Event): Promise<AuthPersona | null> {
42
+ const raw = getCookie(event, SESSION_COOKIE_NAME);
43
+ if (!raw) {
44
+ return null;
45
+ }
46
+ try {
47
+ const session = JSON.parse(raw) as StoredSession;
48
+ if (!session.personaId) {
49
+ return null;
50
+ }
51
+ const personas = await readCoverConfigJson<AuthPersona[]>("account/personas.json");
52
+ return personas.find(p => p.id === session.personaId) ?? null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,105 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { AuthUser, StoredSession } from "../../app/interfaces/auth";
4
+ import type { AuthLoginResult, AuthPersona, IAuthService } from "../interfaces/auth";
5
+
6
+ const SESSION_TTL_MS = 1000 * 60 * 60 * 12; // 12h demo session
7
+
8
+ /**
9
+ * Offline authentication against the bundled B2B demo personas.
10
+ * Any password signs a persona in — this is a demo tool, not a security
11
+ * boundary. Unknown emails are rejected with the same error shape the SDK
12
+ * implementation produces, so the client-side flow behaves identically.
13
+ */
14
+ export class MockAuthService implements IAuthService {
15
+ private readonly personasKey: string;
16
+
17
+ constructor(personasKey: string) {
18
+ this.personasKey = personasKey;
19
+ }
20
+
21
+ async listPersonas(): Promise<AuthPersona[]> {
22
+ return readCoverConfigJson<AuthPersona[]>(this.personasKey);
23
+ }
24
+
25
+ /** Users registered through the storefront (demo persistence), as personas. */
26
+ private async listRegisteredUsers(): Promise<AuthPersona[]> {
27
+ try {
28
+ const store = await readCoverMutableJson<{
29
+ requests: { userId: string; name: string; email: string }[];
30
+ }>("account/registration-requests.json");
31
+ return store.requests.map(request => ({
32
+ id: request.userId,
33
+ name: request.name,
34
+ email: request.email,
35
+ role: "buyer" as const,
36
+ description: "Registered demo user",
37
+ }));
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ private async findPersona(predicate: (p: AuthPersona) => boolean): Promise<AuthPersona | null> {
45
+ const personas = await this.listPersonas();
46
+ const registered = await this.listRegisteredUsers();
47
+ return [...personas, ...registered].find(predicate) ?? null;
48
+ }
49
+
50
+ private toUser(persona: AuthPersona): AuthUser {
51
+ return { $id: persona.id, name: persona.name, email: persona.email, role: persona.role };
52
+ }
53
+
54
+ async login(event: H3Event, email: string): Promise<AuthLoginResult> {
55
+ const persona = await this.findPersona(p => p.email.toLowerCase() === email.toLowerCase());
56
+ if (!persona) {
57
+ throw createError({ statusCode: 401, data: { type: "user_invalid_credentials" } });
58
+ }
59
+
60
+ const expire = new Date(Date.now() + SESSION_TTL_MS).toISOString();
61
+ const session: StoredSession = {
62
+ id: `mock-${persona.id}`,
63
+ expire,
64
+ personaId: persona.id,
65
+ };
66
+ setCookie(event, SESSION_COOKIE_NAME, JSON.stringify(session), sessionCookieOptions(expire));
67
+
68
+ return {
69
+ user: this.toUser(persona),
70
+ session: { id: session.id, expire },
71
+ };
72
+ }
73
+
74
+ async me(event: H3Event): Promise<AuthUser | null> {
75
+ const raw = getCookie(event, SESSION_COOKIE_NAME);
76
+ if (!raw) {
77
+ return null;
78
+ }
79
+
80
+ let session: StoredSession;
81
+ try {
82
+ session = JSON.parse(raw) as StoredSession;
83
+ }
84
+ catch {
85
+ deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
86
+ return null;
87
+ }
88
+
89
+ if (!session.personaId || new Date(session.expire).getTime() < Date.now()) {
90
+ deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
91
+ return null;
92
+ }
93
+
94
+ const persona = await this.findPersona(p => p.id === session.personaId);
95
+ if (!persona) {
96
+ deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
97
+ return null;
98
+ }
99
+ return this.toUser(persona);
100
+ }
101
+
102
+ async logout(event: H3Event): Promise<void> {
103
+ deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
104
+ }
105
+ }