@salifbiaye/create-boutique 0.1.1
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/README.md +1 -0
- package/cli/dist/commands.js +382 -0
- package/cli/dist/create.js +194 -0
- package/cli/dist/docker.js +19 -0
- package/cli/dist/generate.js +322 -0
- package/cli/dist/guide.js +72 -0
- package/cli/dist/index.js +156 -0
- package/cli/dist/paths.js +29 -0
- package/cli/dist/ports.js +54 -0
- package/cli/dist/project.js +119 -0
- package/cli/dist/themes.js +24 -0
- package/docs/fiche-client.en.html +137 -0
- package/docs/fiche-client.html +137 -0
- package/docs/guide-complet.en.html +259 -0
- package/docs/guide-complet.fr.html +259 -0
- package/package.json +51 -0
- package/templates/backend/.dockerignore +9 -0
- package/templates/backend/Dockerfile +30 -0
- package/templates/backend/README.md +78 -0
- package/templates/backend/TEMPLATE_VERSION +1 -0
- package/templates/backend/docker/entrypoint.sh +28 -0
- package/templates/backend/instrumentation.ts +24 -0
- package/templates/backend/medusa-config.ts +114 -0
- package/templates/backend/package-lock.json +16222 -0
- package/templates/backend/package.json +39 -0
- package/templates/backend/src/admin/README.md +33 -0
- package/templates/backend/src/admin/components/image-field.tsx +61 -0
- package/templates/backend/src/admin/i18n/README.md +58 -0
- package/templates/backend/src/admin/i18n/index.ts +8 -0
- package/templates/backend/src/admin/i18n/json/en.json +66 -0
- package/templates/backend/src/admin/i18n/json/fr.json +66 -0
- package/templates/backend/src/admin/lib/client.ts +9 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
- package/templates/backend/src/admin/tsconfig.json +24 -0
- package/templates/backend/src/admin/vite-env.d.ts +1 -0
- package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
- package/templates/backend/src/api/README.md +135 -0
- package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
- package/templates/backend/src/api/middlewares.ts +28 -0
- package/templates/backend/src/api/rate-limit.ts +30 -0
- package/templates/backend/src/api/route.ts +6 -0
- package/templates/backend/src/api/store/campaign/route.ts +25 -0
- package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
- package/templates/backend/src/emails/en.json +64 -0
- package/templates/backend/src/emails/fr.json +64 -0
- package/templates/backend/src/jobs/README.md +38 -0
- package/templates/backend/src/lib/emails.ts +94 -0
- package/templates/backend/src/lib/notify.ts +148 -0
- package/templates/backend/src/lib/project.ts +146 -0
- package/templates/backend/src/links/README.md +26 -0
- package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
- package/templates/backend/src/modules/README.md +117 -0
- package/templates/backend/src/modules/campaign/index.ts +9 -0
- package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
- package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
- package/templates/backend/src/modules/campaign/service.ts +6 -0
- package/templates/backend/src/modules/campaign/validators.ts +94 -0
- package/templates/backend/src/modules/resend/index.ts +6 -0
- package/templates/backend/src/modules/resend/service.ts +63 -0
- package/templates/backend/src/scripts/README.md +63 -0
- package/templates/backend/src/scripts/campaign-export.ts +15 -0
- package/templates/backend/src/scripts/campaign-import.ts +38 -0
- package/templates/backend/src/scripts/price-report.ts +34 -0
- package/templates/backend/src/scripts/seed.ts +418 -0
- package/templates/backend/src/scripts/storage.ts +63 -0
- package/templates/backend/src/subscribers/README.md +61 -0
- package/templates/backend/src/subscribers/account-emails.ts +27 -0
- package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
- package/templates/backend/src/subscribers/order-emails.ts +12 -0
- package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
- package/templates/backend/src/workflows/README.md +81 -0
- package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
- package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
- package/templates/backend/tsconfig.json +36 -0
- package/templates/storefront/.dockerignore +7 -0
- package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
- package/templates/storefront/.prettierrc +8 -0
- package/templates/storefront/Dockerfile +37 -0
- package/templates/storefront/LICENSE +21 -0
- package/templates/storefront/README.md +127 -0
- package/templates/storefront/locales/en.json +269 -0
- package/templates/storefront/locales/fr.json +269 -0
- package/templates/storefront/next-env.d.ts +7 -0
- package/templates/storefront/next-sitemap.js +19 -0
- package/templates/storefront/next.config.js +29 -0
- package/templates/storefront/package-lock.json +9031 -0
- package/templates/storefront/package.json +49 -0
- package/templates/storefront/postcss.config.js +6 -0
- package/templates/storefront/public/favicon.ico +0 -0
- package/templates/storefront/scripts/sync-design.mjs +30 -0
- package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
- package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
- package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
- package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
- package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/layout.tsx +43 -0
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
- package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
- package/templates/storefront/src/app/(main)/page.tsx +68 -0
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
- package/templates/storefront/src/app/api/health/route.ts +13 -0
- package/templates/storefront/src/app/layout.tsx +42 -0
- package/templates/storefront/src/app/not-found.tsx +21 -0
- package/templates/storefront/src/app/opengraph-image.jpg +0 -0
- package/templates/storefront/src/app/twitter-image.jpg +0 -0
- package/templates/storefront/src/instrumentation-node.ts +18 -0
- package/templates/storefront/src/instrumentation.ts +9 -0
- package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
- package/templates/storefront/src/lib/boutique/config.ts +152 -0
- package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
- package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
- package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
- package/templates/storefront/src/lib/boutique/schema.ts +131 -0
- package/templates/storefront/src/lib/config.ts +39 -0
- package/templates/storefront/src/lib/constants.tsx +75 -0
- package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
- package/templates/storefront/src/lib/data/cart.ts +434 -0
- package/templates/storefront/src/lib/data/categories.ts +49 -0
- package/templates/storefront/src/lib/data/collections.ts +59 -0
- package/templates/storefront/src/lib/data/cookies.ts +103 -0
- package/templates/storefront/src/lib/data/customer.ts +261 -0
- package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
- package/templates/storefront/src/lib/data/onboarding.ts +9 -0
- package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
- package/templates/storefront/src/lib/data/orders.ts +112 -0
- package/templates/storefront/src/lib/data/payment.ts +35 -0
- package/templates/storefront/src/lib/data/products.ts +127 -0
- package/templates/storefront/src/lib/data/regions.ts +70 -0
- package/templates/storefront/src/lib/data/variants.ts +38 -0
- package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
- package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
- package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
- package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
- package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
- package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
- package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
- package/templates/storefront/src/lib/util/money.ts +35 -0
- package/templates/storefront/src/lib/util/product.ts +5 -0
- package/templates/storefront/src/lib/util/repeat.ts +5 -0
- package/templates/storefront/src/lib/util/sort-products.ts +50 -0
- package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
- package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
- package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
- package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
- package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
- package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
- package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
- package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
- package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
- package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
- package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
- package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
- package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
- package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
- package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
- package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
- package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
- package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
- package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
- package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
- package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
- package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
- package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
- package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
- package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
- package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
- package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
- package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
- package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
- package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
- package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
- package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
- package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
- package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
- package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
- package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
- package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
- package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
- package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
- package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
- package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
- package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
- package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
- package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
- package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
- package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
- package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
- package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
- package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
- package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
- package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
- package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
- package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
- package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
- package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
- package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
- package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
- package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
- package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
- package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
- package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
- package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
- package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
- package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
- package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
- package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
- package/templates/storefront/src/modules/ub/button.tsx +41 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
- package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
- package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
- package/templates/storefront/src/modules/ub/footer.tsx +56 -0
- package/templates/storefront/src/modules/ub/icons.tsx +42 -0
- package/templates/storefront/src/modules/ub/nav.tsx +90 -0
- package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
- package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
- package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
- package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
- package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
- package/templates/storefront/src/proxy.ts +89 -0
- package/templates/storefront/src/styles/globals.css +98 -0
- package/templates/storefront/src/styles/ub/bundle.css +372 -0
- package/templates/storefront/src/styles/ub/fonts.css +17 -0
- package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
- package/templates/storefront/src/styles/ub/overrides.css +12 -0
- package/templates/storefront/src/styles/ub/tokens.css +9 -0
- package/templates/storefront/src/types/global.ts +24 -0
- package/templates/storefront/src/types/icon.ts +4 -0
- package/templates/storefront/tailwind.config.js +164 -0
- package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
- package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
- package/templates/storefront/themes/black-friday/theme.json +9 -0
- package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
- package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
- package/templates/storefront/themes/cyber-monday/theme.json +9 -0
- package/templates/storefront/themes/halloween/campaign.en.json +31 -0
- package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
- package/templates/storefront/themes/halloween/theme.json +9 -0
- package/templates/storefront/themes/noel/campaign.en.json +31 -0
- package/templates/storefront/themes/noel/campaign.fr.json +31 -0
- package/templates/storefront/themes/noel/theme.json +9 -0
- package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
- package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
- package/templates/storefront/themes/nouvel-an/theme.json +9 -0
- package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
- package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
- package/templates/storefront/themes/thanksgiving/theme.json +9 -0
- package/templates/storefront/tsconfig.json +53 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed idempotent, lancé à chaque démarrage du container backend (docker/entrypoint.sh).
|
|
3
|
+
* Lit config/project.json et crée seulement ce qui manque :
|
|
4
|
+
* - devises du store, une région par devise (un pays = une devise), régions de taxe ;
|
|
5
|
+
* - entrepôt, zone de livraison « Monde », options Standard / Express par devise ;
|
|
6
|
+
* - 1 sales channel + 1 publishable key par boutique, clé écrite dans KEYS_DIR/<slug> ;
|
|
7
|
+
* - produits de seed/products.csv (par handle) et images de seed/images.
|
|
8
|
+
* Aucun SQL : uniquement les workflows et services Medusa.
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import { parse } from "csv-parse/sync"
|
|
13
|
+
import { CreateInventoryLevelInput, CreateProductWorkflowInputDTO, ExecArgs } from "@medusajs/framework/types"
|
|
14
|
+
import { ContainerRegistrationKeys, Modules, ProductStatus } from "@medusajs/framework/utils"
|
|
15
|
+
import {
|
|
16
|
+
createApiKeysWorkflow,
|
|
17
|
+
createInventoryLevelsWorkflow,
|
|
18
|
+
createPriceListsWorkflow,
|
|
19
|
+
createProductsWorkflow,
|
|
20
|
+
createRegionsWorkflow,
|
|
21
|
+
createSalesChannelsWorkflow,
|
|
22
|
+
createShippingOptionsWorkflow,
|
|
23
|
+
createShippingProfilesWorkflow,
|
|
24
|
+
createStockLocationsWorkflow,
|
|
25
|
+
createTaxRegionsWorkflow,
|
|
26
|
+
linkProductsToSalesChannelWorkflow,
|
|
27
|
+
linkSalesChannelsToApiKeyWorkflow,
|
|
28
|
+
linkSalesChannelsToStockLocationWorkflow,
|
|
29
|
+
updateRegionsWorkflow,
|
|
30
|
+
updateShippingOptionsWorkflow,
|
|
31
|
+
updateStoresWorkflow,
|
|
32
|
+
uploadFilesWorkflow,
|
|
33
|
+
} from "@medusajs/medusa/core-flows"
|
|
34
|
+
import { CONFIG_DIR, KEYS_DIR, SEED_DIR, countriesByCurrency, readProject, shippingRates } from "../lib/project"
|
|
35
|
+
import { CAMPAIGN_MODULE } from "../modules/campaign"
|
|
36
|
+
import CampaignModuleService from "../modules/campaign/service"
|
|
37
|
+
import { CampaignFile, CampaignInput } from "../modules/campaign/validators"
|
|
38
|
+
import { upsertCampaignWorkflow } from "../workflows/upsert-campaign"
|
|
39
|
+
|
|
40
|
+
const LOCATION_NAME = "Entrepôt principal"
|
|
41
|
+
const FULFILLMENT_SET_NAME = "Livraison"
|
|
42
|
+
const SERVICE_ZONE_NAME = "Monde"
|
|
43
|
+
|
|
44
|
+
export default async function seed({ container }: ExecArgs) {
|
|
45
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
|
46
|
+
const link = container.resolve(ContainerRegistrationKeys.LINK)
|
|
47
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
48
|
+
const storeService = container.resolve(Modules.STORE)
|
|
49
|
+
const regionService = container.resolve(Modules.REGION)
|
|
50
|
+
const taxService = container.resolve(Modules.TAX)
|
|
51
|
+
const fulfillment = container.resolve(Modules.FULFILLMENT)
|
|
52
|
+
const salesChannels = container.resolve(Modules.SALES_CHANNEL)
|
|
53
|
+
const apiKeys = container.resolve(Modules.API_KEY)
|
|
54
|
+
const stockLocations = container.resolve(Modules.STOCK_LOCATION)
|
|
55
|
+
const productService = container.resolve(Modules.PRODUCT)
|
|
56
|
+
|
|
57
|
+
const project = readProject()
|
|
58
|
+
const currencies = [...new Set([project.defaultCurrency, ...project.boutiques.map((b) => b.currency)])]
|
|
59
|
+
logger.info(`[seed] projet ${project.project} : ${project.boutiques.length} boutique(s), devises ${currencies.join(", ")}`)
|
|
60
|
+
|
|
61
|
+
// ---------- Store : devises ----------
|
|
62
|
+
const [store] = await storeService.listStores({}, { relations: ["supported_currencies"] })
|
|
63
|
+
const knownCurrencies = new Set(store.supported_currencies?.map((c) => c.currency_code))
|
|
64
|
+
if (currencies.some((c) => !knownCurrencies.has(c)) || !store.supported_currencies?.find((c) => c.is_default && c.currency_code === project.defaultCurrency)) {
|
|
65
|
+
await updateStoresWorkflow(container).run({
|
|
66
|
+
input: {
|
|
67
|
+
selector: { id: store.id },
|
|
68
|
+
update: {
|
|
69
|
+
name: project.boutiques[0].storeName,
|
|
70
|
+
supported_currencies: currencies.map((c) => ({ currency_code: c, is_default: c === project.defaultCurrency })),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------- Régions : une par devise ----------
|
|
77
|
+
const allCountries = (await regionService.listCountries({}, { take: 500 })).map((c) => c.iso_2)
|
|
78
|
+
const target = countriesByCurrency(project, allCountries)
|
|
79
|
+
const paymentProviders = ["pp_system_default", ...(process.env.STRIPE_API_KEY ? ["pp_stripe_stripe"] : [])]
|
|
80
|
+
const existing = await regionService.listRegions({}, { relations: ["countries"] })
|
|
81
|
+
const managed = new Map(existing.filter((r) => currencies.includes(r.currency_code) && r.name === r.currency_code.toUpperCase()).map((r) => [r.currency_code, r]))
|
|
82
|
+
|
|
83
|
+
// Pays pris par une région que le seed ne gère pas : erreur claire plutôt qu'un échec obscur.
|
|
84
|
+
for (const r of existing) {
|
|
85
|
+
if (managed.get(r.currency_code)?.id === r.id) continue
|
|
86
|
+
const taken = (r.countries ?? []).map((c) => c.iso_2).filter((c) => [...target.values()].some((s) => s.has(c)))
|
|
87
|
+
if (taken.length) {
|
|
88
|
+
throw new Error(`la région « ${r.name} » (non gérée par le seed) contient ${taken.join(", ")} : retirez ces pays dans l'admin ou supprimez la région.`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 1) rétrécir les régions existantes (libère les pays qui changent de devise), 2) compléter / créer.
|
|
93
|
+
for (const [currency, region] of managed) {
|
|
94
|
+
const keep = (region.countries ?? []).map((c) => c.iso_2).filter((c) => target.get(currency)?.has(c))
|
|
95
|
+
if (keep.length !== (region.countries ?? []).length) {
|
|
96
|
+
await updateRegionsWorkflow(container).run({ input: { selector: { id: region.id }, update: { countries: keep } } })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [currency, countries] of target) {
|
|
100
|
+
const region = managed.get(currency)
|
|
101
|
+
if (region) {
|
|
102
|
+
await updateRegionsWorkflow(container).run({
|
|
103
|
+
input: { selector: { id: region.id }, update: { countries: [...countries], payment_providers: paymentProviders } },
|
|
104
|
+
})
|
|
105
|
+
} else {
|
|
106
|
+
await createRegionsWorkflow(container).run({
|
|
107
|
+
input: { regions: [{ name: currency.toUpperCase(), currency_code: currency, countries: [...countries], payment_providers: paymentProviders }] },
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
logger.info(`[seed] région ${currency.toUpperCase()} : ${countries.size} pays`)
|
|
111
|
+
}
|
|
112
|
+
const shippedCountries = [...target.values()].flatMap((s) => [...s])
|
|
113
|
+
|
|
114
|
+
// ---------- Taxes : une région de taxe par pays (0 % par défaut, réglable dans l'admin) ----------
|
|
115
|
+
const taxed = new Set((await taxService.listTaxRegions({}, { take: 5000 })).filter((t) => !t.parent_id).map((t) => t.country_code))
|
|
116
|
+
const untaxed = shippedCountries.filter((c) => !taxed.has(c))
|
|
117
|
+
if (untaxed.length) {
|
|
118
|
+
await createTaxRegionsWorkflow(container).run({ input: untaxed.map((country_code) => ({ country_code, provider_id: "tp_system" })) })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- Entrepôt ----------
|
|
122
|
+
let [location] = await stockLocations.listStockLocations({ name: LOCATION_NAME })
|
|
123
|
+
if (!location) {
|
|
124
|
+
const { result } = await createStockLocationsWorkflow(container).run({
|
|
125
|
+
input: { locations: [{ name: LOCATION_NAME, address: { country_code: project.boutiques[0].country.toUpperCase(), address_1: "" } }] },
|
|
126
|
+
})
|
|
127
|
+
location = result[0]
|
|
128
|
+
await link.create({ [Modules.STOCK_LOCATION]: { stock_location_id: location.id }, [Modules.FULFILLMENT]: { fulfillment_provider_id: "manual_manual" } })
|
|
129
|
+
await updateStoresWorkflow(container).run({ input: { selector: { id: store.id }, update: { default_location_id: location.id } } })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------- Livraison : zone « Monde » + options par devise ----------
|
|
133
|
+
let [profile] = await fulfillment.listShippingProfiles({ type: "default" })
|
|
134
|
+
if (!profile) {
|
|
135
|
+
const { result } = await createShippingProfilesWorkflow(container).run({ input: { data: [{ name: "Default Shipping Profile", type: "default" }] } })
|
|
136
|
+
profile = result[0]
|
|
137
|
+
}
|
|
138
|
+
let [fset] = await fulfillment.listFulfillmentSets({ name: FULFILLMENT_SET_NAME }, { relations: ["service_zones", "service_zones.geo_zones"] })
|
|
139
|
+
if (!fset) {
|
|
140
|
+
fset = await fulfillment.createFulfillmentSets({ name: FULFILLMENT_SET_NAME, type: "shipping", service_zones: [{ name: SERVICE_ZONE_NAME, geo_zones: [] }] })
|
|
141
|
+
await link.create({ [Modules.STOCK_LOCATION]: { stock_location_id: location.id }, [Modules.FULFILLMENT]: { fulfillment_set_id: fset.id } })
|
|
142
|
+
;[fset] = await fulfillment.listFulfillmentSets({ id: fset.id }, { relations: ["service_zones", "service_zones.geo_zones"] })
|
|
143
|
+
}
|
|
144
|
+
const zone = fset.service_zones[0]
|
|
145
|
+
const zoned = new Set(zone.geo_zones.map((g) => g.country_code))
|
|
146
|
+
const missingZones = shippedCountries.filter((c) => !zoned.has(c))
|
|
147
|
+
if (missingZones.length) {
|
|
148
|
+
await fulfillment.createGeoZones(missingZones.map((country_code) => ({ service_zone_id: zone.id, country_code, type: "country" as const })))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { data: options } = await query.graph({
|
|
152
|
+
entity: "shipping_option",
|
|
153
|
+
fields: ["id", "name", "prices.*"],
|
|
154
|
+
filters: { service_zone_id: zone.id },
|
|
155
|
+
})
|
|
156
|
+
const rules = [
|
|
157
|
+
{ attribute: "enabled_in_store", value: "true", operator: "eq" as const },
|
|
158
|
+
{ attribute: "is_return", value: "false", operator: "eq" as const },
|
|
159
|
+
]
|
|
160
|
+
for (const [i, name, code, description] of [
|
|
161
|
+
[0, "Standard", "standard", "3-7 business days"],
|
|
162
|
+
[1, "Express", "express", "1-3 business days"],
|
|
163
|
+
] as const) {
|
|
164
|
+
// Prix liés via le module Pricing : demandés par query.graph mais absents du type généré.
|
|
165
|
+
const option = options.find((o) => o.name === name) as
|
|
166
|
+
| { id: string; prices?: { id: string; amount: number; currency_code: string }[] }
|
|
167
|
+
| undefined
|
|
168
|
+
if (!option) {
|
|
169
|
+
await createShippingOptionsWorkflow(container).run({
|
|
170
|
+
input: [
|
|
171
|
+
{
|
|
172
|
+
name,
|
|
173
|
+
price_type: "flat",
|
|
174
|
+
provider_id: "manual_manual",
|
|
175
|
+
service_zone_id: zone.id,
|
|
176
|
+
shipping_profile_id: profile.id,
|
|
177
|
+
type: { label: name, description, code },
|
|
178
|
+
prices: currencies.map((c) => ({ currency_code: c, amount: shippingRates(c)[i] })),
|
|
179
|
+
rules,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
} else {
|
|
184
|
+
const priced = new Set((option.prices ?? []).map((p) => p.currency_code))
|
|
185
|
+
const add = currencies.filter((c) => !priced.has(c))
|
|
186
|
+
if (add.length) {
|
|
187
|
+
await updateShippingOptionsWorkflow(container).run({
|
|
188
|
+
input: [
|
|
189
|
+
{
|
|
190
|
+
id: option.id,
|
|
191
|
+
prices: [
|
|
192
|
+
...(option.prices ?? []).map((p) => ({ id: p.id, amount: p.amount })),
|
|
193
|
+
...add.map((c) => ({ currency_code: c, amount: shippingRates(c)[i] })),
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------- Boutiques : sales channel + publishable key ----------
|
|
203
|
+
fs.mkdirSync(KEYS_DIR, { recursive: true })
|
|
204
|
+
const channelIds: Record<string, string> = {}
|
|
205
|
+
for (const b of project.boutiques) {
|
|
206
|
+
let [channel] = await salesChannels.listSalesChannels({ name: b.slug })
|
|
207
|
+
if (!channel) {
|
|
208
|
+
const { result } = await createSalesChannelsWorkflow(container).run({
|
|
209
|
+
input: { salesChannelsData: [{ name: b.slug, description: `${b.storeName} — ${b.theme}` }] },
|
|
210
|
+
})
|
|
211
|
+
channel = result[0]
|
|
212
|
+
await linkSalesChannelsToStockLocationWorkflow(container).run({ input: { id: location.id, add: [channel.id] } })
|
|
213
|
+
// Boutique ajoutée à un projet existant : elle reçoit tout le catalogue déjà en base
|
|
214
|
+
// (le marchand retire ensuite dans l'admin ce qu'il ne veut pas y vendre).
|
|
215
|
+
const existingProducts = await productService.listProducts({}, { select: ["id"], take: 10000 })
|
|
216
|
+
if (existingProducts.length) {
|
|
217
|
+
await linkProductsToSalesChannelWorkflow(container).run({
|
|
218
|
+
input: { id: channel.id, add: existingProducts.map((pr) => pr.id) },
|
|
219
|
+
})
|
|
220
|
+
logger.info(`[seed] boutique ${b.slug} : ${existingProducts.length} produit(s) existant(s) ajouté(s)`)
|
|
221
|
+
}
|
|
222
|
+
logger.info(`[seed] boutique ${b.slug} : sales channel créé`)
|
|
223
|
+
}
|
|
224
|
+
channelIds[b.slug] = channel.id
|
|
225
|
+
|
|
226
|
+
let [key] = await apiKeys.listApiKeys({ title: b.slug, type: "publishable" })
|
|
227
|
+
if (!key) {
|
|
228
|
+
const { result } = await createApiKeysWorkflow(container).run({
|
|
229
|
+
input: { api_keys: [{ title: b.slug, type: "publishable", created_by: "" }] },
|
|
230
|
+
})
|
|
231
|
+
key = result[0]
|
|
232
|
+
await linkSalesChannelsToApiKeyWorkflow(container).run({ input: { id: key.id, add: [channel.id] } })
|
|
233
|
+
}
|
|
234
|
+
fs.writeFileSync(path.join(KEYS_DIR, b.slug), key.token)
|
|
235
|
+
}
|
|
236
|
+
if (!store.default_sales_channel_id) {
|
|
237
|
+
await updateStoresWorkflow(container).run({
|
|
238
|
+
input: { selector: { id: store.id }, update: { default_sales_channel_id: channelIds[project.boutiques[0].slug] } },
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------- Campagnes : valeur initiale depuis config/campaigns/<slug>.json ----------
|
|
243
|
+
// Importée seulement si la boutique n'a pas encore de campagne : une campagne modifiée
|
|
244
|
+
// dans l'admin (source de vérité = base) n'est jamais écrasée.
|
|
245
|
+
const campaigns: CampaignModuleService = container.resolve(CAMPAIGN_MODULE)
|
|
246
|
+
const withCampaign = new Set(
|
|
247
|
+
(await campaigns.listBoutiqueCampaigns({ sales_channel_id: Object.values(channelIds) })).map((c) => c.sales_channel_id)
|
|
248
|
+
)
|
|
249
|
+
for (const b of project.boutiques) {
|
|
250
|
+
if (withCampaign.has(channelIds[b.slug])) continue
|
|
251
|
+
const file = path.join(CONFIG_DIR, "campaigns", `${b.slug}.json`)
|
|
252
|
+
if (!fs.existsSync(file)) {
|
|
253
|
+
logger.warn(`[seed] ${b.slug} : pas de fichier de campagne (${file})`)
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
const raw = CampaignFile.parse(JSON.parse(fs.readFileSync(file, "utf8")))
|
|
257
|
+
// Images du fichier (seed/images/<nom> ou URL) envoyées au stockage.
|
|
258
|
+
const { image, ...hero } = raw.hero ?? {}
|
|
259
|
+
const { image: promoFile, ...promo } = raw.promo ?? {}
|
|
260
|
+
const upload = (name?: string) => (name ? uploadSeedImage(container, name) : Promise.resolve(undefined))
|
|
261
|
+
const campaign = CampaignInput.parse({
|
|
262
|
+
...raw,
|
|
263
|
+
hero: raw.hero ? hero : undefined,
|
|
264
|
+
promo: raw.promo ? promo : undefined,
|
|
265
|
+
heroImage: await upload(image),
|
|
266
|
+
promoImage: await upload(promoFile),
|
|
267
|
+
logo: await upload(raw.logo),
|
|
268
|
+
})
|
|
269
|
+
await upsertCampaignWorkflow(container).run({ input: { sales_channel_id: channelIds[b.slug], campaign } })
|
|
270
|
+
logger.info(`[seed] ${b.slug} : campagne importée`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------- Produits : seed/products.csv ----------
|
|
274
|
+
await importProducts({ container, logger, currencies, channelIds, locationId: location.id, profileId: profile.id, productService })
|
|
275
|
+
|
|
276
|
+
logger.info("[seed] terminé")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Envoie seed/images/<nom> au stockage et renvoie son URL (URL http(s) renvoyée telle quelle). */
|
|
280
|
+
async function uploadSeedImage(container: ExecArgs["container"], name: string): Promise<string | undefined> {
|
|
281
|
+
if (/^https?:\/\//.test(name)) return name
|
|
282
|
+
const file = path.join(SEED_DIR, "images", path.basename(name))
|
|
283
|
+
if (!fs.existsSync(file)) return undefined
|
|
284
|
+
const ext = path.extname(file).slice(1).toLowerCase()
|
|
285
|
+
const { result } = await uploadFilesWorkflow(container).run({
|
|
286
|
+
input: {
|
|
287
|
+
files: [
|
|
288
|
+
{
|
|
289
|
+
filename: path.basename(file),
|
|
290
|
+
mimeType: `image/${ext === "jpg" ? "jpeg" : ext}`,
|
|
291
|
+
content: fs.readFileSync(file).toString("base64"),
|
|
292
|
+
access: "public",
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
return result[0].url
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
type Row = Record<string, string>
|
|
301
|
+
|
|
302
|
+
async function importProducts({
|
|
303
|
+
container,
|
|
304
|
+
logger,
|
|
305
|
+
currencies,
|
|
306
|
+
channelIds,
|
|
307
|
+
locationId,
|
|
308
|
+
profileId,
|
|
309
|
+
productService,
|
|
310
|
+
}: {
|
|
311
|
+
container: ExecArgs["container"]
|
|
312
|
+
logger: { info: (m: string) => void; warn: (m: string) => void }
|
|
313
|
+
currencies: string[]
|
|
314
|
+
channelIds: Record<string, string>
|
|
315
|
+
locationId: string
|
|
316
|
+
profileId: string
|
|
317
|
+
productService: { listProducts: (f: { handle: string[] }, c: { select: string[] }) => Promise<{ handle: string }[]> }
|
|
318
|
+
}) {
|
|
319
|
+
const csvFile = path.join(SEED_DIR, "products.csv")
|
|
320
|
+
if (!fs.existsSync(csvFile)) {
|
|
321
|
+
logger.info("[seed] pas de seed/products.csv : aucun produit importé")
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
const rows: Row[] = parse(fs.readFileSync(csvFile, "utf8"), { columns: true, skip_empty_lines: true, trim: true, bom: true })
|
|
325
|
+
const existing = new Set((await productService.listProducts({ handle: rows.map((r) => r.handle) }, { select: ["handle"] })).map((p) => p.handle))
|
|
326
|
+
const toCreate = rows.filter((r) => r.handle && !existing.has(r.handle))
|
|
327
|
+
if (!toCreate.length) return
|
|
328
|
+
|
|
329
|
+
// Images : fichiers de seed/images envoyés au stockage ; URL http(s) gardées telles quelles.
|
|
330
|
+
const uploaded = new Map<string, string>()
|
|
331
|
+
const imageUrl = async (name: string): Promise<string | undefined> => {
|
|
332
|
+
if (uploaded.has(name)) return uploaded.get(name)
|
|
333
|
+
const url = await uploadSeedImage(container, name)
|
|
334
|
+
if (url) uploaded.set(name, url)
|
|
335
|
+
else logger.warn(`[seed] image introuvable : ${path.join(SEED_DIR, "images", path.basename(name))}`)
|
|
336
|
+
return url
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const salePrices: { variantKey: string; currency_code: string; amount: number }[] = []
|
|
340
|
+
const products: CreateProductWorkflowInputDTO[] = []
|
|
341
|
+
for (const r of toCreate) {
|
|
342
|
+
const missing = currencies.filter((c) => !r[`price_${c}`])
|
|
343
|
+
if (missing.length) logger.warn(`[seed] ${r.handle} : pas de prix en ${missing.join(", ")} (colonne price_<devise>) — invisible dans ces boutiques`)
|
|
344
|
+
|
|
345
|
+
const images = (await Promise.all((r.images || "").split("|").filter(Boolean).map(imageUrl))).filter((u): u is string => !!u)
|
|
346
|
+
const optionTitle = r.variant_option || "Default option"
|
|
347
|
+
const values = r.variant_values ? r.variant_values.split("|").map((v) => v.trim()) : ["Default option value"]
|
|
348
|
+
const boutiques = r.boutiques ? r.boutiques.split("|").map((s) => s.trim()) : Object.keys(channelIds)
|
|
349
|
+
|
|
350
|
+
const prices = Object.keys(r)
|
|
351
|
+
.filter((k) => /^price_[a-z]{3}$/.test(k) && r[k])
|
|
352
|
+
.map((k) => {
|
|
353
|
+
const currency_code = k.slice(6)
|
|
354
|
+
const price = Number(r[k])
|
|
355
|
+
const compareAt = Number(r[`compare_at_price_${currency_code}`] || 0)
|
|
356
|
+
// Prix barré : le prix de base est le prix « avant », le prix réel passe par une price list « sale ».
|
|
357
|
+
if (compareAt > price) {
|
|
358
|
+
for (const v of values) salePrices.push({ variantKey: `${r.handle}::${v}`, currency_code, amount: price })
|
|
359
|
+
return { currency_code, amount: compareAt }
|
|
360
|
+
}
|
|
361
|
+
return { currency_code, amount: price }
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
products.push({
|
|
365
|
+
title: r.title,
|
|
366
|
+
handle: r.handle,
|
|
367
|
+
description: r.description,
|
|
368
|
+
status: ProductStatus.PUBLISHED,
|
|
369
|
+
shipping_profile_id: profileId,
|
|
370
|
+
thumbnail: images[0],
|
|
371
|
+
images: images.map((url) => ({ url })),
|
|
372
|
+
options: [{ title: optionTitle, values }],
|
|
373
|
+
variants: values.map((v) => ({
|
|
374
|
+
title: v,
|
|
375
|
+
sku: `${r.handle}-${v}`.toUpperCase().replace(/[^A-Z0-9]+/g, "-"),
|
|
376
|
+
options: { [optionTitle]: v },
|
|
377
|
+
manage_inventory: true,
|
|
378
|
+
prices,
|
|
379
|
+
})),
|
|
380
|
+
sales_channels: boutiques.filter((s) => channelIds[s]).map((s) => ({ id: channelIds[s] })),
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const { result: created } = await createProductsWorkflow(container).run({ input: { products } })
|
|
385
|
+
logger.info(`[seed] ${created.length} produit(s) importé(s)`)
|
|
386
|
+
|
|
387
|
+
// Stock initial (colonne stock, 100 par défaut)
|
|
388
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
389
|
+
const { data: variants } = await query.graph({
|
|
390
|
+
entity: "product_variant",
|
|
391
|
+
fields: ["id", "title", "product.handle", "inventory_items.inventory_item_id"],
|
|
392
|
+
filters: { product_id: created.map((p) => p.id) },
|
|
393
|
+
})
|
|
394
|
+
const stockOf = new Map(toCreate.map((r) => [r.handle, Number(r.stock || 100)]))
|
|
395
|
+
const levels: CreateInventoryLevelInput[] = variants.flatMap((v) =>
|
|
396
|
+
(v.inventory_items ?? []).map((i) => ({ location_id: locationId, inventory_item_id: i!.inventory_item_id, stocked_quantity: stockOf.get(v.product!.handle) ?? 100 }))
|
|
397
|
+
)
|
|
398
|
+
if (levels.length) await createInventoryLevelsWorkflow(container).run({ input: { inventory_levels: levels } })
|
|
399
|
+
|
|
400
|
+
// Prix promo (compare_at_price_*) : une price list « sale » pour les produits importés
|
|
401
|
+
if (salePrices.length) {
|
|
402
|
+
const variantId = new Map(variants.map((v) => [`${v.product!.handle}::${v.title}`, v.id]))
|
|
403
|
+
await createPriceListsWorkflow(container).run({
|
|
404
|
+
input: {
|
|
405
|
+
price_lists_data: [
|
|
406
|
+
{
|
|
407
|
+
title: `Promotions (import ${new Date().toISOString().slice(0, 10)})`,
|
|
408
|
+
description: "Prix barrés importés depuis seed/products.csv",
|
|
409
|
+
status: "active",
|
|
410
|
+
prices: salePrices
|
|
411
|
+
.filter((p) => variantId.has(p.variantKey))
|
|
412
|
+
.map((p) => ({ variant_id: variantId.get(p.variantKey)!, currency_code: p.currency_code, amount: p.amount })),
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stockage des images (RustFS, compatible S3) : crée le bucket s'il manque et le rend
|
|
3
|
+
* lisible publiquement (lecture seule ; l'écriture exige les identifiants du backend).
|
|
4
|
+
* Idempotent, lancé par docker/entrypoint.sh avant le seed. Sans S3_ENDPOINT : rien à faire.
|
|
5
|
+
*/
|
|
6
|
+
import { ExecArgs } from "@medusajs/framework/types"
|
|
7
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
8
|
+
import {
|
|
9
|
+
CreateBucketCommand,
|
|
10
|
+
HeadBucketCommand,
|
|
11
|
+
PutBucketPolicyCommand,
|
|
12
|
+
S3Client,
|
|
13
|
+
} from "@aws-sdk/client-s3"
|
|
14
|
+
|
|
15
|
+
export default async function storage({ container }: ExecArgs) {
|
|
16
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
|
17
|
+
const endpoint = process.env.S3_ENDPOINT
|
|
18
|
+
if (!endpoint) {
|
|
19
|
+
logger.info("[storage] S3_ENDPOINT absent : stockage local, rien à préparer")
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const bucket = process.env.S3_BUCKET || "medusa"
|
|
24
|
+
const s3 = new S3Client({
|
|
25
|
+
endpoint,
|
|
26
|
+
region: process.env.S3_REGION || "us-east-1",
|
|
27
|
+
forcePathStyle: true,
|
|
28
|
+
credentials: {
|
|
29
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
|
|
30
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Le stockage peut démarrer un peu après le backend : quelques essais avant d'abandonner.
|
|
35
|
+
for (let attempt = 1; ; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
await s3.send(new HeadBucketCommand({ Bucket: bucket }))
|
|
38
|
+
break
|
|
39
|
+
} catch (e) {
|
|
40
|
+
const status = (e as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode
|
|
41
|
+
if (status === 404) {
|
|
42
|
+
await s3.send(new CreateBucketCommand({ Bucket: bucket }))
|
|
43
|
+
logger.info(`[storage] bucket ${bucket} créé`)
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
if (attempt >= 20) throw e
|
|
47
|
+
await new Promise((r) => setTimeout(r, 2000))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await s3.send(
|
|
52
|
+
new PutBucketPolicyCommand({
|
|
53
|
+
Bucket: bucket,
|
|
54
|
+
Policy: JSON.stringify({
|
|
55
|
+
Version: "2012-10-17",
|
|
56
|
+
Statement: [
|
|
57
|
+
{ Effect: "Allow", Principal: { AWS: ["*"] }, Action: ["s3:GetObject"], Resource: [`arn:aws:s3:::${bucket}/*`] },
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
logger.info(`[storage] bucket ${bucket} : lecture publique des fichiers`)
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Custom subscribers
|
|
2
|
+
|
|
3
|
+
Subscribers handle events emitted in the Medusa application.
|
|
4
|
+
|
|
5
|
+
> Learn more about Subscribers in [this documentation](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers).
|
|
6
|
+
|
|
7
|
+
The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
|
|
8
|
+
|
|
9
|
+
For example, create the file `src/subscribers/product-created.ts` with the following content:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
type SubscriberConfig,
|
|
14
|
+
} from "@medusajs/framework"
|
|
15
|
+
|
|
16
|
+
// subscriber function
|
|
17
|
+
export default async function productCreateHandler() {
|
|
18
|
+
console.log("A product was created")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// subscriber config
|
|
22
|
+
export const config: SubscriberConfig = {
|
|
23
|
+
event: "product.created",
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A subscriber file must export:
|
|
28
|
+
|
|
29
|
+
- The subscriber function that is an asynchronous function executed whenever the associated event is triggered.
|
|
30
|
+
- A configuration object defining the event this subscriber is listening to.
|
|
31
|
+
|
|
32
|
+
## Subscriber Parameters
|
|
33
|
+
|
|
34
|
+
A subscriber receives an object having the following properties:
|
|
35
|
+
|
|
36
|
+
- `event`: An object holding the event's details. It has a `data` property, which is the event's data payload.
|
|
37
|
+
- `container`: The Medusa container. Use it to resolve modules' main services and other registered resources.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import type {
|
|
41
|
+
SubscriberArgs,
|
|
42
|
+
SubscriberConfig,
|
|
43
|
+
} from "@medusajs/framework"
|
|
44
|
+
|
|
45
|
+
export default async function productCreateHandler({
|
|
46
|
+
event: { data },
|
|
47
|
+
container,
|
|
48
|
+
}: SubscriberArgs<{ id: string }>) {
|
|
49
|
+
const productId = data.id
|
|
50
|
+
|
|
51
|
+
const productModuleService = container.resolve("product")
|
|
52
|
+
|
|
53
|
+
const product = await productModuleService.retrieveProduct(productId)
|
|
54
|
+
|
|
55
|
+
console.log(`The product ${product.title} was created`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const config: SubscriberConfig = {
|
|
59
|
+
event: "product.created",
|
|
60
|
+
}
|
|
61
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
|
|
2
|
+
import { Modules } from "@medusajs/framework/utils"
|
|
3
|
+
import { adminUrl, sendAccountEmail } from "../lib/notify"
|
|
4
|
+
|
|
5
|
+
type Payload = { entity_id?: string; actor_type?: string; token?: string; id?: string }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mot de passe oublié (admin) et invitation d'un membre de l'équipe.
|
|
9
|
+
* Medusa crée le jeton et émet l'événement ; ce subscriber envoie l'e-mail avec le lien.
|
|
10
|
+
*/
|
|
11
|
+
export default async function accountEmails({ event: { name, data }, container }: SubscriberArgs<Payload>) {
|
|
12
|
+
if (name === "auth.password_reset") {
|
|
13
|
+
// Les comptes clients des boutiques n'ont pas de page de réinitialisation : admin uniquement.
|
|
14
|
+
if (data.actor_type !== "user" || !data.entity_id || !data.token) return
|
|
15
|
+
await sendAccountEmail(container, data.entity_id, "password_reset", `${adminUrl()}/app/reset-password?token=${encodeURIComponent(data.token)}`)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if ((name === "invite.created" || name === "invite.resent") && data.id) {
|
|
20
|
+
const invite = await container.resolve(Modules.USER).retrieveInvite(data.id)
|
|
21
|
+
await sendAccountEmail(container, invite.email, "team_invite", `${adminUrl()}/app/invite?token=${encodeURIComponent(invite.token)}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const config: SubscriberConfig = {
|
|
26
|
+
event: ["auth.password_reset", "invite.created", "invite.resent"],
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
|
|
2
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
3
|
+
import { emailLabel } from "../lib/emails"
|
|
4
|
+
import { sendOrderEmail } from "../lib/notify"
|
|
5
|
+
import { readProject } from "../lib/project"
|
|
6
|
+
|
|
7
|
+
/** Expédition créée (colis en route, avec suivi) ou livraison confirmée. */
|
|
8
|
+
export default async function fulfillmentEmails({
|
|
9
|
+
event: { name, data },
|
|
10
|
+
container,
|
|
11
|
+
}: SubscriberArgs<{ id: string; no_notification?: boolean }>) {
|
|
12
|
+
if (data.no_notification) return
|
|
13
|
+
|
|
14
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
15
|
+
const { data: fulfillments } = await query.graph({
|
|
16
|
+
entity: "fulfillment",
|
|
17
|
+
fields: ["id", "labels.tracking_number", "labels.tracking_url", "order.id"],
|
|
18
|
+
filters: { id: data.id },
|
|
19
|
+
})
|
|
20
|
+
const f = fulfillments[0] as
|
|
21
|
+
| { order?: { id: string }; labels?: { tracking_number?: string; tracking_url?: string }[] }
|
|
22
|
+
| undefined
|
|
23
|
+
const orderId = f?.order?.id
|
|
24
|
+
if (!orderId) return
|
|
25
|
+
|
|
26
|
+
if (name === "delivery.created") {
|
|
27
|
+
await sendOrderEmail(container, orderId, "order_delivered")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const label = f.labels?.[0]
|
|
32
|
+
const language = readProject().language
|
|
33
|
+
const trackingUrl = label?.tracking_url && /^https?:\/\//.test(label.tracking_url) ? label.tracking_url : undefined
|
|
34
|
+
await sendOrderEmail(container, orderId, "order_shipped", {
|
|
35
|
+
extra: label?.tracking_number ? emailLabel(language, "order_shipped", "tracking").replace("{code}", label.tracking_number) : undefined,
|
|
36
|
+
button: trackingUrl ? { label: emailLabel(language, "order_shipped", "track"), url: trackingUrl } : undefined,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const config: SubscriberConfig = {
|
|
41
|
+
event: ["shipment.created", "delivery.created"],
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
|
|
2
|
+
import { sendOrderEmail } from "../lib/notify"
|
|
3
|
+
|
|
4
|
+
/** Commande passée / annulée : e-mail au client (+ au marchand pour une nouvelle commande). */
|
|
5
|
+
export default async function orderEmails({ event: { name, data }, container }: SubscriberArgs<{ id: string }>) {
|
|
6
|
+
if (name === "order.placed") await sendOrderEmail(container, data.id, "order_placed", { withItems: true })
|
|
7
|
+
if (name === "order.canceled") await sendOrderEmail(container, data.id, "order_canceled")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const config: SubscriberConfig = {
|
|
11
|
+
event: ["order.placed", "order.canceled"],
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
|
|
2
|
+
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
3
|
+
import { sendOrderEmail } from "../lib/notify"
|
|
4
|
+
import { readProject } from "../lib/project"
|
|
5
|
+
|
|
6
|
+
type Payload = { id?: string; order_id?: string; return_id?: string }
|
|
7
|
+
|
|
8
|
+
/** Retour demandé / reçu, remboursement effectué. */
|
|
9
|
+
export default async function returnRefundEmails({ event: { name, data }, container }: SubscriberArgs<Payload>) {
|
|
10
|
+
if (name === "order.return_requested" && data.order_id) {
|
|
11
|
+
await sendOrderEmail(container, data.order_id, "return_requested")
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
if (name === "order.return_received" && data.order_id) {
|
|
15
|
+
await sendOrderEmail(container, data.order_id, "return_received")
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if (name !== "payment.refunded" || !data.id) return
|
|
19
|
+
|
|
20
|
+
// Paiement → collection de paiement → commande ; montant = dernier remboursement.
|
|
21
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
22
|
+
const { data: payments } = await query.graph({
|
|
23
|
+
entity: "payment",
|
|
24
|
+
fields: ["id", "currency_code", "refunds.amount", "refunds.created_at", "payment_collection.order.id"],
|
|
25
|
+
filters: { id: data.id },
|
|
26
|
+
})
|
|
27
|
+
const p = payments[0] as
|
|
28
|
+
| {
|
|
29
|
+
currency_code: string
|
|
30
|
+
refunds?: { amount: number; created_at: string }[]
|
|
31
|
+
payment_collection?: { order?: { id: string } }
|
|
32
|
+
}
|
|
33
|
+
| undefined
|
|
34
|
+
const orderId = p?.payment_collection?.order?.id
|
|
35
|
+
if (!orderId) return
|
|
36
|
+
|
|
37
|
+
const last = [...(p.refunds ?? [])].sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))[0]
|
|
38
|
+
const project = readProject()
|
|
39
|
+
const amount = last
|
|
40
|
+
? new Intl.NumberFormat(`${project.language}-${project.boutiques[0].country.toUpperCase()}`, {
|
|
41
|
+
style: "currency",
|
|
42
|
+
currency: p.currency_code,
|
|
43
|
+
}).format(Number(last.amount))
|
|
44
|
+
: ""
|
|
45
|
+
await sendOrderEmail(container, orderId, "refund", { vars: { amount } })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const config: SubscriberConfig = {
|
|
49
|
+
event: ["order.return_requested", "order.return_received", "payment.refunded"],
|
|
50
|
+
}
|