kybernus 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/dist/cli/commands/ecommerce.d.ts +3 -0
  2. package/dist/cli/commands/ecommerce.d.ts.map +1 -0
  3. package/dist/cli/commands/ecommerce.js +164 -0
  4. package/dist/cli/commands/ecommerce.js.map +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/ecommerce/.env.example +10 -0
  9. package/templates/ecommerce/.github/workflows/ci.yml +102 -0
  10. package/templates/ecommerce/.github/workflows/deploy.yml +31 -0
  11. package/templates/ecommerce/.prettierrc +9 -0
  12. package/templates/ecommerce/Dockerfile +54 -0
  13. package/templates/ecommerce/README.md +295 -0
  14. package/templates/ecommerce/apps/api/.env.example +59 -0
  15. package/templates/ecommerce/apps/api/jest.config.ts +50 -0
  16. package/templates/ecommerce/apps/api/jest.integration.config.ts +45 -0
  17. package/templates/ecommerce/apps/api/package.json +59 -0
  18. package/templates/ecommerce/apps/api/prisma/migrations/20260306000137_init/migration.sql +184 -0
  19. package/templates/ecommerce/apps/api/prisma/migrations/migration_lock.toml +3 -0
  20. package/templates/ecommerce/apps/api/prisma/schema.prisma +181 -0
  21. package/templates/ecommerce/apps/api/prisma/seed.ts +159 -0
  22. package/templates/ecommerce/apps/api/src/__tests__/app.test.ts +39 -0
  23. package/templates/ecommerce/apps/api/src/__tests__/globalSetup.ts +34 -0
  24. package/templates/ecommerce/apps/api/src/__tests__/globalTeardown.ts +16 -0
  25. package/templates/ecommerce/apps/api/src/__tests__/setup.db.ts +18 -0
  26. package/templates/ecommerce/apps/api/src/__tests__/setup.env.ts +14 -0
  27. package/templates/ecommerce/apps/api/src/app.ts +133 -0
  28. package/templates/ecommerce/apps/api/src/application/admin/admin-user.service.ts +24 -0
  29. package/templates/ecommerce/apps/api/src/application/admin/dashboard.service.ts +102 -0
  30. package/templates/ecommerce/apps/api/src/application/auth/auth.service.ts +185 -0
  31. package/templates/ecommerce/apps/api/src/application/cart/cart.service.ts +151 -0
  32. package/templates/ecommerce/apps/api/src/application/cart/coupon.service.ts +51 -0
  33. package/templates/ecommerce/apps/api/src/application/catalog/catalog.service.ts +168 -0
  34. package/templates/ecommerce/apps/api/src/application/checkout/checkout.service.ts +114 -0
  35. package/templates/ecommerce/apps/api/src/application/orders/order.service.ts +93 -0
  36. package/templates/ecommerce/apps/api/src/application/ports/email.port.ts +3 -0
  37. package/templates/ecommerce/apps/api/src/application/ports/payment.port.ts +24 -0
  38. package/templates/ecommerce/apps/api/src/application/ports/shipping.port.ts +9 -0
  39. package/templates/ecommerce/apps/api/src/application/ports/storage.port.ts +3 -0
  40. package/templates/ecommerce/apps/api/src/application/ports/token-blacklist.port.ts +4 -0
  41. package/templates/ecommerce/apps/api/src/application/ports/token.port.ts +18 -0
  42. package/templates/ecommerce/apps/api/src/application/profile/profile.service.ts +76 -0
  43. package/templates/ecommerce/apps/api/src/domain/auth/user.entity.ts +109 -0
  44. package/templates/ecommerce/apps/api/src/domain/auth/user.repository.ts +11 -0
  45. package/templates/ecommerce/apps/api/src/domain/cart/cart.entity.ts +136 -0
  46. package/templates/ecommerce/apps/api/src/domain/cart/cart.repository.ts +8 -0
  47. package/templates/ecommerce/apps/api/src/domain/cart/coupon.entity.ts +58 -0
  48. package/templates/ecommerce/apps/api/src/domain/cart/coupon.repository.ts +10 -0
  49. package/templates/ecommerce/apps/api/src/domain/catalog/category.entity.ts +51 -0
  50. package/templates/ecommerce/apps/api/src/domain/catalog/category.repository.ts +10 -0
  51. package/templates/ecommerce/apps/api/src/domain/catalog/product.entity.ts +130 -0
  52. package/templates/ecommerce/apps/api/src/domain/catalog/product.repository.ts +28 -0
  53. package/templates/ecommerce/apps/api/src/domain/checkout/order.entity.ts +121 -0
  54. package/templates/ecommerce/apps/api/src/domain/checkout/order.repository.ts +11 -0
  55. package/templates/ecommerce/apps/api/src/domain/shared/AppError.ts +12 -0
  56. package/templates/ecommerce/apps/api/src/infrastructure/cache/redis.ts +16 -0
  57. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/admin.registry.ts +13 -0
  58. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/auth.registry.ts +34 -0
  59. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/cart.registry.ts +49 -0
  60. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/catalog.registry.ts +24 -0
  61. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/checkout.registry.ts +47 -0
  62. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/orders.registry.ts +6 -0
  63. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/profile.registry.ts +4 -0
  64. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/cart.memory.repository.ts +33 -0
  65. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/category.memory.repository.ts +41 -0
  66. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/coupon.memory.repository.ts +55 -0
  67. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/order.memory.repository.ts +75 -0
  68. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/product.memory.repository.ts +100 -0
  69. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/user.memory.repository.ts +54 -0
  70. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/auth/user.prisma.repository.ts +83 -0
  71. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/category.prisma.repository.ts +69 -0
  72. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/product.prisma.repository.ts +185 -0
  73. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/checkout/order.prisma.repository.ts +149 -0
  74. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma-client.ts +17 -0
  75. package/templates/ecommerce/apps/api/src/infrastructure/services/email/email.registry.ts +18 -0
  76. package/templates/ecommerce/apps/api/src/infrastructure/services/email/ethereal.email.service.ts +38 -0
  77. package/templates/ecommerce/apps/api/src/infrastructure/services/email/noop.email.service.ts +12 -0
  78. package/templates/ecommerce/apps/api/src/infrastructure/services/email/smtp.email.service.ts +36 -0
  79. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe-webhook.handler.ts +83 -0
  80. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe.adapter.ts +39 -0
  81. package/templates/ecommerce/apps/api/src/infrastructure/services/shipping/mock.shipping.service.ts +17 -0
  82. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/in-memory.storage.service.ts +11 -0
  83. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/local-disk.storage.service.ts +27 -0
  84. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/s3.storage.service.ts +52 -0
  85. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/storage.registry.ts +19 -0
  86. package/templates/ecommerce/apps/api/src/infrastructure/services/token/redis.token.blacklist.ts +23 -0
  87. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.blacklist.ts +30 -0
  88. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.service.ts +136 -0
  89. package/templates/ecommerce/apps/api/src/modules/admin/__tests__/admin.routes.integration.test.ts +250 -0
  90. package/templates/ecommerce/apps/api/src/modules/admin/admin.controller.ts +116 -0
  91. package/templates/ecommerce/apps/api/src/modules/admin/admin.registry.ts +1 -0
  92. package/templates/ecommerce/apps/api/src/modules/admin/admin.routes.ts +21 -0
  93. package/templates/ecommerce/apps/api/src/modules/admin/admin.service.ts +1 -0
  94. package/templates/ecommerce/apps/api/src/modules/admin/admin.user.service.ts +1 -0
  95. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.logout.redis.test.ts +104 -0
  96. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.routes.integration.test.ts +211 -0
  97. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.service.unit.test.ts +260 -0
  98. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/email.service.unit.test.ts +94 -0
  99. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/token.blacklist.redis.test.ts +65 -0
  100. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.entity.unit.test.ts +79 -0
  101. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.prisma.repository.test.ts +138 -0
  102. package/templates/ecommerce/apps/api/src/modules/auth/auth.controller.ts +148 -0
  103. package/templates/ecommerce/apps/api/src/modules/auth/auth.registry.ts +1 -0
  104. package/templates/ecommerce/apps/api/src/modules/auth/auth.routes.ts +17 -0
  105. package/templates/ecommerce/apps/api/src/modules/auth/auth.service.ts +1 -0
  106. package/templates/ecommerce/apps/api/src/modules/auth/redis.token.blacklist.ts +1 -0
  107. package/templates/ecommerce/apps/api/src/modules/auth/token.blacklist.ts +2 -0
  108. package/templates/ecommerce/apps/api/src/modules/auth/token.service.ts +2 -0
  109. package/templates/ecommerce/apps/api/src/modules/auth/user.entity.ts +1 -0
  110. package/templates/ecommerce/apps/api/src/modules/auth/user.prisma.repository.ts +1 -0
  111. package/templates/ecommerce/apps/api/src/modules/auth/user.repository.ts +2 -0
  112. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.entity.unit.test.ts +144 -0
  113. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.routes.integration.test.ts +242 -0
  114. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.service.unit.test.ts +151 -0
  115. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/coupon.admin.integration.test.ts +136 -0
  116. package/templates/ecommerce/apps/api/src/modules/cart/cart.controller.ts +94 -0
  117. package/templates/ecommerce/apps/api/src/modules/cart/cart.entity.ts +1 -0
  118. package/templates/ecommerce/apps/api/src/modules/cart/cart.registry.ts +1 -0
  119. package/templates/ecommerce/apps/api/src/modules/cart/cart.repository.ts +2 -0
  120. package/templates/ecommerce/apps/api/src/modules/cart/cart.routes.ts +17 -0
  121. package/templates/ecommerce/apps/api/src/modules/cart/cart.service.ts +1 -0
  122. package/templates/ecommerce/apps/api/src/modules/cart/coupon.entity.ts +1 -0
  123. package/templates/ecommerce/apps/api/src/modules/cart/coupon.repository.ts +2 -0
  124. package/templates/ecommerce/apps/api/src/modules/cart/coupon.service.ts +1 -0
  125. package/templates/ecommerce/apps/api/src/modules/cart/shipping.service.ts +2 -0
  126. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.routes.integration.test.ts +275 -0
  127. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.service.unit.test.ts +223 -0
  128. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.image.integration.test.ts +130 -0
  129. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.prisma.repository.test.ts +174 -0
  130. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.controller.ts +176 -0
  131. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.registry.ts +1 -0
  132. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.routes.ts +38 -0
  133. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.service.ts +1 -0
  134. package/templates/ecommerce/apps/api/src/modules/catalog/category.entity.ts +1 -0
  135. package/templates/ecommerce/apps/api/src/modules/catalog/category.prisma.repository.ts +1 -0
  136. package/templates/ecommerce/apps/api/src/modules/catalog/category.repository.ts +2 -0
  137. package/templates/ecommerce/apps/api/src/modules/catalog/product.entity.ts +1 -0
  138. package/templates/ecommerce/apps/api/src/modules/catalog/product.prisma.repository.ts +1 -0
  139. package/templates/ecommerce/apps/api/src/modules/catalog/product.repository.ts +2 -0
  140. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.routes.integration.test.ts +163 -0
  141. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.service.unit.test.ts +191 -0
  142. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/order.prisma.repository.test.ts +150 -0
  143. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.controller.ts +59 -0
  144. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.registry.ts +1 -0
  145. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.routes.ts +18 -0
  146. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.service.ts +1 -0
  147. package/templates/ecommerce/apps/api/src/modules/checkout/order.entity.ts +1 -0
  148. package/templates/ecommerce/apps/api/src/modules/checkout/order.prisma.repository.ts +1 -0
  149. package/templates/ecommerce/apps/api/src/modules/checkout/order.repository.ts +2 -0
  150. package/templates/ecommerce/apps/api/src/modules/checkout/tax.service.ts +9 -0
  151. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.entity.unit.test.ts +68 -0
  152. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.routes.integration.test.ts +254 -0
  153. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.service.email.unit.test.ts +142 -0
  154. package/templates/ecommerce/apps/api/src/modules/orders/order.controller.ts +96 -0
  155. package/templates/ecommerce/apps/api/src/modules/orders/order.registry.ts +1 -0
  156. package/templates/ecommerce/apps/api/src/modules/orders/order.routes.ts +17 -0
  157. package/templates/ecommerce/apps/api/src/modules/orders/order.service.ts +1 -0
  158. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe-webhook.unit.test.ts +330 -0
  159. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe.adapter.unit.test.ts +84 -0
  160. package/templates/ecommerce/apps/api/src/modules/payment/adapters/stripe.adapter.ts +1 -0
  161. package/templates/ecommerce/apps/api/src/modules/payment/payment.port.ts +1 -0
  162. package/templates/ecommerce/apps/api/src/modules/payment/stripe-webhook.handler.ts +1 -0
  163. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.routes.integration.test.ts +180 -0
  164. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.service.unit.test.ts +187 -0
  165. package/templates/ecommerce/apps/api/src/modules/profile/profile.controller.ts +92 -0
  166. package/templates/ecommerce/apps/api/src/modules/profile/profile.registry.ts +1 -0
  167. package/templates/ecommerce/apps/api/src/modules/profile/profile.routes.ts +14 -0
  168. package/templates/ecommerce/apps/api/src/modules/profile/profile.service.ts +1 -0
  169. package/templates/ecommerce/apps/api/src/presentation/middlewares/authenticate.ts +37 -0
  170. package/templates/ecommerce/apps/api/src/presentation/middlewares/authorize.ts +23 -0
  171. package/templates/ecommerce/apps/api/src/presentation/middlewares/errorHandler.ts +48 -0
  172. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.controller.ts +116 -0
  173. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.routes.ts +21 -0
  174. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.controller.ts +147 -0
  175. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.routes.ts +17 -0
  176. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.controller.ts +94 -0
  177. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.routes.ts +17 -0
  178. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.controller.ts +176 -0
  179. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.routes.ts +38 -0
  180. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.controller.ts +59 -0
  181. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.routes.ts +18 -0
  182. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.controller.ts +96 -0
  183. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.routes.ts +17 -0
  184. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.controller.ts +92 -0
  185. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.routes.ts +14 -0
  186. package/templates/ecommerce/apps/api/src/presentation/validators/uuidParam.ts +20 -0
  187. package/templates/ecommerce/apps/api/src/server.ts +47 -0
  188. package/templates/ecommerce/apps/api/src/shared/__tests__/uuid.validation.test.ts +111 -0
  189. package/templates/ecommerce/apps/api/src/shared/errors/AppError.ts +1 -0
  190. package/templates/ecommerce/apps/api/src/shared/infra/email/EtherealEmailService.ts +1 -0
  191. package/templates/ecommerce/apps/api/src/shared/infra/email/IEmailService.ts +1 -0
  192. package/templates/ecommerce/apps/api/src/shared/infra/email/NoopEmailService.ts +1 -0
  193. package/templates/ecommerce/apps/api/src/shared/infra/email/SmtpEmailService.ts +1 -0
  194. package/templates/ecommerce/apps/api/src/shared/infra/email/__tests__/ethereal.email.integration.test.ts +32 -0
  195. package/templates/ecommerce/apps/api/src/shared/infra/email/email.registry.ts +1 -0
  196. package/templates/ecommerce/apps/api/src/shared/infra/prisma.ts +1 -0
  197. package/templates/ecommerce/apps/api/src/shared/infra/redis.ts +1 -0
  198. package/templates/ecommerce/apps/api/src/shared/infra/storage/IStorageService.ts +1 -0
  199. package/templates/ecommerce/apps/api/src/shared/infra/storage/InMemoryStorageService.ts +1 -0
  200. package/templates/ecommerce/apps/api/src/shared/infra/storage/LocalDiskStorageService.ts +1 -0
  201. package/templates/ecommerce/apps/api/src/shared/infra/storage/S3StorageService.ts +1 -0
  202. package/templates/ecommerce/apps/api/src/shared/infra/storage/__tests__/s3.storage.unit.test.ts +73 -0
  203. package/templates/ecommerce/apps/api/src/shared/infra/storage/storage.registry.ts +1 -0
  204. package/templates/ecommerce/apps/api/src/shared/middlewares/authenticate.ts +1 -0
  205. package/templates/ecommerce/apps/api/src/shared/middlewares/authorize.ts +1 -0
  206. package/templates/ecommerce/apps/api/src/shared/middlewares/errorHandler.ts +1 -0
  207. package/templates/ecommerce/apps/api/src/shared/validators/uuidParam.ts +1 -0
  208. package/templates/ecommerce/apps/api/tsconfig.json +15 -0
  209. package/templates/ecommerce/apps/web/.env.example +8 -0
  210. package/templates/ecommerce/apps/web/index.html +19 -0
  211. package/templates/ecommerce/apps/web/jest.config.ts +45 -0
  212. package/templates/ecommerce/apps/web/package.json +38 -0
  213. package/templates/ecommerce/apps/web/src/App.tsx +133 -0
  214. package/templates/ecommerce/apps/web/src/__mocks__/fileMock.ts +1 -0
  215. package/templates/ecommerce/apps/web/src/__mocks__/styleMock.ts +1 -0
  216. package/templates/ecommerce/apps/web/src/index.css +159 -0
  217. package/templates/ecommerce/apps/web/src/main.tsx +13 -0
  218. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/CouponsAdminPage.test.tsx +134 -0
  219. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/DashboardPage.test.tsx +65 -0
  220. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/OrdersAdminPage.test.tsx +79 -0
  221. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/ProductsAdminPage.test.tsx +84 -0
  222. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/UsersAdminPage.test.tsx +85 -0
  223. package/templates/ecommerce/apps/web/src/modules/admin/pages/CouponsAdminPage.tsx +179 -0
  224. package/templates/ecommerce/apps/web/src/modules/admin/pages/DashboardPage.tsx +58 -0
  225. package/templates/ecommerce/apps/web/src/modules/admin/pages/OrdersAdminPage.tsx +178 -0
  226. package/templates/ecommerce/apps/web/src/modules/admin/pages/ProductsAdminPage.tsx +444 -0
  227. package/templates/ecommerce/apps/web/src/modules/admin/pages/UsersAdminPage.tsx +87 -0
  228. package/templates/ecommerce/apps/web/src/modules/auth/LoginForm.tsx +91 -0
  229. package/templates/ecommerce/apps/web/src/modules/auth/RegisterForm.tsx +109 -0
  230. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ForgotPasswordPage.test.tsx +42 -0
  231. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/LoginForm.test.tsx +76 -0
  232. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/RegisterForm.test.tsx +62 -0
  233. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ResetPasswordPage.test.tsx +66 -0
  234. package/templates/ecommerce/apps/web/src/modules/auth/pages/ForgotPasswordPage.tsx +100 -0
  235. package/templates/ecommerce/apps/web/src/modules/auth/pages/LoginPage.tsx +39 -0
  236. package/templates/ecommerce/apps/web/src/modules/auth/pages/RegisterPage.tsx +39 -0
  237. package/templates/ecommerce/apps/web/src/modules/auth/pages/ResetPasswordPage.tsx +110 -0
  238. package/templates/ecommerce/apps/web/src/modules/auth/useAuthStore.ts +141 -0
  239. package/templates/ecommerce/apps/web/src/modules/cart/__tests__/CartPage.test.tsx +111 -0
  240. package/templates/ecommerce/apps/web/src/modules/cart/pages/CartPage.tsx +313 -0
  241. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductCard.test.tsx +59 -0
  242. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductFilters.test.tsx +56 -0
  243. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductCard.tsx +78 -0
  244. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductFilters.tsx +104 -0
  245. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductDetailPage.tsx +179 -0
  246. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductListPage.tsx +100 -0
  247. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/CheckoutPage.test.tsx +159 -0
  248. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/StripePaymentForm.test.tsx +79 -0
  249. package/templates/ecommerce/apps/web/src/modules/checkout/components/StripePaymentForm.tsx +55 -0
  250. package/templates/ecommerce/apps/web/src/modules/checkout/hooks/useCheckout.ts +56 -0
  251. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutPage.tsx +344 -0
  252. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutSuccessPage.tsx +12 -0
  253. package/templates/ecommerce/apps/web/src/modules/legal/pages/PrivacyPolicyPage.tsx +207 -0
  254. package/templates/ecommerce/apps/web/src/modules/legal/pages/TermsOfServicePage.tsx +175 -0
  255. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderDetailPage.test.tsx +75 -0
  256. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderHistoryPage.test.tsx +87 -0
  257. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderDetailPage.tsx +73 -0
  258. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderHistoryPage.tsx +97 -0
  259. package/templates/ecommerce/apps/web/src/modules/profile/__tests__/ProfilePage.test.tsx +150 -0
  260. package/templates/ecommerce/apps/web/src/modules/profile/pages/ProfilePage.tsx +275 -0
  261. package/templates/ecommerce/apps/web/src/setupTests.ts +10 -0
  262. package/templates/ecommerce/apps/web/src/shared/components/CookieConsent.tsx +108 -0
  263. package/templates/ecommerce/apps/web/src/shared/components/ErrorBoundary.tsx +112 -0
  264. package/templates/ecommerce/apps/web/src/shared/components/Layout.tsx +143 -0
  265. package/templates/ecommerce/apps/web/src/shared/components/ProtectedRoute.tsx +21 -0
  266. package/templates/ecommerce/apps/web/src/shared/config/siteConfig.ts +57 -0
  267. package/templates/ecommerce/apps/web/src/shared/hooks/usePageTitle.ts +16 -0
  268. package/templates/ecommerce/apps/web/src/shared/lib/apiFetch.ts +16 -0
  269. package/templates/ecommerce/apps/web/src/shared/pages/NotFoundPage.tsx +42 -0
  270. package/templates/ecommerce/apps/web/src/shared/theme/ThemeProvider.tsx +45 -0
  271. package/templates/ecommerce/apps/web/src/shared/theme/__tests__/ThemeProvider.test.tsx +78 -0
  272. package/templates/ecommerce/apps/web/src/shared/theme/createTheme.ts +58 -0
  273. package/templates/ecommerce/apps/web/src/shared/theme/tokens.ts +81 -0
  274. package/templates/ecommerce/apps/web/src/vite-env.d.ts +1 -0
  275. package/templates/ecommerce/apps/web/tsconfig.jest.json +12 -0
  276. package/templates/ecommerce/apps/web/tsconfig.json +25 -0
  277. package/templates/ecommerce/apps/web/tsconfig.node.json +11 -0
  278. package/templates/ecommerce/apps/web/vite.config.ts +30 -0
  279. package/templates/ecommerce/docker-compose.yml +85 -0
  280. package/templates/ecommerce/package-lock.json +11255 -0
  281. package/templates/ecommerce/package.json +27 -0
  282. package/templates/ecommerce/packages/shared-types/package.json +13 -0
  283. package/templates/ecommerce/packages/shared-types/src/index.ts +3 -0
  284. package/templates/ecommerce/packages/shared-types/src/theme.ts +44 -0
  285. package/templates/ecommerce/packages/shared-types/tsconfig.json +11 -0
  286. package/templates/ecommerce/scripts/customize.sh +201 -0
  287. package/templates/ecommerce/tsconfig.json +14 -0
