kybernus 3.0.1 → 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 (392) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/ecommerce.d.ts +3 -0
  3. package/dist/cli/commands/ecommerce.d.ts.map +1 -0
  4. package/dist/cli/commands/ecommerce.js +164 -0
  5. package/dist/cli/commands/ecommerce.js.map +1 -0
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/ecommerce/.env.example +10 -0
  10. package/templates/ecommerce/.github/workflows/ci.yml +102 -0
  11. package/templates/ecommerce/.github/workflows/deploy.yml +31 -0
  12. package/templates/ecommerce/.prettierrc +9 -0
  13. package/templates/ecommerce/Dockerfile +54 -0
  14. package/templates/ecommerce/README.md +295 -0
  15. package/templates/ecommerce/apps/api/.env.example +59 -0
  16. package/templates/ecommerce/apps/api/jest.config.ts +50 -0
  17. package/templates/ecommerce/apps/api/jest.integration.config.ts +45 -0
  18. package/templates/ecommerce/apps/api/package.json +59 -0
  19. package/templates/ecommerce/apps/api/prisma/migrations/20260306000137_init/migration.sql +184 -0
  20. package/templates/ecommerce/apps/api/prisma/migrations/migration_lock.toml +3 -0
  21. package/templates/ecommerce/apps/api/prisma/schema.prisma +181 -0
  22. package/templates/ecommerce/apps/api/prisma/seed.ts +159 -0
  23. package/templates/ecommerce/apps/api/src/__tests__/app.test.ts +39 -0
  24. package/templates/ecommerce/apps/api/src/__tests__/globalSetup.ts +34 -0
  25. package/templates/ecommerce/apps/api/src/__tests__/globalTeardown.ts +16 -0
  26. package/templates/ecommerce/apps/api/src/__tests__/setup.db.ts +18 -0
  27. package/templates/ecommerce/apps/api/src/__tests__/setup.env.ts +14 -0
  28. package/templates/ecommerce/apps/api/src/app.ts +133 -0
  29. package/templates/ecommerce/apps/api/src/application/admin/admin-user.service.ts +24 -0
  30. package/templates/ecommerce/apps/api/src/application/admin/dashboard.service.ts +102 -0
  31. package/templates/ecommerce/apps/api/src/application/auth/auth.service.ts +185 -0
  32. package/templates/ecommerce/apps/api/src/application/cart/cart.service.ts +151 -0
  33. package/templates/ecommerce/apps/api/src/application/cart/coupon.service.ts +51 -0
  34. package/templates/ecommerce/apps/api/src/application/catalog/catalog.service.ts +168 -0
  35. package/templates/ecommerce/apps/api/src/application/checkout/checkout.service.ts +114 -0
  36. package/templates/ecommerce/apps/api/src/application/orders/order.service.ts +93 -0
  37. package/templates/ecommerce/apps/api/src/application/ports/email.port.ts +3 -0
  38. package/templates/ecommerce/apps/api/src/application/ports/payment.port.ts +24 -0
  39. package/templates/ecommerce/apps/api/src/application/ports/shipping.port.ts +9 -0
  40. package/templates/ecommerce/apps/api/src/application/ports/storage.port.ts +3 -0
  41. package/templates/ecommerce/apps/api/src/application/ports/token-blacklist.port.ts +4 -0
  42. package/templates/ecommerce/apps/api/src/application/ports/token.port.ts +18 -0
  43. package/templates/ecommerce/apps/api/src/application/profile/profile.service.ts +76 -0
  44. package/templates/ecommerce/apps/api/src/domain/auth/user.entity.ts +109 -0
  45. package/templates/ecommerce/apps/api/src/domain/auth/user.repository.ts +11 -0
  46. package/templates/ecommerce/apps/api/src/domain/cart/cart.entity.ts +136 -0
  47. package/templates/ecommerce/apps/api/src/domain/cart/cart.repository.ts +8 -0
  48. package/templates/ecommerce/apps/api/src/domain/cart/coupon.entity.ts +58 -0
  49. package/templates/ecommerce/apps/api/src/domain/cart/coupon.repository.ts +10 -0
  50. package/templates/ecommerce/apps/api/src/domain/catalog/category.entity.ts +51 -0
  51. package/templates/ecommerce/apps/api/src/domain/catalog/category.repository.ts +10 -0
  52. package/templates/ecommerce/apps/api/src/domain/catalog/product.entity.ts +130 -0
  53. package/templates/ecommerce/apps/api/src/domain/catalog/product.repository.ts +28 -0
  54. package/templates/ecommerce/apps/api/src/domain/checkout/order.entity.ts +121 -0
  55. package/templates/ecommerce/apps/api/src/domain/checkout/order.repository.ts +11 -0
  56. package/templates/ecommerce/apps/api/src/domain/shared/AppError.ts +12 -0
  57. package/templates/ecommerce/apps/api/src/infrastructure/cache/redis.ts +16 -0
  58. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/admin.registry.ts +13 -0
  59. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/auth.registry.ts +34 -0
  60. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/cart.registry.ts +49 -0
  61. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/catalog.registry.ts +24 -0
  62. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/checkout.registry.ts +47 -0
  63. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/orders.registry.ts +6 -0
  64. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/profile.registry.ts +4 -0
  65. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/cart.memory.repository.ts +33 -0
  66. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/category.memory.repository.ts +41 -0
  67. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/coupon.memory.repository.ts +55 -0
  68. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/order.memory.repository.ts +75 -0
  69. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/product.memory.repository.ts +100 -0
  70. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/user.memory.repository.ts +54 -0
  71. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/auth/user.prisma.repository.ts +83 -0
  72. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/category.prisma.repository.ts +69 -0
  73. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/product.prisma.repository.ts +185 -0
  74. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/checkout/order.prisma.repository.ts +149 -0
  75. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma-client.ts +17 -0
  76. package/templates/ecommerce/apps/api/src/infrastructure/services/email/email.registry.ts +18 -0
  77. package/templates/ecommerce/apps/api/src/infrastructure/services/email/ethereal.email.service.ts +38 -0
  78. package/templates/ecommerce/apps/api/src/infrastructure/services/email/noop.email.service.ts +12 -0
  79. package/templates/ecommerce/apps/api/src/infrastructure/services/email/smtp.email.service.ts +36 -0
  80. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe-webhook.handler.ts +83 -0
  81. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe.adapter.ts +39 -0
  82. package/templates/ecommerce/apps/api/src/infrastructure/services/shipping/mock.shipping.service.ts +17 -0
  83. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/in-memory.storage.service.ts +11 -0
  84. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/local-disk.storage.service.ts +27 -0
  85. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/s3.storage.service.ts +52 -0
  86. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/storage.registry.ts +19 -0
  87. package/templates/ecommerce/apps/api/src/infrastructure/services/token/redis.token.blacklist.ts +23 -0
  88. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.blacklist.ts +30 -0
  89. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.service.ts +136 -0
  90. package/templates/ecommerce/apps/api/src/modules/admin/__tests__/admin.routes.integration.test.ts +250 -0
  91. package/templates/ecommerce/apps/api/src/modules/admin/admin.controller.ts +116 -0
  92. package/templates/ecommerce/apps/api/src/modules/admin/admin.registry.ts +1 -0
  93. package/templates/ecommerce/apps/api/src/modules/admin/admin.routes.ts +21 -0
  94. package/templates/ecommerce/apps/api/src/modules/admin/admin.service.ts +1 -0
  95. package/templates/ecommerce/apps/api/src/modules/admin/admin.user.service.ts +1 -0
  96. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.logout.redis.test.ts +104 -0
  97. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.routes.integration.test.ts +211 -0
  98. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.service.unit.test.ts +260 -0
  99. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/email.service.unit.test.ts +94 -0
  100. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/token.blacklist.redis.test.ts +65 -0
  101. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.entity.unit.test.ts +79 -0
  102. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.prisma.repository.test.ts +138 -0
  103. package/templates/ecommerce/apps/api/src/modules/auth/auth.controller.ts +148 -0
  104. package/templates/ecommerce/apps/api/src/modules/auth/auth.registry.ts +1 -0
  105. package/templates/ecommerce/apps/api/src/modules/auth/auth.routes.ts +17 -0
  106. package/templates/ecommerce/apps/api/src/modules/auth/auth.service.ts +1 -0
  107. package/templates/ecommerce/apps/api/src/modules/auth/redis.token.blacklist.ts +1 -0
  108. package/templates/ecommerce/apps/api/src/modules/auth/token.blacklist.ts +2 -0
  109. package/templates/ecommerce/apps/api/src/modules/auth/token.service.ts +2 -0
  110. package/templates/ecommerce/apps/api/src/modules/auth/user.entity.ts +1 -0
  111. package/templates/ecommerce/apps/api/src/modules/auth/user.prisma.repository.ts +1 -0
  112. package/templates/ecommerce/apps/api/src/modules/auth/user.repository.ts +2 -0
  113. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.entity.unit.test.ts +144 -0
  114. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.routes.integration.test.ts +242 -0
  115. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.service.unit.test.ts +151 -0
  116. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/coupon.admin.integration.test.ts +136 -0
  117. package/templates/ecommerce/apps/api/src/modules/cart/cart.controller.ts +94 -0
  118. package/templates/ecommerce/apps/api/src/modules/cart/cart.entity.ts +1 -0
  119. package/templates/ecommerce/apps/api/src/modules/cart/cart.registry.ts +1 -0
  120. package/templates/ecommerce/apps/api/src/modules/cart/cart.repository.ts +2 -0
  121. package/templates/ecommerce/apps/api/src/modules/cart/cart.routes.ts +17 -0
  122. package/templates/ecommerce/apps/api/src/modules/cart/cart.service.ts +1 -0
  123. package/templates/ecommerce/apps/api/src/modules/cart/coupon.entity.ts +1 -0
  124. package/templates/ecommerce/apps/api/src/modules/cart/coupon.repository.ts +2 -0
  125. package/templates/ecommerce/apps/api/src/modules/cart/coupon.service.ts +1 -0
  126. package/templates/ecommerce/apps/api/src/modules/cart/shipping.service.ts +2 -0
  127. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.routes.integration.test.ts +275 -0
  128. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.service.unit.test.ts +223 -0
  129. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.image.integration.test.ts +130 -0
  130. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.prisma.repository.test.ts +174 -0
  131. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.controller.ts +176 -0
  132. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.registry.ts +1 -0
  133. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.routes.ts +38 -0
  134. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.service.ts +1 -0
  135. package/templates/ecommerce/apps/api/src/modules/catalog/category.entity.ts +1 -0
  136. package/templates/ecommerce/apps/api/src/modules/catalog/category.prisma.repository.ts +1 -0
  137. package/templates/ecommerce/apps/api/src/modules/catalog/category.repository.ts +2 -0
  138. package/templates/ecommerce/apps/api/src/modules/catalog/product.entity.ts +1 -0
  139. package/templates/ecommerce/apps/api/src/modules/catalog/product.prisma.repository.ts +1 -0
  140. package/templates/ecommerce/apps/api/src/modules/catalog/product.repository.ts +2 -0
  141. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.routes.integration.test.ts +163 -0
  142. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.service.unit.test.ts +191 -0
  143. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/order.prisma.repository.test.ts +150 -0
  144. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.controller.ts +59 -0
  145. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.registry.ts +1 -0
  146. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.routes.ts +18 -0
  147. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.service.ts +1 -0
  148. package/templates/ecommerce/apps/api/src/modules/checkout/order.entity.ts +1 -0
  149. package/templates/ecommerce/apps/api/src/modules/checkout/order.prisma.repository.ts +1 -0
  150. package/templates/ecommerce/apps/api/src/modules/checkout/order.repository.ts +2 -0
  151. package/templates/ecommerce/apps/api/src/modules/checkout/tax.service.ts +9 -0
  152. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.entity.unit.test.ts +68 -0
  153. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.routes.integration.test.ts +254 -0
  154. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.service.email.unit.test.ts +142 -0
  155. package/templates/ecommerce/apps/api/src/modules/orders/order.controller.ts +96 -0
  156. package/templates/ecommerce/apps/api/src/modules/orders/order.registry.ts +1 -0
  157. package/templates/ecommerce/apps/api/src/modules/orders/order.routes.ts +17 -0
  158. package/templates/ecommerce/apps/api/src/modules/orders/order.service.ts +1 -0
  159. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe-webhook.unit.test.ts +330 -0
  160. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe.adapter.unit.test.ts +84 -0
  161. package/templates/ecommerce/apps/api/src/modules/payment/adapters/stripe.adapter.ts +1 -0
  162. package/templates/ecommerce/apps/api/src/modules/payment/payment.port.ts +1 -0
  163. package/templates/ecommerce/apps/api/src/modules/payment/stripe-webhook.handler.ts +1 -0
  164. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.routes.integration.test.ts +180 -0
  165. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.service.unit.test.ts +187 -0
  166. package/templates/ecommerce/apps/api/src/modules/profile/profile.controller.ts +92 -0
  167. package/templates/ecommerce/apps/api/src/modules/profile/profile.registry.ts +1 -0
  168. package/templates/ecommerce/apps/api/src/modules/profile/profile.routes.ts +14 -0
  169. package/templates/ecommerce/apps/api/src/modules/profile/profile.service.ts +1 -0
  170. package/templates/ecommerce/apps/api/src/presentation/middlewares/authenticate.ts +37 -0
  171. package/templates/ecommerce/apps/api/src/presentation/middlewares/authorize.ts +23 -0
  172. package/templates/ecommerce/apps/api/src/presentation/middlewares/errorHandler.ts +48 -0
  173. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.controller.ts +116 -0
  174. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.routes.ts +21 -0
  175. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.controller.ts +147 -0
  176. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.routes.ts +17 -0
  177. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.controller.ts +94 -0
  178. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.routes.ts +17 -0
  179. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.controller.ts +176 -0
  180. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.routes.ts +38 -0
  181. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.controller.ts +59 -0
  182. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.routes.ts +18 -0
  183. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.controller.ts +96 -0
  184. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.routes.ts +17 -0
  185. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.controller.ts +92 -0
  186. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.routes.ts +14 -0
  187. package/templates/ecommerce/apps/api/src/presentation/validators/uuidParam.ts +20 -0
  188. package/templates/ecommerce/apps/api/src/server.ts +47 -0
  189. package/templates/ecommerce/apps/api/src/shared/__tests__/uuid.validation.test.ts +111 -0
  190. package/templates/ecommerce/apps/api/src/shared/errors/AppError.ts +1 -0
  191. package/templates/ecommerce/apps/api/src/shared/infra/email/EtherealEmailService.ts +1 -0
  192. package/templates/ecommerce/apps/api/src/shared/infra/email/IEmailService.ts +1 -0
  193. package/templates/ecommerce/apps/api/src/shared/infra/email/NoopEmailService.ts +1 -0
  194. package/templates/ecommerce/apps/api/src/shared/infra/email/SmtpEmailService.ts +1 -0
  195. package/templates/ecommerce/apps/api/src/shared/infra/email/__tests__/ethereal.email.integration.test.ts +32 -0
  196. package/templates/ecommerce/apps/api/src/shared/infra/email/email.registry.ts +1 -0
  197. package/templates/ecommerce/apps/api/src/shared/infra/prisma.ts +1 -0
  198. package/templates/ecommerce/apps/api/src/shared/infra/redis.ts +1 -0
  199. package/templates/ecommerce/apps/api/src/shared/infra/storage/IStorageService.ts +1 -0
  200. package/templates/ecommerce/apps/api/src/shared/infra/storage/InMemoryStorageService.ts +1 -0
  201. package/templates/ecommerce/apps/api/src/shared/infra/storage/LocalDiskStorageService.ts +1 -0
  202. package/templates/ecommerce/apps/api/src/shared/infra/storage/S3StorageService.ts +1 -0
  203. package/templates/ecommerce/apps/api/src/shared/infra/storage/__tests__/s3.storage.unit.test.ts +73 -0
  204. package/templates/ecommerce/apps/api/src/shared/infra/storage/storage.registry.ts +1 -0
  205. package/templates/ecommerce/apps/api/src/shared/middlewares/authenticate.ts +1 -0
  206. package/templates/ecommerce/apps/api/src/shared/middlewares/authorize.ts +1 -0
  207. package/templates/ecommerce/apps/api/src/shared/middlewares/errorHandler.ts +1 -0
  208. package/templates/ecommerce/apps/api/src/shared/validators/uuidParam.ts +1 -0
  209. package/templates/ecommerce/apps/api/tsconfig.json +15 -0
  210. package/templates/ecommerce/apps/web/.env.example +8 -0
  211. package/templates/ecommerce/apps/web/index.html +19 -0
  212. package/templates/ecommerce/apps/web/jest.config.ts +45 -0
  213. package/templates/ecommerce/apps/web/package.json +38 -0
  214. package/templates/ecommerce/apps/web/src/App.tsx +133 -0
  215. package/templates/ecommerce/apps/web/src/__mocks__/fileMock.ts +1 -0
  216. package/templates/ecommerce/apps/web/src/__mocks__/styleMock.ts +1 -0
  217. package/templates/ecommerce/apps/web/src/index.css +159 -0
  218. package/templates/ecommerce/apps/web/src/main.tsx +13 -0
  219. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/CouponsAdminPage.test.tsx +134 -0
  220. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/DashboardPage.test.tsx +65 -0
  221. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/OrdersAdminPage.test.tsx +79 -0
  222. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/ProductsAdminPage.test.tsx +84 -0
  223. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/UsersAdminPage.test.tsx +85 -0
  224. package/templates/ecommerce/apps/web/src/modules/admin/pages/CouponsAdminPage.tsx +179 -0
  225. package/templates/ecommerce/apps/web/src/modules/admin/pages/DashboardPage.tsx +58 -0
  226. package/templates/ecommerce/apps/web/src/modules/admin/pages/OrdersAdminPage.tsx +178 -0
  227. package/templates/ecommerce/apps/web/src/modules/admin/pages/ProductsAdminPage.tsx +444 -0
  228. package/templates/ecommerce/apps/web/src/modules/admin/pages/UsersAdminPage.tsx +87 -0
  229. package/templates/ecommerce/apps/web/src/modules/auth/LoginForm.tsx +91 -0
  230. package/templates/ecommerce/apps/web/src/modules/auth/RegisterForm.tsx +109 -0
  231. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ForgotPasswordPage.test.tsx +42 -0
  232. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/LoginForm.test.tsx +76 -0
  233. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/RegisterForm.test.tsx +62 -0
  234. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ResetPasswordPage.test.tsx +66 -0
  235. package/templates/ecommerce/apps/web/src/modules/auth/pages/ForgotPasswordPage.tsx +100 -0
  236. package/templates/ecommerce/apps/web/src/modules/auth/pages/LoginPage.tsx +39 -0
  237. package/templates/ecommerce/apps/web/src/modules/auth/pages/RegisterPage.tsx +39 -0
  238. package/templates/ecommerce/apps/web/src/modules/auth/pages/ResetPasswordPage.tsx +110 -0
  239. package/templates/ecommerce/apps/web/src/modules/auth/useAuthStore.ts +141 -0
  240. package/templates/ecommerce/apps/web/src/modules/cart/__tests__/CartPage.test.tsx +111 -0
  241. package/templates/ecommerce/apps/web/src/modules/cart/pages/CartPage.tsx +313 -0
  242. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductCard.test.tsx +59 -0
  243. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductFilters.test.tsx +56 -0
  244. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductCard.tsx +78 -0
  245. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductFilters.tsx +104 -0
  246. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductDetailPage.tsx +179 -0
  247. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductListPage.tsx +100 -0
  248. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/CheckoutPage.test.tsx +159 -0
  249. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/StripePaymentForm.test.tsx +79 -0
  250. package/templates/ecommerce/apps/web/src/modules/checkout/components/StripePaymentForm.tsx +55 -0
  251. package/templates/ecommerce/apps/web/src/modules/checkout/hooks/useCheckout.ts +56 -0
  252. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutPage.tsx +344 -0
  253. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutSuccessPage.tsx +12 -0
  254. package/templates/ecommerce/apps/web/src/modules/legal/pages/PrivacyPolicyPage.tsx +207 -0
  255. package/templates/ecommerce/apps/web/src/modules/legal/pages/TermsOfServicePage.tsx +175 -0
  256. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderDetailPage.test.tsx +75 -0
  257. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderHistoryPage.test.tsx +87 -0
  258. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderDetailPage.tsx +73 -0
  259. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderHistoryPage.tsx +97 -0
  260. package/templates/ecommerce/apps/web/src/modules/profile/__tests__/ProfilePage.test.tsx +150 -0
  261. package/templates/ecommerce/apps/web/src/modules/profile/pages/ProfilePage.tsx +275 -0
  262. package/templates/ecommerce/apps/web/src/setupTests.ts +10 -0
  263. package/templates/ecommerce/apps/web/src/shared/components/CookieConsent.tsx +108 -0
  264. package/templates/ecommerce/apps/web/src/shared/components/ErrorBoundary.tsx +112 -0
  265. package/templates/ecommerce/apps/web/src/shared/components/Layout.tsx +143 -0
  266. package/templates/ecommerce/apps/web/src/shared/components/ProtectedRoute.tsx +21 -0
  267. package/templates/ecommerce/apps/web/src/shared/config/siteConfig.ts +57 -0
  268. package/templates/ecommerce/apps/web/src/shared/hooks/usePageTitle.ts +16 -0
  269. package/templates/ecommerce/apps/web/src/shared/lib/apiFetch.ts +16 -0
  270. package/templates/ecommerce/apps/web/src/shared/pages/NotFoundPage.tsx +42 -0
  271. package/templates/ecommerce/apps/web/src/shared/theme/ThemeProvider.tsx +45 -0
  272. package/templates/ecommerce/apps/web/src/shared/theme/__tests__/ThemeProvider.test.tsx +78 -0
  273. package/templates/ecommerce/apps/web/src/shared/theme/createTheme.ts +58 -0
  274. package/templates/ecommerce/apps/web/src/shared/theme/tokens.ts +81 -0
  275. package/templates/ecommerce/apps/web/src/vite-env.d.ts +1 -0
  276. package/templates/ecommerce/apps/web/tsconfig.jest.json +12 -0
  277. package/templates/ecommerce/apps/web/tsconfig.json +25 -0
  278. package/templates/ecommerce/apps/web/tsconfig.node.json +11 -0
  279. package/templates/ecommerce/apps/web/vite.config.ts +30 -0
  280. package/templates/ecommerce/docker-compose.yml +85 -0
  281. package/templates/ecommerce/package-lock.json +11255 -0
  282. package/templates/ecommerce/package.json +27 -0
  283. package/templates/ecommerce/packages/shared-types/package.json +13 -0
  284. package/templates/ecommerce/packages/shared-types/src/index.ts +3 -0
  285. package/templates/ecommerce/packages/shared-types/src/theme.ts +44 -0
  286. package/templates/ecommerce/packages/shared-types/tsconfig.json +11 -0
  287. package/templates/ecommerce/scripts/customize.sh +201 -0
  288. package/templates/ecommerce/tsconfig.json +14 -0
  289. package/templates/java-spring/clean/.gitignore.hbs +72 -0
  290. package/templates/java-spring/clean/docker-compose.yml.hbs +6 -3
  291. package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +21 -17
  292. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/entity/UserEntity.java.hbs +52 -0
  293. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/repository/JpaUserRepository.java.hbs +12 -0
  294. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/JwtAuthenticationFilter.java.hbs +64 -0
  295. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/SecurityConfig.java.hbs +36 -0
  296. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/stripe/StripeGateway.java.hbs +63 -0
  297. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +6 -7
  298. package/templates/java-spring/hexagonal/.gitignore.hbs +72 -0
  299. package/templates/java-spring/hexagonal/docker-compose.yml.hbs +6 -3
  300. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/JwtFilter.java.hbs +71 -0
  301. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/SecurityConfig.java.hbs +35 -0
  302. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +3 -3
  303. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +4 -4
  304. package/templates/java-spring/mvc/.gitignore.hbs +72 -0
  305. package/templates/java-spring/mvc/docker-compose.yml.hbs +6 -3
  306. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/config/SecurityConfig.java.hbs +13 -12
  307. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/AuthController.java.hbs +9 -8
  308. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +5 -6
  309. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +3 -3
  310. package/templates/java-spring/mvc/src/main/resources/application.yml.hbs +29 -26
  311. package/templates/nestjs/clean/.gitignore.hbs +42 -0
  312. package/templates/nestjs/clean/Dockerfile.hbs +6 -3
  313. package/templates/nestjs/clean/docker-compose.yml.hbs +1 -11
  314. package/templates/nestjs/clean/src/app.module.ts.hbs +2 -1
  315. package/templates/nestjs/clean/src/application/payment.service.ts.hbs +72 -72
  316. package/templates/nestjs/clean/src/domain/entities/user.entity.ts.hbs +2 -2
  317. package/templates/nestjs/clean/src/domain/repositories/user.repository.ts.hbs +2 -2
  318. package/templates/nestjs/clean/src/infrastructure/database/repositories/prisma.user.repository.ts.hbs +18 -18
  319. package/templates/nestjs/clean/src/infrastructure/http/health.controller.ts.hbs +9 -0
  320. package/templates/nestjs/clean/src/main.ts.hbs +1 -4
  321. package/templates/nestjs/clean/src/payment.module.ts.hbs +12 -12
  322. package/templates/nestjs/hexagonal/.gitignore.hbs +42 -0
  323. package/templates/nestjs/hexagonal/Dockerfile.hbs +6 -3
  324. package/templates/nestjs/hexagonal/docker-compose.yml.hbs +1 -11
  325. package/templates/nestjs/hexagonal/src/adapters/inbound/health.controller.ts.hbs +9 -0
  326. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -1
  327. package/templates/nestjs/hexagonal/src/core/domain/user.entity.ts.hbs +6 -6
  328. package/templates/nestjs/hexagonal/src/core/ports/ports.ts.hbs +4 -4
  329. package/templates/nestjs/hexagonal/src/main.ts.hbs +1 -4
  330. package/templates/nestjs/mvc/.gitignore.hbs +42 -0
  331. package/templates/nestjs/mvc/Dockerfile.hbs +6 -3
  332. package/templates/nestjs/mvc/docker-compose.yml.hbs +1 -11
  333. package/templates/nestjs/mvc/src/auth/auth.controller.ts.hbs +11 -1
  334. package/templates/nestjs/mvc/src/auth/auth.service.ts.hbs +3 -1
  335. package/templates/nestjs/mvc/src/controllers/health.controller.ts.hbs +6 -6
  336. package/templates/nestjs/mvc/src/main.ts.hbs +1 -4
  337. package/templates/nestjs/mvc/src/models/create-item.dto.ts.hbs +5 -2
  338. package/templates/nestjs/mvc/src/prisma/prisma.service.ts.hbs +1 -0
  339. package/templates/nextjs/mvc/.gitignore.hbs +42 -0
  340. package/templates/nextjs/mvc/Dockerfile.hbs +23 -8
  341. package/templates/nextjs/mvc/docker-compose.yml.hbs +1 -1
  342. package/templates/nodejs-express/clean/.gitignore.hbs +42 -0
  343. package/templates/nodejs-express/clean/Dockerfile.hbs +6 -1
  344. package/templates/nodejs-express/clean/docker-compose.yml.hbs +2 -2
  345. package/templates/nodejs-express/clean/package.json.hbs +69 -69
  346. package/templates/nodejs-express/clean/src/config.ts.hbs +11 -0
  347. package/templates/nodejs-express/clean/src/domain/entities/User.ts.hbs +46 -8
  348. package/templates/nodejs-express/hexagonal/.gitignore.hbs +42 -0
  349. package/templates/nodejs-express/hexagonal/Dockerfile.hbs +1 -1
  350. package/templates/nodejs-express/hexagonal/docker-compose.yml.hbs +2 -2
  351. package/templates/nodejs-express/hexagonal/package.json.hbs +69 -69
  352. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +21 -38
  353. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts.hbs +2 -0
  354. package/templates/nodejs-express/hexagonal/src/config.ts.hbs +9 -0
  355. package/templates/nodejs-express/hexagonal/src/core/AuthService.ts.hbs +5 -5
  356. package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +7 -22
  357. package/templates/nodejs-express/hexagonal/src/core/domain/entities/User.ts.hbs +24 -4
  358. package/templates/nodejs-express/mvc/.gitignore.hbs +42 -0
  359. package/templates/nodejs-express/mvc/package.json.hbs +67 -67
  360. package/templates/python-fastapi/clean/.gitignore.hbs +76 -0
  361. package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +3 -3
  362. package/templates/python-fastapi/clean/app/config.py.hbs +6 -7
  363. package/templates/python-fastapi/clean/app/domain/usecases/login_user.py.hbs +15 -0
  364. package/templates/python-fastapi/clean/app/infrastructure/http/auth_controller.py.hbs +40 -6
  365. package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +5 -4
  366. package/templates/python-fastapi/clean/app/infrastructure/security/jwt.py.hbs +23 -0
  367. package/templates/python-fastapi/clean/app/main.py.hbs +3 -0
  368. package/templates/python-fastapi/clean/docker-compose.yml.hbs +5 -12
  369. package/templates/python-fastapi/clean/requirements.txt.hbs +3 -0
  370. package/templates/python-fastapi/hexagonal/.gitignore.hbs +76 -0
  371. package/templates/python-fastapi/hexagonal/app/adapters/inbound/http_adapter.py.hbs +6 -9
  372. package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +4 -3
  373. package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +30 -19
  374. package/templates/python-fastapi/hexagonal/app/config.py.hbs +14 -4
  375. package/templates/python-fastapi/hexagonal/app/core/domain/user.py.hbs +3 -1
  376. package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +28 -18
  377. package/templates/python-fastapi/hexagonal/app/core/ports/__init__.py.hbs +3 -0
  378. package/templates/python-fastapi/hexagonal/app/core/ports/user_repository.py.hbs +15 -0
  379. package/templates/python-fastapi/hexagonal/app/infrastructure/database/session.py.hbs +7 -0
  380. package/templates/python-fastapi/hexagonal/app/infrastructure/database/user_repository.py.hbs +53 -0
  381. package/templates/python-fastapi/hexagonal/app/infrastructure/security/__init__.py.hbs +0 -0
  382. package/templates/python-fastapi/hexagonal/app/infrastructure/security/adapters.py.hbs +23 -0
  383. package/templates/python-fastapi/hexagonal/app/infrastructure/security/jwt.py.hbs +23 -0
  384. package/templates/python-fastapi/hexagonal/docker-compose.yml.hbs +5 -12
  385. package/templates/python-fastapi/hexagonal/requirements.txt.hbs +4 -0
  386. package/templates/python-fastapi/mvc/.gitignore.hbs +76 -0
  387. package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +3 -17
  388. package/templates/python-fastapi/mvc/app/middleware/security.py.hbs +24 -3
  389. package/templates/python-fastapi/mvc/app/schemas/item.py.hbs +3 -1
  390. package/templates/python-fastapi/mvc/docker-compose.yml.hbs +5 -12
  391. package/templates/python-fastapi/mvc/requirements.txt.hbs +3 -1
  392. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts +0 -5
