@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,376 @@
|
|
|
1
|
+
// Method codes are generic — live mode validates them against the
|
|
2
|
+
// payments app; the demo-only methods keep their extra fields.
|
|
3
|
+
interface OrderPayment {
|
|
4
|
+
method: string;
|
|
5
|
+
poNumber?: string;
|
|
6
|
+
costCenter?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Business orders require company data; private (B2C guest) orders don't.
|
|
10
|
+
const REQUIRED_ADDRESS_FIELDS_BUSINESS = ["companyName", "contactName", "street", "city", "postalCode", "country"] as const;
|
|
11
|
+
const REQUIRED_ADDRESS_FIELDS_PRIVATE = ["contactName", "street", "city", "postalCode", "country"] as const;
|
|
12
|
+
const REQUIRED_BILLING_FIELDS = ["street", "city", "postalCode", "country"] as const;
|
|
13
|
+
const VALID_DELIVERY_METHODS = ["standard", "express"] as const;
|
|
14
|
+
|
|
15
|
+
interface OrderPayload {
|
|
16
|
+
checkoutSessionToken: string;
|
|
17
|
+
billingCountry?: string;
|
|
18
|
+
customerType?: "business" | "private";
|
|
19
|
+
contactEmail?: string;
|
|
20
|
+
orderNumber?: string;
|
|
21
|
+
quoteNumber?: string;
|
|
22
|
+
noOrderConfirmation?: boolean;
|
|
23
|
+
address: Record<string, unknown>;
|
|
24
|
+
payment: Record<string, unknown>;
|
|
25
|
+
items: Array<{ id: string; quantity: number; price: number }>;
|
|
26
|
+
deliveryMethod?: string;
|
|
27
|
+
requestedDate?: string;
|
|
28
|
+
deliveryNote?: string;
|
|
29
|
+
partialDelivery?: boolean;
|
|
30
|
+
orderNote?: string;
|
|
31
|
+
promoCode?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
35
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateAddress(address: Record<string, unknown>, customerType: "business" | "private"): string | null {
|
|
39
|
+
const requiredFields = customerType === "private"
|
|
40
|
+
? REQUIRED_ADDRESS_FIELDS_PRIVATE
|
|
41
|
+
: REQUIRED_ADDRESS_FIELDS_BUSINESS;
|
|
42
|
+
for (const field of requiredFields) {
|
|
43
|
+
if (!isNonEmptyString(address[field])) {
|
|
44
|
+
return `Missing required address field: ${field}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (address.billingAddressSameAsShipping === false) {
|
|
48
|
+
const billing = address.billing as Record<string, unknown> | undefined;
|
|
49
|
+
if (!billing || typeof billing !== "object") {
|
|
50
|
+
return "Billing address is required when different from shipping";
|
|
51
|
+
}
|
|
52
|
+
for (const field of REQUIRED_BILLING_FIELDS) {
|
|
53
|
+
if (!isNonEmptyString(billing[field])) {
|
|
54
|
+
return `Missing required billing address field: ${field}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildPayment(raw: Record<string, unknown>, live: boolean): OrderPayment {
|
|
62
|
+
const method = raw.method as string;
|
|
63
|
+
if (!isNonEmptyString(method)) {
|
|
64
|
+
throw createError({ status: 422, message: "Payment method is required" });
|
|
65
|
+
}
|
|
66
|
+
if (method === "po_number") {
|
|
67
|
+
if (!isNonEmptyString(raw.poNumber)) {
|
|
68
|
+
throw createError({ status: 422, message: "PO number is required for po_number payment method" });
|
|
69
|
+
}
|
|
70
|
+
return { method, poNumber: raw.poNumber };
|
|
71
|
+
}
|
|
72
|
+
if (method === "cost_center") {
|
|
73
|
+
if (!isNonEmptyString(raw.costCenter)) {
|
|
74
|
+
throw createError({ status: 422, message: "Cost center is required for cost_center payment method" });
|
|
75
|
+
}
|
|
76
|
+
return { method, costCenter: raw.costCenter };
|
|
77
|
+
}
|
|
78
|
+
// Live codes are validated by the payments app at payment creation;
|
|
79
|
+
// mock mode only knows the demo methods.
|
|
80
|
+
if (!live && method !== "invoice") {
|
|
81
|
+
throw createError({ status: 422, message: `Invalid payment method: ${method}` });
|
|
82
|
+
}
|
|
83
|
+
return { method };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default defineEventHandler(async (event) => {
|
|
87
|
+
const body = await readBody<OrderPayload>(event);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (!body?.checkoutSessionToken || !consumeCheckoutSession(body.checkoutSessionToken)) {
|
|
91
|
+
throw createError({ status: 401, message: "Invalid or expired checkout session" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!body.items?.length) {
|
|
95
|
+
throw createError({ status: 400, message: "Cart is empty" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const customerType = body.customerType === "private" ? "private" : "business";
|
|
99
|
+
const addressError = validateAddress(body.address ?? {}, customerType);
|
|
100
|
+
if (addressError) {
|
|
101
|
+
throw createError({ status: 422, message: addressError });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const liveShipping = resolveShippingServiceKey(event) === "api";
|
|
105
|
+
if (
|
|
106
|
+
!liveShipping
|
|
107
|
+
&& body.deliveryMethod !== undefined
|
|
108
|
+
&& !(VALID_DELIVERY_METHODS as readonly string[]).includes(body.deliveryMethod)
|
|
109
|
+
) {
|
|
110
|
+
throw createError({
|
|
111
|
+
status: 422,
|
|
112
|
+
message: `Invalid delivery method: ${body.deliveryMethod}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const livePayments = resolvePaymentServiceKey(event) === "api";
|
|
117
|
+
const payment = buildPayment(body.payment ?? {}, livePayments);
|
|
118
|
+
|
|
119
|
+
// Approval limits are checked at order time: exceeding a blocking
|
|
120
|
+
// limit turns the order into an approval request (it does not fail).
|
|
121
|
+
const user = await getAuthService(event).me(event);
|
|
122
|
+
const context = await getB2BContextService(event).getContext(user?.$id ?? null);
|
|
123
|
+
const locale = resolveLocale(event);
|
|
124
|
+
const calculation = await getCartCalculationService().calculate(
|
|
125
|
+
{
|
|
126
|
+
items: body.items as never,
|
|
127
|
+
...(body.deliveryMethod ? { deliveryMethod: body.deliveryMethod } : {}),
|
|
128
|
+
},
|
|
129
|
+
context,
|
|
130
|
+
locale,
|
|
131
|
+
);
|
|
132
|
+
const blockingApprovals = calculation.approvalLimits.filter(
|
|
133
|
+
l => l.status === "exceeded" && l.approval.required,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Live shipping: the chosen method must be a real rate for this
|
|
137
|
+
// buyer context; its price replaces the demo calculation's shipping
|
|
138
|
+
// line in the order totals and the amount the payment is created over.
|
|
139
|
+
let liveShippingAdjustment = 0;
|
|
140
|
+
if (liveShipping && body.deliveryMethod) {
|
|
141
|
+
const address = body.address as Record<string, unknown> | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const { rates } = await useRevenexxApi().post<{ rates: Array<{ code: string; price: number }> }>("/v1/shipping/rates", {
|
|
144
|
+
order_value: calculation.totals.find(row => row.key === "subtotal")?.amount
|
|
145
|
+
?? calculation.totals.find(row => row.key === "total")?.amount ?? 0,
|
|
146
|
+
country: String(address?.country ?? ""),
|
|
147
|
+
});
|
|
148
|
+
const rate = rates.find(r => r.code === body.deliveryMethod);
|
|
149
|
+
if (!rate) {
|
|
150
|
+
throw createError({ status: 422, message: `Delivery method '${body.deliveryMethod}' is not available for this order` });
|
|
151
|
+
}
|
|
152
|
+
const mockShipping = calculation.totals.find(row => row.key === "shipping")?.amount ?? 0;
|
|
153
|
+
liveShippingAdjustment = rate.price - mockShipping;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (isError(err)) {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
getLogService().error("Shipping rate resolution failed", apiErrorContext(err));
|
|
160
|
+
throw createError({ status: 502, message: "Shipping service unavailable" });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let orderId = `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`;
|
|
165
|
+
|
|
166
|
+
// Live orders: place through the orders app FIRST — the real order
|
|
167
|
+
// number becomes the payment's order_ref and the cart freeze ref.
|
|
168
|
+
// Orders that await a (demo) approval stay in the mock flow; the
|
|
169
|
+
// approvals domain is deliberately not part of OM phase 1.
|
|
170
|
+
const liveOrders = resolveOrderServiceKey(event) === "api" && blockingApprovals.length === 0;
|
|
171
|
+
let liveOrderUuid: string | null = null;
|
|
172
|
+
if (liveOrders) {
|
|
173
|
+
const address = body.address as Record<string, unknown>;
|
|
174
|
+
const billing = address?.billingAddressSameAsShipping === false
|
|
175
|
+
? address?.billing as Record<string, unknown> | undefined
|
|
176
|
+
: address;
|
|
177
|
+
const totalRow = calculation.totals.find(row => row.key === "total");
|
|
178
|
+
const shippingRow = calculation.totals.find(row => row.key === "shipping");
|
|
179
|
+
try {
|
|
180
|
+
const placed = await useRevenexxApi().post<{ id: string; number: string }>("/v1/orders/place", {
|
|
181
|
+
...sessionOrderRefs(event),
|
|
182
|
+
currency: calculation.currency,
|
|
183
|
+
...(body.orderNumber ? { customer_order_number: body.orderNumber } : {}),
|
|
184
|
+
buyer: user ? { name: user.name, email: user.email } : { email: body.contactEmail ?? null },
|
|
185
|
+
billing_address: billing ?? null,
|
|
186
|
+
shipping_address: address ?? null,
|
|
187
|
+
payment: { method: payment.method },
|
|
188
|
+
shipping: {
|
|
189
|
+
method: body.deliveryMethod ?? "standard",
|
|
190
|
+
price: Math.max(0, (shippingRow?.amount ?? 0) + liveShippingAdjustment),
|
|
191
|
+
},
|
|
192
|
+
// The order carries what the customer saw (and the payment
|
|
193
|
+
// charges): the checkout calculation's total.
|
|
194
|
+
grand_total: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
|
|
195
|
+
items: (body.items as Array<Record<string, unknown>>).map((item, index) => ({
|
|
196
|
+
product_id: String(item.id),
|
|
197
|
+
sku: String(item.sku ?? "") || undefined,
|
|
198
|
+
name: String(item.name ?? item.sku ?? item.id),
|
|
199
|
+
quantity: calculation.lines[index]?.adjustedQuantity ?? Number(item.quantity),
|
|
200
|
+
unit_price: calculation.lines[index]?.unitPrice ?? Number(item.price ?? 0),
|
|
201
|
+
tax_rate: 19,
|
|
202
|
+
product: {
|
|
203
|
+
...(item.image ? { image: String(item.image) } : {}),
|
|
204
|
+
...(item.categorySlug ? { categorySlug: String(item.categorySlug) } : {}),
|
|
205
|
+
...(item.subcategorySlug ? { subcategorySlug: String(item.subcategorySlug) } : {}),
|
|
206
|
+
},
|
|
207
|
+
})),
|
|
208
|
+
user_data: {
|
|
209
|
+
...(body.deliveryNote ? { delivery_note: body.deliveryNote } : {}),
|
|
210
|
+
...(body.orderNote ? { order_note: body.orderNote } : {}),
|
|
211
|
+
...(body.partialDelivery !== undefined ? { partial_delivery: body.partialDelivery } : {}),
|
|
212
|
+
...(body.requestedDate ? { requested_date: body.requestedDate } : {}),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
orderId = placed.number;
|
|
216
|
+
liveOrderUuid = placed.id;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err instanceof RevenexxApiError && (err.statusCode === 400 || err.statusCode === 422)) {
|
|
220
|
+
throw createError({ status: 422, message: err.message });
|
|
221
|
+
}
|
|
222
|
+
getLogService().error("Order placement failed", apiErrorContext(err));
|
|
223
|
+
throw createError({ status: 502, message: "Order service unavailable" });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Live payments: create + authorize through the payments app —
|
|
228
|
+
// eligibility (countries, order-value bounds) is enforced there
|
|
229
|
+
// again; a declined payment fails the order with 402 (and cancels
|
|
230
|
+
// the already placed live order).
|
|
231
|
+
let livePaymentStatus: string | null = null;
|
|
232
|
+
if (livePayments) {
|
|
233
|
+
const totalRow = calculation.totals.find(row => row.key === "total");
|
|
234
|
+
const address = body.address as Record<string, unknown> | undefined;
|
|
235
|
+
const billing = address?.billingAddressSameAsShipping === false
|
|
236
|
+
? address?.billing as Record<string, unknown> | undefined
|
|
237
|
+
: address;
|
|
238
|
+
const country = String(body.billingCountry ?? billing?.country ?? "");
|
|
239
|
+
try {
|
|
240
|
+
const created = await useRevenexxApi().post<{ id: string; status: string; error_message?: string | null }>("/v1/payments", {
|
|
241
|
+
method_code: payment.method,
|
|
242
|
+
amount: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
|
|
243
|
+
currency: calculation.currency,
|
|
244
|
+
country,
|
|
245
|
+
order_ref: orderId,
|
|
246
|
+
contact_id: undefined,
|
|
247
|
+
idempotency_key: body.checkoutSessionToken,
|
|
248
|
+
metadata: {
|
|
249
|
+
...(payment.poNumber ? { po_number: payment.poNumber } : {}),
|
|
250
|
+
...(payment.costCenter ? { cost_center: payment.costCenter } : {}),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
if (created.status === "failed") {
|
|
254
|
+
throw createError({ status: 402, message: created.error_message ?? "Payment was declined" });
|
|
255
|
+
}
|
|
256
|
+
livePaymentStatus = created.status;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
// A placed live order must not survive a failed payment.
|
|
260
|
+
if (liveOrderUuid) {
|
|
261
|
+
try {
|
|
262
|
+
await useRevenexxApi().post(`/v1/orders/${liveOrderUuid}/cancel`, { reason: "payment failed" });
|
|
263
|
+
}
|
|
264
|
+
catch (cancelErr) {
|
|
265
|
+
getLogService().error("Order cancel after payment failure failed", apiErrorContext(cancelErr));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (isError(err)) {
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
if (err instanceof RevenexxApiError && (err.statusCode === 400 || err.statusCode === 422)) {
|
|
272
|
+
throw createError({ status: 422, message: err.message });
|
|
273
|
+
}
|
|
274
|
+
getLogService().error("Payment creation failed", apiErrorContext(err));
|
|
275
|
+
throw createError({ status: 502, message: "Payment service unavailable" });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The payment dimension of the live order follows the payment outcome.
|
|
280
|
+
if (liveOrderUuid && livePaymentStatus) {
|
|
281
|
+
const mapped = livePaymentStatus === "succeeded" || livePaymentStatus === "paid"
|
|
282
|
+
? "paid"
|
|
283
|
+
: livePaymentStatus === "authorized" ? "authorized" : "pending";
|
|
284
|
+
try {
|
|
285
|
+
await useRevenexxApi().post(`/v1/orders/${liveOrderUuid}/payment-status`, { status: mapped });
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
getLogService().error("Order payment-status sync failed", apiErrorContext(err));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The live cart freezes as the order source (best-effort).
|
|
293
|
+
if (livePayments || liveOrders) {
|
|
294
|
+
const manager = getCartManager(event);
|
|
295
|
+
if (manager) {
|
|
296
|
+
try {
|
|
297
|
+
await manager.orderActiveCart(event, orderId);
|
|
298
|
+
}
|
|
299
|
+
catch (cartErr) {
|
|
300
|
+
getLogService().error("Cart order hand-over failed", toErrorContext(cartErr));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Demo persistence: signed-in orders land in the account history
|
|
306
|
+
// (mutable store) so the checkout outcome shows up immediately.
|
|
307
|
+
// With live orders the history comes from the orders app instead.
|
|
308
|
+
if (user && !liveOrders) {
|
|
309
|
+
try {
|
|
310
|
+
const lineByIndex = calculation.lines;
|
|
311
|
+
const positions = (body.items as Array<Record<string, unknown>>).map((item, index) => ({
|
|
312
|
+
id: String(item.id),
|
|
313
|
+
sku: String(item.sku ?? ""),
|
|
314
|
+
name: String(item.name ?? ""),
|
|
315
|
+
...(item.image ? { image: String(item.image) } : {}),
|
|
316
|
+
quantity: lineByIndex[index]?.adjustedQuantity ?? Number(item.quantity),
|
|
317
|
+
unitPrice: lineByIndex[index]?.unitPrice ?? Number(item.price ?? 0),
|
|
318
|
+
lineTotal: lineByIndex[index]?.lineTotal ?? 0,
|
|
319
|
+
...(item.categorySlug ? { categorySlug: String(item.categorySlug) } : {}),
|
|
320
|
+
...(item.subcategorySlug ? { subcategorySlug: String(item.subcategorySlug) } : {}),
|
|
321
|
+
}));
|
|
322
|
+
const totalRow = calculation.totals.find(row => row.key === "total");
|
|
323
|
+
const address = body.address as Record<string, unknown>;
|
|
324
|
+
const billing = address?.billingAddressSameAsShipping === false
|
|
325
|
+
? address?.billing as Record<string, unknown> | undefined
|
|
326
|
+
: address;
|
|
327
|
+
const addressLine = (a: Record<string, unknown> | undefined): string =>
|
|
328
|
+
[a?.companyName, a?.street, `${a?.postalCode ?? ""} ${a?.city ?? ""}`.trim()]
|
|
329
|
+
.filter(Boolean).join(" · ");
|
|
330
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
331
|
+
const history = await readCoverMutableJson<{ orders: unknown[] }>("account/order-list.json");
|
|
332
|
+
history.orders.unshift({
|
|
333
|
+
id: orderId,
|
|
334
|
+
date: today,
|
|
335
|
+
status: blockingApprovals.length ? "approval-pending" : "processing",
|
|
336
|
+
paymentStatus: "open",
|
|
337
|
+
total: totalRow?.amount ?? 0,
|
|
338
|
+
currency: calculation.currency,
|
|
339
|
+
...(body.orderNumber ? { orderNumber: body.orderNumber } : {}),
|
|
340
|
+
positions,
|
|
341
|
+
deliveryAddress: addressLine(address),
|
|
342
|
+
billingAddress: addressLine(billing),
|
|
343
|
+
paymentMethod: payment.method,
|
|
344
|
+
progress: blockingApprovals.length
|
|
345
|
+
? [{ step: "ordered", date: today }]
|
|
346
|
+
: [{ step: "ordered", date: today }, { step: "processing", date: today }],
|
|
347
|
+
...(blockingApprovals.length
|
|
348
|
+
? { approvers: [...new Set(blockingApprovals.map(l => l.approval.approverName).filter(Boolean))] }
|
|
349
|
+
: {}),
|
|
350
|
+
});
|
|
351
|
+
await writeCoverMutableJson("account/order-list.json", history);
|
|
352
|
+
}
|
|
353
|
+
catch (persistErr) {
|
|
354
|
+
getLogService().error("Order history persistence failed", toErrorContext(persistErr));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
orderId,
|
|
360
|
+
status: blockingApprovals.length ? "approval-pending" : "confirmed",
|
|
361
|
+
createdAt: new Date().toISOString(),
|
|
362
|
+
payment,
|
|
363
|
+
approvalRequired: blockingApprovals.length > 0,
|
|
364
|
+
approvers: [...new Set(blockingApprovals
|
|
365
|
+
.map(l => l.approval.approverName)
|
|
366
|
+
.filter((name): name is string => Boolean(name)))],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
if (err !== null && typeof err === "object" && "statusCode" in err) {
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
getLogService().error("Service error: orders/create", toErrorContext(err));
|
|
374
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PaymentOption } from "../../../app/interfaces/payment";
|
|
2
|
+
import type { Locale } from "../../utils/locale";
|
|
3
|
+
|
|
4
|
+
/** Eligible method row of the payments app. */
|
|
5
|
+
interface ApiEligibleMethod {
|
|
6
|
+
code: string;
|
|
7
|
+
name: string;
|
|
8
|
+
labels: Record<string, string> | null;
|
|
9
|
+
description: string | null;
|
|
10
|
+
kind: string;
|
|
11
|
+
provider: string | null;
|
|
12
|
+
fee: number;
|
|
13
|
+
fee_type: "none" | "fixed" | "percent";
|
|
14
|
+
currency: string;
|
|
15
|
+
position: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The checkout's payment question — "what can THIS buyer pay with?".
|
|
20
|
+
* Live mode resolves against the payments app (restrictions + fees are
|
|
21
|
+
* evaluated server-side there); mock mode answers the demo profile's
|
|
22
|
+
* static methods. Body: { amount, country, currency? }.
|
|
23
|
+
*/
|
|
24
|
+
export default defineEventHandler(async (event) => {
|
|
25
|
+
const { amount = 0, country = "", currency = "EUR" } = await readBody<{
|
|
26
|
+
amount?: number; country?: string; currency?: string;
|
|
27
|
+
}>(event);
|
|
28
|
+
|
|
29
|
+
if (resolvePaymentServiceKey(event) !== "api") {
|
|
30
|
+
const user = await getAuthService(event).me(event);
|
|
31
|
+
const profile = await getCheckoutProfileService().getProfile(user?.$id ?? null);
|
|
32
|
+
return {
|
|
33
|
+
managed: false,
|
|
34
|
+
methods: profile.availablePaymentMethods,
|
|
35
|
+
defaultMethod: profile.defaultPaymentMethod,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const locale: Locale = resolveLocale(event);
|
|
40
|
+
try {
|
|
41
|
+
const { methods } = await useRevenexxApi().post<{ methods: ApiEligibleMethod[] }>(
|
|
42
|
+
"/v1/payments/methods/eligible",
|
|
43
|
+
{ amount, country, currency },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const options: PaymentOption[] = methods.map(m => ({
|
|
47
|
+
method: m.code,
|
|
48
|
+
label: m.labels?.[locale] ?? m.name,
|
|
49
|
+
...(m.description ? { description: m.description } : {}),
|
|
50
|
+
fee: m.fee,
|
|
51
|
+
feeType: m.fee_type,
|
|
52
|
+
kind: m.kind,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
managed: true,
|
|
57
|
+
methods: options,
|
|
58
|
+
defaultMethod: options[0]?.method ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
getLogService().error("Service error: payment/methods", apiErrorContext(err));
|
|
63
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
const id = getRouterParam(event, "id") ?? "";
|
|
3
|
+
if (!id) {
|
|
4
|
+
throw createError({ statusCode: 400, message: "Invalid product id" });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const locale = resolveLocale(event);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
let detail = await getProductService(event).getProductDetail(id, locale);
|
|
11
|
+
if (!detail) {
|
|
12
|
+
throw createError({ statusCode: 404, message: "Product not found" });
|
|
13
|
+
}
|
|
14
|
+
if (resolvePriceServiceKey(event) === "api") {
|
|
15
|
+
detail = await enrichDetailWithLivePrices(event, detail);
|
|
16
|
+
}
|
|
17
|
+
if (resolveInventoryServiceKey(event) === "api") {
|
|
18
|
+
detail = await enrichDetailWithLiveAvailability(event, detail);
|
|
19
|
+
}
|
|
20
|
+
return detail;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err !== null && typeof err === "object" && "statusCode" in err) {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
getLogService().error("Service error: product/detail", { id, ...toErrorContext(err) });
|
|
27
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
const locale = resolveLocale(event);
|
|
3
|
+
const id = getRouterParam(event, "id") ?? "";
|
|
4
|
+
if (!id) {
|
|
5
|
+
throw createError({ status: 400, message: "Invalid product id" });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
let product = await getProductService(event).getProductById(id, locale);
|
|
10
|
+
if (!product) {
|
|
11
|
+
throw createError({ status: 404, message: "Product not found" });
|
|
12
|
+
}
|
|
13
|
+
if (resolvePriceServiceKey(event) === "api") {
|
|
14
|
+
const [enriched] = await enrichProductsWithLivePrices(event, [product]);
|
|
15
|
+
product = enriched ?? product;
|
|
16
|
+
}
|
|
17
|
+
if (resolveInventoryServiceKey(event) === "api") {
|
|
18
|
+
const [enriched] = await enrichProductsWithLiveAvailability(event, [product]);
|
|
19
|
+
product = enriched ?? product;
|
|
20
|
+
}
|
|
21
|
+
return product;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err !== null && typeof err === "object" && "statusCode" in err) {
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
getLogService().error("Service error: products/by-id", { id, ...toErrorContext(err) });
|
|
28
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
const locale = resolveLocale(event);
|
|
3
|
+
const { category, subcategory } = getQuery(event) as {
|
|
4
|
+
category?: string; subcategory?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
let products = await getProductService(event).listProducts({ category, subcategory, locale });
|
|
9
|
+
if (resolvePriceServiceKey(event) === "api") {
|
|
10
|
+
products = await enrichProductsWithLivePrices(event, products);
|
|
11
|
+
}
|
|
12
|
+
if (resolveInventoryServiceKey(event) === "api") {
|
|
13
|
+
products = await enrichProductsWithLiveAvailability(event, products);
|
|
14
|
+
}
|
|
15
|
+
return products;
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
getLogService().error("Service error: products/list", toErrorContext(err));
|
|
19
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Requisition, RequisitionItemSnapshot } from "../../../app/interfaces/b2b";
|
|
2
|
+
|
|
3
|
+
interface RequisitionPayload {
|
|
4
|
+
workflowId: string;
|
|
5
|
+
items: RequisitionItemSnapshot[];
|
|
6
|
+
deliveryAddress?: Requisition["deliveryAddress"];
|
|
7
|
+
note?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const r2 = (n: number): number => Math.round(n * 100) / 100;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a requisition (order request): the requester's checkout outcome.
|
|
14
|
+
* The requisition enters its workflow with a `submitted` audit entry and
|
|
15
|
+
* becomes loadable by the workflow's orderers.
|
|
16
|
+
*/
|
|
17
|
+
export default defineEventHandler(async (event) => {
|
|
18
|
+
const user = await getAuthService(event).me(event);
|
|
19
|
+
if (!user) {
|
|
20
|
+
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const body = await readBody<RequisitionPayload>(event);
|
|
24
|
+
if (!body?.workflowId || !Array.isArray(body.items) || body.items.length === 0) {
|
|
25
|
+
throw createError({ statusCode: 400, message: "workflowId and items[] required" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const context = await getB2BContextService(event).getContext(user.$id);
|
|
29
|
+
if (!context.settings.workflows.enabled) {
|
|
30
|
+
throw createError({ statusCode: 403, message: "Workflows are disabled" });
|
|
31
|
+
}
|
|
32
|
+
const workflow = context.workflows.find(wf => wf.id === body.workflowId);
|
|
33
|
+
if (!workflow) {
|
|
34
|
+
throw createError({ statusCode: 403, message: "Not a member of this workflow" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const requisitions = await readCoverMutableJson<Requisition[]>("account/requisitions.json");
|
|
39
|
+
|
|
40
|
+
const requisition: Requisition = {
|
|
41
|
+
id: `REQ-${Date.now().toString(36).toUpperCase()}`,
|
|
42
|
+
workflowId: workflow.id,
|
|
43
|
+
workflowName: workflow.name,
|
|
44
|
+
requestedBy: { id: user.$id, name: user.name },
|
|
45
|
+
requestedAt: new Date().toISOString(),
|
|
46
|
+
status: "open",
|
|
47
|
+
totalValue: r2(body.items.reduce((sum, i) => sum + i.price * i.quantity, 0)),
|
|
48
|
+
items: body.items,
|
|
49
|
+
history: [{
|
|
50
|
+
id: `wh-${Date.now().toString(36)}`,
|
|
51
|
+
stepSequence: 1,
|
|
52
|
+
action: "submitted",
|
|
53
|
+
actorName: user.name,
|
|
54
|
+
actionDate: new Date().toISOString(),
|
|
55
|
+
...(body.note ? { comment: body.note } : {}),
|
|
56
|
+
}],
|
|
57
|
+
...(body.deliveryAddress ? { deliveryAddress: body.deliveryAddress } : {}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await writeCoverMutableJson("account/requisitions.json", [requisition, ...requisitions]);
|
|
61
|
+
return { requisitionId: requisition.id, status: requisition.status };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
getLogService().error("Service error: requisitions/create", toErrorContext(err));
|
|
65
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DeliveryOption } from "../../../app/interfaces/delivery";
|
|
2
|
+
import type { Locale } from "../../utils/locale";
|
|
3
|
+
|
|
4
|
+
/** Rate row of the shipping app. */
|
|
5
|
+
interface ApiShippingRate {
|
|
6
|
+
code: string;
|
|
7
|
+
name: string;
|
|
8
|
+
labels: Record<string, string> | null;
|
|
9
|
+
price: number;
|
|
10
|
+
eta_days_min: number | null;
|
|
11
|
+
eta_days_max: number | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatEta(rate: ApiShippingRate): string {
|
|
15
|
+
if (rate.eta_days_min === null && rate.eta_days_max === null) {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
if (rate.eta_days_min !== null && rate.eta_days_max !== null && rate.eta_days_min !== rate.eta_days_max) {
|
|
19
|
+
return `${rate.eta_days_min}–${rate.eta_days_max}`;
|
|
20
|
+
}
|
|
21
|
+
return String(rate.eta_days_max ?? rate.eta_days_min);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The checkout's delivery question — "how can THIS order ship, at what
|
|
26
|
+
* price?". Live mode resolves against the shipping app (fixed/free/matrix
|
|
27
|
+
* pricing, free-above, country restrictions evaluated server-side); mock
|
|
28
|
+
* mode answers the demo profile's static methods.
|
|
29
|
+
* Body: { amount, country, weight?, quantity? }.
|
|
30
|
+
*/
|
|
31
|
+
export default defineEventHandler(async (event) => {
|
|
32
|
+
const { amount = 0, country = "", weight, quantity } = await readBody<{
|
|
33
|
+
amount?: number; country?: string; weight?: number; quantity?: number;
|
|
34
|
+
}>(event);
|
|
35
|
+
|
|
36
|
+
if (resolveShippingServiceKey(event) !== "api") {
|
|
37
|
+
const user = await getAuthService(event).me(event);
|
|
38
|
+
const profile = await getCheckoutProfileService().getProfile(user?.$id ?? null);
|
|
39
|
+
return {
|
|
40
|
+
managed: false,
|
|
41
|
+
methods: profile.availableDeliveryMethods,
|
|
42
|
+
defaultMethod: profile.defaultDeliveryMethod,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const locale: Locale = resolveLocale(event);
|
|
47
|
+
try {
|
|
48
|
+
const { rates } = await useRevenexxApi().post<{ rates: ApiShippingRate[] }>(
|
|
49
|
+
"/v1/shipping/rates",
|
|
50
|
+
{ order_value: amount, country, weight, quantity },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const methods: DeliveryOption[] = rates.map(rate => ({
|
|
54
|
+
method: rate.code,
|
|
55
|
+
label: rate.labels?.[locale] ?? rate.name,
|
|
56
|
+
price: rate.price,
|
|
57
|
+
eta: formatEta(rate),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
managed: true,
|
|
62
|
+
methods,
|
|
63
|
+
defaultMethod: methods[0]?.method ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
getLogService().error("Service error: shipping/rates", apiErrorContext(err));
|
|
68
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default defineEventHandler(async (event) => {
|
|
2
|
+
try {
|
|
3
|
+
return await getThemeService(event).listThemes();
|
|
4
|
+
}
|
|
5
|
+
catch (err) {
|
|
6
|
+
getLogService().error("Service error: themes/list", toErrorContext(err));
|
|
7
|
+
throw createError({ statusCode: 500, message: "Internal server error" });
|
|
8
|
+
}
|
|
9
|
+
});
|