@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,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"order_placed": {
|
|
3
|
+
"subject": "Commande n° {number} confirmée",
|
|
4
|
+
"title": "Merci pour votre commande !",
|
|
5
|
+
"intro": "Bonjour {name}, nous avons bien reçu votre commande n° {number}. En voici le récapitulatif."
|
|
6
|
+
},
|
|
7
|
+
"order_shipped": {
|
|
8
|
+
"subject": "Votre commande n° {number} est en route",
|
|
9
|
+
"title": "Votre colis est en route",
|
|
10
|
+
"intro": "Bonne nouvelle {name} : votre commande n° {number} a été expédiée.",
|
|
11
|
+
"tracking": "Numéro de suivi : {code}",
|
|
12
|
+
"track": "Suivre mon colis"
|
|
13
|
+
},
|
|
14
|
+
"order_delivered": {
|
|
15
|
+
"subject": "Votre commande n° {number} est livrée",
|
|
16
|
+
"title": "Votre commande est livrée",
|
|
17
|
+
"intro": "Bonjour {name}, votre commande n° {number} a été livrée. Nous espérons qu'elle vous plaira !"
|
|
18
|
+
},
|
|
19
|
+
"order_canceled": {
|
|
20
|
+
"subject": "Votre commande n° {number} est annulée",
|
|
21
|
+
"title": "Commande annulée",
|
|
22
|
+
"intro": "Bonjour {name}, votre commande n° {number} a été annulée. Si vous avez été débité, vous serez remboursé."
|
|
23
|
+
},
|
|
24
|
+
"refund": {
|
|
25
|
+
"subject": "Remboursement de votre commande n° {number}",
|
|
26
|
+
"title": "Remboursement effectué",
|
|
27
|
+
"intro": "Bonjour {name}, nous vous avons remboursé {amount} pour la commande n° {number}. Le montant peut mettre quelques jours à apparaître sur votre compte."
|
|
28
|
+
},
|
|
29
|
+
"return_requested": {
|
|
30
|
+
"subject": "Retour demandé pour la commande n° {number}",
|
|
31
|
+
"title": "Votre demande de retour",
|
|
32
|
+
"intro": "Bonjour {name}, nous avons enregistré un retour pour votre commande n° {number}. Merci de renvoyer les articles dans leur emballage d'origine."
|
|
33
|
+
},
|
|
34
|
+
"return_received": {
|
|
35
|
+
"subject": "Retour reçu pour la commande n° {number}",
|
|
36
|
+
"title": "Retour reçu",
|
|
37
|
+
"intro": "Bonjour {name}, nous avons reçu les articles retournés de la commande n° {number}. Votre remboursement est en cours."
|
|
38
|
+
},
|
|
39
|
+
"merchant_new_order": {
|
|
40
|
+
"subject": "Nouvelle commande n° {number} — {total}",
|
|
41
|
+
"title": "Nouvelle commande sur {store}",
|
|
42
|
+
"intro": "Commande n° {number} de {email}, total {total}."
|
|
43
|
+
},
|
|
44
|
+
"common": {
|
|
45
|
+
"item": "Article",
|
|
46
|
+
"qty": "Qté",
|
|
47
|
+
"total": "Total",
|
|
48
|
+
"shippingTo": "Livraison à",
|
|
49
|
+
"questions": "Une question ? Répondez simplement à cet e-mail.",
|
|
50
|
+
"customer": "client"
|
|
51
|
+
},
|
|
52
|
+
"password_reset": {
|
|
53
|
+
"subject": "Réinitialiser votre mot de passe {store}",
|
|
54
|
+
"title": "Réinitialiser votre mot de passe",
|
|
55
|
+
"intro": "Nous avons reçu une demande de réinitialisation du mot de passe de votre compte admin {store}. Le lien est valable pendant une durée limitée. Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail.",
|
|
56
|
+
"button": "Choisir un nouveau mot de passe"
|
|
57
|
+
},
|
|
58
|
+
"team_invite": {
|
|
59
|
+
"subject": "Invitation à l'admin {store}",
|
|
60
|
+
"title": "Rejoindre l'équipe {store}",
|
|
61
|
+
"intro": "Vous êtes invité à gérer les boutiques {store}. Cliquez sur le bouton pour créer votre compte.",
|
|
62
|
+
"button": "Accepter l'invitation"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Custom scheduled jobs
|
|
2
|
+
|
|
3
|
+
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
|
|
4
|
+
|
|
5
|
+
> Learn more about scheduled jobs in [this documentation](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs).
|
|
6
|
+
|
|
7
|
+
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
|
|
8
|
+
|
|
9
|
+
For example, create the file `src/jobs/hello-world.ts` with the following content:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
MedusaContainer
|
|
14
|
+
} from "@medusajs/framework/types";
|
|
15
|
+
|
|
16
|
+
export default async function myCustomJob(container: MedusaContainer) {
|
|
17
|
+
const productService = container.resolve("product")
|
|
18
|
+
|
|
19
|
+
const products = await productService.listAndCountProducts();
|
|
20
|
+
|
|
21
|
+
// Do something with the products
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const config = {
|
|
25
|
+
name: "daily-product-report",
|
|
26
|
+
schedule: "0 0 * * *", // Every day at midnight
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A scheduled job file must export:
|
|
31
|
+
|
|
32
|
+
- The function to be executed whenever it’s time to run the scheduled job.
|
|
33
|
+
- A configuration object defining the job. It has three properties:
|
|
34
|
+
- `name`: a unique name for the job.
|
|
35
|
+
- `schedule`: a [cron expression](https://crontab.guru/).
|
|
36
|
+
- `numberOfExecutions`: an optional integer, specifying how many times the job will execute before being removed
|
|
37
|
+
|
|
38
|
+
The `handler` is a function that accepts one parameter, `container`, which is a `MedusaContainer` instance used to resolve services.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import en from "../emails/en.json"
|
|
2
|
+
import fr from "../emails/fr.json"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rendu des e-mails : un texte par type et par langue (src/emails/<lang>.json),
|
|
6
|
+
* aux couleurs du thème de la boutique d'où vient la commande.
|
|
7
|
+
* Toute valeur insérée est échappée : jamais de HTML venant d'une saisie.
|
|
8
|
+
*/
|
|
9
|
+
export type EmailType =
|
|
10
|
+
| "order_placed"
|
|
11
|
+
| "order_shipped"
|
|
12
|
+
| "order_delivered"
|
|
13
|
+
| "order_canceled"
|
|
14
|
+
| "refund"
|
|
15
|
+
| "return_requested"
|
|
16
|
+
| "return_received"
|
|
17
|
+
| "merchant_new_order"
|
|
18
|
+
| "password_reset"
|
|
19
|
+
| "team_invite"
|
|
20
|
+
|
|
21
|
+
const DICTS: Record<string, typeof en> = { en, fr }
|
|
22
|
+
|
|
23
|
+
// Couleurs principales des thèmes (design/tokens.css). Fond clair pour tous : lisible dans toutes les messageries.
|
|
24
|
+
const THEMES: Record<string, { primary: string; onPrimary: string }> = {
|
|
25
|
+
halloween: { primary: "#ff7a1a", onPrimary: "#0e0c14" },
|
|
26
|
+
thanksgiving: { primary: "#c2410c", onPrimary: "#ffffff" },
|
|
27
|
+
"black-friday": { primary: "#f97316", onPrimary: "#09090b" },
|
|
28
|
+
"cyber-monday": { primary: "#4a2bd6", onPrimary: "#ffffff" },
|
|
29
|
+
noel: { primary: "#b91c1c", onPrimary: "#ffffff" },
|
|
30
|
+
"nouvel-an": { primary: "#d9b54a", onPrimary: "#111018" },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const escapeHtml = (s: unknown) =>
|
|
34
|
+
String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!)
|
|
35
|
+
|
|
36
|
+
const fill = (text: string, vars: Record<string, string | number | undefined>) =>
|
|
37
|
+
text.replace(/\{(\w+)\}/g, (_, k: string) => String(vars[k] ?? ""))
|
|
38
|
+
|
|
39
|
+
export type EmailData = {
|
|
40
|
+
storeName: string
|
|
41
|
+
theme: string
|
|
42
|
+
language: string
|
|
43
|
+
vars: Record<string, string | number | undefined>
|
|
44
|
+
items?: { title: string; quantity: number; total: string }[]
|
|
45
|
+
total?: string
|
|
46
|
+
address?: string
|
|
47
|
+
button?: { label: string; url: string }
|
|
48
|
+
extra?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderEmail(type: EmailType, d: EmailData) {
|
|
52
|
+
const dict = DICTS[d.language] ?? en
|
|
53
|
+
const t = dict[type] as Record<string, string>
|
|
54
|
+
const c = dict.common
|
|
55
|
+
const color = THEMES[d.theme] ?? THEMES["black-friday"]
|
|
56
|
+
const vars = { ...d.vars, store: d.storeName }
|
|
57
|
+
|
|
58
|
+
const subject = fill(t.subject, vars)
|
|
59
|
+
const title = fill(t.title, vars)
|
|
60
|
+
const intro = fill(t.intro, vars)
|
|
61
|
+
|
|
62
|
+
const rows = (d.items ?? [])
|
|
63
|
+
.map(
|
|
64
|
+
(i) =>
|
|
65
|
+
`<tr><td style="padding:8px 0;border-bottom:1px solid #eee">${escapeHtml(i.title)}</td>` +
|
|
66
|
+
`<td style="padding:8px 0;border-bottom:1px solid #eee;text-align:center">${escapeHtml(i.quantity)}</td>` +
|
|
67
|
+
`<td style="padding:8px 0;border-bottom:1px solid #eee;text-align:right">${escapeHtml(i.total)}</td></tr>`
|
|
68
|
+
)
|
|
69
|
+
.join("")
|
|
70
|
+
|
|
71
|
+
const html = `<!doctype html><html lang="${escapeHtml(d.language)}"><body style="margin:0;background:#f4f4f5;font-family:Arial,Helvetica,sans-serif;color:#18181b">
|
|
72
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="padding:24px 12px"><tr><td align="center">
|
|
73
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:#ffffff;border-radius:12px;overflow:hidden">
|
|
74
|
+
<tr><td style="background:${color.primary};color:${color.onPrimary};padding:20px 28px;font-size:18px;font-weight:bold">${escapeHtml(d.storeName)}</td></tr>
|
|
75
|
+
<tr><td style="padding:28px">
|
|
76
|
+
<h1 style="margin:0 0 12px;font-size:22px">${escapeHtml(title)}</h1>
|
|
77
|
+
<p style="margin:0 0 20px;line-height:1.5">${escapeHtml(intro)}</p>
|
|
78
|
+
${d.extra ? `<p style="margin:0 0 20px;line-height:1.5">${escapeHtml(d.extra)}</p>` : ""}
|
|
79
|
+
${rows ? `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:14px;margin-bottom:16px"><tr style="color:#71717a"><td>${escapeHtml(c.item)}</td><td style="text-align:center">${escapeHtml(c.qty)}</td><td style="text-align:right">${escapeHtml(c.total)}</td></tr>${rows}</table>` : ""}
|
|
80
|
+
${d.total ? `<p style="margin:0 0 20px;font-size:16px;text-align:right"><strong>${escapeHtml(c.total)} : ${escapeHtml(d.total)}</strong></p>` : ""}
|
|
81
|
+
${d.address ? `<p style="margin:0 0 20px;font-size:14px;color:#52525b">${escapeHtml(c.shippingTo)} : ${escapeHtml(d.address)}</p>` : ""}
|
|
82
|
+
${d.button ? `<p style="margin:0 0 20px"><a href="${escapeHtml(d.button.url)}" style="display:inline-block;background:${color.primary};color:${color.onPrimary};padding:12px 20px;border-radius:999px;text-decoration:none;font-weight:bold">${escapeHtml(d.button.label)}</a></p>` : ""}
|
|
83
|
+
<p style="margin:24px 0 0;font-size:13px;color:#71717a">${escapeHtml(c.questions)}</p>
|
|
84
|
+
</td></tr></table></td></tr></table></body></html>`
|
|
85
|
+
|
|
86
|
+
const text = [title, intro, d.extra, ...(d.items ?? []).map((i) => `- ${i.title} x${i.quantity} : ${i.total}`), d.total && `${c.total} : ${d.total}`, d.address && `${c.shippingTo} : ${d.address}`, d.button && `${d.button.label} : ${d.button.url}`, c.questions]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join("\n")
|
|
89
|
+
|
|
90
|
+
return { subject, html, text, button: t.track }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const emailLabel = (language: string, type: EmailType, key: string) =>
|
|
94
|
+
((DICTS[language] ?? en)[type] as Record<string, string>)[key] ?? ""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { MedusaContainer } from "@medusajs/framework/types"
|
|
2
|
+
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
|
3
|
+
import { type EmailType, renderEmail, emailLabel } from "./emails"
|
|
4
|
+
import { readProject } from "./project"
|
|
5
|
+
|
|
6
|
+
type Options = {
|
|
7
|
+
/** Texte en plus sous l'introduction (ex. numéro de suivi) */
|
|
8
|
+
extra?: string
|
|
9
|
+
/** Bouton (ex. lien de suivi du colis) */
|
|
10
|
+
button?: { label: string; url: string }
|
|
11
|
+
/** Variables en plus (ex. montant remboursé) */
|
|
12
|
+
vars?: Record<string, string>
|
|
13
|
+
/** Envoyer le récapitulatif des articles */
|
|
14
|
+
withItems?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let warned = false
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* E-mail au client d'une commande (et au marchand pour une nouvelle commande).
|
|
21
|
+
* Langue, nom et couleurs : ceux de la boutique d'où vient la commande (sales channel → project.json).
|
|
22
|
+
* Sans RESEND_API_KEY : rien n'est envoyé (la boutique fonctionne quand même, `check` le signale).
|
|
23
|
+
* Une erreur d'envoi est journalisée mais ne bloque jamais la commande.
|
|
24
|
+
*/
|
|
25
|
+
export async function sendOrderEmail(container: MedusaContainer, orderId: string, type: EmailType, opts: Options = {}) {
|
|
26
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
|
27
|
+
if (!process.env.RESEND_API_KEY) {
|
|
28
|
+
if (!warned) logger.warn("[emails] RESEND_API_KEY absente : aucun e-mail n'est envoyé")
|
|
29
|
+
warned = true
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
35
|
+
const { data: orders } = await query.graph({
|
|
36
|
+
entity: "order",
|
|
37
|
+
fields: [
|
|
38
|
+
"id",
|
|
39
|
+
"display_id",
|
|
40
|
+
"email",
|
|
41
|
+
"currency_code",
|
|
42
|
+
"total",
|
|
43
|
+
"sales_channel_id",
|
|
44
|
+
"items.product_title",
|
|
45
|
+
"items.variant_title",
|
|
46
|
+
"items.quantity",
|
|
47
|
+
"items.total",
|
|
48
|
+
"shipping_address.*",
|
|
49
|
+
],
|
|
50
|
+
filters: { id: orderId },
|
|
51
|
+
})
|
|
52
|
+
const order = orders[0]
|
|
53
|
+
if (!order?.email) return
|
|
54
|
+
|
|
55
|
+
const { data: channels } = await query.graph({ entity: "sales_channel", fields: ["name"], filters: { id: order.sales_channel_id! } })
|
|
56
|
+
const project = readProject()
|
|
57
|
+
const boutique = project.boutiques.find((b) => b.slug === channels[0]?.name) ?? project.boutiques[0]
|
|
58
|
+
const locale = `${project.language}-${boutique.country.toUpperCase()}`
|
|
59
|
+
const money = (n: unknown) => new Intl.NumberFormat(locale, { style: "currency", currency: order.currency_code }).format(Number(n ?? 0))
|
|
60
|
+
|
|
61
|
+
const a = order.shipping_address
|
|
62
|
+
const address = a ? [a.address_1, a.postal_code, a.city, a.country_code?.toUpperCase()].filter(Boolean).join(", ") : undefined
|
|
63
|
+
const vars = {
|
|
64
|
+
number: order.display_id ?? "",
|
|
65
|
+
name: a?.first_name || emailLabel(project.language, "order_placed", "customer") || "",
|
|
66
|
+
total: money(order.total),
|
|
67
|
+
email: order.email,
|
|
68
|
+
...opts.vars,
|
|
69
|
+
}
|
|
70
|
+
const items = (order.items ?? []).filter(Boolean).map((i) => ({
|
|
71
|
+
title: [i!.product_title, i!.variant_title].filter(Boolean).join(" — "),
|
|
72
|
+
quantity: Number(i!.quantity),
|
|
73
|
+
total: money(i!.total),
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
const base = { storeName: boutique.storeName, theme: boutique.theme, language: project.language, vars }
|
|
77
|
+
const customer = renderEmail(type, {
|
|
78
|
+
...base,
|
|
79
|
+
items: opts.withItems ? items : undefined,
|
|
80
|
+
total: opts.withItems ? money(order.total) : undefined,
|
|
81
|
+
address: opts.withItems ? address : undefined,
|
|
82
|
+
extra: opts.extra,
|
|
83
|
+
button: opts.button,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const notifications = container.resolve(Modules.NOTIFICATION)
|
|
87
|
+
const common = { channel: "email", resource_id: order.id, resource_type: "order", trigger_type: type }
|
|
88
|
+
const data = { fromName: boutique.storeName, replyTo: project.merchantEmail }
|
|
89
|
+
|
|
90
|
+
await notifications.createNotifications({
|
|
91
|
+
...common,
|
|
92
|
+
to: order.email,
|
|
93
|
+
template: type,
|
|
94
|
+
content: { subject: customer.subject, html: customer.html, text: customer.text },
|
|
95
|
+
data,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (type === "order_placed" && project.merchantEmail) {
|
|
99
|
+
const merchant = renderEmail("merchant_new_order", { ...base, items, total: money(order.total), address })
|
|
100
|
+
await notifications.createNotifications({
|
|
101
|
+
...common,
|
|
102
|
+
to: project.merchantEmail,
|
|
103
|
+
template: "merchant_new_order",
|
|
104
|
+
content: { subject: merchant.subject, html: merchant.html, text: merchant.text },
|
|
105
|
+
data: { fromName: boutique.storeName, replyTo: order.email },
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
logger.error(`[emails] ${type} pour la commande ${orderId} : ${(e as Error).message}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Adresse publique de l'admin (liens des e-mails) : ADMIN_URL, sinon la 1re origine d'ADMIN_CORS. */
|
|
114
|
+
export const adminUrl = () => (process.env.ADMIN_URL || process.env.ADMIN_CORS?.split(",")[0] || "http://localhost:9000").replace(/\/$/, "")
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* E-mails de compte admin (mot de passe oublié, invitation d'équipe) : un bouton vers l'admin.
|
|
118
|
+
* Sans RESEND_API_KEY : rien n'est envoyé (le lien reste visible dans les logs du backend en dev).
|
|
119
|
+
*/
|
|
120
|
+
export async function sendAccountEmail(container: MedusaContainer, to: string, type: "password_reset" | "team_invite", url: string) {
|
|
121
|
+
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
|
122
|
+
if (!process.env.RESEND_API_KEY) {
|
|
123
|
+
logger.warn(`[emails] RESEND_API_KEY absente : e-mail « ${type} » non envoyé à ${to}`)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const project = readProject()
|
|
128
|
+
const first = project.boutiques[0]
|
|
129
|
+
const storeName = first.storeName
|
|
130
|
+
const email = renderEmail(type, {
|
|
131
|
+
storeName,
|
|
132
|
+
theme: first.theme,
|
|
133
|
+
language: project.language,
|
|
134
|
+
vars: {},
|
|
135
|
+
button: { label: emailLabel(project.language, type, "button"), url },
|
|
136
|
+
})
|
|
137
|
+
await container.resolve(Modules.NOTIFICATION).createNotifications({
|
|
138
|
+
to,
|
|
139
|
+
channel: "email",
|
|
140
|
+
template: type,
|
|
141
|
+
trigger_type: type,
|
|
142
|
+
content: { subject: email.subject, html: email.html, text: email.text },
|
|
143
|
+
data: { fromName: storeName, replyTo: project.merchantEmail },
|
|
144
|
+
})
|
|
145
|
+
} catch (e) {
|
|
146
|
+
logger.error(`[emails] ${type} pour ${to} : ${(e as Error).message}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { z } from "@medusajs/framework/zod"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lecture de config/project.json côté backend (même fichier que le storefront).
|
|
7
|
+
* Seuls les champs utiles au backend sont validés ici ; le storefront valide le reste.
|
|
8
|
+
*/
|
|
9
|
+
export const CONFIG_DIR = process.env.CONFIG_DIR || "/app/config"
|
|
10
|
+
export const SEED_DIR = process.env.SEED_DIR || "/app/seed"
|
|
11
|
+
export const KEYS_DIR = process.env.KEYS_DIR || "/app/keys"
|
|
12
|
+
|
|
13
|
+
const code2 = z.string().regex(/^[a-z]{2}$/)
|
|
14
|
+
const code3 = z.string().regex(/^[a-z]{3}$/)
|
|
15
|
+
const shipsTo = z.union([z.literal("all"), z.array(code2).min(1)])
|
|
16
|
+
|
|
17
|
+
const projectSchema = z.object({
|
|
18
|
+
project: z.string(),
|
|
19
|
+
defaults: z.looseObject({
|
|
20
|
+
storeName: z.string(),
|
|
21
|
+
currency: code3,
|
|
22
|
+
country: code2,
|
|
23
|
+
language: z.string().default("en"),
|
|
24
|
+
shipsTo: shipsTo.default("all"),
|
|
25
|
+
merchantEmail: z.email().optional(),
|
|
26
|
+
}),
|
|
27
|
+
boutiques: z
|
|
28
|
+
.array(
|
|
29
|
+
z.looseObject({
|
|
30
|
+
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
31
|
+
theme: z.string(),
|
|
32
|
+
port: z.number(),
|
|
33
|
+
storeName: z.string().optional(),
|
|
34
|
+
currency: code3.optional(),
|
|
35
|
+
country: code2.optional(),
|
|
36
|
+
shipsTo: shipsTo.optional(),
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
.min(1),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export type Boutique = {
|
|
43
|
+
slug: string
|
|
44
|
+
theme: string
|
|
45
|
+
storeName: string
|
|
46
|
+
currency: string
|
|
47
|
+
country: string
|
|
48
|
+
shipsTo: "all" | string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type Project = {
|
|
52
|
+
project: string
|
|
53
|
+
defaultCurrency: string
|
|
54
|
+
language: string
|
|
55
|
+
merchantEmail?: string
|
|
56
|
+
boutiques: Boutique[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readProject(): Project {
|
|
60
|
+
const file = path.join(CONFIG_DIR, "project.json")
|
|
61
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"))
|
|
62
|
+
const res = projectSchema.safeParse(raw)
|
|
63
|
+
if (!res.success) {
|
|
64
|
+
throw new Error(`${file} invalide :\n${z.prettifyError(res.error)}`)
|
|
65
|
+
}
|
|
66
|
+
const { defaults, boutiques } = res.data
|
|
67
|
+
return {
|
|
68
|
+
project: res.data.project,
|
|
69
|
+
defaultCurrency: defaults.currency,
|
|
70
|
+
language: defaults.language,
|
|
71
|
+
merchantEmail: defaults.merchantEmail,
|
|
72
|
+
boutiques: boutiques.map((b) => ({
|
|
73
|
+
slug: b.slug,
|
|
74
|
+
theme: b.theme,
|
|
75
|
+
storeName: b.storeName ?? defaults.storeName,
|
|
76
|
+
currency: b.currency ?? defaults.currency,
|
|
77
|
+
country: b.country ?? defaults.country,
|
|
78
|
+
shipsTo: b.shipsTo ?? defaults.shipsTo,
|
|
79
|
+
})),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Pays qui utilisent officiellement une devise : zone d'une boutique en devise secondaire avec shipsTo "all". */
|
|
84
|
+
export const CURRENCY_COUNTRIES: Record<string, string[]> = {
|
|
85
|
+
eur: ["at", "be", "cy", "de", "ee", "es", "fi", "fr", "gr", "hr", "ie", "it", "lt", "lu", "lv", "mt", "nl", "pt", "si", "sk"],
|
|
86
|
+
xof: ["bj", "bf", "ci", "gw", "ml", "ne", "sn", "tg"],
|
|
87
|
+
xaf: ["cm", "cf", "td", "cg", "gq", "ga"],
|
|
88
|
+
usd: ["us", "ec", "sv", "pa", "pr"],
|
|
89
|
+
gbp: ["gb"],
|
|
90
|
+
chf: ["ch", "li"],
|
|
91
|
+
cad: ["ca"],
|
|
92
|
+
aud: ["au"],
|
|
93
|
+
jpy: ["jp"],
|
|
94
|
+
mad: ["ma"],
|
|
95
|
+
ngn: ["ng"],
|
|
96
|
+
ghs: ["gh"],
|
|
97
|
+
kes: ["ke"],
|
|
98
|
+
zar: ["za"],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Un pays n'appartient qu'à une région Medusa, donc qu'à une devise.
|
|
103
|
+
* Règles : listes explicites d'abord (conflit = erreur) ; une devise secondaire en "all"
|
|
104
|
+
* prend les pays de sa zone monétaire ; la devise principale en "all" prend le reste.
|
|
105
|
+
*/
|
|
106
|
+
export function countriesByCurrency(project: Project, allCountries: string[]): Map<string, Set<string>> {
|
|
107
|
+
const owner = new Map<string, string>()
|
|
108
|
+
const result = new Map<string, Set<string>>()
|
|
109
|
+
const claim = (country: string, currency: string, strict: boolean, why: string) => {
|
|
110
|
+
const current = owner.get(country)
|
|
111
|
+
if (current && current !== currency) {
|
|
112
|
+
if (strict) {
|
|
113
|
+
throw new Error(`le pays « ${country} » est demandé en ${currency.toUpperCase()} (${why}) mais appartient déjà à ${current.toUpperCase()} : un pays ne peut avoir qu'une devise.`)
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
owner.set(country, currency)
|
|
118
|
+
if (!result.has(currency)) result.set(currency, new Set())
|
|
119
|
+
result.get(currency)!.add(country)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const b of project.boutiques) {
|
|
123
|
+
if (Array.isArray(b.shipsTo)) for (const c of b.shipsTo) claim(c, b.currency, true, `shipsTo de ${b.slug}`)
|
|
124
|
+
}
|
|
125
|
+
for (const b of project.boutiques) claim(b.country, b.currency, true, `pays principal de ${b.slug}`)
|
|
126
|
+
for (const b of project.boutiques) {
|
|
127
|
+
if (b.shipsTo === "all" && b.currency !== project.defaultCurrency) {
|
|
128
|
+
for (const c of CURRENCY_COUNTRIES[b.currency] ?? []) claim(c, b.currency, false, b.slug)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (project.boutiques.some((b) => b.shipsTo === "all" && b.currency === project.defaultCurrency)) {
|
|
132
|
+
for (const c of allCountries) if (!owner.has(c)) claim(c, project.defaultCurrency, false, "reste du monde")
|
|
133
|
+
}
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Tarifs de livraison par défaut (modifiables ensuite dans l'admin) : [standard, express]. */
|
|
138
|
+
export const SHIPPING_RATES: Record<string, [number, number]> = {
|
|
139
|
+
usd: [5, 15],
|
|
140
|
+
eur: [5, 15],
|
|
141
|
+
gbp: [5, 12],
|
|
142
|
+
cad: [7, 20],
|
|
143
|
+
xof: [2000, 5000],
|
|
144
|
+
xaf: [2000, 5000],
|
|
145
|
+
}
|
|
146
|
+
export const shippingRates = (currency: string): [number, number] => SHIPPING_RATES[currency] ?? [0, 0]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Module Links
|
|
2
|
+
|
|
3
|
+
A module link forms an association between two data models of different modules, while maintaining module isolation.
|
|
4
|
+
|
|
5
|
+
> Learn more about links in [this documentation](https://docs.medusajs.com/learn/fundamentals/module-links)
|
|
6
|
+
|
|
7
|
+
For example:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import BlogModule from "../modules/blog"
|
|
11
|
+
import ProductModule from "@medusajs/medusa/product"
|
|
12
|
+
import { defineLink } from "@medusajs/framework/utils"
|
|
13
|
+
|
|
14
|
+
export default defineLink(
|
|
15
|
+
ProductModule.linkable.product,
|
|
16
|
+
BlogModule.linkable.post
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This defines a link between the Product Module's `product` data model and the Blog Module (custom module)'s `post` data model.
|
|
21
|
+
|
|
22
|
+
Then, in the Medusa application, run the following command to sync the links to the database:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx medusa db:migrate
|
|
26
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineLink } from "@medusajs/framework/utils"
|
|
2
|
+
import SalesChannelModule from "@medusajs/medusa/sales-channel"
|
|
3
|
+
import CampaignModule from "../modules/campaign"
|
|
4
|
+
|
|
5
|
+
// Une campagne par boutique (sales channel). Supprimer la campagne supprime le lien.
|
|
6
|
+
export default defineLink(SalesChannelModule.linkable.salesChannel, {
|
|
7
|
+
linkable: CampaignModule.linkable.boutiqueCampaign,
|
|
8
|
+
deleteCascade: true,
|
|
9
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Custom Module
|
|
2
|
+
|
|
3
|
+
A module is a package of reusable functionalities. It can be integrated into your Medusa application without affecting the overall system. You can create a module as part of a plugin.
|
|
4
|
+
|
|
5
|
+
> Learn more about modules in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules).
|
|
6
|
+
|
|
7
|
+
To create a module:
|
|
8
|
+
|
|
9
|
+
## 1. Create a Data Model
|
|
10
|
+
|
|
11
|
+
A data model represents a table in the database. You create a data model in a TypeScript or JavaScript file under the `models` directory of a module.
|
|
12
|
+
|
|
13
|
+
For example, create the file `src/modules/blog/models/post.ts` with the following content:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { model } from "@medusajs/framework/utils"
|
|
17
|
+
|
|
18
|
+
const Post = model.define("post", {
|
|
19
|
+
id: model.id().primaryKey(),
|
|
20
|
+
title: model.text(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default Post
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2. Create a Service
|
|
27
|
+
|
|
28
|
+
A module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.
|
|
29
|
+
|
|
30
|
+
For example, create the file `src/modules/blog/service.ts` with the following content:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { MedusaService } from "@medusajs/framework/utils"
|
|
34
|
+
import Post from "./models/post"
|
|
35
|
+
|
|
36
|
+
class BlogModuleService extends MedusaService({
|
|
37
|
+
Post,
|
|
38
|
+
}){
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default BlogModuleService
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 3. Export Module Definition
|
|
45
|
+
|
|
46
|
+
A module must have an `index.ts` file in its root directory that exports its definition. The definition specifies the main service of the module.
|
|
47
|
+
|
|
48
|
+
For example, create the file `src/modules/blog/index.ts` with the following content:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import BlogModuleService from "./service"
|
|
52
|
+
import { Module } from "@medusajs/framework/utils"
|
|
53
|
+
|
|
54
|
+
export const BLOG_MODULE = "blog"
|
|
55
|
+
|
|
56
|
+
export default Module(BLOG_MODULE, {
|
|
57
|
+
service: BlogModuleService,
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 4. Add Module to Medusa's Configurations
|
|
62
|
+
|
|
63
|
+
To start using the module, add it to `medusa-config.ts`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
module.exports = defineConfig({
|
|
67
|
+
projectConfig: {
|
|
68
|
+
// ...
|
|
69
|
+
},
|
|
70
|
+
modules: [
|
|
71
|
+
{
|
|
72
|
+
resolve: "./src/modules/blog",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 5. Generate and Run Migrations
|
|
79
|
+
|
|
80
|
+
To generate migrations for your module, run the following command:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx medusa db:generate blog
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then, to run migrations, run the following command:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx medusa db:migrate
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Use Module
|
|
93
|
+
|
|
94
|
+
You can use the module in customizations within the Medusa application, such as workflows and API routes.
|
|
95
|
+
|
|
96
|
+
For example, to use the module in an API route:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
|
|
100
|
+
import BlogModuleService from "../../../modules/blog/service"
|
|
101
|
+
import { BLOG_MODULE } from "../../../modules/blog"
|
|
102
|
+
|
|
103
|
+
export async function GET(
|
|
104
|
+
req: MedusaRequest,
|
|
105
|
+
res: MedusaResponse
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const blogModuleService: BlogModuleService = req.scope.resolve(
|
|
108
|
+
BLOG_MODULE
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const posts = await blogModuleService.listPosts()
|
|
112
|
+
|
|
113
|
+
res.json({
|
|
114
|
+
posts
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from "@medusajs/framework/utils"
|
|
2
|
+
import CampaignModuleService from "./service"
|
|
3
|
+
|
|
4
|
+
// « campaign » est déjà pris par le module Promotion de Medusa.
|
|
5
|
+
export const CAMPAIGN_MODULE = "boutiqueCampaign"
|
|
6
|
+
|
|
7
|
+
export default Module(CAMPAIGN_MODULE, {
|
|
8
|
+
service: CampaignModuleService,
|
|
9
|
+
})
|