@@ -0,0 +1,55 @@
1
+ import { CouponEntity } from '../../../domain/cart/coupon.entity';
2
+ import type { ICouponRepository } from '../../../domain/cart/coupon.repository';
3
+
4
+ export class InMemoryCouponRepository implements ICouponRepository {
5
+ private store = new Map<string, CouponEntity>();
6
+
7
+ async findByCode(code: string): Promise<CouponEntity | null> {
8
+ return this.store.get(code.toUpperCase()) ?? null;
9
+ }
10
+
11
+ async findById(id: string): Promise<CouponEntity | null> {
12
+ for (const coupon of this.store.values()) {
13
+ if (coupon.id === id) return coupon;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ async findAll(): Promise<CouponEntity[]> {
19
+ return [...this.store.values()];
20
+ }
21
+
22
+ async save(coupon: CouponEntity): Promise<CouponEntity> {
23
+ this.store.set(coupon.code.toUpperCase(), coupon);
24
+ return coupon;
25
+ }
26
+
27
+ async deleteById(id: string): Promise<void> {
28
+ for (const [key, coupon] of this.store.entries()) {
29
+ if (coupon.id === id) {
30
+ this.store.delete(key);
31
+ return;
32
+ }
33
+ }
34
+ }
35
+
36
+ async incrementUsage(code: string): Promise<void> {
37
+ const coupon = this.store.get(code.toUpperCase());
38
+ if (!coupon) return;
39
+ const props = coupon.toRecord();
40
+ const updated = CouponEntity.reconstitute({
41
+ ...props,
42
+ usageCount: props.usageCount + 1,
43
+ });
44
+ this.store.set(code.toUpperCase(), updated);
45
+ }
46
+
47
+ // ── Test / seeding helpers ────────────────────────────────────────────────
48
+ seed(coupon: CouponEntity): void {
49
+ this.store.set(coupon.code.toUpperCase(), coupon);
50
+ }
51
+
52
+ clear(): void {
53
+ this.store.clear();
54
+ }
55
+ }
@@ -0,0 +1,75 @@
1
+ import { OrderEntity } from '../../../domain/checkout/order.entity';
2
+ import type { OrderStatus } from '../../../domain/checkout/order.entity';
3
+ import type { IOrderRepository } from '../../../domain/checkout/order.repository';
4
+
5
+ export class InMemoryOrderRepository implements IOrderRepository {
6
+ private store = new Map<string, OrderEntity>();
7
+
8
+ async findById(id: string): Promise<OrderEntity | null> {
9
+ return this.store.get(id) ?? null;
10
+ }
11
+
12
+ async findByPaymentIntentId(paymentIntentId: string): Promise<OrderEntity | null> {
13
+ for (const order of this.store.values()) {
14
+ if (order.paymentIntentId === paymentIntentId) return order;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ async findByUserId(
20
+ userId: string,
21
+ opts: { cursor?: string; limit?: number } = {},
22
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
23
+ const limit = opts.limit ?? 20;
24
+ const all = [...this.store.values()]
25
+ .filter((o) => o.userId === userId)
26
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
27
+
28
+ let startIdx = 0;
29
+ if (opts.cursor) {
30
+ const idx = all.findIndex((o) => o.id === opts.cursor);
31
+ if (idx !== -1) startIdx = idx + 1;
32
+ }
33
+
34
+ const page = all.slice(startIdx, startIdx + limit);
35
+ const nextCursor = all.length > startIdx + limit ? page[page.length - 1]!.id : null;
36
+ return { items: page, nextCursor };
37
+ }
38
+
39
+ async create(order: OrderEntity): Promise<OrderEntity> {
40
+ this.store.set(order.id, order);
41
+ return order;
42
+ }
43
+
44
+ async update(order: OrderEntity): Promise<OrderEntity> {
45
+ this.store.set(order.id, order);
46
+ return order;
47
+ }
48
+
49
+ async findAll(
50
+ opts: { status?: OrderStatus; cursor?: string; limit?: number } = {},
51
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
52
+ const limit = opts.limit ?? 20;
53
+ let all = [...this.store.values()]
54
+ .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
55
+
56
+ if (opts.status) {
57
+ all = all.filter((o) => o.status === opts.status);
58
+ }
59
+
60
+ let startIdx = 0;
61
+ if (opts.cursor) {
62
+ const idx = all.findIndex((o) => o.id === opts.cursor);
63
+ if (idx !== -1) startIdx = idx + 1;
64
+ }
65
+
66
+ const page = all.slice(startIdx, startIdx + limit);
67
+ const nextCursor = all.length > startIdx + limit ? page[page.length - 1]!.id : null;
68
+ return { items: page, nextCursor };
69
+ }
70
+
71
+ /** Test helper */
72
+ clear(): void {
73
+ this.store.clear();
74
+ }
75
+ }
@@ -0,0 +1,100 @@
1
+ import { ProductEntity, ProductProps, ProductStatus } from '../../../domain/catalog/product.entity';
2
+ import type { IProductRepository, SearchParams, SearchResult } from '../../../domain/catalog/product.repository';
3
+
4
+ export class InMemoryProductRepository implements IProductRepository {
5
+ private store = new Map<string, ProductProps>();
6
+
7
+ async findById(id: string): Promise<ProductEntity | null> {
8
+ const props = this.store.get(id);
9
+ if (!props || props.status === ProductStatus.DELETED) return null;
10
+ return ProductEntity.reconstitute(props);
11
+ }
12
+
13
+ async findBySlug(slug: string): Promise<ProductEntity | null> {
14
+ for (const props of this.store.values()) {
15
+ if (props.slug === slug && props.status !== ProductStatus.DELETED) {
16
+ return ProductEntity.reconstitute(props);
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ async findAll(): Promise<ProductEntity[]> {
23
+ return Array.from(this.store.values())
24
+ .filter((p) => p.status === ProductStatus.ACTIVE)
25
+ .map(ProductEntity.reconstitute);
26
+ }
27
+
28
+ async search(params: SearchParams): Promise<SearchResult> {
29
+ const { q, categoryId, minPrice, maxPrice, sortBy, cursor, limit = 20 } = params;
30
+ let items = Array.from(this.store.values()).filter(
31
+ (p) => p.status === ProductStatus.ACTIVE,
32
+ );
33
+
34
+ if (categoryId) items = items.filter((p) => p.categoryId === categoryId);
35
+ if (minPrice !== undefined) items = items.filter((p) => p.price >= minPrice);
36
+ if (maxPrice !== undefined) items = items.filter((p) => p.price <= maxPrice);
37
+ if (q) {
38
+ const lower = q.toLowerCase();
39
+ items = items.filter((p) => p.name.toLowerCase().includes(lower));
40
+ }
41
+
42
+ // Sorting
43
+ if (sortBy === 'price_asc') items.sort((a, b) => a.price - b.price);
44
+ else if (sortBy === 'price_desc') items.sort((a, b) => b.price - a.price);
45
+ else items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
46
+
47
+ // Cursor-based pagination
48
+ let start = 0;
49
+ if (cursor) {
50
+ const idx = items.findIndex((p) => p.id === cursor);
51
+ if (idx !== -1) start = idx + 1;
52
+ }
53
+
54
+ const page = items.slice(start, start + limit);
55
+ const nextItem = items[start + limit];
56
+ const nextCursor = nextItem ? nextItem.id : null;
57
+
58
+ return { items: page.map(ProductEntity.reconstitute), nextCursor };
59
+ }
60
+
61
+ async create(product: ProductEntity): Promise<ProductEntity> {
62
+ this.store.set(product.id, product.toRecord());
63
+ return product;
64
+ }
65
+
66
+ async update(product: ProductEntity): Promise<ProductEntity> {
67
+ this.store.set(product.id, product.toRecord());
68
+ return product;
69
+ }
70
+
71
+ async softDelete(id: string): Promise<undefined> {
72
+ const props = this.store.get(id);
73
+ if (props) {
74
+ this.store.set(id, { ...props, status: ProductStatus.DELETED, updatedAt: new Date() });
75
+ }
76
+ return undefined;
77
+ }
78
+
79
+ async updateVariantStock(productId: string, variantId: string, newStock: number): Promise<undefined> {
80
+ const props = this.store.get(productId);
81
+ if (props) {
82
+ const updatedVariants = props.variants.map((v) =>
83
+ v.id === variantId ? { ...v, stock: newStock } : v,
84
+ );
85
+ this.store.set(productId, { ...props, variants: updatedVariants });
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ async slugExists(slug: string): Promise<boolean> {
91
+ for (const props of this.store.values()) {
92
+ if (props.slug === slug) return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ clear(): void {
98
+ this.store.clear();
99
+ }
100
+ }
@@ -0,0 +1,54 @@
1
+ import { UserEntity } from '../../../domain/auth/user.entity';
2
+ import type { UserRole } from '../../../domain/auth/user.entity';
3
+ import type { IUserRepository } from '../../../domain/auth/user.repository';
4
+
5
+ export class InMemoryUserRepository implements IUserRepository {
6
+ private readonly store = new Map<string, ReturnType<UserEntity['toRecord']>>();
7
+
8
+ async findByEmail(email: string): Promise<UserEntity | null> {
9
+ const record = [...this.store.values()].find(
10
+ (u) => u.email === email.toLowerCase(),
11
+ );
12
+ return record ? UserEntity.reconstitute(record) : null;
13
+ }
14
+
15
+ async findById(id: string): Promise<UserEntity | null> {
16
+ const record = this.store.get(id);
17
+ return record ? UserEntity.reconstitute(record) : null;
18
+ }
19
+
20
+ async create(user: UserEntity): Promise<UserEntity> {
21
+ this.store.set(user.id, user.toRecord());
22
+ return user;
23
+ }
24
+
25
+ async update(user: UserEntity): Promise<UserEntity> {
26
+ this.store.set(user.id, user.toRecord());
27
+ return user;
28
+ }
29
+
30
+ async delete(id: string): Promise<void> {
31
+ this.store.delete(id);
32
+ }
33
+
34
+ async findAll(
35
+ opts: { role?: UserRole; cursor?: string; limit?: number } = {},
36
+ ): Promise<{ items: UserEntity[]; nextCursor: string | null }> {
37
+ const { role, cursor, limit = 20 } = opts;
38
+ let all = [...this.store.values()].map((u) => UserEntity.reconstitute(u));
39
+ if (role) all = all.filter((u) => u.role === role);
40
+ all.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
41
+ const startIdx = cursor ? all.findIndex((u) => u.id === cursor) + 1 : 0;
42
+ const page = all.slice(startIdx, startIdx + limit);
43
+ const nextCursor =
44
+ page.length === limit && startIdx + limit < all.length
45
+ ? (page[page.length - 1]!.id)
46
+ : null;
47
+ return { items: page, nextCursor };
48
+ }
49
+
50
+ /** Test helper — reset state between test suites */
51
+ clear(): void {
52
+ this.store.clear();
53
+ }
54
+ }
@@ -0,0 +1,83 @@
1
+ import type { PrismaClient, User } from '@prisma/client';
2
+ import { UserEntity } from '../../../../domain/auth/user.entity';
3
+ import type { IUserRepository } from '../../../../domain/auth/user.repository';
4
+ import type { UserRole } from '../../../../domain/auth/user.entity';
5
+
6
+ function toEntity(row: User): UserEntity {
7
+ return UserEntity.reconstitute({
8
+ id: row.id,
9
+ name: row.name,
10
+ email: row.email,
11
+ passwordHash: row.passwordHash,
12
+ role: row.role as UserRole,
13
+ createdAt: row.createdAt,
14
+ updatedAt: row.updatedAt,
15
+ });
16
+ }
17
+
18
+ export class PrismaUserRepository implements IUserRepository {
19
+ constructor(private readonly db: PrismaClient) {}
20
+
21
+ async findByEmail(email: string): Promise<UserEntity | null> {
22
+ const row = await this.db.user.findUnique({ where: { email } });
23
+ return row ? toEntity(row) : null;
24
+ }
25
+
26
+ async findById(id: string): Promise<UserEntity | null> {
27
+ const row = await this.db.user.findUnique({ where: { id } });
28
+ return row ? toEntity(row) : null;
29
+ }
30
+
31
+ async create(user: UserEntity): Promise<UserEntity> {
32
+ const row = await this.db.user.create({
33
+ data: {
34
+ id: user.id,
35
+ name: user.name,
36
+ email: user.email,
37
+ passwordHash: user.passwordHash,
38
+ role: user.role,
39
+ createdAt: user.createdAt,
40
+ updatedAt: user.updatedAt,
41
+ },
42
+ });
43
+ return toEntity(row);
44
+ }
45
+
46
+ async update(user: UserEntity): Promise<UserEntity> {
47
+ const row = await this.db.user.update({
48
+ where: { id: user.id },
49
+ data: {
50
+ name: user.name,
51
+ email: user.email,
52
+ passwordHash: user.passwordHash,
53
+ role: user.role,
54
+ },
55
+ });
56
+ return toEntity(row);
57
+ }
58
+
59
+ async delete(id: string): Promise<void> {
60
+ await this.db.user.delete({ where: { id } });
61
+ }
62
+
63
+ async findAll(
64
+ opts: { role?: UserRole; cursor?: string; limit?: number } = {},
65
+ ): Promise<{ items: UserEntity[]; nextCursor: string | null }> {
66
+ const limit = opts.limit ?? 20;
67
+
68
+ const rows = await this.db.user.findMany({
69
+ where: opts.role ? { role: opts.role } : undefined,
70
+ orderBy: { createdAt: 'desc' },
71
+ take: limit + 1,
72
+ ...(opts.cursor
73
+ ? { cursor: { id: opts.cursor }, skip: 1 }
74
+ : {}),
75
+ });
76
+
77
+ const hasMore = rows.length > limit;
78
+ const items = hasMore ? rows.slice(0, limit) : rows;
79
+ const nextCursor = hasMore ? items[items.length - 1]!.id : null;
80
+
81
+ return { items: items.map(toEntity), nextCursor };
82
+ }
83
+ }
@@ -0,0 +1,69 @@
1
+ import type { PrismaClient, Category } from '@prisma/client';
2
+ import { CategoryEntity } from '../../../../domain/catalog/category.entity';
3
+ import type { ICategoryRepository } from '../../../../domain/catalog/category.repository';
4
+
5
+ function toEntity(row: Category): CategoryEntity {
6
+ return CategoryEntity.reconstitute({
7
+ id: row.id,
8
+ name: row.name,
9
+ slug: row.slug,
10
+ description: row.description,
11
+ parentId: row.parentId,
12
+ createdAt: row.createdAt,
13
+ updatedAt: row.updatedAt,
14
+ });
15
+ }
16
+
17
+ export class PrismaCategoryRepository implements ICategoryRepository {
18
+ constructor(private readonly db: PrismaClient) {}
19
+
20
+ async findById(id: string): Promise<CategoryEntity | null> {
21
+ const row = await this.db.category.findUnique({ where: { id } });
22
+ return row ? toEntity(row) : null;
23
+ }
24
+
25
+ async findBySlug(slug: string): Promise<CategoryEntity | null> {
26
+ const row = await this.db.category.findUnique({ where: { slug } });
27
+ return row ? toEntity(row) : null;
28
+ }
29
+
30
+ async findAll(): Promise<CategoryEntity[]> {
31
+ const rows = await this.db.category.findMany({
32
+ orderBy: { name: 'asc' },
33
+ });
34
+ return rows.map(toEntity);
35
+ }
36
+
37
+ async create(category: CategoryEntity): Promise<CategoryEntity> {
38
+ const row = await this.db.category.create({
39
+ data: {
40
+ id: category.id,
41
+ name: category.name,
42
+ slug: category.slug,
43
+ description: category.description,
44
+ parentId: category.parentId,
45
+ createdAt: category.createdAt,
46
+ updatedAt: category.updatedAt,
47
+ },
48
+ });
49
+ return toEntity(row);
50
+ }
51
+
52
+ async update(category: CategoryEntity): Promise<CategoryEntity> {
53
+ const row = await this.db.category.update({
54
+ where: { id: category.id },
55
+ data: {
56
+ name: category.name,
57
+ slug: category.slug,
58
+ description: category.description,
59
+ parentId: category.parentId,
60
+ },
61
+ });
62
+ return toEntity(row);
63
+ }
64
+
65
+ async delete(id: string): Promise<undefined> {
66
+ await this.db.category.delete({ where: { id } });
67
+ return undefined;
68
+ }
69
+ }
@@ -0,0 +1,185 @@
1
+ import type { PrismaClient, Product, ProductVariant } from '@prisma/client';
2
+ import { ProductEntity, ProductStatus } from '../../../../domain/catalog/product.entity';
3
+ import type { IProductRepository, SearchParams, SearchResult } from '../../../../domain/catalog/product.repository';
4
+
5
+ type ProductWithVariants = Product & { variants: ProductVariant[] };
6
+
7
+ function toEntity(row: ProductWithVariants): ProductEntity {
8
+ return ProductEntity.reconstitute({
9
+ id: row.id,
10
+ name: row.name,
11
+ slug: row.slug,
12
+ description: row.description,
13
+ price: Number(row.price),
14
+ categoryId: row.categoryId,
15
+ status: row.status as ProductStatus,
16
+ images: row.images,
17
+ variants: row.variants.map((v) => ({
18
+ id: v.id,
19
+ productId: v.productId,
20
+ sku: v.sku,
21
+ size: v.size,
22
+ color: v.color,
23
+ stock: v.stock,
24
+ price: v.price !== null ? Number(v.price) : null,
25
+ })),
26
+ createdAt: row.createdAt,
27
+ updatedAt: row.updatedAt,
28
+ });
29
+ }
30
+
31
+ const INCLUDE_VARIANTS = { variants: true } as const;
32
+
33
+ export class PrismaProductRepository implements IProductRepository {
34
+ constructor(private readonly db: PrismaClient) {}
35
+
36
+ async findById(id: string): Promise<ProductEntity | null> {
37
+ const row = await this.db.product.findFirst({
38
+ where: { id, status: { not: 'DELETED' } },
39
+ include: INCLUDE_VARIANTS,
40
+ });
41
+ return row ? toEntity(row) : null;
42
+ }
43
+
44
+ async findBySlug(slug: string): Promise<ProductEntity | null> {
45
+ const row = await this.db.product.findFirst({
46
+ where: { slug, status: { not: 'DELETED' } },
47
+ include: INCLUDE_VARIANTS,
48
+ });
49
+ return row ? toEntity(row) : null;
50
+ }
51
+
52
+ async findAll(): Promise<ProductEntity[]> {
53
+ const rows = await this.db.product.findMany({
54
+ where: { status: 'ACTIVE' },
55
+ include: INCLUDE_VARIANTS,
56
+ orderBy: { createdAt: 'desc' },
57
+ });
58
+ return rows.map(toEntity);
59
+ }
60
+
61
+ async search(params: SearchParams): Promise<SearchResult> {
62
+ const { q, categoryId, minPrice, maxPrice, sortBy, cursor, limit = 20 } = params;
63
+
64
+ const rows = await this.db.product.findMany({
65
+ where: {
66
+ status: 'ACTIVE',
67
+ ...(categoryId ? { categoryId } : {}),
68
+ ...(q ? { name: { contains: q, mode: 'insensitive' } } : {}),
69
+ ...(minPrice !== undefined || maxPrice !== undefined
70
+ ? {
71
+ price: {
72
+ ...(minPrice !== undefined ? { gte: minPrice } : {}),
73
+ ...(maxPrice !== undefined ? { lte: maxPrice } : {}),
74
+ },
75
+ }
76
+ : {}),
77
+ },
78
+ orderBy:
79
+ sortBy === 'price_asc'
80
+ ? { price: 'asc' }
81
+ : sortBy === 'price_desc'
82
+ ? { price: 'desc' }
83
+ : { createdAt: 'desc' },
84
+ include: INCLUDE_VARIANTS,
85
+ take: limit + 1,
86
+ ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
87
+ });
88
+
89
+ const hasMore = rows.length > limit;
90
+ const items = hasMore ? rows.slice(0, limit) : rows;
91
+ const nextCursor = hasMore ? items[items.length - 1]!.id : null;
92
+
93
+ return { items: items.map(toEntity), nextCursor };
94
+ }
95
+
96
+ async create(product: ProductEntity): Promise<ProductEntity> {
97
+ const row = await this.db.product.create({
98
+ data: {
99
+ id: product.id,
100
+ name: product.name,
101
+ slug: product.slug,
102
+ description: product.description,
103
+ price: product.price,
104
+ categoryId: product.categoryId,
105
+ status: product.status,
106
+ images: product.images,
107
+ createdAt: product.createdAt,
108
+ updatedAt: product.updatedAt,
109
+ variants: {
110
+ createMany: {
111
+ data: product.variants.map((v) => ({
112
+ id: v.id,
113
+ sku: v.sku,
114
+ size: v.size,
115
+ color: v.color,
116
+ stock: v.stock,
117
+ price: v.price ?? null,
118
+ })),
119
+ },
120
+ },
121
+ },
122
+ include: INCLUDE_VARIANTS,
123
+ });
124
+ return toEntity(row);
125
+ }
126
+
127
+ async update(product: ProductEntity): Promise<ProductEntity> {
128
+ const row = await this.db.$transaction(async (tx) => {
129
+ // Replace all variants atomically
130
+ await tx.productVariant.deleteMany({ where: { productId: product.id } });
131
+
132
+ return tx.product.update({
133
+ where: { id: product.id },
134
+ data: {
135
+ name: product.name,
136
+ slug: product.slug,
137
+ description: product.description,
138
+ price: product.price,
139
+ categoryId: product.categoryId,
140
+ status: product.status,
141
+ images: product.images,
142
+ variants: {
143
+ createMany: {
144
+ data: product.variants.map((v) => ({
145
+ id: v.id,
146
+ sku: v.sku,
147
+ size: v.size,
148
+ color: v.color,
149
+ stock: v.stock,
150
+ price: v.price ?? null,
151
+ })),
152
+ },
153
+ },
154
+ },
155
+ include: INCLUDE_VARIANTS,
156
+ });
157
+ });
158
+ return toEntity(row);
159
+ }
160
+
161
+ async softDelete(id: string): Promise<undefined> {
162
+ await this.db.product.update({
163
+ where: { id },
164
+ data: { status: 'DELETED' },
165
+ });
166
+ return undefined;
167
+ }
168
+
169
+ async updateVariantStock(
170
+ productId: string,
171
+ variantId: string,
172
+ newStock: number,
173
+ ): Promise<undefined> {
174
+ await this.db.productVariant.updateMany({
175
+ where: { id: variantId, productId },
176
+ data: { stock: newStock },
177
+ });
178
+ return undefined;
179
+ }
180
+
181
+ async slugExists(slug: string): Promise<boolean> {
182
+ const count = await this.db.product.count({ where: { slug } });
183
+ return count > 0;
184
+ }
185
+ }