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,114 @@
1
+ import { AppError } from '../../domain/shared/AppError';
2
+ import { OrderEntity } from '../../domain/checkout/order.entity';
3
+ import { IOrderRepository } from '../../domain/checkout/order.repository';
4
+ import { IPaymentAdapter } from '../ports/payment.port';
5
+ import { ICartRepository } from '../../domain/cart/cart.repository';
6
+ import { IProductRepository } from '../../domain/catalog/product.repository';
7
+
8
+ interface CheckoutDto {
9
+ userId: string;
10
+ shippingCost: number;
11
+ shippingAddress: Record<string, string> | null;
12
+ }
13
+
14
+ export class CheckoutService {
15
+ constructor(
16
+ private readonly orderRepo: IOrderRepository,
17
+ private readonly paymentAdapter: IPaymentAdapter,
18
+ private readonly cartRepo: ICartRepository,
19
+ private readonly productRepo: IProductRepository,
20
+ ) {}
21
+
22
+ async checkout(dto: CheckoutDto): Promise<{ orderId: string; clientSecret: string }> {
23
+ const { userId, shippingCost, shippingAddress } = dto;
24
+
25
+ // 1. Get cart — throw if empty
26
+ const cart = await this.cartRepo.findByUserId(userId);
27
+ if (!cart || cart.items.length === 0) {
28
+ throw new AppError('Carrinho vazio', 400);
29
+ }
30
+
31
+ // 2. Validate stock for every item and snapshot current levels
32
+ const stockSnapshot: Array<{
33
+ productId: string;
34
+ variantId: string;
35
+ currentStock: number;
36
+ newStock: number;
37
+ }> = [];
38
+
39
+ for (const item of cart.items) {
40
+ const product = await this.productRepo.findById(item.productId);
41
+ if (!product) throw new AppError('Produto não encontrado', 404);
42
+ const variant = product.variants.find((v) => v.id === item.variantId);
43
+ if (!variant) throw new AppError('Variante não encontrada', 404);
44
+ if (variant.stock < item.qty) {
45
+ throw new AppError('Estoque insuficiente', 409);
46
+ }
47
+ stockSnapshot.push({
48
+ productId: item.productId,
49
+ variantId: item.variantId,
50
+ currentStock: variant.stock,
51
+ newStock: variant.stock - item.qty,
52
+ });
53
+ }
54
+
55
+ // 3. Compute totals
56
+ const subtotal = cart.subtotal;
57
+ const discount = cart.discount;
58
+ const tax = 0; // TaxService placeholder — returns 0
59
+ const total = subtotal - discount + shippingCost + tax;
60
+
61
+ // 4. Create Order (PENDING)
62
+ const orderItems = cart.items.map((item) => ({
63
+ id: crypto.randomUUID(),
64
+ variantId: item.variantId,
65
+ productId: item.productId,
66
+ name: item.name,
67
+ sku: item.sku,
68
+ price: item.price,
69
+ qty: item.qty,
70
+ image: item.image,
71
+ }));
72
+
73
+ const order = OrderEntity.create({
74
+ userId,
75
+ items: orderItems,
76
+ subtotal,
77
+ discount,
78
+ shippingCost,
79
+ tax,
80
+ total,
81
+ couponCode: cart.coupon?.code ?? null,
82
+ paymentIntentId: null,
83
+ shippingAddress,
84
+ });
85
+ await this.orderRepo.create(order);
86
+
87
+ // 5. Decrement stock
88
+ for (const snap of stockSnapshot) {
89
+ await this.productRepo.updateVariantStock(snap.productId, snap.variantId, snap.newStock);
90
+ }
91
+
92
+ // 6. Create PaymentIntent — rollback on failure
93
+ try {
94
+ const pi = await this.paymentAdapter.createPaymentIntent(total, 'brl', { orderId: order.id });
95
+
96
+ // Attach paymentIntentId to order
97
+ const updatedOrder = order.withPaymentIntentId(pi.id);
98
+ await this.orderRepo.update(updatedOrder);
99
+
100
+ // 7. Clear cart
101
+ await this.cartRepo.delete({ userId });
102
+
103
+ return { orderId: order.id, clientSecret: pi.clientSecret };
104
+ } catch (err) {
105
+ // Rollback stock to previous levels
106
+ for (const snap of stockSnapshot) {
107
+ await this.productRepo.updateVariantStock(snap.productId, snap.variantId, snap.currentStock);
108
+ }
109
+ // Mark order as failed
110
+ await this.orderRepo.update(order.fail());
111
+ throw err;
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,93 @@
1
+ import { AppError } from '../../domain/shared/AppError';
2
+ import { OrderEntity } from '../../domain/checkout/order.entity';
3
+ import type { OrderStatus } from '../../domain/checkout/order.entity';
4
+ import { IOrderRepository } from '../../domain/checkout/order.repository';
5
+ import { IEmailService } from '../ports/email.port';
6
+ import { IUserRepository } from '../../domain/auth/user.repository';
7
+
8
+ export class OrderService {
9
+ constructor(
10
+ private readonly orderRepo: IOrderRepository,
11
+ private readonly emailService?: IEmailService,
12
+ private readonly userRepo?: IUserRepository,
13
+ ) {}
14
+
15
+ async getOrdersByUser(
16
+ userId: string,
17
+ opts?: { cursor?: string; limit?: number },
18
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
19
+ return this.orderRepo.findByUserId(userId, opts);
20
+ }
21
+
22
+ async getOrderById(orderId: string, userId: string): Promise<OrderEntity> {
23
+ const order = await this.orderRepo.findById(orderId);
24
+ if (!order) throw new AppError('Pedido não encontrado', 404);
25
+ if (order.userId !== userId) throw new AppError('Acesso negado', 403);
26
+ return order;
27
+ }
28
+
29
+ async updateOrderStatus(
30
+ orderId: string,
31
+ newStatus: OrderStatus,
32
+ trackingCode?: string,
33
+ ): Promise<OrderEntity> {
34
+ const order = await this.orderRepo.findById(orderId);
35
+ if (!order) throw new AppError('Pedido não encontrado', 404);
36
+
37
+ let updated: OrderEntity;
38
+ switch (newStatus) {
39
+ case 'PAID':
40
+ updated = order.pay();
41
+ break;
42
+ case 'FAILED':
43
+ updated = order.fail();
44
+ break;
45
+ case 'SHIPPED':
46
+ updated = order.ship(trackingCode);
47
+ break;
48
+ case 'DELIVERED':
49
+ updated = order.deliver();
50
+ break;
51
+ case 'CANCELLED':
52
+ updated = order.cancel();
53
+ break;
54
+ default:
55
+ throw new AppError(`Status desconhecido: ${newStatus}`, 400);
56
+ }
57
+
58
+ const saved = await this.orderRepo.update(updated);
59
+ await this._sendStatusEmail(saved, newStatus);
60
+ return saved;
61
+ }
62
+
63
+ private async _sendStatusEmail(order: OrderEntity, status: OrderStatus): Promise<void> {
64
+ if (!this.emailService || !this.userRepo) return;
65
+ if (status !== 'SHIPPED' && status !== 'DELIVERED') return;
66
+ try {
67
+ const user = await this.userRepo.findById(order.userId);
68
+ if (!user) return;
69
+ if (status === 'SHIPPED') {
70
+ const html = `
71
+ <h2>Seu pedido foi enviado! 🚚</h2>
72
+ <p>Olá ${user.name}, seu pedido <strong>#${order.id.slice(0, 8)}</strong> está a caminho.</p>
73
+ <p>Código de rastreio: <strong>${order.trackingCode ?? '—'}</strong></p>
74
+ `;
75
+ await this.emailService.send(user.email, 'Pedido enviado 🚚', html);
76
+ } else {
77
+ const html = `
78
+ <h2>Pedido entregue! ✅</h2>
79
+ <p>Olá ${user.name}, seu pedido <strong>#${order.id.slice(0, 8)}</strong> foi entregue. Aproveite!</p>
80
+ `;
81
+ await this.emailService.send(user.email, 'Pedido entregue ✅', html);
82
+ }
83
+ } catch {
84
+ // Email failure must never break the order update flow
85
+ }
86
+ }
87
+
88
+ async getAllOrders(
89
+ opts?: { status?: OrderStatus; cursor?: string; limit?: number },
90
+ ): Promise<{ items: OrderEntity[]; nextCursor: string | null }> {
91
+ return this.orderRepo.findAll(opts);
92
+ }
93
+ }
@@ -0,0 +1,3 @@
1
+ export interface IEmailService {
2
+ send(to: string, subject: string, html: string): Promise<void>;
3
+ }
@@ -0,0 +1,24 @@
1
+ // ── PaymentError ──────────────────────────────────────────────────────────────
2
+ export class PaymentError extends Error {
3
+ constructor(
4
+ public readonly code: string,
5
+ message: string,
6
+ ) {
7
+ super(message);
8
+ this.name = 'PaymentError';
9
+ }
10
+ }
11
+
12
+ // ── IPaymentAdapter ───────────────────────────────────────────────────────────
13
+ export interface PaymentIntentResult {
14
+ id: string;
15
+ clientSecret: string;
16
+ }
17
+
18
+ export interface IPaymentAdapter {
19
+ createPaymentIntent(
20
+ amount: number,
21
+ currency: string,
22
+ metadata: Record<string, string>,
23
+ ): Promise<PaymentIntentResult>;
24
+ }
@@ -0,0 +1,9 @@
1
+ export interface ShippingOption {
2
+ name: string;
3
+ price: number;
4
+ estimatedDays: number;
5
+ }
6
+
7
+ export interface IShippingService {
8
+ calculate(cep: string, items: Array<{ qty: number; weight?: number }>): Promise<ShippingOption[]>;
9
+ }
@@ -0,0 +1,3 @@
1
+ export interface IStorageService {
2
+ upload(key: string, buffer: Buffer, mimetype: string): Promise<string>;
3
+ }
@@ -0,0 +1,4 @@
1
+ export interface ITokenBlacklist {
2
+ add(jti: string, ttlSeconds: number): Promise<void>;
3
+ has(jti: string): Promise<boolean>;
4
+ }
@@ -0,0 +1,18 @@
1
+ export interface TokenPayload {
2
+ sub: string;
3
+ email: string;
4
+ role?: string;
5
+ jti?: string;
6
+ }
7
+
8
+ export interface ITokenService {
9
+ generateAccessToken(payload: TokenPayload): string;
10
+ generateRefreshToken(payload: TokenPayload): string;
11
+ verifyAccessToken(token: string): TokenPayload;
12
+ verifyRefreshToken(token: string): TokenPayload;
13
+ invalidateRefreshToken(token: string): Promise<undefined>;
14
+ isRefreshTokenBlacklisted(token: string): Promise<boolean>;
15
+ generatePasswordResetToken(userId: string): Promise<string>;
16
+ verifyPasswordResetToken(token: string): Promise<string>;
17
+ invalidatePasswordResetToken(token: string): Promise<undefined>;
18
+ }
@@ -0,0 +1,76 @@
1
+ import { compare, hash } from 'bcryptjs';
2
+ import { IUserRepository } from '../../domain/auth/user.repository';
3
+ import { PublicUser } from '../../domain/auth/user.entity';
4
+ import { AppError } from '../../domain/shared/AppError';
5
+
6
+ export interface UpdateProfileDto {
7
+ name?: string;
8
+ email?: string;
9
+ }
10
+
11
+ export interface ChangePasswordDto {
12
+ currentPassword: string;
13
+ newPassword: string;
14
+ }
15
+
16
+ const BCRYPT_ROUNDS = 10;
17
+
18
+ export class ProfileService {
19
+ constructor(private readonly userRepository: IUserRepository) {}
20
+
21
+ async getProfile(userId: string): Promise<PublicUser> {
22
+ const user = await this.userRepository.findById(userId);
23
+ if (!user) throw new AppError('Usuário não encontrado', 404);
24
+ return user.toPublic();
25
+ }
26
+
27
+ async updateProfile(userId: string, dto: UpdateProfileDto): Promise<PublicUser> {
28
+ const user = await this.userRepository.findById(userId);
29
+ if (!user) throw new AppError('Usuário não encontrado', 404);
30
+
31
+ let updated = user;
32
+
33
+ if (dto.name !== undefined) {
34
+ const name = dto.name.trim();
35
+ if (name.length < 2) throw new AppError('Nome deve ter pelo menos 2 caracteres', 400);
36
+ updated = updated.withName(name);
37
+ }
38
+
39
+ if (dto.email !== undefined) {
40
+ const email = dto.email.toLowerCase();
41
+ if (email !== user.email) {
42
+ const existing = await this.userRepository.findByEmail(email);
43
+ if (existing) throw new AppError('Email já está em uso', 409);
44
+ }
45
+ updated = updated.withEmail(email);
46
+ }
47
+
48
+ const saved = await this.userRepository.update(updated);
49
+ return saved.toPublic();
50
+ }
51
+
52
+ async changePassword(userId: string, dto: ChangePasswordDto): Promise<void> {
53
+ const user = await this.userRepository.findById(userId);
54
+ if (!user) throw new AppError('Usuário não encontrado', 404);
55
+
56
+ const valid = await compare(dto.currentPassword, user.passwordHash);
57
+ if (!valid) throw new AppError('Senha atual incorreta', 401);
58
+
59
+ if (dto.newPassword.length < 8) {
60
+ throw new AppError('Nova senha deve ter pelo menos 8 caracteres', 400);
61
+ }
62
+
63
+ const newHash = await hash(dto.newPassword, BCRYPT_ROUNDS);
64
+ await this.userRepository.update(user.withPasswordHash(newHash));
65
+ }
66
+
67
+ async deleteAccount(userId: string, password: string): Promise<void> {
68
+ const user = await this.userRepository.findById(userId);
69
+ if (!user) throw new AppError('Usuário não encontrado', 404);
70
+
71
+ const valid = await compare(password, user.passwordHash);
72
+ if (!valid) throw new AppError('Senha incorreta', 401);
73
+
74
+ await this.userRepository.delete(userId);
75
+ }
76
+ }
@@ -0,0 +1,109 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { z } from 'zod';
3
+ import { AppError } from '../shared/AppError';
4
+
5
+ // ── Types ─────────────────────────────────────────────────────────────────────
6
+ export type UserRole = 'CUSTOMER' | 'ADMIN';
7
+
8
+ export interface UserProps {
9
+ id: string;
10
+ name: string;
11
+ email: string;
12
+ passwordHash: string;
13
+ role: UserRole;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ }
17
+
18
+ export interface CreateUserInput {
19
+ name: string;
20
+ email: string;
21
+ passwordHash: string;
22
+ role?: UserRole;
23
+ }
24
+
25
+ export interface PublicUser {
26
+ id: string;
27
+ name: string;
28
+ email: string;
29
+ role: UserRole;
30
+ createdAt: Date;
31
+ // Allow casting to Record<string, unknown> in tests to verify absent keys
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ // ── Validation schemas ────────────────────────────────────────────────────────
36
+ const emailSchema = z.string().email('Email inválido');
37
+ const nameSchema = z.string().trim().min(1, 'Nome não pode ser vazio');
38
+
39
+ // ── Entity ────────────────────────────────────────────────────────────────────
40
+ export class UserEntity {
41
+ private constructor(private readonly props: UserProps) {}
42
+
43
+ // ── Factories ──────────────────────────────────────────────────────────────
44
+ static create(input: CreateUserInput): UserEntity {
45
+ const emailResult = emailSchema.safeParse(input.email);
46
+ if (!emailResult.success) {
47
+ throw new AppError(emailResult.error.errors[0]?.message ?? 'Email inválido', 422);
48
+ }
49
+
50
+ const nameResult = nameSchema.safeParse(input.name);
51
+ if (!nameResult.success) {
52
+ throw new AppError(nameResult.error.errors[0]?.message ?? 'Nome inválido', 422);
53
+ }
54
+
55
+ return new UserEntity({
56
+ id: randomUUID(),
57
+ name: input.name.trim(),
58
+ email: input.email.toLowerCase(),
59
+ passwordHash: input.passwordHash,
60
+ role: input.role ?? 'CUSTOMER',
61
+ createdAt: new Date(),
62
+ updatedAt: new Date(),
63
+ });
64
+ }
65
+
66
+ static reconstitute(props: UserProps): UserEntity {
67
+ return new UserEntity(props);
68
+ }
69
+
70
+ // ── Getters ────────────────────────────────────────────────────────────────
71
+ get id(): string { return this.props.id; }
72
+ get name(): string { return this.props.name; }
73
+ get email(): string { return this.props.email; }
74
+ get passwordHash(): string { return this.props.passwordHash; }
75
+ get role(): UserRole { return this.props.role; }
76
+ get createdAt(): Date { return this.props.createdAt; }
77
+ get updatedAt(): Date { return this.props.updatedAt; }
78
+
79
+ withPasswordHash(hash: string): UserEntity {
80
+ return new UserEntity({ ...this.props, passwordHash: hash, updatedAt: new Date() });
81
+ }
82
+
83
+ withName(name: string): UserEntity {
84
+ return new UserEntity({ ...this.props, name: name.trim(), updatedAt: new Date() });
85
+ }
86
+
87
+ withEmail(email: string): UserEntity {
88
+ return new UserEntity({ ...this.props, email: email.toLowerCase(), updatedAt: new Date() });
89
+ }
90
+
91
+ withRole(role: UserRole): UserEntity {
92
+ return new UserEntity({ ...this.props, role, updatedAt: new Date() });
93
+ }
94
+
95
+ // ── Projections ────────────────────────────────────────────────────────────
96
+ toPublic(): PublicUser {
97
+ return {
98
+ id: this.props.id,
99
+ name: this.props.name,
100
+ email: this.props.email,
101
+ role: this.props.role,
102
+ createdAt: this.props.createdAt,
103
+ };
104
+ }
105
+
106
+ toRecord(): UserProps {
107
+ return { ...this.props };
108
+ }
109
+ }
@@ -0,0 +1,11 @@
1
+ import { UserEntity } from './user.entity';
2
+ import type { UserRole } from './user.entity';
3
+
4
+ export interface IUserRepository {
5
+ findByEmail(email: string): Promise<UserEntity | null>;
6
+ findById(id: string): Promise<UserEntity | null>;
7
+ create(user: UserEntity): Promise<UserEntity>;
8
+ update(user: UserEntity): Promise<UserEntity>;
9
+ delete(id: string): Promise<void>;
10
+ findAll(opts?: { role?: UserRole; cursor?: string; limit?: number }): Promise<{ items: UserEntity[]; nextCursor: string | null }>;
11
+ }
@@ -0,0 +1,136 @@
1
+ import { AppError } from '../shared/AppError';
2
+ import type { CouponEntity } from './coupon.entity';
3
+
4
+ // ── Types ─────────────────────────────────────────────────────────────────────
5
+ export interface CartItemProps {
6
+ variantId: string;
7
+ productId: string;
8
+ name: string;
9
+ sku: string;
10
+ price: number;
11
+ qty: number;
12
+ image: string | null;
13
+ }
14
+
15
+ export interface CartProps {
16
+ id: string;
17
+ userId: string | null;
18
+ sessionId: string | null;
19
+ items: CartItemProps[];
20
+ coupon: CouponEntity | null;
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ }
24
+
25
+ // ── CartEntity ────────────────────────────────────────────────────────────────
26
+ export class CartEntity {
27
+ private constructor(private readonly props: CartProps) {}
28
+
29
+ // ── Factory ───────────────────────────────────────────────────────────────
30
+ static create(userId: string | null, sessionId?: string | null): CartEntity {
31
+ return new CartEntity({
32
+ id: crypto.randomUUID(),
33
+ userId: userId ?? null,
34
+ sessionId: sessionId ?? null,
35
+ items: [],
36
+ coupon: null,
37
+ createdAt: new Date(),
38
+ updatedAt: new Date(),
39
+ });
40
+ }
41
+
42
+ static reconstitute(props: CartProps): CartEntity {
43
+ return new CartEntity({ ...props });
44
+ }
45
+
46
+ // ── Getters ───────────────────────────────────────────────────────────────
47
+ get id(): string { return this.props.id; }
48
+ get userId(): string | null { return this.props.userId; }
49
+ get sessionId(): string | null { return this.props.sessionId; }
50
+ get items(): ReadonlyArray<CartItemProps> { return this.props.items; }
51
+ get coupon(): CouponEntity | null { return this.props.coupon; }
52
+ get createdAt(): Date { return this.props.createdAt; }
53
+
54
+ get subtotal(): number {
55
+ return this.props.items.reduce((sum, item) => sum + item.price * item.qty, 0);
56
+ }
57
+
58
+ get discount(): number {
59
+ if (!this.props.coupon) return 0;
60
+ const coupon = this.props.coupon;
61
+ if (coupon.discountType === 'percent') {
62
+ return Math.round(this.subtotal * coupon.discountValue) / 100;
63
+ }
64
+ return Math.min(coupon.discountValue, this.subtotal);
65
+ }
66
+
67
+ get total(): number {
68
+ return Math.max(0, this.subtotal - this.discount);
69
+ }
70
+
71
+ // ── Operations (immutable) ────────────────────────────────────────────────
72
+ addItem(item: CartItemProps): CartEntity {
73
+ const existing = this.props.items.find((i) => i.variantId === item.variantId);
74
+ let newItems: CartItemProps[];
75
+ if (existing) {
76
+ newItems = this.props.items.map((i) =>
77
+ i.variantId === item.variantId ? { ...i, qty: i.qty + item.qty } : i,
78
+ );
79
+ } else {
80
+ newItems = [...this.props.items, { ...item }];
81
+ }
82
+ return new CartEntity({ ...this.props, items: newItems, updatedAt: new Date() });
83
+ }
84
+
85
+ updateItem(variantId: string, qty: number): CartEntity {
86
+ if (qty <= 0) {
87
+ return this.removeItem(variantId);
88
+ }
89
+ const newItems = this.props.items.map((i) =>
90
+ i.variantId === variantId ? { ...i, qty } : i,
91
+ );
92
+ return new CartEntity({ ...this.props, items: newItems, updatedAt: new Date() });
93
+ }
94
+
95
+ removeItem(variantId: string): CartEntity {
96
+ const newItems = this.props.items.filter((i) => i.variantId !== variantId);
97
+ return new CartEntity({ ...this.props, items: newItems, updatedAt: new Date() });
98
+ }
99
+
100
+ applyCoupon(coupon: CouponEntity): CartEntity {
101
+ if (coupon.isExpired()) {
102
+ throw new AppError('Cupom expirado', 400);
103
+ }
104
+ if (this.subtotal < coupon.minOrderValue) {
105
+ throw new AppError(
106
+ `Valor mínimo para este cupom é R$${coupon.minOrderValue.toFixed(2)}`,
107
+ 400,
108
+ );
109
+ }
110
+ return new CartEntity({ ...this.props, coupon, updatedAt: new Date() });
111
+ }
112
+
113
+ removeCoupon(): CartEntity {
114
+ return new CartEntity({ ...this.props, coupon: null, updatedAt: new Date() });
115
+ }
116
+
117
+ withUserId(userId: string): CartEntity {
118
+ return new CartEntity({ ...this.props, userId, updatedAt: new Date() });
119
+ }
120
+
121
+ // ── Serialization ─────────────────────────────────────────────────────────
122
+ toRecord() {
123
+ return {
124
+ id: this.props.id,
125
+ userId: this.props.userId,
126
+ sessionId: this.props.sessionId,
127
+ items: [...this.props.items],
128
+ couponCode: this.props.coupon?.code ?? null,
129
+ subtotal: this.subtotal,
130
+ discount: this.discount,
131
+ total: this.total,
132
+ createdAt: this.props.createdAt,
133
+ updatedAt: this.props.updatedAt,
134
+ };
135
+ }
136
+ }
@@ -0,0 +1,8 @@
1
+ import { CartEntity } from './cart.entity';
2
+
3
+ export interface ICartRepository {
4
+ findByUserId(userId: string): Promise<CartEntity | null>;
5
+ findBySessionId(sessionId: string): Promise<CartEntity | null>;
6
+ save(cart: CartEntity): Promise<CartEntity>;
7
+ delete(key: { userId?: string; sessionId?: string }): Promise<void>;
8
+ }
@@ -0,0 +1,58 @@
1
+ // ── Types ─────────────────────────────────────────────────────────────────────
2
+ export type DiscountType = 'percent' | 'fixed';
3
+
4
+ export interface CouponProps {
5
+ id: string;
6
+ code: string;
7
+ discountType: DiscountType;
8
+ discountValue: number;
9
+ minOrderValue: number;
10
+ usageLimit: number | null;
11
+ usageCount: number;
12
+ expiresAt: Date | null;
13
+ createdAt: Date;
14
+ }
15
+
16
+ // ── CouponEntity ──────────────────────────────────────────────────────────────
17
+ export class CouponEntity {
18
+ private constructor(private readonly props: CouponProps) {}
19
+
20
+ static create(input: Omit<CouponProps, 'id' | 'usageCount' | 'createdAt'>): CouponEntity {
21
+ return new CouponEntity({
22
+ ...input,
23
+ id: crypto.randomUUID(),
24
+ usageCount: 0,
25
+ createdAt: new Date(),
26
+ });
27
+ }
28
+
29
+ static reconstitute(props: CouponProps): CouponEntity {
30
+ return new CouponEntity({ ...props });
31
+ }
32
+
33
+ // ── Getters ───────────────────────────────────────────────────────────────
34
+ get id(): string { return this.props.id; }
35
+ get code(): string { return this.props.code; }
36
+ get discountType(): DiscountType { return this.props.discountType; }
37
+ get discountValue(): number { return this.props.discountValue; }
38
+ get minOrderValue(): number { return this.props.minOrderValue; }
39
+ get usageLimit(): number | null { return this.props.usageLimit; }
40
+ get usageCount(): number { return this.props.usageCount; }
41
+ get expiresAt(): Date | null { return this.props.expiresAt; }
42
+
43
+ // ── Business rules ────────────────────────────────────────────────────────
44
+ isExpired(): boolean {
45
+ if (!this.props.expiresAt) return false;
46
+ return this.props.expiresAt < new Date();
47
+ }
48
+
49
+ isUsageLimitReached(): boolean {
50
+ if (this.props.usageLimit === null) return false;
51
+ return this.props.usageCount >= this.props.usageLimit;
52
+ }
53
+
54
+ // ── Serialization ─────────────────────────────────────────────────────────
55
+ toRecord(): CouponProps {
56
+ return { ...this.props };
57
+ }
58
+ }