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