@unbox-plus/cli 0.20.4

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 (358) hide show
  1. package/LICENSE.md +46 -0
  2. package/README.md +175 -0
  3. package/bin/cli.js +439 -0
  4. package/package.json +43 -0
  5. package/src/colors.js +100 -0
  6. package/src/presets.js +241 -0
  7. package/src/theme.js +253 -0
  8. package/template/.claude/agents/assets-marca.md +40 -0
  9. package/template/.claude/agents/avaliador-visual.md +48 -0
  10. package/template/.claude/agents/branding-briefing.md +401 -0
  11. package/template/.claude/agents/conteudo-secao.md +46 -0
  12. package/template/.claude/agents/paginas-legais.md +44 -0
  13. package/template/.claude/settings.json +3 -0
  14. package/template/.claude/skills/frontend-design/ATTRIBUTION.md +3 -0
  15. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  16. package/template/.claude/skills/frontend-design/SKILL.md +55 -0
  17. package/template/.claude/skills/web-design-guidelines/ATTRIBUTION.md +3 -0
  18. package/template/.claude/skills/web-design-guidelines/SKILL.md +50 -0
  19. package/template/.claude/skills/web-design-guidelines/guidelines-snapshot.md +180 -0
  20. package/template/.env.example +158 -0
  21. package/template/.mcp.json.example +14 -0
  22. package/template/.nvmrc +1 -0
  23. package/template/CLAUDE.md +333 -0
  24. package/template/COVERAGE.md +80 -0
  25. package/template/DEPLOY.md +179 -0
  26. package/template/QA.md +108 -0
  27. package/template/README.md +152 -0
  28. package/template/agents/CONSTRUCAO.md +207 -0
  29. package/template/agents/MANAGER.md +192 -0
  30. package/template/agents/PADROES.md +255 -0
  31. package/template/agents/definitions/00-scaffold.md +56 -0
  32. package/template/agents/definitions/01-layout.md +47 -0
  33. package/template/agents/definitions/02-homepage.md +70 -0
  34. package/template/agents/definitions/03-catalog.md +86 -0
  35. package/template/agents/definitions/04-pdp.md +97 -0
  36. package/template/agents/definitions/05-cart.md +116 -0
  37. package/template/agents/definitions/06-checkout.md +146 -0
  38. package/template/agents/definitions/07-auth.md +130 -0
  39. package/template/agents/definitions/08-customer.md +55 -0
  40. package/template/agents/definitions/09-promotions.md +62 -0
  41. package/template/agents/definitions/10-feedback.md +128 -0
  42. package/template/agents/definitions/11-seo-infra.md +159 -0
  43. package/template/agents/definitions/12-deploy.md +173 -0
  44. package/template/agents/definitions/13-cro.md +275 -0
  45. package/template/agents/definitions/14-seo.md +388 -0
  46. package/template/agents/definitions/15-branding.md +26 -0
  47. package/template/agents/definitions/16-qa-visual.md +138 -0
  48. package/template/agents/definitions/17-aeo.md +257 -0
  49. package/template/app/(loja)/busca/page.tsx +72 -0
  50. package/template/app/(loja)/carrinho/oferta/page.tsx +57 -0
  51. package/template/app/(loja)/carrinho/page.tsx +160 -0
  52. package/template/app/(loja)/categoria/[tagSlug]/layout.tsx +31 -0
  53. package/template/app/(loja)/categoria/[tagSlug]/loading.tsx +17 -0
  54. package/template/app/(loja)/categoria/[tagSlug]/page.tsx +49 -0
  55. package/template/app/(loja)/checkout/page.tsx +80 -0
  56. package/template/app/(loja)/checkout/pix/[ref]/page.tsx +133 -0
  57. package/template/app/(loja)/conta/assinaturas/[referenceId]/page.tsx +112 -0
  58. package/template/app/(loja)/conta/assinaturas/page.tsx +51 -0
  59. package/template/app/(loja)/conta/enderecos/page.tsx +20 -0
  60. package/template/app/(loja)/conta/entrar/page.tsx +119 -0
  61. package/template/app/(loja)/conta/page.tsx +45 -0
  62. package/template/app/(loja)/conta/pedidos/[referenceId]/page.tsx +49 -0
  63. package/template/app/(loja)/conta/pedidos/page.tsx +104 -0
  64. package/template/app/(loja)/conta/preferencias/page.tsx +24 -0
  65. package/template/app/(loja)/devolucoes/page.tsx +36 -0
  66. package/template/app/(loja)/layout.tsx +22 -0
  67. package/template/app/(loja)/oferta/page.tsx +57 -0
  68. package/template/app/(loja)/page.tsx +86 -0
  69. package/template/app/(loja)/pedido/[referenceId]/page.tsx +78 -0
  70. package/template/app/(loja)/privacidade/page.tsx +113 -0
  71. package/template/app/(loja)/produto/[productSlug]/layout.tsx +23 -0
  72. package/template/app/(loja)/produto/[productSlug]/loading.tsx +20 -0
  73. package/template/app/(loja)/produto/[productSlug]/page.tsx +372 -0
  74. package/template/app/(loja)/produtos/loading.tsx +16 -0
  75. package/template/app/(loja)/produtos/page.tsx +33 -0
  76. package/template/app/(loja)/termos/page.tsx +104 -0
  77. package/template/app/acesso/page.tsx +129 -0
  78. package/template/app/api/account/addresses/route.ts +39 -0
  79. package/template/app/api/account/exists/route.ts +21 -0
  80. package/template/app/api/account/me/route.ts +25 -0
  81. package/template/app/api/account/otp/route.ts +26 -0
  82. package/template/app/api/account/preferences/route.ts +19 -0
  83. package/template/app/api/account/signin/route.ts +27 -0
  84. package/template/app/api/account/signout/route.ts +9 -0
  85. package/template/app/api/acesso/route.ts +171 -0
  86. package/template/app/api/capi/route.ts +34 -0
  87. package/template/app/api/cart/coupon/route.ts +49 -0
  88. package/template/app/api/cart/items/route.ts +45 -0
  89. package/template/app/api/cart/link/route.ts +69 -0
  90. package/template/app/api/cart/route.ts +113 -0
  91. package/template/app/api/cart/share/route.ts +21 -0
  92. package/template/app/api/cep/[code]/route.ts +18 -0
  93. package/template/app/api/checkout/address/route.ts +24 -0
  94. package/template/app/api/checkout/email/route.ts +41 -0
  95. package/template/app/api/checkout/installments/route.ts +16 -0
  96. package/template/app/api/checkout/route.ts +110 -0
  97. package/template/app/api/checkout/shipping/route.ts +65 -0
  98. package/template/app/api/checkout-destination/route.ts +31 -0
  99. package/template/app/api/order/[ref]/route.ts +17 -0
  100. package/template/app/api/payment-link/route.ts +28 -0
  101. package/template/app/api/revalidate/route.ts +52 -0
  102. package/template/app/api/shipping/quote/route.ts +29 -0
  103. package/template/app/api/subscriptions/[id]/route.ts +52 -0
  104. package/template/app/api/track/route.ts +28 -0
  105. package/template/app/api/unbox/catalogo/route.ts +136 -0
  106. package/template/app/api/unbox/paginas/route.ts +167 -0
  107. package/template/app/api/unbox/vitrine/route.ts +84 -0
  108. package/template/app/api/webhooks/unbox/route.ts +97 -0
  109. package/template/app/apple-icon.svg +5 -0
  110. package/template/app/error.tsx +15 -0
  111. package/template/app/globals.css +457 -0
  112. package/template/app/icon.svg +4 -0
  113. package/template/app/layout.tsx +90 -0
  114. package/template/app/llms.txt/route.ts +43 -0
  115. package/template/app/manifest.ts +21 -0
  116. package/template/app/not-found.tsx +18 -0
  117. package/template/app/opengraph-image.tsx +35 -0
  118. package/template/app/robots.ts +31 -0
  119. package/template/app/sitemap.ts +42 -0
  120. package/template/bootstrap.sh +87 -0
  121. package/template/components/account/account-shell.tsx +47 -0
  122. package/template/components/account/address-book.tsx +147 -0
  123. package/template/components/account/preferences-form.tsx +77 -0
  124. package/template/components/account/reorder-button.tsx +47 -0
  125. package/template/components/account/signout-button.tsx +20 -0
  126. package/template/components/account/subscription-actions.tsx +204 -0
  127. package/template/components/account-nav.tsx +41 -0
  128. package/template/components/address-fields.tsx +102 -0
  129. package/template/components/analytics/data-layer-ready.tsx +16 -0
  130. package/template/components/analytics/purchase-tracker.tsx +32 -0
  131. package/template/components/brand-search.tsx +34 -0
  132. package/template/components/cart/cart-provider.tsx +340 -0
  133. package/template/components/cart/mini-cart.tsx +228 -0
  134. package/template/components/cart-button.tsx +35 -0
  135. package/template/components/catalog/catalog-client.tsx +523 -0
  136. package/template/components/catalog/grid-skeleton.tsx +19 -0
  137. package/template/components/catalog/pager.tsx +66 -0
  138. package/template/components/catalog/product-grid-card.tsx +52 -0
  139. package/template/components/catalog/product-grid.tsx +27 -0
  140. package/template/components/checkout/checkout-client.tsx +1333 -0
  141. package/template/components/chrome/announce-bar.tsx +57 -0
  142. package/template/components/chrome/chrome-recipe.ts +13 -0
  143. package/template/components/chrome/footers/captura-botao.tsx +28 -0
  144. package/template/components/chrome/footers/colunas.tsx +110 -0
  145. package/template/components/chrome/footers/conversao.tsx +90 -0
  146. package/template/components/chrome/footers/editorial.tsx +98 -0
  147. package/template/components/chrome/footers/minimal.tsx +55 -0
  148. package/template/components/chrome/header-bar-mobile.tsx +64 -0
  149. package/template/components/chrome/headers/centralizado.tsx +71 -0
  150. package/template/components/chrome/headers/classico.tsx +64 -0
  151. package/template/components/chrome/headers/compacto.tsx +66 -0
  152. package/template/components/chrome/headers/equilibrado.tsx +73 -0
  153. package/template/components/chrome/headers/imersivo.tsx +62 -0
  154. package/template/components/chrome/registry.ts +70 -0
  155. package/template/components/chrome/solid-on-scroll.tsx +39 -0
  156. package/template/components/empty-state.tsx +22 -0
  157. package/template/components/home/combo-card-compact.tsx +67 -0
  158. package/template/components/home/combos-home.tsx +64 -0
  159. package/template/components/home/combos-section.tsx +177 -0
  160. package/template/components/home/home-recipe.ts +41 -0
  161. package/template/components/home/sections/attributes-marquee.tsx +178 -0
  162. package/template/components/home/sections/benefits.tsx +80 -0
  163. package/template/components/home/sections/bloco-html.tsx +47 -0
  164. package/template/components/home/sections/catalogo.ts +165 -0
  165. package/template/components/home/sections/category-pills.tsx +32 -0
  166. package/template/components/home/sections/combos-carousel.tsx +141 -0
  167. package/template/components/home/sections/comparison.tsx +73 -0
  168. package/template/components/home/sections/founder-story.tsx +40 -0
  169. package/template/components/home/sections/hero.tsx +187 -0
  170. package/template/components/home/sections/kits.tsx +11 -0
  171. package/template/components/home/sections/media-cards.tsx +70 -0
  172. package/template/components/home/sections/newsletter.tsx +62 -0
  173. package/template/components/home/sections/product-showcase.tsx +175 -0
  174. package/template/components/home/sections/purchase-hero.tsx +187 -0
  175. package/template/components/home/sections/quote-banner.tsx +50 -0
  176. package/template/components/home/sections/registry.ts +186 -0
  177. package/template/components/home/sections/reviews-carousel.tsx +66 -0
  178. package/template/components/home/sections/reviews.tsx +68 -0
  179. package/template/components/home/sections/ritual.tsx +96 -0
  180. package/template/components/home/sections/savings.tsx +47 -0
  181. package/template/components/home/sections/social-row.tsx +65 -0
  182. package/template/components/home/sections/spec-table.tsx +117 -0
  183. package/template/components/home/sections/stars.tsx +12 -0
  184. package/template/components/home/sections/stats-grid.tsx +54 -0
  185. package/template/components/home/sections/trust-bar.tsx +56 -0
  186. package/template/components/home/sections/trust-strip.tsx +40 -0
  187. package/template/components/home/sections/video-wall.tsx +59 -0
  188. package/template/components/landing/landing-recipe.ts +20 -0
  189. package/template/components/landing/oferta-sections.tsx +40 -0
  190. package/template/components/landing/product-picker.tsx +257 -0
  191. package/template/components/mobile-nav.tsx +71 -0
  192. package/template/components/order-status.tsx +163 -0
  193. package/template/components/powered-by-unbox.tsx +27 -0
  194. package/template/components/product/pdp/buy-box.tsx +604 -0
  195. package/template/components/product/pdp/catalog-grid.tsx +113 -0
  196. package/template/components/product/pdp/faq-modelo.ts +86 -0
  197. package/template/components/product/pdp/gallery.tsx +87 -0
  198. package/template/components/product/pdp/interactive.tsx +200 -0
  199. package/template/components/product/pdp/newsletter.tsx +64 -0
  200. package/template/components/product/pdp/payment-chips.tsx +22 -0
  201. package/template/components/product/pdp/pdp-view.tsx +227 -0
  202. package/template/components/product/pdp/recommendations.tsx +144 -0
  203. package/template/components/product/pdp/sections.tsx +173 -0
  204. package/template/components/quantity-stepper.tsx +36 -0
  205. package/template/components/search-box.tsx +37 -0
  206. package/template/components/site-footer.tsx +59 -0
  207. package/template/components/site-header.tsx +64 -0
  208. package/template/components/ui/accordion.tsx +72 -0
  209. package/template/components/ui/alert-dialog.tsx +187 -0
  210. package/template/components/ui/alert.tsx +76 -0
  211. package/template/components/ui/aspect-ratio.tsx +22 -0
  212. package/template/components/ui/avatar.tsx +109 -0
  213. package/template/components/ui/badge.tsx +52 -0
  214. package/template/components/ui/breadcrumb.tsx +125 -0
  215. package/template/components/ui/button.tsx +58 -0
  216. package/template/components/ui/card.tsx +103 -0
  217. package/template/components/ui/carousel.tsx +242 -0
  218. package/template/components/ui/command.tsx +196 -0
  219. package/template/components/ui/dialog.tsx +160 -0
  220. package/template/components/ui/dropdown-menu.tsx +268 -0
  221. package/template/components/ui/foto.tsx +54 -0
  222. package/template/components/ui/input-group.tsx +158 -0
  223. package/template/components/ui/input-otp.tsx +87 -0
  224. package/template/components/ui/input.tsx +20 -0
  225. package/template/components/ui/label.tsx +20 -0
  226. package/template/components/ui/navigation-menu.tsx +168 -0
  227. package/template/components/ui/pagination.tsx +132 -0
  228. package/template/components/ui/popover.tsx +90 -0
  229. package/template/components/ui/progress.tsx +83 -0
  230. package/template/components/ui/radio-group.tsx +38 -0
  231. package/template/components/ui/scroll-area.tsx +55 -0
  232. package/template/components/ui/select.tsx +201 -0
  233. package/template/components/ui/separator.tsx +25 -0
  234. package/template/components/ui/sheet.tsx +138 -0
  235. package/template/components/ui/skeleton.tsx +13 -0
  236. package/template/components/ui/sonner.tsx +44 -0
  237. package/template/components/ui/switch.tsx +32 -0
  238. package/template/components/ui/table.tsx +116 -0
  239. package/template/components/ui/tabs.tsx +82 -0
  240. package/template/components/ui/textarea.tsx +18 -0
  241. package/template/components/ui/toggle-group.tsx +89 -0
  242. package/template/components/ui/toggle.tsx +45 -0
  243. package/template/components/ui/tooltip.tsx +66 -0
  244. package/template/components.json +25 -0
  245. package/template/eslint.config.mjs +36 -0
  246. package/template/gitignore +21 -0
  247. package/template/lib/analytics.ts +369 -0
  248. package/template/lib/api.ts +60 -0
  249. package/template/lib/capi.ts +87 -0
  250. package/template/lib/cart-link.ts +51 -0
  251. package/template/lib/cart-normalize.ts +142 -0
  252. package/template/lib/cart-recovery.ts +23 -0
  253. package/template/lib/cart-response.ts +48 -0
  254. package/template/lib/catalog-map.ts +54 -0
  255. package/template/lib/catalog.ts +3 -0
  256. package/template/lib/checkout-lock.ts +21 -0
  257. package/template/lib/checkout-nav.ts +22 -0
  258. package/template/lib/config.ts +51 -0
  259. package/template/lib/crm.ts +40 -0
  260. package/template/lib/customer-session.ts +21 -0
  261. package/template/lib/dataloader.ts +23 -0
  262. package/template/lib/editable/config.ts +11 -0
  263. package/template/lib/editable/document.ts +1762 -0
  264. package/template/lib/editable/index.ts +3 -0
  265. package/template/lib/editable/primitives.tsx +821 -0
  266. package/template/lib/editable/provider.tsx +861 -0
  267. package/template/lib/editable/rastreio-navegacao.tsx +65 -0
  268. package/template/lib/editable/rastreio.tsx +168 -0
  269. package/template/lib/editable/server.ts +189 -0
  270. package/template/lib/editable/tokens.ts +22 -0
  271. package/template/lib/editable/verify.ts +24 -0
  272. package/template/lib/enrichment/combos.ts +184 -0
  273. package/template/lib/enrichment/index.ts +188 -0
  274. package/template/lib/enrichment/products.json +1 -0
  275. package/template/lib/env-check.ts +47 -0
  276. package/template/lib/format.ts +130 -0
  277. package/template/lib/icons.ts +67 -0
  278. package/template/lib/json-ld.ts +9 -0
  279. package/template/lib/llms-txt.ts +75 -0
  280. package/template/lib/mockup.ts +24 -0
  281. package/template/lib/newsletter.ts +12 -0
  282. package/template/lib/orders.ts +160 -0
  283. package/template/lib/queries.ts +66 -0
  284. package/template/lib/ratelimit.ts +47 -0
  285. package/template/lib/rotas-editaveis.ts +192 -0
  286. package/template/lib/sanitize.ts +36 -0
  287. package/template/lib/schemas.ts +78 -0
  288. package/template/lib/session.ts +98 -0
  289. package/template/lib/store-config.ts +67 -0
  290. package/template/lib/unbox/client.ts +829 -0
  291. package/template/lib/unbox/customer.ts +224 -0
  292. package/template/lib/unbox/errors.ts +97 -0
  293. package/template/lib/unbox/index.ts +10 -0
  294. package/template/lib/unbox/store.ts +113 -0
  295. package/template/lib/unbox/types.ts +195 -0
  296. package/template/lib/unbox/webhooks.ts +66 -0
  297. package/template/lib/utils.ts +6 -0
  298. package/template/lib/vitrine.ts +242 -0
  299. package/template/lib/webhook-store.ts +22 -0
  300. package/template/middleware.ts +178 -0
  301. package/template/next.config.ts +63 -0
  302. package/template/package.json +62 -0
  303. package/template/postcss.config.mjs +5 -0
  304. package/template/public/brand/coll/default.png +0 -0
  305. package/template/public/brand/hero-desktop.svg +12 -0
  306. package/template/public/brand/hero-mobile.svg +12 -0
  307. package/template/public/brand/heros/boutique-desktop.svg +11 -0
  308. package/template/public/brand/heros/boutique-mobile.svg +11 -0
  309. package/template/public/brand/heros/editorial-desktop.svg +11 -0
  310. package/template/public/brand/heros/editorial-mobile.svg +11 -0
  311. package/template/public/brand/heros/essencial-desktop.svg +12 -0
  312. package/template/public/brand/heros/essencial-mobile.svg +12 -0
  313. package/template/public/brand/heros/promocional-desktop.svg +12 -0
  314. package/template/public/brand/heros/promocional-mobile.svg +12 -0
  315. package/template/public/brand/logo-chrome.svg +6 -0
  316. package/template/public/brand/logo-white.svg +6 -0
  317. package/template/public/brand/logo.svg +7 -0
  318. package/template/public/brand/pay-amex.webp +0 -0
  319. package/template/public/brand/pay-elo.webp +0 -0
  320. package/template/public/brand/pay-mastercard.webp +0 -0
  321. package/template/public/brand/pay-pix.webp +0 -0
  322. package/template/public/brand/pay-visa.webp +0 -0
  323. package/template/public/brand/ph/avatar-a.svg +5 -0
  324. package/template/public/brand/ph/avatar-b.svg +5 -0
  325. package/template/public/brand/ph/avatar-c.svg +5 -0
  326. package/template/public/brand/ph/avatar-d.svg +5 -0
  327. package/template/public/brand/ph/photo-a.svg +7 -0
  328. package/template/public/brand/ph/photo-b.svg +6 -0
  329. package/template/public/brand/ph/photo-c.svg +6 -0
  330. package/template/public/brand/ph/poster-a.svg +6 -0
  331. package/template/public/brand/ph/poster-b.svg +6 -0
  332. package/template/public/brand/ph/poster-c.svg +6 -0
  333. package/template/public/unbox/powered-by-fundo-preto.png +0 -0
  334. package/template/public/unbox/powered-by-transparente.png +0 -0
  335. package/template/public/unbox/powered-by.png +0 -0
  336. package/template/scripts/abandoned-cart.ts +82 -0
  337. package/template/scripts/check-editable.mjs +467 -0
  338. package/template/scripts/check-honestidade.mjs +111 -0
  339. package/template/scripts/check-placeholder.mjs +200 -0
  340. package/template/scripts/check-recipe.mjs +76 -0
  341. package/template/scripts/check-unbox-brand.mjs +298 -0
  342. package/template/scripts/contraste.js +177 -0
  343. package/template/scripts/dump-catalog.ts +95 -0
  344. package/template/scripts/load-env.ts +13 -0
  345. package/template/scripts/medir-sistema.mjs +56 -0
  346. package/template/scripts/place-order-pix.ts +61 -0
  347. package/template/scripts/qa-screenshots.mjs +150 -0
  348. package/template/scripts/sistema.py +231 -0
  349. package/template/scripts/subscribe-webhook.ts +36 -0
  350. package/template/scripts/test-live.ts +186 -0
  351. package/template/scripts/vocabulario.py +264 -0
  352. package/template/tsconfig.json +27 -0
  353. package/tools/LEIA-ME.md +39 -0
  354. package/tools/check-template-neutro.mjs +111 -0
  355. package/tools/notion-doc.mjs +191 -0
  356. package/tools/tokenize-neutrals.mjs +0 -0
  357. package/tools/tokenize-radius.mjs +52 -0
  358. package/tools/workflow-storefront.legado.js +1207 -0
