@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,234 @@
1
+ import type { ProductCategory, ProductSubcategory } from "../../app/config/navigation";
2
+ import type { Product } from "../../app/composables/useProducts";
3
+ import type { ProductDetail } from "../../app/interfaces/product-detail";
4
+ import type { Locale } from "./locale";
5
+
6
+ /**
7
+ * Live catalog access — products and categories from the public revenexx
8
+ * API. Product reads go against the platform's real Typesense index (the
9
+ * products app's search collection, exposed at /v1/search/*), categories
10
+ * come from the products app's category tree (/v1/products/categories).
11
+ *
12
+ * The live index has no prices, stocks or images yet — the mapping fills
13
+ * the storefront contracts with explicit neutral defaults so every surface
14
+ * (cards, detail, cart) renders without special-casing.
15
+ */
16
+
17
+ /** Document shape of the products search collection (search.json v2). */
18
+ export interface LiveProductDoc {
19
+ id: string;
20
+ entity_id: string;
21
+ sku: string;
22
+ kind: string;
23
+ enabled: boolean;
24
+ name: string;
25
+ description?: string;
26
+ manufacturer?: string;
27
+ ean?: string;
28
+ categories?: string[];
29
+ category_labels?: string[];
30
+ attrs?: Record<string, unknown>;
31
+ locale: string;
32
+ updated_at?: number;
33
+ }
34
+
35
+ export interface LiveSearchResponse {
36
+ found: number;
37
+ page: number;
38
+ hits?: { document: LiveProductDoc; highlight?: unknown }[];
39
+ facet_counts?: { field_name: string; counts?: { value: string; count: number }[] }[];
40
+ }
41
+
42
+ export interface LiveSearchParams {
43
+ q: string;
44
+ queryBy?: string;
45
+ filterBy?: string;
46
+ facetBy?: string;
47
+ sortBy?: string;
48
+ page?: number;
49
+ perPage?: number;
50
+ }
51
+
52
+ const PRODUCT_PLACEHOLDER_IMAGE = "/img/product-placeholder.svg";
53
+
54
+ /** TODO(markets): resolve from the market's tax classes instead of a constant. */
55
+ const LIVE_DEFAULT_TAX_RATE = 19;
56
+
57
+ /** The live index has no stock data — treat everything as orderable. */
58
+ const LIVE_DEFAULT_STOCK = { quantity: 9999, maxOrderQuantity: 999999 };
59
+
60
+ export async function searchLiveProducts(params: LiveSearchParams): Promise<LiveSearchResponse> {
61
+ return useRevenexxApi().get<LiveSearchResponse>(
62
+ "/v1/search/collections/products/documents/search",
63
+ {
64
+ q: params.q,
65
+ query_by: params.queryBy ?? "name,sku,description,manufacturer",
66
+ filter_by: params.filterBy,
67
+ facet_by: params.facetBy,
68
+ sort_by: params.sortBy,
69
+ page: params.page ?? 1,
70
+ per_page: params.perPage ?? 24,
71
+ },
72
+ );
73
+ }
74
+
75
+ /** Escape a value for a Typesense `filter_by` exact-match clause. */
76
+ export function tsFilterValue(value: string): string {
77
+ return "`" + value.replace(/`/g, "") + "`";
78
+ }
79
+
80
+ export function mapLiveDocToProduct(doc: LiveProductDoc): Product {
81
+ const categorySlug = doc.categories?.[0] ?? "";
82
+ const category = doc.category_labels?.[0] ?? categorySlug;
83
+
84
+ return {
85
+ id: doc.entity_id,
86
+ name: doc.name,
87
+ sku: doc.sku,
88
+ available: doc.enabled,
89
+ isOutOfStock: false,
90
+ prices: [],
91
+ stocks: [LIVE_DEFAULT_STOCK],
92
+ tax: { rate: LIVE_DEFAULT_TAX_RATE },
93
+ image: PRODUCT_PLACEHOLDER_IMAGE,
94
+ category,
95
+ categorySlug,
96
+ };
97
+ }
98
+
99
+ export function mapLiveDocToDetail(doc: LiveProductDoc): ProductDetail {
100
+ const syncedAt = doc.updated_at ? new Date(doc.updated_at * 1000).toISOString() : new Date().toISOString();
101
+
102
+ return {
103
+ product: [{
104
+ id: doc.entity_id,
105
+ version: 1,
106
+ name: doc.name,
107
+ sku: doc.sku,
108
+ manufacturer: doc.manufacturer ?? "",
109
+ createdAt: syncedAt,
110
+ updatedAt: syncedAt,
111
+ deletedAt: null,
112
+ available: doc.enabled,
113
+ }],
114
+ stocks: [LIVE_DEFAULT_STOCK],
115
+ tax: { rate: LIVE_DEFAULT_TAX_RATE },
116
+ descriptions: doc.description
117
+ ? [{ type: "long", content: doc.description }]
118
+ : [],
119
+ categories: [
120
+ ...(doc.category_labels?.[0] ? [{ type: "main", name: doc.category_labels[0] }] : []),
121
+ ...(doc.categories?.[0] ? [{ type: "slug", name: doc.categories[0] }] : []),
122
+ ],
123
+ images: [],
124
+ attributes: Object.entries(doc.attrs ?? {}).map(([name, value], index) => ({
125
+ id: index + 1,
126
+ name,
127
+ value: String(value),
128
+ })),
129
+ prices: [],
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Fetch one product document (`{entity_id}:{locale}`) from the live index.
135
+ * Uses a filtered search instead of the document-GET route — the search
136
+ * proxy's document endpoint currently rejects gateway-trust calls
137
+ * (responds 401 "x-typesense-api-key"), and `id` is filterable anyway.
138
+ */
139
+ export async function getLiveProductDoc(entityId: string, locale: Locale): Promise<LiveProductDoc | null> {
140
+ const result = await searchLiveProducts({
141
+ q: "*",
142
+ queryBy: "name",
143
+ filterBy: `id:=${tsFilterValue(`${entityId}:${locale}`)}`,
144
+ perPage: 1,
145
+ });
146
+ return result.hits?.[0]?.document ?? null;
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Categories — products app tree, cached briefly (it changes rarely and the
151
+ // storefront asks on every navigation render).
152
+ // ---------------------------------------------------------------------------
153
+
154
+ interface LiveCategoryRow {
155
+ id: string;
156
+ code: string;
157
+ parent_id: string | null;
158
+ position: number;
159
+ labels?: Record<string, string> | null;
160
+ }
161
+
162
+ interface LiveCategoryPage {
163
+ items: LiveCategoryRow[];
164
+ page: { total: number; returned: number; hasMore: boolean };
165
+ }
166
+
167
+ const CATEGORY_CACHE_TTL_MS = 5 * 60 * 1000;
168
+ const CATEGORY_PAGE_LIMIT = 200;
169
+
170
+ let categoryCache: { rows: LiveCategoryRow[]; loadedAt: number } | null = null;
171
+
172
+ async function loadLiveCategoryRows(): Promise<LiveCategoryRow[]> {
173
+ if (categoryCache && Date.now() - categoryCache.loadedAt < CATEGORY_CACHE_TTL_MS) {
174
+ return categoryCache.rows;
175
+ }
176
+
177
+ const api = useRevenexxApi();
178
+ const rows: LiveCategoryRow[] = [];
179
+ let offset = 0;
180
+ // Paginate defensively — the demo tenant has a few hundred categories.
181
+ for (let pageIndex = 0; pageIndex < 25; pageIndex++) {
182
+ const page = await api.get<LiveCategoryPage>("/v1/products/categories", {
183
+ limit: CATEGORY_PAGE_LIMIT,
184
+ offset,
185
+ });
186
+ rows.push(...page.items);
187
+ if (!page.page.hasMore) {
188
+ break;
189
+ }
190
+ offset += page.page.returned;
191
+ }
192
+
193
+ categoryCache = { rows, loadedAt: Date.now() };
194
+ return rows;
195
+ }
196
+
197
+ function categoryLabel(row: LiveCategoryRow, locale: Locale): string {
198
+ return row.labels?.[locale] ?? row.labels?.de ?? row.labels?.en ?? row.code;
199
+ }
200
+
201
+ /**
202
+ * Build the storefront's two-level category tree from the flat live rows.
203
+ * A single artificial root (e.g. an imported "web catalog" node) is skipped
204
+ * so its children become the top-level categories.
205
+ */
206
+ export async function getLiveCategories(locale: Locale): Promise<ProductCategory[]> {
207
+ const rows = await loadLiveCategoryRows();
208
+
209
+ const byParent = new Map<string | null, LiveCategoryRow[]>();
210
+ for (const row of rows) {
211
+ const list = byParent.get(row.parent_id) ?? [];
212
+ list.push(row);
213
+ byParent.set(row.parent_id, list);
214
+ }
215
+ for (const list of byParent.values()) {
216
+ list.sort((a, b) => a.position - b.position || a.code.localeCompare(b.code));
217
+ }
218
+
219
+ let roots = byParent.get(null) ?? [];
220
+ if (roots.length === 1) {
221
+ roots = byParent.get(roots[0]!.id) ?? [];
222
+ }
223
+
224
+ return roots.map((root): ProductCategory => ({
225
+ label: categoryLabel(root, locale),
226
+ slug: root.code,
227
+ icon: "case",
228
+ subcategories: (byParent.get(root.id) ?? []).map((child): ProductSubcategory => ({
229
+ label: categoryLabel(child, locale),
230
+ slug: child.code,
231
+ icon: "box",
232
+ })),
233
+ }));
234
+ }
@@ -0,0 +1,76 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { Product } from "../../app/composables/useProducts";
4
+ import type { ProductDetail } from "../../app/interfaces/product-detail";
5
+
6
+ /**
7
+ * Live availability from the inventories app — THE stock call (and the
8
+ * designated gateway override point for ERP stock). Tracked items get
9
+ * their real `available` quantity and `orderable` flag; items unknown to
10
+ * inventory (`tracked: false`) keep selling freely — that is the
11
+ * storefront's call, and the demo shop makes it permissive.
12
+ */
13
+
14
+ const UNTRACKED_STOCK = { quantity: 9999, maxOrderQuantity: 999999 };
15
+
16
+ interface ItemAvailability {
17
+ product_id: string | null;
18
+ sku: string | null;
19
+ available: number;
20
+ tracked: boolean;
21
+ orderable: boolean;
22
+ }
23
+
24
+ function toStock(entry: ItemAvailability): { quantity: number; maxOrderQuantity: number } {
25
+ if (!entry.tracked) {
26
+ return UNTRACKED_STOCK;
27
+ }
28
+ const available = Math.max(0, Math.floor(entry.available));
29
+ return { quantity: available, maxOrderQuantity: available };
30
+ }
31
+
32
+ async function fetchAvailability(items: Array<{ product_id?: string; sku?: string }>): Promise<Map<string, ItemAvailability>> {
33
+ const { availability } = await useRevenexxApi().post<{ availability: ItemAvailability[] }>(
34
+ "/v1/inventories/availability",
35
+ { items },
36
+ );
37
+ const byKey = new Map<string, ItemAvailability>();
38
+ for (const entry of availability) {
39
+ if (entry.product_id) byKey.set(entry.product_id, entry);
40
+ else if (entry.sku) byKey.set(entry.sku, entry);
41
+ }
42
+ return byKey;
43
+ }
44
+
45
+ /** Batch-enrich live catalog products with real stock (cards, lists, search). */
46
+ export async function enrichProductsWithLiveAvailability(event: H3Event, products: Product[]): Promise<Product[]> {
47
+ if (products.length === 0) {
48
+ return products;
49
+ }
50
+ const byKey = await fetchAvailability(products.map(p => ({ product_id: p.id, sku: p.sku })));
51
+ return products.map((product) => {
52
+ const entry = byKey.get(product.id) ?? byKey.get(product.sku);
53
+ if (!entry) {
54
+ return product;
55
+ }
56
+ return {
57
+ ...product,
58
+ stocks: [toStock(entry)],
59
+ isOutOfStock: entry.tracked && !entry.orderable,
60
+ };
61
+ });
62
+ }
63
+
64
+ /** Single-product variant for the PDP detail payload. */
65
+ export async function enrichDetailWithLiveAvailability(event: H3Event, detail: ProductDetail): Promise<ProductDetail> {
66
+ const product = detail.product[0];
67
+ if (!product) {
68
+ return detail;
69
+ }
70
+ const byKey = await fetchAvailability([{ product_id: product.id, sku: product.sku }]);
71
+ const entry = byKey.get(product.id) ?? byKey.get(product.sku);
72
+ if (!entry) {
73
+ return detail;
74
+ }
75
+ return { ...detail, stocks: [toStock(entry)] };
76
+ }
@@ -0,0 +1,139 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { StoredSession } from "../../app/interfaces/auth";
4
+ import type { AccountOrder, AccountOrderPosition, AccountOrderStatus, AccountOrderPaymentStatus } from "../../app/interfaces/account/order-list";
5
+
6
+ /**
7
+ * Live orders via the orders app — placing from the checkout snapshot and
8
+ * mapping the order aggregate back into the account history contracts.
9
+ */
10
+
11
+ export interface LiveOrderItem {
12
+ id: string;
13
+ position: number;
14
+ product_id: string | null;
15
+ sku: string | null;
16
+ name: string;
17
+ product: Record<string, unknown> | null;
18
+ quantity: number;
19
+ unit_price: number;
20
+ line_total: number;
21
+ quantity_shipped: number;
22
+ }
23
+
24
+ export interface LiveOrder {
25
+ id: string;
26
+ number: string;
27
+ customer_order_number: string | null;
28
+ status: "pending" | "placed" | "in_fulfillment" | "completed" | "cancelled";
29
+ payment_status: string;
30
+ fulfillment_status: string;
31
+ currency: string;
32
+ grand_total: number;
33
+ billing_address: Record<string, unknown> | null;
34
+ shipping_address: Record<string, unknown> | null;
35
+ payment: { method?: string } | null;
36
+ placed_at: string | null;
37
+ created_at: string;
38
+ acknowledged_at: string | null;
39
+ completed_at: string | null;
40
+ items?: LiveOrderItem[];
41
+ shipments?: Array<{ number: string; carrier: string | null; tracking_code: string | null; shipped_at: string }>;
42
+ }
43
+
44
+ /** The buyer's identity refs from the session cookie (guests place openly). */
45
+ export function sessionOrderRefs(event: H3Event): { contact_id?: string; organization_id?: string } {
46
+ const raw = getCookie(event, SESSION_COOKIE_NAME);
47
+ if (!raw) {
48
+ return {};
49
+ }
50
+ try {
51
+ const parsed = JSON.parse(raw) as StoredSession;
52
+ return {
53
+ ...(parsed.contactId ? { contact_id: parsed.contactId } : {}),
54
+ ...(parsed.organizationId ? { organization_id: parsed.organizationId } : {}),
55
+ };
56
+ }
57
+ catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ function mapStatus(order: LiveOrder): AccountOrderStatus {
63
+ if (order.status === "cancelled") return "cancelled";
64
+ if (order.status === "completed") return "shipped";
65
+ return "processing";
66
+ }
67
+
68
+ function mapPaymentStatus(status: string): AccountOrderPaymentStatus {
69
+ if (status === "paid") return "paid";
70
+ if (status === "refunded") return "refunded";
71
+ return "open";
72
+ }
73
+
74
+ const addressLine = (a: Record<string, unknown> | null | undefined): string =>
75
+ [a?.companyName, a?.street, `${a?.postalCode ?? ""} ${a?.city ?? ""}`.trim()]
76
+ .filter(Boolean).join(" · ");
77
+
78
+ function mapPositions(items: LiveOrderItem[] | undefined): AccountOrderPosition[] {
79
+ return (items ?? []).map((item) => {
80
+ const snapshot = (item.product ?? {}) as Record<string, unknown>;
81
+ return {
82
+ id: item.product_id ?? item.sku ?? item.id,
83
+ sku: item.sku ?? "",
84
+ name: item.name,
85
+ ...(snapshot.image ? { image: String(snapshot.image) } : {}),
86
+ quantity: Number(item.quantity),
87
+ unitPrice: Number(item.unit_price),
88
+ lineTotal: Number(item.line_total),
89
+ ...(snapshot.categorySlug ? { categorySlug: String(snapshot.categorySlug) } : {}),
90
+ ...(snapshot.subcategorySlug ? { subcategorySlug: String(snapshot.subcategorySlug) } : {}),
91
+ };
92
+ });
93
+ }
94
+
95
+ /** Live order aggregate → the account history contract. */
96
+ export function mapLiveOrderToAccount(order: LiveOrder): AccountOrder {
97
+ const placed = order.placed_at ?? order.created_at;
98
+ const day = (iso: string | null | undefined): string => (iso ?? placed).slice(0, 10);
99
+ const shipments = order.shipments ?? [];
100
+ const lastTracked = [...shipments].reverse().find(s => s.tracking_code);
101
+ const shippedAt = shipments[0]?.shipped_at;
102
+
103
+ const progress: NonNullable<AccountOrder["progress"]> = [{ step: "ordered", date: day(placed) }];
104
+ if (order.status !== "pending" && order.status !== "cancelled") {
105
+ progress.push({ step: "processing", date: day(order.acknowledged_at ?? placed) });
106
+ }
107
+ if (shipments.length > 0) {
108
+ progress.push({ step: "shipped", date: day(shippedAt) });
109
+ }
110
+
111
+ return {
112
+ id: order.number,
113
+ date: day(placed),
114
+ status: mapStatus(order),
115
+ paymentStatus: mapPaymentStatus(order.payment_status),
116
+ total: Number(order.grand_total),
117
+ currency: order.currency,
118
+ ...(order.customer_order_number ? { orderNumber: order.customer_order_number } : {}),
119
+ positions: mapPositions(order.items),
120
+ deliveryAddress: addressLine(order.shipping_address),
121
+ billingAddress: addressLine(order.billing_address),
122
+ ...(order.payment?.method ? { paymentMethod: order.payment.method } : {}),
123
+ ...(lastTracked
124
+ ? { tracking: { carrier: lastTracked.carrier ?? "", trackingNumber: lastTracked.tracking_code ?? "" } }
125
+ : {}),
126
+ progress,
127
+ };
128
+ }
129
+
130
+ /** Fetch one live order by its display number (the account history id). */
131
+ export async function getLiveOrderByNumber(number: string): Promise<LiveOrder | null> {
132
+ const api = useRevenexxApi();
133
+ const { items } = await api.get<{ items: LiveOrder[] }>("/v1/orders", { number, limit: 1 });
134
+ const row = items[0];
135
+ if (!row) {
136
+ return null;
137
+ }
138
+ return api.get<LiveOrder>(`/v1/orders/${row.id}`);
139
+ }
@@ -0,0 +1,93 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { StoredSession } from "../../app/interfaces/auth";
4
+ import type { Product } from "../../app/composables/useProducts";
5
+ import type { ProductDetail } from "../../app/interfaces/product-detail";
6
+
7
+ /**
8
+ * Live prices from the prices app — THE live price call. The storefront's
9
+ * price shape is the tier map (`prices: [{ "<qty>": totalCents }]`), so the
10
+ * resolve response (unit prices per tier) converts to total cents per tier
11
+ * quantity. A product the prices app answers `on_request` for keeps an
12
+ * EMPTY prices array — the UI renders "price on request", never €0.
13
+ */
14
+
15
+ interface ResolvedPrice {
16
+ product_id: string | null;
17
+ sku: string | null;
18
+ on_request: boolean;
19
+ unit_price: number | null;
20
+ tiers: Array<{ quantity_min: number; unit_price: number }>;
21
+ }
22
+
23
+ /** The buyer's price scope from the session (guests resolve openly). */
24
+ function priceContext(event: H3Event): { contact_id?: string; organization_id?: string } {
25
+ const raw = getCookie(event, SESSION_COOKIE_NAME);
26
+ if (!raw) {
27
+ return {};
28
+ }
29
+ try {
30
+ const parsed = JSON.parse(raw) as StoredSession;
31
+ return {
32
+ ...(parsed.contactId ? { contact_id: parsed.contactId } : {}),
33
+ ...(parsed.organizationId ? { organization_id: parsed.organizationId } : {}),
34
+ };
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+
41
+ /** Resolve tier ladder → the storefront tier map (quantity → TOTAL cents). */
42
+ function toPriceMap(resolved: ResolvedPrice): { [key: string]: number }[] {
43
+ if (resolved.on_request || resolved.unit_price === null) {
44
+ return [];
45
+ }
46
+ const tiers = resolved.tiers.length
47
+ ? resolved.tiers
48
+ : [{ quantity_min: 1, unit_price: resolved.unit_price }];
49
+ const map: { [key: string]: number } = {};
50
+ for (const tier of tiers) {
51
+ map[String(tier.quantity_min)] = Math.round(tier.unit_price * tier.quantity_min * 100);
52
+ }
53
+ return [map];
54
+ }
55
+
56
+ /**
57
+ * Batch-enrich live catalog products with resolved prices. Products the
58
+ * prices app does not price keep `prices: []` (→ price on request).
59
+ */
60
+ export async function enrichProductsWithLivePrices(event: H3Event, products: Product[]): Promise<Product[]> {
61
+ if (products.length === 0) {
62
+ return products;
63
+ }
64
+ const { prices } = await useRevenexxApi().post<{ prices: ResolvedPrice[] }>("/v1/prices/resolve", {
65
+ items: products.map(p => ({ product_id: p.id, sku: p.sku, quantity: 1 })),
66
+ ...priceContext(event),
67
+ });
68
+
69
+ const byKey = new Map<string, ResolvedPrice>();
70
+ for (const price of prices) {
71
+ if (price.product_id) byKey.set(price.product_id, price);
72
+ else if (price.sku) byKey.set(price.sku, price);
73
+ }
74
+
75
+ return products.map((product) => {
76
+ const resolved = byKey.get(product.id) ?? byKey.get(product.sku);
77
+ return resolved ? { ...product, prices: toPriceMap(resolved) } : { ...product, prices: [] };
78
+ });
79
+ }
80
+
81
+ /** Single-product variant for the PDP detail payload. */
82
+ export async function enrichDetailWithLivePrices(event: H3Event, detail: ProductDetail): Promise<ProductDetail> {
83
+ const product = detail.product[0];
84
+ if (!product) {
85
+ return detail;
86
+ }
87
+ const { prices } = await useRevenexxApi().post<{ prices: ResolvedPrice[] }>("/v1/prices/resolve", {
88
+ items: [{ product_id: product.id, sku: product.sku, quantity: 1 }],
89
+ ...priceContext(event),
90
+ });
91
+ const resolved = prices[0];
92
+ return { ...detail, prices: resolved ? toPriceMap(resolved) : [] };
93
+ }
@@ -0,0 +1,39 @@
1
+ import { getCookie, getHeader, type H3Event } from "h3";
2
+
3
+ // Update when adding/removing locale subdirectories in i18n/locales/
4
+ const SUPPORTED = new Set(["de", "en"] as const);
5
+ export type Locale = "de" | "en";
6
+ export interface LocalizedText {
7
+ de?: string;
8
+ en?: string;
9
+ }
10
+
11
+ function normalizeLocale(input?: string | null): Locale | null {
12
+ if (!input) {
13
+ return null;
14
+ }
15
+
16
+ const code = input.toLowerCase().split("-")[0];
17
+ return SUPPORTED.has(code as Locale) ? (code as Locale) : null;
18
+ }
19
+
20
+ export function resolveLocale(event: H3Event): Locale {
21
+ const fromHeader = normalizeLocale(getHeader(event, "x-locale"));
22
+ if (fromHeader) {
23
+ return fromHeader;
24
+ }
25
+
26
+ const fromCookie = normalizeLocale(getCookie(event, "cover-locale"));
27
+ if (fromCookie) {
28
+ return fromCookie;
29
+ }
30
+
31
+ const accept = getHeader(event, "accept-language");
32
+ const fromAccept = normalizeLocale(accept?.split(",")[0] ?? null);
33
+ if (fromAccept) {
34
+ return fromAccept;
35
+ }
36
+
37
+ const configDefault = useRuntimeConfig().public.defaultLocale;
38
+ return (SUPPORTED.has(configDefault as Locale) ? configDefault : "de") as Locale;
39
+ }
@@ -0,0 +1,19 @@
1
+ import { resolve } from "node:path";
2
+
3
+ import type { ILogService } from "../interfaces/log";
4
+ import { LogService } from "../services/LogService";
5
+
6
+ const logService: ILogService = new LogService(
7
+ resolve(process.cwd(), "logs/server.log"),
8
+ );
9
+
10
+ export function getLogService(): ILogService {
11
+ return logService;
12
+ }
13
+
14
+ export function toErrorContext(err: unknown): Record<string, unknown> {
15
+ if (err instanceof Error) {
16
+ return { errorMessage: err.message };
17
+ }
18
+ return { errorMessage: String(err) };
19
+ }
@@ -0,0 +1,24 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import type { IMarketService } from "../interfaces/market";
4
+ import { ApiMarketService } from "../services/ApiMarketService";
5
+ import { MockMarketService } from "../services/MockMarketService";
6
+
7
+ /**
8
+ * Registry of available market implementations:
9
+ * - "mock" — bundled demo markets (markets.json)
10
+ * - "api" — live markets via the public revenexx API (markets app)
11
+ */
12
+ const serviceRegistry: Record<string, IMarketService> = {
13
+ mock: new MockMarketService("markets.json"),
14
+ api: new ApiMarketService(),
15
+ };
16
+
17
+ export function getMarketService(event?: H3Event): IMarketService {
18
+ const key = resolveServiceKey(event, {
19
+ domain: "marketService",
20
+ mockKey: "mock",
21
+ liveKey: "api",
22
+ });
23
+ return serviceRegistry[key] ?? serviceRegistry["mock"]!;
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ /**
4
+ * The orders domain has no service class — endpoints branch on the
5
+ * resolved registry key ("mock" = demo order ids + mutable JSON history,
6
+ * "api" = the orders app via the public revenexx API).
7
+ */
8
+ export function resolveOrderServiceKey(event?: H3Event): string {
9
+ return resolveServiceKey(event, {
10
+ domain: "orderService",
11
+ mockKey: "mock",
12
+ liveKey: "api",
13
+ });
14
+ }
@@ -0,0 +1,14 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ /**
4
+ * The payments domain has no service class — the BFF endpoints branch on
5
+ * the resolved registry key ("mock" = demo profile methods, "api" = the
6
+ * payments app via the public revenexx API).
7
+ */
8
+ export function resolvePaymentServiceKey(event?: H3Event): string {
9
+ return resolveServiceKey(event, {
10
+ domain: "paymentService",
11
+ mockKey: "mock",
12
+ liveKey: "api",
13
+ });
14
+ }
@@ -0,0 +1,14 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ /**
4
+ * The prices domain has no service class — endpoints branch on the
5
+ * resolved registry key ("mock" = demo data already carries prices,
6
+ * "api" = the prices app via the public revenexx API).
7
+ */
8
+ export function resolvePriceServiceKey(event?: H3Event): string {
9
+ return resolveServiceKey(event, {
10
+ domain: "priceService",
11
+ mockKey: "mock",
12
+ liveKey: "api",
13
+ });
14
+ }