@@ -0,0 +1,79 @@
1
+ /**
2
+ * StripePaymentForm — Frontend Tests
3
+ * @stripe/react-stripe-js mocked via __mocks__ pattern.
4
+ */
5
+
6
+ jest.mock('@stripe/react-stripe-js', () => ({
7
+ Elements: ({ children }: { children: React.ReactNode }) => <>{children}</>,
8
+ CardElement: () => <div data-testid="card-element">CardElement mock</div>,
9
+ useStripe: () => mockStripe,
10
+ useElements: () => mockElements,
11
+ }));
12
+
13
+ import React from 'react';
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ import userEvent from '@testing-library/user-event';
16
+ import { StripePaymentForm } from '../components/StripePaymentForm';
17
+
18
+ const mockConfirmCardPayment = jest.fn();
19
+ const mockStripe = { confirmCardPayment: mockConfirmCardPayment };
20
+ const mockElements = { getElement: jest.fn().mockReturnValue({}) };
21
+
22
+ function renderForm(props: Partial<React.ComponentProps<typeof StripePaymentForm>> = {}) {
23
+ const defaults = {
24
+ clientSecret: 'pi_test_secret',
25
+ onSuccess: jest.fn(),
26
+ onError: jest.fn(),
27
+ ...props,
28
+ };
29
+ return { ...render(<StripePaymentForm {...defaults} />), ...defaults };
30
+ }
31
+
32
+ describe('StripePaymentForm', () => {
33
+ beforeEach(() => jest.clearAllMocks());
34
+
35
+ it('deve renderizar o CardElement do Stripe', () => {
36
+ renderForm();
37
+ expect(screen.getByTestId('card-element')).toBeInTheDocument();
38
+ });
39
+
40
+ it('deve chamar stripe.confirmCardPayment com clientSecret ao submeter', async () => {
41
+ mockConfirmCardPayment.mockResolvedValue({ paymentIntent: { status: 'succeeded' } });
42
+ const { onSuccess } = renderForm({ clientSecret: 'pi_secret_abc' });
43
+ const user = userEvent.setup();
44
+
45
+ await user.click(screen.getByRole('button', { name: /confirmar pagamento/i }));
46
+
47
+ await waitFor(() =>
48
+ expect(mockConfirmCardPayment).toHaveBeenCalledWith('pi_secret_abc', expect.any(Object)),
49
+ );
50
+ expect(onSuccess).toHaveBeenCalled();
51
+ });
52
+
53
+ it('deve exibir spinner enquanto pagamento é processado', async () => {
54
+ // Never resolves during this test
55
+ mockConfirmCardPayment.mockImplementation(() => new Promise(() => {}));
56
+ renderForm();
57
+ const user = userEvent.setup();
58
+
59
+ await user.click(screen.getByRole('button', { name: /confirmar pagamento/i }));
60
+
61
+ expect(screen.getByRole('button', { name: /confirmar pagamento/i })).toBeDisabled();
62
+ // Spinner or loading indicator
63
+ expect(screen.getByRole('button', { name: /confirmar pagamento/i }).hasAttribute('disabled')).toBe(true);
64
+ });
65
+
66
+ it('deve exibir mensagem de erro do Stripe em português', async () => {
67
+ mockConfirmCardPayment.mockResolvedValue({
68
+ error: { code: 'card_declined', message: 'Your card was declined.' },
69
+ });
70
+ const { onError } = renderForm();
71
+ const user = userEvent.setup();
72
+
73
+ await user.click(screen.getByRole('button', { name: /confirmar pagamento/i }));
74
+
75
+ await waitFor(() => expect(onError).toHaveBeenCalled());
76
+ // Portuguese error message should be displayed
77
+ expect(screen.getByText(/cart[aã]o recusado|recusado/i)).toBeInTheDocument();
78
+ });
79
+ });
@@ -0,0 +1,55 @@
1
+ import { useState } from 'react';
2
+ import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
3
+
4
+ const errorMessages: Record<string, string> = {
5
+ card_declined: 'Cartão recusado',
6
+ expired_card: 'Cartão expirado',
7
+ incorrect_cvc: 'CVC incorreto',
8
+ insufficient_funds: 'Saldo insuficiente',
9
+ };
10
+
11
+ export interface StripePaymentFormProps {
12
+ clientSecret: string;
13
+ onSuccess: () => void;
14
+ onError: (err: Error) => void;
15
+ }
16
+
17
+ export function StripePaymentForm({ clientSecret, onSuccess, onError }: StripePaymentFormProps) {
18
+ const stripe = useStripe();
19
+ const elements = useElements();
20
+ const [processing, setProcessing] = useState(false);
21
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
22
+
23
+ async function handleSubmit(e: React.FormEvent) {
24
+ e.preventDefault();
25
+ if (!stripe || !elements) return;
26
+
27
+ setProcessing(true);
28
+ setErrorMessage(null);
29
+
30
+ const cardElement = elements.getElement(CardElement);
31
+ const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
32
+ payment_method: { card: cardElement! },
33
+ });
34
+
35
+ setProcessing(false);
36
+
37
+ if (error) {
38
+ const msg = errorMessages[error.code ?? ''] ?? error.message ?? 'Erro ao processar pagamento';
39
+ setErrorMessage(msg);
40
+ onError(new Error(msg));
41
+ } else if (paymentIntent?.status === 'succeeded') {
42
+ onSuccess();
43
+ }
44
+ }
45
+
46
+ return (
47
+ <form onSubmit={handleSubmit}>
48
+ <CardElement />
49
+ {errorMessage && <p>{errorMessage}</p>}
50
+ <button type="submit" disabled={processing || !stripe}>
51
+ Confirmar pagamento
52
+ </button>
53
+ </form>
54
+ );
55
+ }
@@ -0,0 +1,56 @@
1
+ import { useState } from 'react';
2
+ import { apiFetch } from '../../../shared/lib/apiFetch';
3
+
4
+ interface CartItem {
5
+ variantId: string;
6
+ productId: string;
7
+ name: string;
8
+ sku: string;
9
+ price: number;
10
+ qty: number;
11
+ image: string | null;
12
+ }
13
+
14
+ export interface Cart {
15
+ items: CartItem[];
16
+ subtotal: number;
17
+ discount: number;
18
+ total: number;
19
+ couponCode: string | null;
20
+ }
21
+
22
+ interface CheckoutDto {
23
+ shippingCost: number;
24
+ shippingAddress: Record<string, string> | null;
25
+ }
26
+
27
+ interface CheckoutResult {
28
+ orderId: string;
29
+ clientSecret: string;
30
+ }
31
+
32
+ export function useCheckout() {
33
+ const [cart] = useState<Cart | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ async function checkout(dto: CheckoutDto): Promise<CheckoutResult> {
38
+ setIsLoading(true);
39
+ setError(null);
40
+ try {
41
+ return await apiFetch<CheckoutResult>('/api/checkout', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify(dto),
45
+ });
46
+ } catch (err) {
47
+ const msg = err instanceof Error ? err.message : 'Erro ao criar pedido';
48
+ setError(msg);
49
+ throw new Error(msg);
50
+ } finally {
51
+ setIsLoading(false);
52
+ }
53
+ }
54
+
55
+ return { cart, checkout, isLoading, error };
56
+ }
@@ -0,0 +1,344 @@
1
+ import { useState, useEffect } from 'react';
2
+ import type React from 'react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
5
+ import { loadStripe, type StripeElementsOptions } from '@stripe/stripe-js';
6
+ import { useCheckout, type Cart } from '../hooks/useCheckout';
7
+ import { apiFetch } from '../../../shared/lib/apiFetch';
8
+ import { usePageTitle } from '../../../shared/hooks/usePageTitle';
9
+
10
+ const stripePromise = loadStripe(
11
+ // VITE_STRIPE_PUBLIC_KEY is injected via window.__STRIPE_KEY__ by vite.config.ts (define plugin).
12
+ // Falls back to 'pk_test_dummy' in test environments (loadStripe is fully mocked in tests).
13
+ (typeof window !== 'undefined' && (window as Window & { __STRIPE_KEY__?: string }).__STRIPE_KEY__) ||
14
+ 'pk_test_dummy',
15
+ );
16
+
17
+ type Step = 'address' | 'shipping' | 'payment';
18
+
19
+ function CartSummary({ cart }: { cart: Cart | null }) {
20
+ if (!cart) return null;
21
+ return (
22
+ <div aria-label="Resumo do pedido" style={{ background: '#f9fafb', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1rem 1.25rem', marginBottom: '1.5rem' }}>
23
+ <h3 style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.75rem' }}>Resumo do pedido</h3>
24
+ {cart.items.map((item) => (
25
+ <div key={item.variantId} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.9rem', color: '#374151', marginBottom: '0.375rem' }}>
26
+ <span>{item.name}</span>
27
+ </div>
28
+ ))}
29
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, borderTop: '1px solid #e5e7eb', paddingTop: '0.75rem', marginTop: '0.5rem' }}>
30
+ <span>Subtotal:</span>
31
+ <span style={{ color: '#6366f1' }}>{`R$ ${cart.subtotal.toFixed(2).replace('.', ',')}`}</span>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ // ── PIX next-action types (not exported by @stripe/stripe-js directly) ─────
38
+ interface PixQrCode {
39
+ data: string;
40
+ expires_at: number;
41
+ hosted_instructions_url: string;
42
+ image_url_png: string;
43
+ image_url_svg: string;
44
+ }
45
+
46
+ // ── Boleto next-action types ────────────────────────────────────────────────
47
+ interface BoletoDetails {
48
+ expires_after: number; // Unix timestamp
49
+ hosted_voucher_url: string | null;
50
+ number: string | null; // Formatted barcode digits
51
+ pdf: string | null;
52
+ }
53
+
54
+ // ── BoletoWaiting — rendered when Stripe returns boleto_display_details ──────
55
+ function BoletoWaiting({ boleto, orderId, onSuccess }: { boleto: BoletoDetails; orderId: string; onSuccess: () => void }) {
56
+ const expiryDate = new Date(boleto.expires_after * 1000).toLocaleDateString('pt-BR');
57
+
58
+ // Poll every 30 s — boleto confirmation can take up to 3 business days
59
+ useEffect(() => {
60
+ const poll = setInterval(async () => {
61
+ try {
62
+ const order = await apiFetch<{ status: string }>(`/api/orders/${orderId}`);
63
+ if (order.status === 'PAID') {
64
+ clearInterval(poll);
65
+ onSuccess();
66
+ }
67
+ } catch { /* silently retry */ }
68
+ }, 30_000);
69
+ return () => clearInterval(poll);
70
+ }, [orderId, onSuccess]);
71
+
72
+ const box: React.CSSProperties = { background: '#fff', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1.5rem', maxWidth: 520 };
73
+ const infoBox: React.CSSProperties = { background: '#fffbeb', border: '1px solid #fde68a', borderRadius: '0.5rem', padding: '0.875rem 1rem', marginBottom: '1.25rem', fontSize: '0.875rem', color: '#92400e' };
74
+ const codeBox: React.CSSProperties = { display: 'block', wordBreak: 'break-all', background: '#f3f4f6', borderRadius: '0.5rem', padding: '0.75rem', fontSize: '0.8rem', fontFamily: 'monospace', marginBottom: '1rem', userSelect: 'all' };
75
+ const btn: React.CSSProperties = { display: 'inline-block', padding: '0.5rem 1.25rem', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '0.5rem', fontWeight: 600, cursor: 'pointer', textDecoration: 'none', fontSize: '0.9rem' };
76
+
77
+ return (
78
+ <div style={box}>
79
+ <h2 style={{ marginBottom: '0.5rem', fontSize: '1.1rem' }}>Boleto gerado!</h2>
80
+ <div style={infoBox}>
81
+ ⏳ O pagamento será confirmado em até <strong>3 dias úteis</strong> após o pagamento.<br />
82
+ Você receberá um email de confirmação assim que processar.
83
+ </div>
84
+ {boleto.number && (
85
+ <>
86
+ <p style={{ fontSize: '0.8rem', color: '#6b7280', marginBottom: '0.375rem' }}>Código de barras:</p>
87
+ <code style={codeBox}>{boleto.number}</code>
88
+ </>
89
+ )}
90
+ <p style={{ fontSize: '0.8rem', color: '#6b7280', marginBottom: '1rem' }}>
91
+ Vencimento: <strong style={{ color: '#374151' }}>{expiryDate}</strong>
92
+ </p>
93
+ {boleto.hosted_voucher_url && (
94
+ <a href={boleto.hosted_voucher_url} target="_blank" rel="noopener noreferrer" style={btn}>
95
+ Ver / Imprimir Boleto →
96
+ </a>
97
+ )}
98
+ </div>
99
+ );
100
+ }
101
+
102
+ // ── PixWaiting — rendered when Stripe returns pix_display_qr_code ────────────
103
+ function PixWaiting({ pixData, orderId, onSuccess }: { pixData: PixQrCode; orderId: string; onSuccess: () => void }) {
104
+ const [copied, setCopied] = useState(false);
105
+ const [timeLeft, setTimeLeft] = useState(() => Math.max(0, pixData.expires_at - Math.floor(Date.now() / 1000)));
106
+
107
+ // Countdown timer
108
+ useEffect(() => {
109
+ if (timeLeft <= 0) return;
110
+ const t = setInterval(() => setTimeLeft((s) => Math.max(0, s - 1)), 1000);
111
+ return () => clearInterval(t);
112
+ }, [timeLeft]);
113
+
114
+ // Poll order status every 5 s — navigate to success once PAID
115
+ useEffect(() => {
116
+ const poll = setInterval(async () => {
117
+ try {
118
+ const order = await apiFetch<{ status: string }>(`/api/orders/${orderId}`);
119
+ if (order.status === 'PAID') {
120
+ clearInterval(poll);
121
+ onSuccess();
122
+ }
123
+ } catch { /* silently retry */ }
124
+ }, 5000);
125
+ return () => clearInterval(poll);
126
+ }, [orderId, onSuccess]);
127
+
128
+ function copyCode() {
129
+ navigator.clipboard.writeText(pixData.data).catch(() => {});
130
+ setCopied(true);
131
+ setTimeout(() => setCopied(false), 3000);
132
+ }
133
+
134
+ const mins = Math.floor(timeLeft / 60);
135
+ const secs = (timeLeft % 60).toString().padStart(2, '0');
136
+ const box: React.CSSProperties = { background: '#fff', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1.5rem', maxWidth: 520, textAlign: 'center' };
137
+ const codeBox: React.CSSProperties = { display: 'block', wordBreak: 'break-all', background: '#f3f4f6', borderRadius: '0.5rem', padding: '0.75rem', fontSize: '0.75rem', fontFamily: 'monospace', marginBottom: '0.75rem', userSelect: 'all', textAlign: 'left' };
138
+ const btn: React.CSSProperties = { width: '100%', padding: '0.5rem 1.25rem', background: copied ? '#10b981' : '#6366f1', color: '#fff', border: 'none', borderRadius: '0.5rem', fontWeight: 600, cursor: 'pointer', marginBottom: '1.25rem', fontSize: '0.9rem' };
139
+
140
+ return (
141
+ <div style={box}>
142
+ <h2 style={{ marginBottom: '0.5rem', fontSize: '1.1rem' }}>Pague com PIX</h2>
143
+ <p style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '1.25rem' }}>
144
+ Escaneie o QR code ou copie o código no seu aplicativo de banco.
145
+ </p>
146
+ <img src={pixData.image_url_png} alt="QR Code PIX" style={{ width: 200, height: 200, marginBottom: '1.25rem', border: '1px solid #e5e7eb', borderRadius: '0.5rem' }} />
147
+ <code style={codeBox}>{pixData.data}</code>
148
+ <button type="button" onClick={copyCode} style={btn}>
149
+ {copied ? '✓ Código copiado!' : 'Copiar código PIX'}
150
+ </button>
151
+ {timeLeft > 0 ? (
152
+ <p style={{ fontSize: '0.8rem', color: '#9ca3af' }}>
153
+ Expira em <strong style={{ color: '#374151' }}>{mins}:{secs}</strong> · Aguardando confirmação...
154
+ </p>
155
+ ) : (
156
+ <p style={{ fontSize: '0.8rem', color: '#dc2626' }}>PIX expirado. <a href="/checkout">Tente novamente.</a></p>
157
+ )}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ interface CheckoutInnerProps {
163
+ street: string;
164
+ cep: string;
165
+ orderId: string;
166
+ onPaymentSuccess: () => void;
167
+ }
168
+
169
+ function PaymentStep({ street, cep, orderId, onPaymentSuccess }: CheckoutInnerProps) {
170
+ const stripe = useStripe();
171
+ const elements = useElements();
172
+ const [processing, setProcessing] = useState(false);
173
+ const [paymentError, setPaymentError] = useState<string | null>(null);
174
+ const [pixData, setPixData] = useState<PixQrCode | null>(null);
175
+ const [boletoData, setBoletoData] = useState<BoletoDetails | null>(null);
176
+
177
+ async function handleSubmit(e: React.FormEvent) {
178
+ e.preventDefault();
179
+ if (!stripe || !elements) return;
180
+
181
+ setProcessing(true);
182
+ setPaymentError(null);
183
+
184
+ try {
185
+ const { error, paymentIntent } = await stripe.confirmPayment({
186
+ elements,
187
+ confirmParams: {
188
+ return_url: `${window.location.origin}/checkout/success`,
189
+ shipping: street ? { name: 'Entrega', address: { line1: street, postal_code: cep, country: 'BR' } } : undefined,
190
+ },
191
+ // Cards confirm inline; PIX returns next_action; boleto may redirect
192
+ redirect: 'if_required',
193
+ });
194
+
195
+ if (error) {
196
+ setPaymentError(error.message ?? 'Erro no pagamento');
197
+ } else if (paymentIntent?.status === 'requires_action') {
198
+ type StripeNextAction = {
199
+ type?: string;
200
+ pix_display_qr_code?: PixQrCode;
201
+ boleto_display_details?: BoletoDetails;
202
+ };
203
+ const na = paymentIntent.next_action as StripeNextAction | null;
204
+ if (na?.type === 'pix_display_qr_code' && na.pix_display_qr_code) {
205
+ setPixData(na.pix_display_qr_code);
206
+ } else if (na?.type === 'boleto_display_details' && na.boleto_display_details) {
207
+ setBoletoData(na.boleto_display_details);
208
+ } else {
209
+ // Unknown async method — Stripe will redirect to return_url
210
+ onPaymentSuccess();
211
+ }
212
+ } else {
213
+ onPaymentSuccess();
214
+ }
215
+ } catch (err: unknown) {
216
+ if (err instanceof Error) setPaymentError(err.message);
217
+ } finally {
218
+ setProcessing(false);
219
+ }
220
+ }
221
+
222
+ const card: React.CSSProperties = { background: '#fff', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1.5rem', maxWidth: 520 };
223
+
224
+ if (pixData) {
225
+ return <PixWaiting pixData={pixData} orderId={orderId} onSuccess={onPaymentSuccess} />;
226
+ }
227
+
228
+ if (boletoData) {
229
+ return <BoletoWaiting boleto={boletoData} orderId={orderId} onSuccess={onPaymentSuccess} />;
230
+ }
231
+
232
+ return (
233
+ <form onSubmit={handleSubmit} style={card}>
234
+ <h2 style={{ marginBottom: '1.25rem', fontSize: '1.1rem' }}>Pagamento</h2>
235
+ <div style={{ marginBottom: '1rem' }}>
236
+ {/* PaymentElement auto-shows card, PIX, boleto, etc. based on currency/locale */}
237
+ <PaymentElement options={{ layout: 'tabs' }} />
238
+ </div>
239
+ {paymentError && (
240
+ <p style={{ padding: '0.625rem 0.75rem', background: '#fef2f2', color: '#dc2626', borderRadius: '0.5rem', fontSize: '0.875rem', marginBottom: '1rem' }}>{paymentError}</p>
241
+ )}
242
+ <button type="submit" disabled={processing}>
243
+ {processing ? 'Processando...' : 'Confirmar pagamento'}
244
+ </button>
245
+ </form>
246
+ );
247
+ }
248
+
249
+ function CheckoutInner() {
250
+ usePageTitle('Checkout');
251
+ const navigate = useNavigate();
252
+ const { cart, checkout } = useCheckout();
253
+
254
+ const [step, setStep] = useState<Step>('address');
255
+ const [street, setStreet] = useState('');
256
+ const [cep, setCep] = useState('');
257
+ const [clientSecret, setClientSecret] = useState<string | null>(null);
258
+ const [orderId, setOrderId] = useState<string | null>(null);
259
+ const [creatingIntent, setCreatingIntent] = useState(false);
260
+ const [intentError, setIntentError] = useState<string | null>(null);
261
+ const shippingCost = 18.5;
262
+
263
+ async function goToPayment() {
264
+ setCreatingIntent(true);
265
+ setIntentError(null);
266
+ try {
267
+ const shippingAddress = street ? { street, zip: cep } : null;
268
+ const { clientSecret: cs, orderId: oid } = await checkout({ shippingCost, shippingAddress });
269
+ setClientSecret(cs);
270
+ setOrderId(oid);
271
+ setStep('payment');
272
+ } catch (err: unknown) {
273
+ setIntentError(err instanceof Error ? err.message : 'Erro ao iniciar pagamento');
274
+ } finally {
275
+ setCreatingIntent(false);
276
+ }
277
+ }
278
+
279
+ const card: React.CSSProperties = { background: '#fff', border: '1px solid #e5e7eb', borderRadius: '0.75rem', padding: '1.5rem', maxWidth: 520 };
280
+ const fieldStyle: React.CSSProperties = { marginBottom: '1rem' };
281
+ const nextBtnStyle: React.CSSProperties = { marginTop: '1.25rem', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '0.625rem 1.5rem', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '0.5rem', fontWeight: 600, fontSize: '0.9375rem', cursor: 'pointer' };
282
+ const steps = ['address', 'shipping', 'payment'] as const;
283
+ const stepLabels = ['Endereço', 'Frete', 'Pagamento'];
284
+
285
+ const elementsOptions: StripeElementsOptions | undefined = clientSecret ? { clientSecret, locale: 'pt-BR' } : undefined;
286
+
287
+ return (
288
+ <div style={{ maxWidth: 560 }}>
289
+ <h1 style={{ marginBottom: '1.25rem' }}>Checkout</h1>
290
+
291
+ {/* Step indicator */}
292
+ <div style={{ display: 'flex', gap: '0', marginBottom: '1.75rem' }}>
293
+ {steps.map((s, i) => (
294
+ <div key={s} style={{ flex: 1, textAlign: 'center', fontSize: '0.8rem', fontWeight: 600, padding: '0.5rem', borderBottom: `2.5px solid ${step === s ? '#6366f1' : '#e5e7eb'}`, color: step === s ? '#6366f1' : '#9ca3af' }}>
295
+ {i + 1}. {stepLabels[i]}
296
+ </div>
297
+ ))}
298
+ </div>
299
+
300
+ <CartSummary cart={cart} />
301
+
302
+ {step === 'address' && (
303
+ <div style={card}>
304
+ <h2 style={{ marginBottom: '1.25rem', fontSize: '1.1rem' }}>Dados de entrega</h2>
305
+ <div style={fieldStyle}>
306
+ <label htmlFor="checkout-street">Rua</label>
307
+ <input id="checkout-street" aria-label="Rua" value={street} onChange={(e) => setStreet(e.target.value)} placeholder="Av. Paulista, 1000" />
308
+ </div>
309
+ <div style={fieldStyle}>
310
+ <label htmlFor="checkout-cep">CEP</label>
311
+ <input id="checkout-cep" aria-label="CEP" value={cep} onChange={(e) => setCep(e.target.value)} placeholder="00000-000" />
312
+ </div>
313
+ <button type="button" onClick={() => setStep('shipping')} style={nextBtnStyle}>
314
+ Próximo →
315
+ </button>
316
+ </div>
317
+ )}
318
+
319
+ {step === 'shipping' && (
320
+ <div style={card}>
321
+ <h2 style={{ marginBottom: '1rem', fontSize: '1.1rem' }}>Selecione a entrega</h2>
322
+ <div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.875rem 1rem', background: '#f0f0ff', border: '2px solid #6366f1', borderRadius: '0.5rem', marginBottom: '1.25rem' }}>
323
+ <span style={{ fontWeight: 500 }}>Entrega Padrão (5–7 dias)</span>
324
+ <span style={{ fontWeight: 700, color: '#6366f1' }}>R$ 18,50</span>
325
+ </div>
326
+ {intentError && <p style={{ color: '#dc2626', marginBottom: '0.75rem', fontSize: '0.875rem' }}>{intentError}</p>}
327
+ <button type="button" onClick={goToPayment} disabled={creatingIntent} style={{ ...nextBtnStyle, opacity: creatingIntent ? 0.7 : 1 }}>
328
+ {creatingIntent ? 'Aguarde...' : 'Próximo →'}
329
+ </button>
330
+ </div>
331
+ )}
332
+
333
+ {step === 'payment' && clientSecret && (
334
+ <Elements stripe={stripePromise} options={elementsOptions}>
335
+ <PaymentStep street={street} cep={cep} orderId={orderId ?? ''} onPaymentSuccess={() => navigate('/checkout/success')} />
336
+ </Elements>
337
+ )}
338
+ </div>
339
+ );
340
+ }
341
+
342
+ export function CheckoutPage() {
343
+ return <CheckoutInner />;
344
+ }
@@ -0,0 +1,12 @@
1
+ import { usePageTitle } from '../../../shared/hooks/usePageTitle';
2
+
3
+ export function CheckoutSuccessPage() {
4
+ usePageTitle('Pedido Confirmado');
5
+ return (
6
+ <div>
7
+ <h1>Pedido confirmado!</h1>
8
+ <p>Seu pagamento foi processado com sucesso.</p>
9
+ <a href="/">Continuar comprando</a>
10
+ </div>
11
+ );
12
+ }