@@ -0,0 +1,21 @@
1
+ // Gera o link compartilhável do carrinho atual: lê {cartId, cartToken} do cookie httpOnly e
2
+ // monta a URL de restauração (ver app/api/cart/link/route.ts, modo id+token). Quem tem o link
3
+ // tem o carrinho — e-mail/endereço/CPF ficam legíveis e editáveis por quem abrir (ver doc 12).
4
+ import { getCartRef, getRecurFreq } from "@/lib/session";
5
+ import { ok, fail } from "@/lib/api";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function GET(req: Request) {
10
+ const ref = await getCartRef();
11
+ if (!ref) return fail("Nenhum carrinho ativo para compartilhar.", 404);
12
+ const origin = new URL(req.url).origin;
13
+ // No modo hospedado o /checkout deste projeto não existe: o link vai pro checkout da Unbox.
14
+ const base = process.env.UNBOX_HOSTED_CHECKOUT_URL || `${origin}/checkout`;
15
+ let url = `${base}?id=${encodeURIComponent(ref.cartId)}&token=${encodeURIComponent(ref.cartToken)}`;
16
+ // A frequência de assinatura vive só em cookie — sem incluí-la, restaurar esse link em outra
17
+ // sessão perde a frequência escolhida (ver app/api/cart/link/route.ts).
18
+ const freq = await getRecurFreq();
19
+ if (freq) url += `&freq=${encodeURIComponent(freq)}`;
20
+ return ok({ url });
21
+ }
@@ -0,0 +1,18 @@
1
+ // Autocomplete de endereço por CEP (getAddressByPostalCode). Usado no checkout e address book.
2
+ import { withStoreClient } from "@/lib/unbox/store";
3
+ import { ok, fail, failFrom } from "@/lib/api";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ code: string }> }) {
8
+ try {
9
+ const { code } = await params;
10
+ const cep = (code ?? "").replace(/\D/g, "");
11
+ if (cep.length !== 8) return fail("CEP inválido.", 422);
12
+ const addr = await withStoreClient((c) => c.getAddressByPostalCode(cep));
13
+ if (!addr) return fail("CEP não encontrado.", 404);
14
+ return ok({ address: addr });
15
+ } catch (e) {
16
+ return failFrom(e);
17
+ }
18
+ }
@@ -0,0 +1,24 @@
1
+ // Define o endereço de entrega no carrinho → devolve os ids dos fulfillmentGroups.
2
+ import { withStoreClient } from "@/lib/unbox/store";
3
+ import { getCartRef } from "@/lib/session";
4
+ import { ok, fail, failFrom } from "@/lib/api";
5
+ import { addressSchema } from "@/lib/schemas";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ // POST /api/checkout/address { address }
10
+ export async function POST(req: Request) {
11
+ try {
12
+ const ref = await getCartRef();
13
+ if (!ref) return fail("Carrinho não encontrado.", 404);
14
+ const body = await req.json();
15
+ const parsed = addressSchema.safeParse(body.address);
16
+ if (!parsed.success) return fail("Endereço inválido. Confira os campos.", 422, { issues: parsed.error.flatten() });
17
+
18
+ await withStoreClient((c) => c.setShippingAddress(ref.cartId, ref.cartToken, parsed.data as any));
19
+ const groupIds = await withStoreClient((c) => c.getFulfillmentGroupIds(ref.cartId, ref.cartToken));
20
+ return ok({ fulfillmentGroupIds: groupIds });
21
+ } catch (e) {
22
+ return failFrom(e);
23
+ }
24
+ }
@@ -0,0 +1,41 @@
1
+ // Grava o e-mail no carrinho (etapa de contato) → habilita recuperação de carrinho abandonado.
2
+ // Chamado pelo checkout assim que o e-mail é válido, antes do placeOrder.
3
+ //
4
+ // É AQUI que o carrinho vira recuperável: com o e-mail gravado, despachamos pro CRM o
5
+ // ponteiro completo (cartId + link pronto com id/token/freq). Sem este evento, o id+token
6
+ // vive só no cookie do cliente e nenhum cron/CRM tem como montar o e-mail de recuperação.
7
+ import { withStoreClient } from "@/lib/unbox/store";
8
+ import { getCartRef, getRecurFreq } from "@/lib/session";
9
+ import { ok, fail, failFrom } from "@/lib/api";
10
+ import { emailSchema } from "@/lib/schemas";
11
+ import { dispatchCrm } from "@/lib/crm";
12
+ import { buildRecoveryUrl } from "@/lib/cart-recovery";
13
+
14
+ export const dynamic = "force-dynamic";
15
+
16
+ // POST /api/checkout/email { email }
17
+ export async function POST(req: Request) {
18
+ try {
19
+ const ref = await getCartRef();
20
+ if (!ref) return fail("Carrinho não encontrado.", 404);
21
+ const body = await req.json();
22
+ const parsed = emailSchema.safeParse(body.email);
23
+ if (!parsed.success) return fail("E-mail inválido.", 422);
24
+
25
+ await withStoreClient((c) => c.setEmailOnCart(ref.cartId, ref.cartToken, parsed.data));
26
+
27
+ // Fire-and-forget: falha de CRM nunca pode travar o checkout.
28
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
29
+ const freq = await getRecurFreq();
30
+ dispatchCrm({
31
+ type: "checkout_started",
32
+ email: parsed.data,
33
+ cartId: ref.cartId,
34
+ recoveryUrl: buildRecoveryUrl(siteUrl, ref.cartId, ref.cartToken, freq),
35
+ }).catch(() => {});
36
+
37
+ return ok({ ok: true });
38
+ } catch (e) {
39
+ return failFrom(e);
40
+ }
41
+ }
@@ -0,0 +1,16 @@
1
+ // Parcelas (sem juros) para um valor — exibição no checkout (cartão, não assinatura).
2
+ import { withStoreClient } from "@/lib/unbox/store";
3
+ import { ok, fail, failFrom } from "@/lib/api";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export async function GET(req: Request) {
8
+ const amount = Number(new URL(req.url).searchParams.get("amount"));
9
+ if (!Number.isFinite(amount) || amount <= 0) return fail("Valor inválido.", 400);
10
+ try {
11
+ const installments = await withStoreClient((c) => c.getInstallments(amount));
12
+ return ok({ installments });
13
+ } catch (e) {
14
+ return failFrom(e, 502);
15
+ }
16
+ }
@@ -0,0 +1,110 @@
1
+ // placeOrder — PRODUÇÃO REAL e NÃO-idempotente (doc 11). Proteções:
2
+ // - rate-limit por IP
3
+ // - lock por cartId (recusa chamadas concorrentes do mesmo carrinho)
4
+ // - RELÊ o carrinho imediatamente antes (estado final: itens sem brindes + total do servidor)
5
+ // - exige frete já selecionado
6
+ // - em erro: NÃO retenta cegamente (poderia gerar cobrança dupla)
7
+ // - sucesso: grava token de posse (cookie) e INVALIDA o carrinho
8
+ import { withStoreClient } from "@/lib/unbox/store";
9
+ import { getCartRef, setOrderToken, clearCartRef, getRecurFreq } from "@/lib/session";
10
+ import { acquireCheckoutLock, releaseCheckoutLock } from "@/lib/checkout-lock";
11
+ import { rateLimit, clientIp, LIMITS } from "@/lib/ratelimit";
12
+ import { dispatchCrm } from "@/lib/crm";
13
+ import { placeOrderSchema } from "@/lib/schemas";
14
+ import { ok, fail, failFrom } from "@/lib/api";
15
+ import { isUnboxTimeout } from "@/lib/unbox/client";
16
+
17
+ export const dynamic = "force-dynamic";
18
+
19
+ export async function POST(req: Request) {
20
+ const ip = clientIp(req);
21
+ const rl = rateLimit(`checkout:${ip}`, LIMITS.checkout.limit, LIMITS.checkout.windowMs);
22
+ if (!rl.ok) return fail("Muitas tentativas. Aguarde um instante.", 429);
23
+
24
+ const ref = await getCartRef();
25
+ if (!ref) return fail("Carrinho não encontrado.", 404);
26
+
27
+ const body = await req.json().catch(() => null);
28
+ const parsed = placeOrderSchema.safeParse(body);
29
+ if (!parsed.success) return fail("Dados de pagamento inválidos.", 422, { issues: parsed.error.flatten() });
30
+
31
+ if (!acquireCheckoutLock(ref.cartId)) {
32
+ return fail("Já existe um pedido em andamento para este carrinho.", 409);
33
+ }
34
+
35
+ try {
36
+ const { email, address, payment, recurringItemsFrequencyId, device } = parsed.data;
37
+
38
+ // RELEITURA do carrinho — estado final real (doc 11): evita FULFILLMENT_GROUP_AND_CART_ITEMS_DO_NOT_MATCH_ERROR
39
+ const cart = await withStoreClient((c) => c.getCart(ref.cartId, ref.cartToken));
40
+ if (!cart) return fail("Carrinho expirado. Recomece a compra.", 410);
41
+
42
+ const selected = (cart.checkout?.fulfillmentGroups ?? []).find((g: any) => g.selectedFulfillmentOption);
43
+ const fulfillmentMethodId = selected?.selectedFulfillmentOption?.fulfillmentMethod?._id;
44
+ if (!fulfillmentMethodId) return fail("Selecione uma opção de frete antes de pagar.", 422);
45
+
46
+ const total = cart.checkout?.summary?.total?.amount;
47
+ if (typeof total !== "number") return fail("Não foi possível calcular o total.", 422);
48
+
49
+ // Assinatura: o carrinho tem item(ns) isRecurring? A frequência veio em cookie (preferência) ou no body.
50
+ const hasRecurring = (cart.items?.edges ?? []).some((e: any) => e.node?.isRecurring === true);
51
+ const freqId = (await getRecurFreq()) ?? recurringItemsFrequencyId ?? undefined;
52
+ if (hasRecurring) {
53
+ if (payment.type !== "card") return fail("Assinaturas exigem pagamento com cartão de crédito.", 422);
54
+ if (!freqId) return fail("Selecione a frequência da assinatura.", 422);
55
+ }
56
+
57
+ const order = await withStoreClient((c) => {
58
+ const items = c.buildOrderItems(cart); // TODOS os itens, com flags isRecurring/isDiscountedBonusItem (casar c/ o carrinho — senão 502)
59
+ return c.placeOrder({
60
+ cartId: ref.cartId,
61
+ email,
62
+ address: address as any,
63
+ fulfillmentMethodId,
64
+ total,
65
+ items,
66
+ payment: payment.type === "pix" ? { type: "pix" } : { type: "card", card: payment.card as any },
67
+ recurrence: hasRecurring && freqId ? { recurringItemsFrequencyId: freqId } : undefined,
68
+ // antifraude: device coletado no browser; sem ele o client envia { type: "API" }.
69
+ device,
70
+ });
71
+ });
72
+
73
+ const placed = order.orders?.[0];
74
+ if (!placed) return fail("Não foi possível concluir o pedido.", 502);
75
+
76
+ // posse do pedido (guest) + invalida o carrinho (não reutilizar o cartId)
77
+ if (placed.referenceId && order.token) await setOrderToken(placed.referenceId, order.token);
78
+ await clearCartRef();
79
+
80
+ // CRM (efeito real se CRM_WEBHOOK_URL estiver configurada)
81
+ // cartId incluso pro CRM conseguir CANCELAR o fluxo de abandono armado no checkout_started.
82
+ dispatchCrm({ type: "order_created", email, cartId: ref.cartId, orderId: placed._id, referenceId: placed.referenceId }).catch(() => {});
83
+
84
+ const pix = placed.payments?.find((p: any) => p?.data?.qrCode)?.data ?? null;
85
+ return ok({
86
+ referenceId: placed.referenceId,
87
+ status: placed.status,
88
+ paymentType: payment.type,
89
+ total: placed.summary?.total?.displayAmount,
90
+ pix: pix ? { qrCode: pix.qrCode } : null,
91
+ recurringOrderId: placed.recurringOrderId ?? null,
92
+ });
93
+ } catch (e) {
94
+ // ⚠️ NÃO retentar: em timeout/erro o pedido pode ter sido criado. Orientar o cliente a entrar
95
+ // e checar "meus pedidos" (/conta/pedidos) antes de tentar de novo (evita cobrança dupla).
96
+ if (isUnboxTimeout(e)) {
97
+ // Estado DESCONHECIDO: sem resposta dentro do prazo, o pedido pode ter sido criado e cobrado. O
98
+ // código PLACE_ORDER_TIMEOUT diz ao checkout-client pra NÃO reabrir o botão de pagar.
99
+ console.error(JSON.stringify({ tag: "[api-erro]", status: 504, erro: "PLACE_ORDER_TIMEOUT", rota: "checkout", cartId: ref.cartId, quando: new Date().toISOString() }));
100
+ return fail(
101
+ "O pagamento demorou mais que o esperado e não sabemos se foi concluído. Não pague de novo: confira em Meus pedidos ou no seu e-mail antes de tentar outra vez.",
102
+ 504,
103
+ { code: "PLACE_ORDER_TIMEOUT" },
104
+ );
105
+ }
106
+ return failFrom(e, 502, { rota: "checkout", cartId: ref.cartId });
107
+ } finally {
108
+ releaseCheckoutLock(ref.cartId);
109
+ }
110
+ }
@@ -0,0 +1,65 @@
1
+ // Frete: POST cota TODOS os grupos (doc 11 — N grupos, não 1); PUT seleciona o método por grupo.
2
+ import { withStoreClient } from "@/lib/unbox/store";
3
+ import { getCartRef } from "@/lib/session";
4
+ import { cartResponse } from "@/lib/cart-response";
5
+ import { ok, fail, failFrom } from "@/lib/api";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ // POST /api/checkout/shipping → { groups: [{ groupId, options:[{methodId,name,displayName,price,days}] }] }
10
+ export async function POST() {
11
+ try {
12
+ const ref = await getCartRef();
13
+ if (!ref) return fail("Carrinho não encontrado.", 404);
14
+
15
+ const groups = await withStoreClient(async (c) => {
16
+ const ids = await c.getFulfillmentGroupIds(ref.cartId, ref.cartToken);
17
+ const out = [];
18
+ for (const groupId of ids) {
19
+ const opts = await c.quoteShipping(ref.cartId, ref.cartToken, groupId); // obrigatório (senão options vem vazio)
20
+ out.push({
21
+ groupId,
22
+ options: opts.map((o) => ({
23
+ methodId: o.fulfillmentMethod._id,
24
+ name: o.fulfillmentMethod.name,
25
+ displayName: o.fulfillmentMethod.displayName ?? o.fulfillmentMethod.name,
26
+ price: o.price?.displayAmount,
27
+ discountPrice: o.discountPrice?.displayAmount,
28
+ days: o.fulfillmentMethod.daysToDeliver,
29
+ })),
30
+ });
31
+ }
32
+ return out;
33
+ });
34
+
35
+ if (!groups.length || groups.every((g) => g.options.length === 0)) {
36
+ return fail("Não há opções de entrega para este CEP.", 422);
37
+ }
38
+ return ok({ groups });
39
+ } catch (e) {
40
+ return failFrom(e);
41
+ }
42
+ }
43
+
44
+ // PUT /api/checkout/shipping { selections: [{ groupId, methodId }] } → seleciona e devolve o carrinho final
45
+ export async function PUT(req: Request) {
46
+ try {
47
+ const ref = await getCartRef();
48
+ if (!ref) return fail("Carrinho não encontrado.", 404);
49
+ const { selections } = await req.json();
50
+ if (!Array.isArray(selections) || !selections.length) return fail("Selecione o frete.");
51
+
52
+ const finalCart = await withStoreClient(async (c) => {
53
+ let last: any = null;
54
+ for (const s of selections) {
55
+ last = await c.selectShipping(ref.cartId, ref.cartToken, s.groupId, s.methodId);
56
+ }
57
+ return last;
58
+ });
59
+ // relê o estado completo (descontos + total final)
60
+ const full = await withStoreClient((c) => c.getCart(ref.cartId, ref.cartToken));
61
+ return ok({ cart: await cartResponse(full ?? finalCart) });
62
+ } catch (e) {
63
+ return failFrom(e);
64
+ }
65
+ }
@@ -0,0 +1,31 @@
1
+ // Decide pro onde o botão "ir para o checkout" navega — SEMPRE no servidor, nunca no client
2
+ // (ver lib/checkout-nav.ts). Dois modos, escolhidos no setup do CLI (create-unbox-store) e nunca
3
+ // misturados no mesmo projeto — não há UI de checkout customizada quando o modo é "hospedado":
4
+ //
5
+ // 1) UNBOX_HOSTED_CHECKOUT_URL vazia (padrão) → checkout customizado interno, /checkout.
6
+ // 2) UNBOX_HOSTED_CHECKOUT_URL preenchida → checkout PADRÃO hospedado pela Unbox, no mesmo
7
+ // domínio da loja (ex.: https://sualoja.com.br/carrinho/finalizar-pedido). Precisa do
8
+ // {cartId, cartToken} na URL — mesmo contrato do link de recuperação de carrinho (doc 06) —
9
+ // porque cartToken é httpOnly e o client não tem acesso a ele para montar a URL sozinho.
10
+ import { getCartRef, getRecurFreq } from "@/lib/session";
11
+ import { ok } from "@/lib/api";
12
+
13
+ export const dynamic = "force-dynamic";
14
+
15
+ export async function GET() {
16
+ const ref = await getCartRef();
17
+ const hostedUrl = process.env.UNBOX_HOSTED_CHECKOUT_URL;
18
+
19
+ if (!ref) return ok({ url: "/carrinho" }); // nada pra finalizar ainda
20
+
21
+ // NOS DOIS MODOS a URL do checkout carrega ?id=&token=[&freq=]&step=1 — mesmo contrato do
22
+ // link de recuperação. É isso que deixa o CRM montar o e-mail de abandono a partir da URL
23
+ // navegada (pixel/GTM capturam a URL) e o link colar em outro aparelho. Sem os params na
24
+ // URL, a recuperação de carrinho não existe pra quem só enxerga a navegação.
25
+ const base = hostedUrl || "/checkout";
26
+ let url = `${base}?id=${encodeURIComponent(ref.cartId)}&token=${encodeURIComponent(ref.cartToken)}`;
27
+ const freq = await getRecurFreq();
28
+ if (freq) url += `&freq=${encodeURIComponent(freq)}`;
29
+ url += "&step=1";
30
+ return ok({ url });
31
+ }
@@ -0,0 +1,17 @@
1
+ // Status do pedido com VERIFICAÇÃO DE POSSE (doc 09): referenceId é curto/adivinhável.
2
+ // Usado pelo polling do Pix (checkout e app/checkout/pix/[ref]).
3
+ import { getOwnedOrder } from "@/lib/orders";
4
+ import { ok, fail, failFrom } from "@/lib/api";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ ref: string }> }) {
9
+ try {
10
+ const { ref } = await params;
11
+ const order = await getOwnedOrder(ref);
12
+ if (!order) return fail("Pedido não encontrado ou acesso negado.", 403);
13
+ return ok({ order });
14
+ } catch (e) {
15
+ return failFrom(e);
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ // Cria um Payment Link hospedado pela Unbox (compartilhar carrinho / WhatsApp).
2
+ // ⚠️ Operação de loja (admin-ish). Protegida por REVALIDATE_SECRET (header x-admin-secret) por padrão.
3
+ import { withStoreClient } from "@/lib/unbox/store";
4
+ import { serverEnv } from "@/lib/config";
5
+ import { ok, fail, failFrom } from "@/lib/api";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function POST(req: Request) {
10
+ // FAIL-CLOSED: sem REVALIDATE_SECRET configurado a rota fica FECHADA, não aberta. A versão
11
+ // anterior era `if (secret && ...)`: com o secret vazio (o default do .env) a guarda sumia e
12
+ // qualquer pessoa criava Payment Links reais com a marca da loja, em massa ou pré-preenchidos
13
+ // para phishing. Mesmo padrão da /api/revalidate.
14
+ if (!serverEnv.revalidateSecret || req.headers.get("x-admin-secret") !== serverEnv.revalidateSecret) {
15
+ return fail("Não autorizado.", 401);
16
+ }
17
+ try {
18
+ const body = await req.json();
19
+ const items = body.items;
20
+ if (!Array.isArray(items) || !items.length) return fail("Itens inválidos.");
21
+ const link = await withStoreClient((c) =>
22
+ c.createPaymentLink({ items, constraints: body.constraints, customerData: body.customerData }),
23
+ );
24
+ return ok({ paymentLink: link });
25
+ } catch (e) {
26
+ return failFrom(e);
27
+ }
28
+ }
@@ -0,0 +1,52 @@
1
+ // Revalidação on-demand (ISR) — chamada por webhook/cron quando catálogo/estoque mudam.
2
+ // Protegida por REVALIDATE_SECRET.
3
+ import { revalidatePath, revalidateTag } from "next/cache";
4
+ import { serverEnv } from "@/lib/config";
5
+ import { ok, fail } from "@/lib/api";
6
+ import { verifyEditorToken } from "@/lib/editable/verify";
7
+ import { conteudoPublicadoAgora } from "@/lib/editable/server";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ export async function POST(req: Request) {
12
+ const secret = new URL(req.url).searchParams.get("secret") ?? req.headers.get("x-revalidate-secret");
13
+ const viaSecret = Boolean(serverEnv.revalidateSecret) && secret === serverEnv.revalidateSecret;
14
+ // O editor publica conteúdo e chama aqui com um JWT (ES256, purpose "revalidate") verificado pela
15
+ // chave PÚBLICA dele — a loja não guarda segredo do editor.
16
+ const tokenDoEditor = req.headers.get("x-editor-token");
17
+ const viaEditor = tokenDoEditor ? await verifyEditorToken(tokenDoEditor, "revalidate") : null;
18
+ if (!viaSecret && !viaEditor) {
19
+ return fail("Não autorizado.", 401);
20
+ }
21
+ const body = await req.json().catch(() => ({}));
22
+ const paths: string[] = Array.isArray(body.paths) && body.paths.length ? body.paths : ["/", "/produtos"];
23
+ // o conteúdo publicado é lido no layout com esta tag; sem purgá-la, o path re-renderiza com o
24
+ // JSON antigo do Data Cache.
25
+ if (viaEditor) revalidateTag("unbox-editor-content");
26
+ // O editor manda as páginas que o conteúdo publicado realmente alcança, e uma delas pode ser uma
27
+ // ROTA com segmento dinâmico ("/produto/[productSlug]"): trocar o cabeçalho muda todas as páginas
28
+ // de produto, não uma. Sem o segundo argumento o Next leria os colchetes como caminho literal e não
29
+ // revalidaria nada — em silêncio.
30
+ // Caminho que falha NÃO some do recibo: ele fica FORA de `revalidated`, e é assim que o editor
31
+ // descobre que aquela página continua no ar com o conteúdo antigo (ver lib/recibo.ts do editor).
32
+ const revalidados: string[] = [];
33
+ const falhas: string[] = [];
34
+ for (const p of paths) {
35
+ try {
36
+ if (p.includes("[")) revalidatePath(p, "page");
37
+ else revalidatePath(p);
38
+ revalidados.push(p);
39
+ } catch (err) {
40
+ falhas.push(`${p}: ${err instanceof Error ? err.message : String(err)}`);
41
+ }
42
+ }
43
+ if (falhas.length) console.warn("[revalidate] caminhos que não revalidaram:", falhas.join(" · "));
44
+ // O RECIBO. Quem chamou daqui foi o editor, e é com esta resposta que ele decide se pode dizer ao
45
+ // lojista "a loja já está no ar com a versão N". Purgar o cache não prova isso: prova que a PRÓXIMA
46
+ // leitura vai buscar de novo. Então a loja vai ler o conteúdo publicado agora e devolve QUAL
47
+ // documento ela passou a servir (ver `conteudoPublicadoAgora` em lib/editable/server.ts). Sem isto o
48
+ // editor afirmava "no ar" até para uma loja que nem está ligada a ele.
49
+ // Só para o editor: o webhook por segredo não precisa do recibo e não paga essa leitura.
50
+ const conteudo = viaEditor ? await conteudoPublicadoAgora() : null;
51
+ return ok({ revalidated: revalidados, ...(falhas.length ? { falhas } : {}), via: viaEditor ? "editor" : "secret", conteudo });
52
+ }
@@ -0,0 +1,29 @@
1
+ // "Calcule o frete" na PDP (fora do checkout): cota o frete de 1 produto para um CEP.
2
+ import { withStoreClient } from "@/lib/unbox/store";
3
+ import { ok, fail, failFrom } from "@/lib/api";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ // POST /api/shipping/quote { productId, variantId, price, postal }
8
+ export async function POST(req: Request) {
9
+ try {
10
+ const { productId, variantId, price, postal } = await req.json();
11
+ const cep = (postal ?? "").replace(/\D/g, "");
12
+ if (!productId || !variantId) return fail("Produto inválido.");
13
+ if (cep.length !== 8) return fail("CEP inválido.", 422);
14
+
15
+ const opts = await withStoreClient((c) =>
16
+ c.quoteShippingForProduct({ productId, productVariantId: variantId, price: price ?? 0.01, quantity: 1 }, cep),
17
+ );
18
+ if (!opts.length) return fail("Não há opções de entrega para este CEP.", 422);
19
+ return ok({
20
+ options: opts.map((o) => ({
21
+ displayName: o.fulfillmentMethod.displayName ?? o.fulfillmentMethod.name,
22
+ price: o.price?.displayAmount,
23
+ days: o.fulfillmentMethod.daysToDeliver,
24
+ })),
25
+ });
26
+ } catch (e) {
27
+ return failFrom(e);
28
+ }
29
+ }
@@ -0,0 +1,52 @@
1
+ // Ações de assinatura (recurring order) — só variantes customer* (token do cliente).
2
+ // id = recurringOrderId. action: pause | skip | cancel | items | card | address
3
+ import { requireCustomerClient } from "@/lib/customer-session";
4
+ import { cardSchema, addressSchema } from "@/lib/schemas";
5
+ import { ok, fail, failFrom } from "@/lib/api";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ try {
11
+ const me = await requireCustomerClient();
12
+ const { id } = await params;
13
+ const body = await req.json();
14
+ const action = body.action as string;
15
+
16
+ switch (action) {
17
+ case "pause":
18
+ return ok({ subscription: await me.pause(id) });
19
+ case "skip":
20
+ return ok({ subscription: await me.skipNextCycle(id) });
21
+ case "cancel":
22
+ return ok({ subscription: await me.cancel(id) });
23
+ case "items": {
24
+ if (!Array.isArray(body.items)) return fail("Itens inválidos.");
25
+ return ok({ subscription: await me.updateItems(id, body.items) });
26
+ }
27
+ case "card": {
28
+ const parsed = cardSchema.safeParse(body.card);
29
+ if (!parsed.success) return fail("Dados do cartão inválidos.", 422);
30
+ const c = parsed.data;
31
+ return ok({
32
+ subscription: await me.updateCard(id, {
33
+ holderName: c.cardHolder,
34
+ cardNumber: c.cardNumber,
35
+ expirationMonth: c.expirationMonth,
36
+ expirationYear: c.expirationYear,
37
+ securityCode: c.securityCode,
38
+ }),
39
+ });
40
+ }
41
+ case "address": {
42
+ const parsed = addressSchema.safeParse(body.address);
43
+ if (!parsed.success) return fail("Endereço inválido.", 422);
44
+ return ok({ subscription: await me.updateAddress(id, parsed.data as any) });
45
+ }
46
+ default:
47
+ return fail("Ação desconhecida.", 400);
48
+ }
49
+ } catch (e) {
50
+ return failFrom(e);
51
+ }
52
+ }
@@ -0,0 +1,28 @@
1
+ // Acompanhamento de pedido por convidado: exige referenceId + e-mail CORRESPONDENTE.
2
+ // Mitiga a enumeração por referenceId (doc 09): conhecer o código não basta — tem que bater o e-mail.
3
+ import { withStoreClient } from "@/lib/unbox/store";
4
+ import { shapeOrder } from "@/lib/orders";
5
+ import { rateLimit, clientIp } from "@/lib/ratelimit";
6
+ import { emailSchema } from "@/lib/schemas";
7
+ import { ok, fail, failFrom } from "@/lib/api";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ export async function POST(req: Request) {
12
+ const rl = rateLimit(`track:${clientIp(req)}`, 15, 10 * 60 * 1000);
13
+ if (!rl.ok) return fail("Muitas tentativas. Aguarde.", 429);
14
+ try {
15
+ const { referenceId, email } = await req.json();
16
+ const e = emailSchema.safeParse(email);
17
+ if (!referenceId || !e.success) return fail("Informe o código do pedido e o e-mail.", 422);
18
+
19
+ const o = await withStoreClient((c) => c.getOrder(String(referenceId).trim()));
20
+ if (!o || (o.email ?? "").toLowerCase() !== e.data.toLowerCase()) {
21
+ // resposta uniforme — não revela se o pedido existe
22
+ return fail("Pedido não encontrado para este código e e-mail.", 404);
23
+ }
24
+ return ok({ order: shapeOrder(o) });
25
+ } catch (e) {
26
+ return failFrom(e);
27
+ }
28
+ }