@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.
Files changed (316) hide show
  1. package/README.md +1 -0
  2. package/cli/dist/commands.js +382 -0
  3. package/cli/dist/create.js +194 -0
  4. package/cli/dist/docker.js +19 -0
  5. package/cli/dist/generate.js +322 -0
  6. package/cli/dist/guide.js +72 -0
  7. package/cli/dist/index.js +156 -0
  8. package/cli/dist/paths.js +29 -0
  9. package/cli/dist/ports.js +54 -0
  10. package/cli/dist/project.js +119 -0
  11. package/cli/dist/themes.js +24 -0
  12. package/docs/fiche-client.en.html +137 -0
  13. package/docs/fiche-client.html +137 -0
  14. package/docs/guide-complet.en.html +259 -0
  15. package/docs/guide-complet.fr.html +259 -0
  16. package/package.json +51 -0
  17. package/templates/backend/.dockerignore +9 -0
  18. package/templates/backend/Dockerfile +30 -0
  19. package/templates/backend/README.md +78 -0
  20. package/templates/backend/TEMPLATE_VERSION +1 -0
  21. package/templates/backend/docker/entrypoint.sh +28 -0
  22. package/templates/backend/instrumentation.ts +24 -0
  23. package/templates/backend/medusa-config.ts +114 -0
  24. package/templates/backend/package-lock.json +16222 -0
  25. package/templates/backend/package.json +39 -0
  26. package/templates/backend/src/admin/README.md +33 -0
  27. package/templates/backend/src/admin/components/image-field.tsx +61 -0
  28. package/templates/backend/src/admin/i18n/README.md +58 -0
  29. package/templates/backend/src/admin/i18n/index.ts +8 -0
  30. package/templates/backend/src/admin/i18n/json/en.json +66 -0
  31. package/templates/backend/src/admin/i18n/json/fr.json +66 -0
  32. package/templates/backend/src/admin/lib/client.ts +9 -0
  33. package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
  34. package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
  35. package/templates/backend/src/admin/tsconfig.json +24 -0
  36. package/templates/backend/src/admin/vite-env.d.ts +1 -0
  37. package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
  38. package/templates/backend/src/api/README.md +135 -0
  39. package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
  40. package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
  41. package/templates/backend/src/api/middlewares.ts +28 -0
  42. package/templates/backend/src/api/rate-limit.ts +30 -0
  43. package/templates/backend/src/api/route.ts +6 -0
  44. package/templates/backend/src/api/store/campaign/route.ts +25 -0
  45. package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
  46. package/templates/backend/src/emails/en.json +64 -0
  47. package/templates/backend/src/emails/fr.json +64 -0
  48. package/templates/backend/src/jobs/README.md +38 -0
  49. package/templates/backend/src/lib/emails.ts +94 -0
  50. package/templates/backend/src/lib/notify.ts +148 -0
  51. package/templates/backend/src/lib/project.ts +146 -0
  52. package/templates/backend/src/links/README.md +26 -0
  53. package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
  54. package/templates/backend/src/modules/README.md +117 -0
  55. package/templates/backend/src/modules/campaign/index.ts +9 -0
  56. package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
  57. package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
  58. package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
  59. package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
  60. package/templates/backend/src/modules/campaign/service.ts +6 -0
  61. package/templates/backend/src/modules/campaign/validators.ts +94 -0
  62. package/templates/backend/src/modules/resend/index.ts +6 -0
  63. package/templates/backend/src/modules/resend/service.ts +63 -0
  64. package/templates/backend/src/scripts/README.md +63 -0
  65. package/templates/backend/src/scripts/campaign-export.ts +15 -0
  66. package/templates/backend/src/scripts/campaign-import.ts +38 -0
  67. package/templates/backend/src/scripts/price-report.ts +34 -0
  68. package/templates/backend/src/scripts/seed.ts +418 -0
  69. package/templates/backend/src/scripts/storage.ts +63 -0
  70. package/templates/backend/src/subscribers/README.md +61 -0
  71. package/templates/backend/src/subscribers/account-emails.ts +27 -0
  72. package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
  73. package/templates/backend/src/subscribers/order-emails.ts +12 -0
  74. package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
  75. package/templates/backend/src/workflows/README.md +81 -0
  76. package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
  77. package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
  78. package/templates/backend/tsconfig.json +36 -0
  79. package/templates/storefront/.dockerignore +7 -0
  80. package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
  81. package/templates/storefront/.prettierrc +8 -0
  82. package/templates/storefront/Dockerfile +37 -0
  83. package/templates/storefront/LICENSE +21 -0
  84. package/templates/storefront/README.md +127 -0
  85. package/templates/storefront/locales/en.json +269 -0
  86. package/templates/storefront/locales/fr.json +269 -0
  87. package/templates/storefront/next-env.d.ts +7 -0
  88. package/templates/storefront/next-sitemap.js +19 -0
  89. package/templates/storefront/next.config.js +29 -0
  90. package/templates/storefront/package-lock.json +9031 -0
  91. package/templates/storefront/package.json +49 -0
  92. package/templates/storefront/postcss.config.js +6 -0
  93. package/templates/storefront/public/favicon.ico +0 -0
  94. package/templates/storefront/scripts/sync-design.mjs +30 -0
  95. package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
  96. package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
  97. package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
  98. package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
  99. package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
  100. package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
  101. package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
  102. package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
  103. package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
  104. package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
  105. package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
  106. package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
  107. package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
  108. package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
  109. package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
  110. package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
  111. package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
  112. package/templates/storefront/src/app/(main)/layout.tsx +43 -0
  113. package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
  114. package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
  115. package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
  116. package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
  117. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
  118. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
  119. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
  120. package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
  121. package/templates/storefront/src/app/(main)/page.tsx +68 -0
  122. package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
  123. package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
  124. package/templates/storefront/src/app/api/health/route.ts +13 -0
  125. package/templates/storefront/src/app/layout.tsx +42 -0
  126. package/templates/storefront/src/app/not-found.tsx +21 -0
  127. package/templates/storefront/src/app/opengraph-image.jpg +0 -0
  128. package/templates/storefront/src/app/twitter-image.jpg +0 -0
  129. package/templates/storefront/src/instrumentation-node.ts +18 -0
  130. package/templates/storefront/src/instrumentation.ts +9 -0
  131. package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
  132. package/templates/storefront/src/lib/boutique/config.ts +152 -0
  133. package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
  134. package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
  135. package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
  136. package/templates/storefront/src/lib/boutique/schema.ts +131 -0
  137. package/templates/storefront/src/lib/config.ts +39 -0
  138. package/templates/storefront/src/lib/constants.tsx +75 -0
  139. package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
  140. package/templates/storefront/src/lib/data/cart.ts +434 -0
  141. package/templates/storefront/src/lib/data/categories.ts +49 -0
  142. package/templates/storefront/src/lib/data/collections.ts +59 -0
  143. package/templates/storefront/src/lib/data/cookies.ts +103 -0
  144. package/templates/storefront/src/lib/data/customer.ts +261 -0
  145. package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
  146. package/templates/storefront/src/lib/data/onboarding.ts +9 -0
  147. package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
  148. package/templates/storefront/src/lib/data/orders.ts +112 -0
  149. package/templates/storefront/src/lib/data/payment.ts +35 -0
  150. package/templates/storefront/src/lib/data/products.ts +127 -0
  151. package/templates/storefront/src/lib/data/regions.ts +70 -0
  152. package/templates/storefront/src/lib/data/variants.ts +38 -0
  153. package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
  154. package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
  155. package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
  156. package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
  157. package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
  158. package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
  159. package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
  160. package/templates/storefront/src/lib/util/money.ts +35 -0
  161. package/templates/storefront/src/lib/util/product.ts +5 -0
  162. package/templates/storefront/src/lib/util/repeat.ts +5 -0
  163. package/templates/storefront/src/lib/util/sort-products.ts +50 -0
  164. package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
  165. package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
  166. package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
  167. package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
  168. package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
  169. package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
  170. package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
  171. package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
  172. package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
  173. package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
  174. package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
  175. package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
  176. package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
  177. package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
  178. package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
  179. package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
  180. package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
  181. package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
  182. package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
  183. package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
  184. package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
  185. package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
  186. package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
  187. package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
  188. package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
  189. package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
  190. package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
  191. package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
  192. package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
  193. package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
  194. package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
  195. package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
  196. package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
  197. package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
  198. package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
  199. package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
  200. package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
  201. package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
  202. package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
  203. package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
  204. package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
  205. package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
  206. package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
  207. package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
  208. package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
  209. package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
  210. package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
  211. package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
  212. package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
  213. package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
  214. package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
  215. package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
  216. package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
  217. package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
  218. package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
  219. package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
  220. package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
  221. package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
  222. package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
  223. package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
  224. package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
  225. package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
  226. package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
  227. package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
  228. package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
  229. package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
  230. package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
  231. package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
  232. package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
  233. package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
  234. package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
  235. package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
  236. package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
  237. package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
  238. package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
  239. package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
  240. package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
  241. package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
  242. package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
  243. package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
  244. package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
  245. package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
  246. package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
  247. package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
  248. package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
  249. package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
  250. package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
  251. package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
  252. package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
  253. package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
  254. package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
  255. package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
  256. package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
  257. package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
  258. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
  259. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
  260. package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
  261. package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
  262. package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
  263. package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
  264. package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
  265. package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
  266. package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
  267. package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
  268. package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
  269. package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
  270. package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
  271. package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
  272. package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
  273. package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
  274. package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
  275. package/templates/storefront/src/modules/ub/button.tsx +41 -0
  276. package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
  277. package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
  278. package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
  279. package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
  280. package/templates/storefront/src/modules/ub/footer.tsx +56 -0
  281. package/templates/storefront/src/modules/ub/icons.tsx +42 -0
  282. package/templates/storefront/src/modules/ub/nav.tsx +90 -0
  283. package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
  284. package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
  285. package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
  286. package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
  287. package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
  288. package/templates/storefront/src/proxy.ts +89 -0
  289. package/templates/storefront/src/styles/globals.css +98 -0
  290. package/templates/storefront/src/styles/ub/bundle.css +372 -0
  291. package/templates/storefront/src/styles/ub/fonts.css +17 -0
  292. package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
  293. package/templates/storefront/src/styles/ub/overrides.css +12 -0
  294. package/templates/storefront/src/styles/ub/tokens.css +9 -0
  295. package/templates/storefront/src/types/global.ts +24 -0
  296. package/templates/storefront/src/types/icon.ts +4 -0
  297. package/templates/storefront/tailwind.config.js +164 -0
  298. package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
  299. package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
  300. package/templates/storefront/themes/black-friday/theme.json +9 -0
  301. package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
  302. package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
  303. package/templates/storefront/themes/cyber-monday/theme.json +9 -0
  304. package/templates/storefront/themes/halloween/campaign.en.json +31 -0
  305. package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
  306. package/templates/storefront/themes/halloween/theme.json +9 -0
  307. package/templates/storefront/themes/noel/campaign.en.json +31 -0
  308. package/templates/storefront/themes/noel/campaign.fr.json +31 -0
  309. package/templates/storefront/themes/noel/theme.json +9 -0
  310. package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
  311. package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
  312. package/templates/storefront/themes/nouvel-an/theme.json +9 -0
  313. package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
  314. package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
  315. package/templates/storefront/themes/thanksgiving/theme.json +9 -0
  316. 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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
+ })