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,174 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { PrismaProductRepository } from '../product.prisma.repository';
3
+ import { PrismaCategoryRepository } from '../category.prisma.repository';
4
+ import { ProductEntity, ProductStatus } from '../product.entity';
5
+ import { CategoryEntity } from '../category.entity';
6
+
7
+ const db = new PrismaClient();
8
+ const productRepo = new PrismaProductRepository(db);
9
+ const categoryRepo = new PrismaCategoryRepository(db);
10
+
11
+ async function cleanDb() {
12
+ await db.productVariant.deleteMany();
13
+ await db.product.deleteMany();
14
+ await db.category.deleteMany();
15
+ }
16
+
17
+ let testCategoryId: string;
18
+
19
+ beforeAll(async () => {
20
+ await db.$connect();
21
+ });
22
+
23
+ afterAll(async () => {
24
+ await cleanDb();
25
+ await db.$disconnect();
26
+ });
27
+
28
+ beforeEach(async () => {
29
+ await cleanDb();
30
+ // Each test gets a fresh category to attach products to
31
+ const cat = await categoryRepo.create(
32
+ CategoryEntity.create({ name: 'Test Category', slug: `cat-${Date.now()}` }),
33
+ );
34
+ testCategoryId = cat.id;
35
+ });
36
+
37
+ let skuCounter = 0;
38
+ function makeProduct(overrides: Partial<{ name: string; slug: string }> = {}) {
39
+ const unique = `${Date.now()}-${++skuCounter}`;
40
+ return ProductEntity.create({
41
+ name: overrides.name ?? 'Test Product',
42
+ slug: overrides.slug ?? `slug-${unique}`,
43
+ description: 'A test product',
44
+ price: 99.99,
45
+ categoryId: testCategoryId,
46
+ images: ['img1.jpg'],
47
+ variants: [
48
+ { sku: `SKU-${unique}`, size: 'M', color: 'red', stock: 10 },
49
+ ],
50
+ });
51
+ }
52
+
53
+ describe('PrismaProductRepository', () => {
54
+ it('create: deve persistir produto com variantes', async () => {
55
+ const product = makeProduct();
56
+ const saved = await productRepo.create(product);
57
+
58
+ expect(saved.id).toBe(product.id);
59
+ expect(saved.name).toBe('Test Product');
60
+ expect(saved.price).toBe(99.99);
61
+ expect(saved.variants).toHaveLength(1);
62
+ expect(saved.variants[0]!.stock).toBe(10);
63
+ });
64
+
65
+ it('findById: deve retornar produto pelo id', async () => {
66
+ const product = makeProduct();
67
+ await productRepo.create(product);
68
+
69
+ const found = await productRepo.findById(product.id);
70
+ expect(found).not.toBeNull();
71
+ expect(found!.id).toBe(product.id);
72
+ });
73
+
74
+ it('findById: deve retornar null para produto deletado', async () => {
75
+ const product = makeProduct();
76
+ await productRepo.create(product);
77
+ await productRepo.softDelete(product.id);
78
+
79
+ const found = await productRepo.findById(product.id);
80
+ expect(found).toBeNull();
81
+ });
82
+
83
+ it('findBySlug: deve retornar produto pelo slug', async () => {
84
+ const product = makeProduct({ slug: 'unique-slug-test' });
85
+ await productRepo.create(product);
86
+
87
+ const found = await productRepo.findBySlug('unique-slug-test');
88
+ expect(found).not.toBeNull();
89
+ expect(found!.slug).toBe('unique-slug-test');
90
+ });
91
+
92
+ it('findAll: deve retornar apenas produtos ACTIVE', async () => {
93
+ const p1 = makeProduct({ name: 'P1', slug: 'p1' });
94
+ const p2 = makeProduct({ name: 'P2', slug: 'p2' });
95
+ await productRepo.create(p1);
96
+ await productRepo.create(p2);
97
+ await productRepo.softDelete(p2.id);
98
+
99
+ const all = await productRepo.findAll();
100
+ expect(all).toHaveLength(1);
101
+ expect(all[0]!.id).toBe(p1.id);
102
+ });
103
+
104
+ it('search: deve filtrar por texto (q)', async () => {
105
+ await productRepo.create(makeProduct({ name: 'Blue Sneaker', slug: 'blue-sneaker' }));
106
+ await productRepo.create(makeProduct({ name: 'Red Hat', slug: 'red-hat' }));
107
+
108
+ const result = await productRepo.search({ q: 'sneaker' });
109
+ expect(result.items).toHaveLength(1);
110
+ expect(result.items[0]!.name).toBe('Blue Sneaker');
111
+ });
112
+
113
+ it('search: deve paginar com cursor', async () => {
114
+ for (let i = 0; i < 3; i++) {
115
+ await productRepo.create(
116
+ makeProduct({ name: `Product ${i}`, slug: `product-${i}-${Date.now()}-${i}` }),
117
+ );
118
+ }
119
+
120
+ const page1 = await productRepo.search({ limit: 2 });
121
+ expect(page1.items).toHaveLength(2);
122
+ expect(page1.nextCursor).not.toBeNull();
123
+
124
+ const page2 = await productRepo.search({ cursor: page1.nextCursor!, limit: 2 });
125
+ expect(page2.items).toHaveLength(1);
126
+ expect(page2.nextCursor).toBeNull();
127
+ });
128
+
129
+ it('updateVariantStock: deve alterar o estoque do variante', async () => {
130
+ const product = makeProduct();
131
+ const saved = await productRepo.create(product);
132
+ const variantId = saved.variants[0]!.id;
133
+
134
+ await productRepo.updateVariantStock(product.id, variantId, 42);
135
+
136
+ const updated = await productRepo.findById(product.id);
137
+ expect(updated!.variants[0]!.stock).toBe(42);
138
+ });
139
+
140
+ it('softDelete: deve marcar status como DELETED sem remover do banco', async () => {
141
+ const product = makeProduct();
142
+ await productRepo.create(product);
143
+
144
+ await productRepo.softDelete(product.id);
145
+
146
+ // findById returns null for DELETED products
147
+ const found = await productRepo.findById(product.id);
148
+ expect(found).toBeNull();
149
+
150
+ // But the row still exists in the database
151
+ const raw = await db.product.findUnique({ where: { id: product.id } });
152
+ expect(raw).not.toBeNull();
153
+ expect(raw!.status).toBe(ProductStatus.DELETED);
154
+ });
155
+
156
+ it('slugExists: deve retornar true para slug existente', async () => {
157
+ const product = makeProduct({ slug: 'existing-slug' });
158
+ await productRepo.create(product);
159
+
160
+ expect(await productRepo.slugExists('existing-slug')).toBe(true);
161
+ expect(await productRepo.slugExists('nonexistent-slug')).toBe(false);
162
+ });
163
+
164
+ it('update: deve persistir alterações no produto', async () => {
165
+ const product = makeProduct({ name: 'Original', slug: 'original' });
166
+ await productRepo.create(product);
167
+
168
+ const changed = product.withUpdates({ name: 'Updated', price: 199.99 });
169
+ const saved = await productRepo.update(changed);
170
+
171
+ expect(saved.name).toBe('Updated');
172
+ expect(saved.price).toBe(199.99);
173
+ });
174
+ });
@@ -0,0 +1,176 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { z } from 'zod';
3
+ import { catalogService } from './catalog.registry';
4
+
5
+ // ── Validation schemas ────────────────────────────────────────────────────────
6
+ const variantSchema = z.object({
7
+ sku: z.string().min(1),
8
+ size: z.string().nullish(),
9
+ color: z.string().nullish(),
10
+ stock: z.number().int().min(0),
11
+ price: z.number().positive().nullish(),
12
+ });
13
+
14
+ const createProductSchema = z.object({
15
+ name: z.string().min(1, 'Nome é obrigatório'),
16
+ description: z.string().nullish(),
17
+ price: z.number({ required_error: 'Preço é obrigatório' }),
18
+ categoryId: z.string().optional(),
19
+ categorySlug: z.string().optional(),
20
+ images: z.array(z.string().url()).default([]),
21
+ variants: z.array(variantSchema).min(1, 'Pelo menos uma variação é necessária'),
22
+ });
23
+
24
+ const updateProductSchema = z.object({
25
+ name: z.string().min(1).optional(),
26
+ description: z.string().nullish(),
27
+ price: z.number().optional(),
28
+ images: z.array(z.string().url()).optional(),
29
+ });
30
+
31
+ const stockUpdateSchema = z.object({
32
+ variantId: z.string().min(1),
33
+ delta: z.number().int(),
34
+ });
35
+
36
+ const listQuerySchema = z.object({
37
+ categorySlug: z.string().optional(),
38
+ categoryId: z.string().optional(),
39
+ minPrice: z.coerce.number().optional(),
40
+ maxPrice: z.coerce.number().optional(),
41
+ q: z.string().optional(),
42
+ sortBy: z.enum(['price_asc', 'price_desc', 'newest']).optional(),
43
+ cursor: z.string().optional(),
44
+ limit: z.coerce.number().int().min(1).max(100).default(20),
45
+ });
46
+
47
+ // ── Controllers ───────────────────────────────────────────────────────────────
48
+ export async function listProducts(req: Request, res: Response, next: NextFunction): Promise<void> {
49
+ const parsed = listQuerySchema.safeParse(req.query);
50
+ if (!parsed.success) {
51
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors });
52
+ return;
53
+ }
54
+
55
+ try {
56
+ // Resolve categorySlug → categoryId if needed
57
+ let categoryId = parsed.data.categoryId;
58
+ if (!categoryId && parsed.data.categorySlug) {
59
+ const cats = await catalogService.listCategories();
60
+ const cat = cats.find((c) => c.slug === parsed.data.categorySlug);
61
+ if (cat) categoryId = cat.id;
62
+ }
63
+
64
+ const result = await catalogService.searchProducts({
65
+ ...parsed.data,
66
+ categoryId,
67
+ });
68
+
69
+ res.status(200).json({
70
+ items: result.items.map((p) => p.toRecord()),
71
+ nextCursor: result.nextCursor,
72
+ });
73
+ } catch (err) {
74
+ next(err);
75
+ }
76
+ }
77
+
78
+ export async function getProduct(req: Request, res: Response, next: NextFunction): Promise<void> {
79
+ try {
80
+ const product = await catalogService.getProductBySlug(req.params['slug'] ?? '');
81
+ res.status(200).json(product.toRecord());
82
+ } catch (err) {
83
+ next(err);
84
+ }
85
+ }
86
+
87
+ export async function createProduct(req: Request, res: Response, next: NextFunction): Promise<void> {
88
+ const parsed = createProductSchema.safeParse(req.body);
89
+ if (!parsed.success) {
90
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors });
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const product = await catalogService.createProduct(parsed.data);
96
+ res.status(201).json(product.toRecord());
97
+ } catch (err) {
98
+ next(err);
99
+ }
100
+ }
101
+
102
+ export async function updateProduct(req: Request, res: Response, next: NextFunction): Promise<void> {
103
+ const parsed = updateProductSchema.safeParse(req.body);
104
+ if (!parsed.success) {
105
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors });
106
+ return;
107
+ }
108
+
109
+ try {
110
+ const product = await catalogService.updateProduct(req.params['id'] ?? '', parsed.data);
111
+ res.status(200).json(product.toRecord());
112
+ } catch (err) {
113
+ next(err);
114
+ }
115
+ }
116
+
117
+ export async function deleteProduct(req: Request, res: Response, next: NextFunction): Promise<void> {
118
+ try {
119
+ await catalogService.deleteProduct(req.params['id'] ?? '');
120
+ res.status(204).send();
121
+ } catch (err) {
122
+ next(err);
123
+ }
124
+ }
125
+
126
+ export async function updateStock(req: Request, res: Response, next: NextFunction): Promise<void> {
127
+ const parsed = stockUpdateSchema.safeParse(req.body);
128
+ if (!parsed.success) {
129
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors });
130
+ return;
131
+ }
132
+
133
+ try {
134
+ const result = await catalogService.updateStock({
135
+ productId: req.params['id'] ?? '',
136
+ variantId: parsed.data.variantId,
137
+ delta: parsed.data.delta,
138
+ });
139
+ res.status(200).json(result);
140
+ } catch (err) {
141
+ next(err);
142
+ }
143
+ }
144
+
145
+ export async function listCategories(_req: Request, res: Response, next: NextFunction): Promise<void> {
146
+ try {
147
+ const cats = await catalogService.listCategories();
148
+ res.status(200).json({ data: cats.map((c) => c.toRecord()) });
149
+ } catch (err) {
150
+ next(err);
151
+ }
152
+ }
153
+
154
+ export async function uploadProductImage(req: Request, res: Response, next: NextFunction): Promise<void> {
155
+ if (!req.file) {
156
+ res.status(400).json({ error: 'Arquivo de imagem obrigatório' });
157
+ return;
158
+ }
159
+
160
+ const ALLOWED_MIMETYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
161
+ if (!ALLOWED_MIMETYPES.includes(req.file.mimetype)) {
162
+ res.status(400).json({ error: 'Apenas imagens JPEG, PNG, GIF ou WebP são aceitas' });
163
+ return;
164
+ }
165
+
166
+ try {
167
+ const product = await catalogService.uploadProductImage(
168
+ req.params['id'] ?? '',
169
+ req.file.buffer,
170
+ req.file.mimetype,
171
+ );
172
+ res.status(200).json(product.toRecord());
173
+ } catch (err) {
174
+ next(err);
175
+ }
176
+ }
@@ -0,0 +1 @@
1
+ export * from '../../infrastructure/config/registry/catalog.registry';
@@ -0,0 +1,38 @@
1
+ import { Router } from 'express';
2
+ import multer from 'multer';
3
+ import { authenticate } from '../../shared/middlewares/authenticate';
4
+ import { authorize } from '../../shared/middlewares/authorize';
5
+ import { validateUuidParam } from '../../shared/validators/uuidParam';
6
+ import * as catalogController from './catalog.controller';
7
+
8
+ const router = Router();
9
+
10
+ // Multer: memory storage, 5 MB limit — MIME validation done in controller
11
+ const imageUpload = multer({
12
+ storage: multer.memoryStorage(),
13
+ limits: { fileSize: 5 * 1024 * 1024 },
14
+ });
15
+
16
+ // ── Public routes ─────────────────────────────────────────────────────────────
17
+ router.get('/', catalogController.listProducts);
18
+ router.get('/:slug', catalogController.getProduct);
19
+
20
+ // ── Admin-only routes ─────────────────────────────────────────────────────────
21
+ router.post('/', authenticate, authorize('ADMIN'), catalogController.createProduct);
22
+ router.put('/:id', authenticate, authorize('ADMIN'), validateUuidParam(), catalogController.updateProduct);
23
+ router.delete('/:id', authenticate, authorize('ADMIN'), validateUuidParam(), catalogController.deleteProduct);
24
+ router.patch('/:id/stock', authenticate, authorize('ADMIN'), validateUuidParam(), catalogController.updateStock);
25
+ router.post(
26
+ '/:id/image',
27
+ authenticate,
28
+ authorize('ADMIN'),
29
+ validateUuidParam(),
30
+ imageUpload.single('image'),
31
+ catalogController.uploadProductImage,
32
+ );
33
+
34
+ export default router;
35
+
36
+ // ── Stand-alone categories router ─────────────────────────────────────────────
37
+ export const categoryRouter = Router();
38
+ categoryRouter.get('/', catalogController.listCategories);
@@ -0,0 +1 @@
1
+ export * from '../../application/catalog/catalog.service';
@@ -0,0 +1 @@
1
+ export * from '../../domain/catalog/category.entity';
@@ -0,0 +1 @@
1
+ export * from '../../infrastructure/persistence/prisma/catalog/category.prisma.repository';
@@ -0,0 +1,2 @@
1
+ export * from '../../domain/catalog/category.repository';
2
+ export * from '../../infrastructure/persistence/in-memory/category.memory.repository';
@@ -0,0 +1 @@
1
+ export * from '../../domain/catalog/product.entity';
@@ -0,0 +1 @@
1
+ export * from '../../infrastructure/persistence/prisma/catalog/product.prisma.repository';
@@ -0,0 +1,2 @@
1
+ export * from '../../domain/catalog/product.repository';
2
+ export * from '../../infrastructure/persistence/in-memory/product.memory.repository';
@@ -0,0 +1,163 @@
1
+ import request from 'supertest';
2
+ import app from '../../../app';
3
+
4
+ /**
5
+ * Checkout Routes — Integration Tests
6
+ * Stripe is NOT called in tests — StripeAdapter is replaced with a mock
7
+ * implementation via the checkout.registry singleton override.
8
+ */
9
+
10
+ let customerToken: string;
11
+ let adminToken: string;
12
+ let productId: string;
13
+ let variantId: string;
14
+
15
+ const CUSTOMER = { name: 'Checkout Customer', email: 'checkout.c@test.com', password: 'pass1234' };
16
+ const ADMIN = { name: 'Checkout Admin', email: 'checkout.a@test.com', password: 'admin1234' };
17
+
18
+ beforeAll(async () => {
19
+ await request(app).post('/api/auth/register').send(CUSTOMER);
20
+
21
+ const { userRepository } = await import('../../auth/auth.registry');
22
+ const { UserEntity } = await import('../../auth/user.entity');
23
+ const { hash } = await import('bcryptjs');
24
+ const adminEntity = UserEntity.create({
25
+ name: ADMIN.name, email: ADMIN.email,
26
+ passwordHash: await hash(ADMIN.password, 10), role: 'ADMIN',
27
+ });
28
+ await userRepository.create(adminEntity);
29
+
30
+ const [cRes, aRes] = await Promise.all([
31
+ request(app).post('/api/auth/login').send({ email: CUSTOMER.email, password: CUSTOMER.password }),
32
+ request(app).post('/api/auth/login').send({ email: ADMIN.email, password: ADMIN.password }),
33
+ ]);
34
+ customerToken = (cRes.body as { accessToken: string }).accessToken;
35
+ adminToken = (aRes.body as { accessToken: string }).accessToken;
36
+
37
+ // Create product via API
38
+ const { catalogRegistry } = await import('../../catalog/catalog.registry');
39
+ const { CategoryEntity } = await import('../../catalog/category.entity');
40
+ await catalogRegistry.categoryRepository.create(
41
+ CategoryEntity.create({ name: 'Checkout Test', slug: 'checkout-test', description: null, parentId: null }),
42
+ );
43
+
44
+ const productRes = await request(app)
45
+ .post('/api/products')
46
+ .set('Authorization', `Bearer ${adminToken}`)
47
+ .send({
48
+ name: 'Produto Checkout',
49
+ price: 100,
50
+ categorySlug: 'checkout-test',
51
+ images: [],
52
+ variants: [{ sku: 'CHK-1', size: 'U', color: 'Azul', stock: 20 }],
53
+ });
54
+ const body = productRes.body as { id: string; variants: Array<{ id: string }> };
55
+ productId = body.id;
56
+ variantId = body.variants[0]!.id;
57
+
58
+ // Override the payment adapter with a mock to avoid real Stripe calls
59
+ const { checkoutRegistry } = await import('../checkout.registry');
60
+ checkoutRegistry.paymentAdapter.createPaymentIntent = jest.fn().mockResolvedValue({
61
+ id: 'pi_test_123',
62
+ clientSecret: 'pi_test_123_secret',
63
+ });
64
+ });
65
+
66
+ // ── POST /api/checkout ────────────────────────────────────────────────────────
67
+ describe('POST /api/checkout', () => {
68
+ it('401: deve exigir autenticação', async () => {
69
+ const res = await request(app).post('/api/checkout').send({});
70
+ expect(res.status).toBe(401);
71
+ });
72
+
73
+ it('400: deve rejeitar carrinho vazio', async () => {
74
+ const res = await request(app)
75
+ .post('/api/checkout')
76
+ .set('Authorization', `Bearer ${customerToken}`)
77
+ .send({ shippingCost: 18.5, shippingAddress: null });
78
+ expect(res.status).toBe(400);
79
+ });
80
+
81
+ it('201: deve criar pedido e retornar { orderId, clientSecret }', async () => {
82
+ // Add item to cart first
83
+ await request(app)
84
+ .post('/api/cart/items')
85
+ .set('Authorization', `Bearer ${customerToken}`)
86
+ .send({ productId, variantId, qty: 1 });
87
+
88
+ const res = await request(app)
89
+ .post('/api/checkout')
90
+ .set('Authorization', `Bearer ${customerToken}`)
91
+ .send({ shippingCost: 18.5, shippingAddress: { street: 'Rua das Flores, 100', city: 'São Paulo', zip: '01310-100' } });
92
+
93
+ expect(res.status).toBe(201);
94
+ expect(res.body).toHaveProperty('orderId');
95
+ expect(res.body).toHaveProperty('clientSecret');
96
+ });
97
+
98
+ it('deve retornar 409 se estoque foi esgotado entre carrinho e checkout', async () => {
99
+ // Put cart item back (previous test cleared cart)
100
+ await request(app)
101
+ .post('/api/cart/items')
102
+ .set('Authorization', `Bearer ${customerToken}`)
103
+ .send({ productId, variantId, qty: 1 });
104
+
105
+ // Exhaust stock directly via stock update API
106
+ await request(app)
107
+ .patch(`/api/products/${productId}/stock`)
108
+ .set('Authorization', `Bearer ${adminToken}`)
109
+ .send({ variantId, delta: -19 }); // bring stock to 0
110
+
111
+ const res = await request(app)
112
+ .post('/api/checkout')
113
+ .set('Authorization', `Bearer ${customerToken}`)
114
+ .send({ shippingCost: 0, shippingAddress: null });
115
+
116
+ expect(res.status).toBe(409);
117
+
118
+ // Restore stock
119
+ await request(app)
120
+ .patch(`/api/products/${productId}/stock`)
121
+ .set('Authorization', `Bearer ${adminToken}`)
122
+ .send({ variantId, delta: 19 });
123
+ });
124
+ });
125
+
126
+ // ── POST /api/checkout/webhook ────────────────────────────────────────────────
127
+ describe('POST /api/checkout/webhook', () => {
128
+ it('400: deve rejeitar webhook sem header stripe-signature', async () => {
129
+ const res = await request(app)
130
+ .post('/api/checkout/webhook')
131
+ .send({ type: 'payment_intent.succeeded' });
132
+ expect(res.status).toBe(400);
133
+ });
134
+
135
+ it('400: deve rejeitar webhook com assinatura inválida', async () => {
136
+ // Override handler with one that throws on bad signature
137
+ const { checkoutRegistry } = await import('../checkout.registry');
138
+ const original = checkoutRegistry.webhookHandler.handleEvent.bind(checkoutRegistry.webhookHandler);
139
+ checkoutRegistry.webhookHandler.handleEvent = jest.fn().mockRejectedValue(
140
+ Object.assign(new Error('Assinatura inválida'), { statusCode: 400 }),
141
+ );
142
+
143
+ const res = await request(app)
144
+ .post('/api/checkout/webhook')
145
+ .set('stripe-signature', 'bad-sig')
146
+ .send(JSON.stringify({ type: 'payment_intent.succeeded' }));
147
+
148
+ expect(res.status).toBe(400);
149
+ checkoutRegistry.webhookHandler.handleEvent = original;
150
+ });
151
+
152
+ it('200: deve processar payment_intent.succeeded com assinatura válida', async () => {
153
+ const { checkoutRegistry } = await import('../checkout.registry');
154
+ checkoutRegistry.webhookHandler.handleEvent = jest.fn().mockResolvedValue(undefined);
155
+
156
+ const res = await request(app)
157
+ .post('/api/checkout/webhook')
158
+ .set('stripe-signature', 'valid-sig')
159
+ .send(JSON.stringify({ type: 'payment_intent.succeeded' }));
160
+
161
+ expect(res.status).toBe(200);
162
+ });
163
+ });