@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/app/api/account.ts +8 -0
- package/app/api/categories.ts +3 -0
- package/app/api/checkout.ts +6 -0
- package/app/api/images.ts +4 -0
- package/app/api/markets.ts +3 -0
- package/app/api/product.ts +3 -0
- package/app/api/products.ts +4 -0
- package/app/api/search.ts +4 -0
- package/app/app.config.ts +29 -0
- package/app/assets/css/cover.css +25 -0
- package/app/components/account/AccountMenu.vue +30 -0
- package/app/components/account/AccountShell.vue +18 -0
- package/app/components/account/AccountSidebar.vue +90 -0
- package/app/components/account/address/AccountAddressCard.vue +287 -0
- package/app/components/account/carts/AccountCartsTable.vue +284 -0
- package/app/components/account/dashboard/AccountDashboard.vue +351 -0
- package/app/components/account/directorder/AccountDirectOrder.vue +512 -0
- package/app/components/account/governance/AccountApprovalLimitsTable.vue +221 -0
- package/app/components/account/governance/AccountCostCenterDetail.vue +276 -0
- package/app/components/account/governance/AccountCostCentersTable.vue +252 -0
- package/app/components/account/governance/AccountRequisitionDetail.vue +295 -0
- package/app/components/account/governance/AccountRequisitionsTable.vue +255 -0
- package/app/components/account/governance/AccountWorkflowDetail.vue +215 -0
- package/app/components/account/governance/AccountWorkflowsTable.vue +168 -0
- package/app/components/account/governance/GovernanceApprovalLimitModal.vue +183 -0
- package/app/components/account/governance/GovernanceCostCenterModal.vue +188 -0
- package/app/components/account/governance/GovernanceWorkflowModal.vue +191 -0
- package/app/components/account/orderlists/AccountOrderListDetail.vue +349 -0
- package/app/components/account/orderlists/AccountOrderListsTable.vue +352 -0
- package/app/components/account/orders/AccountOrderDetail.vue +376 -0
- package/app/components/account/orders/AccountOrdersTable.vue +281 -0
- package/app/components/account/preferences/AccountPreferences.vue +50 -0
- package/app/components/auth/AuthForgotPasswordPanel.vue +88 -0
- package/app/components/auth/AuthLoginPanel.vue +103 -0
- package/app/components/auth/AuthLoginTeaser.vue +24 -0
- package/app/components/auth/AuthPageHeader.vue +21 -0
- package/app/components/auth/AuthRegisterPanel.vue +431 -0
- package/app/components/auth/AuthRegisterTeaser.vue +24 -0
- package/app/components/auth/AuthResetPasswordPanel.vue +115 -0
- package/app/components/cart/CartButton.vue +42 -0
- package/app/components/cart/actions/CartCostCenterModal.vue +110 -0
- package/app/components/cart/actions/CartExportModal.vue +82 -0
- package/app/components/cart/actions/CartPositionTextsModal.vue +92 -0
- package/app/components/cart/actions/CartSaveToListModal.vue +177 -0
- package/app/components/cart/actions/CartWorkflowPickerModal.vue +64 -0
- package/app/components/cart/drawer/CartDrawer.vue +49 -0
- package/app/components/cart/item/CartItem.vue +318 -0
- package/app/components/cart/manage/CartNameModal.vue +68 -0
- package/app/components/cart/position/CartPosition.vue +369 -0
- package/app/components/cart/quickadd/CartQuickAdd.vue +145 -0
- package/app/components/cart/recommend/CartCrossSell.vue +102 -0
- package/app/components/cart/recommend/CartRecommendCard.vue +59 -0
- package/app/components/cart/recommend/CartReorderStrip.vue +108 -0
- package/app/components/cart/requisition/CartRequisitionGroup.vue +74 -0
- package/app/components/cart/skeleton/CartSkeleton.vue +18 -0
- package/app/components/cart/summary/CartSummary.vue +25 -0
- package/app/components/cart/summary/CartSummaryBreakdown.vue +35 -0
- package/app/components/cart/switcher/CartSwitcher.vue +147 -0
- package/app/components/cart/target/CartTargetModal.vue +129 -0
- package/app/components/cart/view/CartApprovalsPanel.vue +89 -0
- package/app/components/cart/view/CartListHeader.vue +86 -0
- package/app/components/cart/view/CartPositionList.vue +94 -0
- package/app/components/cart/view/CartSummaryPanel.vue +254 -0
- package/app/components/cart/view/CartView.vue +310 -0
- package/app/components/category/info/CategoryPrice.vue +47 -0
- package/app/components/category/info/CategorySku.vue +15 -0
- package/app/components/category/info/CategoryStock.vue +25 -0
- package/app/components/category/list/CategoryItemCount.vue +27 -0
- package/app/components/category/list/CategoryListPagination.vue +81 -0
- package/app/components/category/skeleton/CategoryDetailSkeleton.vue +12 -0
- package/app/components/category/skeleton/CategoryListSkeleton.vue +65 -0
- package/app/components/category/view/CategoryViewToggle.vue +33 -0
- package/app/components/category/view/GridView.vue +78 -0
- package/app/components/category/view/ListView.vue +80 -0
- package/app/components/checkout/confirmation/CheckoutConfirmationView.vue +289 -0
- package/app/components/checkout/form/CheckoutSchemaForm.vue +72 -0
- package/app/components/checkout/onepage/CheckoutAddressPickerModal.vue +260 -0
- package/app/components/checkout/onepage/CheckoutAddressSection.vue +186 -0
- package/app/components/checkout/onepage/CheckoutDeliverySection.vue +158 -0
- package/app/components/checkout/onepage/CheckoutGate.vue +67 -0
- package/app/components/checkout/onepage/CheckoutGuestDetailsSection.vue +154 -0
- package/app/components/checkout/onepage/CheckoutNotesSection.vue +62 -0
- package/app/components/checkout/onepage/CheckoutOnePageView.vue +77 -0
- package/app/components/checkout/onepage/CheckoutPaymentSection.vue +181 -0
- package/app/components/checkout/onepage/CheckoutSectionTitle.vue +18 -0
- package/app/components/checkout/onepage/CheckoutSummarySection.vue +211 -0
- package/app/components/checkout/onepage/CheckoutWorkflowPanel.vue +38 -0
- package/app/components/layout/AppLogo.vue +21 -0
- package/app/components/layout/footer/Footer.vue +148 -0
- package/app/components/layout/header/AppBar.vue +75 -0
- package/app/components/layout/header/CategoryNav.vue +93 -0
- package/app/components/layout/header/HeaderActionItem.vue +81 -0
- package/app/components/layout/header/LocaleSwitcher.vue +123 -0
- package/app/components/layout/header/MainNav.vue +61 -0
- package/app/components/layout/header/MobileMenu.vue +81 -0
- package/app/components/layout/header/TopBar.vue +63 -0
- package/app/components/listing/ListingView.vue +227 -0
- package/app/components/product/detail/ProductDetailAddToCart.vue +188 -0
- package/app/components/product/detail/ProductDetailDescription.vue +20 -0
- package/app/components/product/detail/ProductDetailImageGallery.vue +58 -0
- package/app/components/product/detail/ProductDetailPricing.vue +108 -0
- package/app/components/product/detail/ProductDetailSaveToList.vue +291 -0
- package/app/components/product/detail/ProductDetailSpecifications.vue +33 -0
- package/app/components/product/detail/ProductDetailStock.vue +36 -0
- package/app/components/search/filter/SearchFacets.vue +256 -0
- package/app/components/search/filter/SearchFacetsDrawer.vue +58 -0
- package/app/components/ui/AppShell.vue +7 -0
- package/app/components/ui/debug/PiniaStateCard.vue +106 -0
- package/app/components/ui/feedback/EmptyState.vue +30 -0
- package/app/components/ui/feedback/ErrorBoundary.vue +44 -0
- package/app/components/ui/feedback/NotFound.vue +34 -0
- package/app/components/ui/forms/CountrySelection.vue +14 -0
- package/app/components/ui/forms/FormKitDatePicker.vue +124 -0
- package/app/components/ui/forms/PasswordInput.vue +136 -0
- package/app/components/ui/forms/SearchInput.vue +213 -0
- package/app/components/ui/table/TableSortButton.vue +51 -0
- package/app/components/ui/table/TableToolbar.vue +66 -0
- package/app/composables/useAccount.ts +27 -0
- package/app/composables/useAccountAddresses.ts +116 -0
- package/app/composables/useAccountOrders.ts +18 -0
- package/app/composables/useAppUrl.ts +38 -0
- package/app/composables/useAuthStore.ts +202 -0
- package/app/composables/useB2BContext.ts +23 -0
- package/app/composables/useCartActions.ts +25 -0
- package/app/composables/useCartCalculation.ts +90 -0
- package/app/composables/useCartSelection.ts +46 -0
- package/app/composables/useCartStore.ts +837 -0
- package/app/composables/useCartSummaryFormatting.ts +28 -0
- package/app/composables/useCategories.ts +29 -0
- package/app/composables/useCheckoutOnePage.ts +438 -0
- package/app/composables/useCheckoutProfileSource.ts +8 -0
- package/app/composables/useCheckoutSchema.ts +28 -0
- package/app/composables/useDataTable.ts +14 -0
- package/app/composables/useFormValidation.ts +29 -0
- package/app/composables/useIcons.ts +17 -0
- package/app/composables/useLocalePreferences.ts +45 -0
- package/app/composables/useMarkets.ts +15 -0
- package/app/composables/useProduct.ts +89 -0
- package/app/composables/useProductCard.ts +8 -0
- package/app/composables/useProductCardActions.ts +106 -0
- package/app/composables/useProductImage.ts +12 -0
- package/app/composables/useProductListing.ts +210 -0
- package/app/composables/useProductUrl.ts +17 -0
- package/app/composables/useProducts.ts +130 -0
- package/app/composables/useSearch.ts +12 -0
- package/app/composables/useThemeStore.ts +81 -0
- package/app/composables/useViewMode.ts +18 -0
- package/app/config/countries.ts +74 -0
- package/app/config/icons.ts +283 -0
- package/app/config/navigation.ts +191 -0
- package/app/config/palette.ts +13 -0
- package/app/config/themes.ts +20 -0
- package/app/formkit.config.ts +50 -0
- package/app/interfaces/account/address-list.ts +34 -0
- package/app/interfaces/account/order-list.ts +47 -0
- package/app/interfaces/account/order-lists.ts +35 -0
- package/app/interfaces/account/profile.ts +17 -0
- package/app/interfaces/account.ts +3 -0
- package/app/interfaces/address.ts +9 -0
- package/app/interfaces/auth.ts +104 -0
- package/app/interfaces/b2b.ts +234 -0
- package/app/interfaces/cart-calculation.ts +131 -0
- package/app/interfaces/cart-item.ts +45 -0
- package/app/interfaces/checkout-draft.ts +4 -0
- package/app/interfaces/checkout.ts +43 -0
- package/app/interfaces/delivery.ts +13 -0
- package/app/interfaces/market.ts +24 -0
- package/app/interfaces/payment.ts +19 -0
- package/app/interfaces/persisted-cart.ts +10 -0
- package/app/interfaces/product-detail.ts +54 -0
- package/app/interfaces/product-list.ts +33 -0
- package/app/interfaces/search-facets.ts +14 -0
- package/app/interfaces/validation.ts +14 -0
- package/app/layouts/default.vue +78 -0
- package/app/layouts/focus.vue +80 -0
- package/app/plugins/formkit-locale.client.ts +23 -0
- package/app/shared/constants.ts +1 -0
- package/app/validations/companyName.ts +18 -0
- package/app/validations/emailFormat.ts +10 -0
- package/app/validations/formValidationConfig.ts +50 -0
- package/app/validations/maxLength.ts +12 -0
- package/app/validations/minLength.ts +9 -0
- package/app/validations/optionalCompanyName.ts +23 -0
- package/app/validations/passwordsMatch.ts +5 -0
- package/app/validations/phoneNumber.ts +19 -0
- package/app/validations/required.ts +11 -0
- package/app/validations/termsRequired.ts +4 -0
- package/app/validations/types.ts +10 -0
- package/app/validations/zipCode.ts +15 -0
- package/i18n/locales/de/account.json +458 -0
- package/i18n/locales/de/auth.json +155 -0
- package/i18n/locales/de/cart.json +263 -0
- package/i18n/locales/de/checkout.json +217 -0
- package/i18n/locales/de/common.json +80 -0
- package/i18n/locales/de/cover.json +33 -0
- package/i18n/locales/de/order.json +1 -0
- package/i18n/locales/de/product.json +71 -0
- package/i18n/locales/de/search.json +54 -0
- package/i18n/locales/de/validation.json +12 -0
- package/i18n/locales/en/account.json +458 -0
- package/i18n/locales/en/auth.json +155 -0
- package/i18n/locales/en/cart.json +263 -0
- package/i18n/locales/en/checkout.json +217 -0
- package/i18n/locales/en/common.json +80 -0
- package/i18n/locales/en/cover.json +33 -0
- package/i18n/locales/en/order.json +1 -0
- package/i18n/locales/en/product.json +71 -0
- package/i18n/locales/en/search.json +54 -0
- package/i18n/locales/en/validation.json +12 -0
- package/nuxt.config.ts +109 -0
- package/package.json +65 -0
- package/public/img/product-placeholder.svg +8 -0
- package/public/templates/direct-order-template.csv +3 -0
- package/public/templates/direct-order-template.xlsx +0 -0
- package/server/api/account/address/[id].delete.ts +51 -0
- package/server/api/account/address/[id].put.ts +83 -0
- package/server/api/account/address/index.post.ts +60 -0
- package/server/api/account/addresses.get.ts +25 -0
- package/server/api/account/context.get.ts +16 -0
- package/server/api/account/governance/[domain]/[id].put.ts +57 -0
- package/server/api/account/governance/[domain].post.ts +111 -0
- package/server/api/account/order-lists/[id].delete.ts +22 -0
- package/server/api/account/order-lists/[id].get.ts +17 -0
- package/server/api/account/order-lists/[id].put.ts +46 -0
- package/server/api/account/order-lists/index.get.ts +14 -0
- package/server/api/account/order-lists/index.post.ts +38 -0
- package/server/api/account/orders/[id].get.ts +35 -0
- package/server/api/account/orders.get.ts +35 -0
- package/server/api/account/profile.get.ts +56 -0
- package/server/api/account/profile.put.ts +128 -0
- package/server/api/account/requisitions/[id].get.ts +15 -0
- package/server/api/account/requisitions.get.ts +23 -0
- package/server/api/auth/login.post.ts +37 -0
- package/server/api/auth/logout.post.ts +4 -0
- package/server/api/auth/me.get.ts +3 -0
- package/server/api/auth/personas.get.ts +7 -0
- package/server/api/auth/recovery.post.ts +32 -0
- package/server/api/auth/recovery.put.ts +37 -0
- package/server/api/auth/register.post.ts +126 -0
- package/server/api/cart/calculate.post.ts +25 -0
- package/server/api/cart/export.post.ts +81 -0
- package/server/api/cart/index.delete.ts +10 -0
- package/server/api/cart/index.get.ts +10 -0
- package/server/api/cart/sync.post.ts +14 -0
- package/server/api/carts/[id]/activate.post.ts +18 -0
- package/server/api/carts/[id]/index.delete.ts +18 -0
- package/server/api/carts/[id]/index.put.ts +19 -0
- package/server/api/carts/[id]/items.post.ts +21 -0
- package/server/api/carts/index.get.ts +14 -0
- package/server/api/carts/index.post.ts +14 -0
- package/server/api/categories/[slug].get.ts +22 -0
- package/server/api/categories/index.get.ts +11 -0
- package/server/api/checkout/profile.get.ts +21 -0
- package/server/api/checkout/schema/[key].get.ts +19 -0
- package/server/api/checkout/session.post.ts +8 -0
- package/server/api/images/detail/[filename].get.ts +18 -0
- package/server/api/images/list/[filename].get.ts +17 -0
- package/server/api/markets.get.ts +9 -0
- package/server/api/orders/index.post.ts +376 -0
- package/server/api/payment/methods.post.ts +65 -0
- package/server/api/product/[id].get.ts +29 -0
- package/server/api/products/[id].get.ts +30 -0
- package/server/api/products/index.get.ts +21 -0
- package/server/api/requisitions/index.post.ts +67 -0
- package/server/api/shipping/rates.post.ts +70 -0
- package/server/api/themes/index.get.ts +9 -0
- package/server/api/typesense/drop.get.ts +22 -0
- package/server/api/typesense/health.get.ts +9 -0
- package/server/api/typesense/search.post.ts +199 -0
- package/server/api/typesense/seed.get.ts +112 -0
- package/server/api/typesense/suggest.post.ts +69 -0
- package/server/config/account/organization.json +146 -0
- package/server/config/account/personas.json +169 -0
- package/server/config/account/user.json +8 -0
- package/server/config/account/workflows.json +19 -0
- package/server/data/account/address-list.json +103 -0
- package/server/data/account/order-list.json +491 -0
- package/server/data/account/order-lists.json +149 -0
- package/server/data/account/profile.json +9 -0
- package/server/data/account/registration-requests.json +3 -0
- package/server/data/account/requisitions.json +686 -0
- package/server/data/categories.json +186 -0
- package/server/data/forms/checkout/add-address.json +24 -0
- package/server/data/list/5137-1.png +0 -0
- package/server/data/list/5498-1.png +0 -0
- package/server/data/list/5498-2.png +0 -0
- package/server/data/list/5498-3.png +0 -0
- package/server/data/list/5498-4.png +0 -0
- package/server/data/list/5498-5.png +0 -0
- package/server/data/list/5498-6.png +0 -0
- package/server/data/list/5519-1.png +0 -0
- package/server/data/list/5713-1.png +0 -0
- package/server/data/list/5789-1.png +0 -0
- package/server/data/list/5930-1.png +0 -0
- package/server/data/list/6127-1.png +0 -0
- package/server/data/list/6234-1.png +0 -0
- package/server/data/list/6238-1.png +0 -0
- package/server/data/list/6246-1.png +0 -0
- package/server/data/list/6270-1.png +0 -0
- package/server/data/list/6330-1.png +0 -0
- package/server/data/list/6336-1.png +0 -0
- package/server/data/list/6360-1.png +0 -0
- package/server/data/list/6363-1.png +0 -0
- package/server/data/list/6375-1.png +0 -0
- package/server/data/list/6385-1.png +0 -0
- package/server/data/list/6413-1.png +0 -0
- package/server/data/list/6418-1.png +0 -0
- package/server/data/list/6465-1.png +0 -0
- package/server/data/list/6477-1.png +0 -0
- package/server/data/list/6509-1.png +0 -0
- package/server/data/list/6545-1.png +0 -0
- package/server/data/list/6548-1.png +0 -0
- package/server/data/list/6566-1.png +0 -0
- package/server/data/list/6581-1.png +0 -0
- package/server/data/list/6609-1.png +0 -0
- package/server/data/list/6611-1.png +0 -0
- package/server/data/list/6641-1.png +0 -0
- package/server/data/list/6659-1.png +0 -0
- package/server/data/list/6662-1.png +0 -0
- package/server/data/list/6689-1.png +0 -0
- package/server/data/list/6698-1.png +0 -0
- package/server/data/list/6701-1.png +0 -0
- package/server/data/list/6752-1.png +0 -0
- package/server/data/list/6755-1.png +0 -0
- package/server/data/list/6837-1.png +0 -0
- package/server/data/list/6841-1.png +0 -0
- package/server/data/list/6844-1.png +0 -0
- package/server/data/list/6846-1.png +0 -0
- package/server/data/list/6886-1.png +0 -0
- package/server/data/list/6895-1.png +0 -0
- package/server/data/list/6897-1.png +0 -0
- package/server/data/list/6919-1.png +0 -0
- package/server/data/list/6977-1.png +0 -0
- package/server/data/list/6983-1.png +0 -0
- package/server/data/list/6984-1.png +0 -0
- package/server/data/list/6985-1.png +0 -0
- package/server/data/list/6986-1.png +0 -0
- package/server/data/list/6989-1.png +0 -0
- package/server/data/list/6995-1.png +0 -0
- package/server/data/list/6998-1.png +0 -0
- package/server/data/markets.json +24 -0
- package/server/data/product-detail.json +2450 -0
- package/server/data/product-list.json +2450 -0
- package/server/data/themes.json +8 -0
- package/server/interfaces/account.ts +20 -0
- package/server/interfaces/auth.ts +32 -0
- package/server/interfaces/b2bContext.ts +23 -0
- package/server/interfaces/cart.ts +46 -0
- package/server/interfaces/cartCalculation.ts +17 -0
- package/server/interfaces/category.ts +12 -0
- package/server/interfaces/checkoutProfile.ts +5 -0
- package/server/interfaces/log.ts +3 -0
- package/server/interfaces/market.ts +10 -0
- package/server/interfaces/product.ts +21 -0
- package/server/interfaces/schema.ts +20 -0
- package/server/interfaces/theme.ts +10 -0
- package/server/services/ApiAuthService.ts +138 -0
- package/server/services/ApiCartService.ts +254 -0
- package/server/services/ApiCategoryService.ts +25 -0
- package/server/services/ApiMarketService.ts +71 -0
- package/server/services/ApiProductService.ts +53 -0
- package/server/services/LocalFileCategoryService.ts +23 -0
- package/server/services/LocalFileCheckoutProfileService.ts +117 -0
- package/server/services/LocalFileProductService.ts +128 -0
- package/server/services/LocalFileSchemaService.ts +70 -0
- package/server/services/LocalFileThemeService.ts +18 -0
- package/server/services/LogService.ts +28 -0
- package/server/services/MockAccountService.ts +58 -0
- package/server/services/MockAuthService.ts +105 -0
- package/server/services/MockB2BContextService.ts +149 -0
- package/server/services/MockCartCalculationService.ts +395 -0
- package/server/services/MockMarketService.ts +18 -0
- package/server/services/SdkAccountService.ts +56 -0
- package/server/services/SdkAuthService.ts +83 -0
- package/server/services/SessionCartService.ts +31 -0
- package/server/utils/accountService.ts +30 -0
- package/server/utils/authCookie.ts +13 -0
- package/server/utils/authService.ts +43 -0
- package/server/utils/b2bService.ts +49 -0
- package/server/utils/cartService.ts +59 -0
- package/server/utils/categoryService.ts +19 -0
- package/server/utils/checkoutProfileService.ts +9 -0
- package/server/utils/checkoutSession.ts +38 -0
- package/server/utils/coverData.ts +84 -0
- package/server/utils/governanceStore.ts +38 -0
- package/server/utils/i18n.ts +76 -0
- package/server/utils/inventoryService.ts +14 -0
- package/server/utils/liveCatalog.ts +234 -0
- package/server/utils/liveInventories.ts +76 -0
- package/server/utils/liveOrders.ts +139 -0
- package/server/utils/livePrices.ts +93 -0
- package/server/utils/locale.ts +39 -0
- package/server/utils/logService.ts +19 -0
- package/server/utils/marketService.ts +24 -0
- package/server/utils/orderService.ts +14 -0
- package/server/utils/paymentService.ts +14 -0
- package/server/utils/priceService.ts +14 -0
- package/server/utils/productService.ts +28 -0
- package/server/utils/productsSchema.ts +30 -0
- package/server/utils/revenexxApi.ts +136 -0
- package/server/utils/schemaService.ts +25 -0
- package/server/utils/serviceMode.ts +70 -0
- package/server/utils/shippingService.ts +14 -0
- package/server/utils/shopSdk.ts +88 -0
- package/server/utils/themeService.ts +16 -0
- 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
|
+
}
|