@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,829 @@
1
+ // UnboxClient — SDK headless da Unbox para Next.js / Vercel (server-side).
2
+ // Zero dependências: usa fetch nativo (Node 18+, Edge Runtime).
3
+ //
4
+ // ⚠️ Este client guarda a API key e o token de loja — use APENAS no servidor
5
+ // (Route Handlers, Server Actions, RSC). Nunca instancie no browser.
6
+ //
7
+ // DOIS ENDPOINTS (transição para a API pública de PARCEIROS):
8
+ // - partners.unbox.com.br/graphql — nova API pública. Uma api key por PARCEIRO (vale para
9
+ // todas as lojas dele); a loja específica autentica por user/senha no signIn. Com
10
+ // `partnerApiKey` configurada, vão para cá: signIn, VITRINE COMPLETA (catalogItems,
11
+ // catalogItemProductBySlug, byId via productIdsOrERPCodes, shopBySlug), tags, cupons
12
+ // (leitura), pedido por referenceId, parcelas, inventário (simpleInventory),
13
+ // subscribeWebhook e createCartByTemplate — tudo validado ao vivo contra o gateway.
14
+ // - core.unbox.com.br/graphql — segue atendendo o que a API de parceiros ainda não expõe:
15
+ // carrinho/checkout/placeOrder, CEP, OTP, área do cliente e availablePaymentMethods.
16
+ // Conforme a Unbox publicar as escritas, cada método ganha o mesmo roteamento condicional.
17
+ // Peculiaridades do gateway (validadas ao vivo): args opcionais não aceitam null (montar
18
+ // query só com args presentes); unions exigem __typename; Authorization aceita token puro
19
+ // E Bearer (a doc pública diz só puro — na prática ambos passam).
20
+
21
+ import type {
22
+ UnboxConfig, AddressInput, CartItemInput, CartResult,
23
+ PlaceOrderParams, FulfillmentOption, CatalogProduct, Connection,
24
+ PaymentLinkConstraints, DeviceInput, SimpleInventoryInfo,
25
+ } from "./types";
26
+
27
+ const DEFAULTS = {
28
+ apiBaseUrl: "https://api.unbox.com.br",
29
+ gqlUrl: "https://core.unbox.com.br/graphql",
30
+ // API pública de PARCEIROS — gateway na frente do core. Uma api key por parceiro
31
+ // (vale p/ todas as lojas dele); a loja é autenticada pelo user/senha no signIn.
32
+ partnerGqlUrl: "https://partners.unbox.com.br/graphql",
33
+ language: "pt-BR",
34
+ };
35
+
36
+ /** Bloco do endereço de entrega dentro de `fulfillmentGroups`. É uma UNIÃO
37
+ * (`OrderFulfillmentGroupData`), e o servidor precisa resolver o tipo concreto em runtime.
38
+ * Medido: o front nativo da Unbox lê `data.shippingAddress` na conta do cliente (funciona lá),
39
+ * e a consulta por `referenceId` falhou em produção com "must resolve to an Object type".
40
+ * Como a falha derruba a consulta INTEIRA e não só o campo, quem consulta pede o endereço e,
41
+ * se o servidor não conseguir resolver, repete sem ele: a loja mostra o endereço onde a API
42
+ * entrega e a página do pedido nunca deixa de abrir. */
43
+ export const BLOCO_ENDERECO_GRUPO =
44
+ "data{ ... on ShippingOrderFulfillmentGroupData { shippingAddress{fullName address1 number neighborhood city region postal} } }";
45
+
46
+ function ehUniaoNaoResolvida(e: unknown): boolean {
47
+ const msgs = e instanceof UnboxError
48
+ ? [e.message, ...(e.errors ?? []).map((x: any) => x?.message ?? "")].join(" ")
49
+ : String(e);
50
+ return /must resolve to an Object type|Could not determine the exact type|Can't resolve/i.test(msgs);
51
+ }
52
+
53
+ /** Roda `exec` com o bloco de endereço; só repete sem ele se o erro for a união não resolvida. */
54
+ export async function comEnderecoDoGrupo<T>(exec: (blocoEndereco: string) => Promise<T>): Promise<T> {
55
+ try {
56
+ return await exec(BLOCO_ENDERECO_GRUPO);
57
+ } catch (e) {
58
+ if (!ehUniaoNaoResolvida(e)) throw e;
59
+ console.warn("[unbox] fulfillmentGroups.data não resolveu no servidor: repetindo a consulta sem o endereço de entrega.");
60
+ return await exec("");
61
+ }
62
+ }
63
+
64
+ /** Código do erro lançado quando a Unbox não respondeu dentro do prazo (ver gql()). */
65
+ export const UNBOX_TIMEOUT = "TIMEOUT";
66
+ export function isUnboxTimeout(e: unknown): boolean {
67
+ return e instanceof UnboxError && e.message === UNBOX_TIMEOUT;
68
+ }
69
+
70
+ /** placeOrder cria pedido + cobra cartão/gera Pix: passa por antifraude e adquirente e pode
71
+ * levar bem mais que os 15 s do timeout padrão. Abortar no cliente NÃO aborta no servidor —
72
+ * o pedido pode nascer depois que a loja já desistiu, e a tela liberava "Pagar" de novo
73
+ * (cobrança dupla, caso real). Prazo próprio, longo, e erro distinguível (UNBOX_TIMEOUT). */
74
+ export const PLACE_ORDER_TIMEOUT_MS = 90_000;
75
+
76
+ export class UnboxError extends Error {
77
+ errors: any[];
78
+ constructor(message: string, errors: any[] = []) {
79
+ super(message);
80
+ this.name = "UnboxError";
81
+ this.errors = errors;
82
+ }
83
+ }
84
+
85
+ /** Erro que indica formato de Authorization rejeitado (gateway de parceiros). */
86
+ function isAuthSchemeError(errors: any[]): boolean {
87
+ const msg = errors.map((e: any) => `${e.errorType ?? ""} ${e.message ?? ""}`).join(" ").toUpperCase();
88
+ return /UNAUTHORIZED|NOT AUTHORIZED|ACCESS_DENIED|UNAUTHENTICATED|INVALID TOKEN|401|403/.test(msg);
89
+ }
90
+
91
+ export class UnboxClient {
92
+ apiKey: string;
93
+ shopId: string;
94
+ apiBaseUrl: string;
95
+ gqlUrl: string;
96
+ partnerApiKey: string;
97
+ partnerGqlUrl: string;
98
+ captchaBypass: string;
99
+ language: string;
100
+ timeoutMs: number;
101
+ token: string | null = null;
102
+ /** Formato do Authorization no gateway de parceiros. A doc oficial (docs.unbox.com.br)
103
+ * manda o token PURO, sem "Bearer " ("prefixá-lo quebra a autenticação") — por isso o
104
+ * default é "raw". O fallback pra "bearer" fica só como defesa se o gateway mudar. */
105
+ private partnerAuthScheme: "bearer" | "raw" = "raw";
106
+
107
+ constructor(cfg: UnboxConfig) {
108
+ this.apiKey = cfg.apiKey;
109
+ this.shopId = cfg.shopId;
110
+ this.apiBaseUrl = cfg.apiBaseUrl ?? DEFAULTS.apiBaseUrl;
111
+ this.gqlUrl = cfg.gqlUrl ?? DEFAULTS.gqlUrl;
112
+ this.partnerApiKey = cfg.partnerApiKey ?? "";
113
+ this.partnerGqlUrl = cfg.partnerGqlUrl ?? DEFAULTS.partnerGqlUrl;
114
+ this.captchaBypass = cfg.captchaBypass ?? "";
115
+ this.language = cfg.language ?? DEFAULTS.language;
116
+ this.timeoutMs = cfg.timeoutMs ?? 15000;
117
+ }
118
+
119
+ /** true = api key de PARCEIRO configurada → signIn e leituras com paridade vão pra
120
+ * API de parceiros; carrinho/checkout/cliente seguem no core (escritas ainda não
121
+ * existem lá). Sem a key, comportamento 100% igual ao anterior (só core). */
122
+ get usesPartnerApi(): boolean { return Boolean(this.partnerApiKey); }
123
+
124
+ // -------------------------------------------------------------------- auth
125
+ /**
126
+ * Autentica com user/senha DA LOJA e guarda o access_token (JWT, ~24h).
127
+ * - Modo parceiro: mutation `signIn` na API de parceiros (x-api-key do parceiro +
128
+ * x-captcha-verification opcional). O token retornado é o mesmo JWT do core — o
129
+ * gateway de parceiros é um proxy do core — e vale nos dois endpoints.
130
+ * - Modo antigo: REST /auth/signin com a api key da loja.
131
+ */
132
+ async signIn(username: string, password: string): Promise<string> {
133
+ if (this.usesPartnerApi) return this.signInPartner(username, password);
134
+ const res = await fetch(`${this.apiBaseUrl}/auth/signin`, {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
137
+ body: JSON.stringify({ username, password }),
138
+ signal: AbortSignal.timeout(this.timeoutMs),
139
+ });
140
+ if (!res.ok) throw new UnboxError(`signin HTTP ${res.status}: ${await res.text()}`);
141
+ const data = await res.json();
142
+ if (!data.access_token) throw new UnboxError("signin sem access_token");
143
+ this.token = data.access_token;
144
+ return data.access_token as string;
145
+ }
146
+
147
+ /** signIn via API de parceiros (mutation GQL). Não exige token prévio.
148
+ * Doc oficial: o signIn EXIGE x-captcha-verification (UNBOX_CAPTCHA_BYPASS). */
149
+ private async signInPartner(username: string, password: string): Promise<string> {
150
+ const headers: Record<string, string> = {
151
+ "Content-Type": "application/json",
152
+ "x-api-key": this.partnerApiKey,
153
+ };
154
+ if (this.captchaBypass) headers["x-captcha-verification"] = this.captchaBypass;
155
+ const q = `mutation($i:SignInInput!){ signIn(input:$i){ access_token id_token } }`;
156
+ const res = await fetch(this.partnerGqlUrl, {
157
+ method: "POST",
158
+ headers,
159
+ body: JSON.stringify({ query: q, variables: { i: { username, password } } }),
160
+ signal: AbortSignal.timeout(this.timeoutMs),
161
+ });
162
+ const json = await res.json();
163
+ if (json.errors?.length) {
164
+ const hint = this.captchaBypass ? "" : " (x-captcha-verification ausente — preencha UNBOX_CAPTCHA_BYPASS, obrigatório no signIn de parceiros)";
165
+ throw new UnboxError(`signIn (partner): ${json.errors.map((e: any) => e.message).join(" | ")}${hint}`, json.errors);
166
+ }
167
+ const token = json.data?.signIn?.access_token;
168
+ if (!token) throw new UnboxError("signIn (partner) sem access_token");
169
+ this.token = token;
170
+ return token as string;
171
+ }
172
+
173
+ setToken(token: string) { this.token = token; }
174
+
175
+ // ------------------------------------------------------------- GraphQL (low)
176
+ async gql<T = any>(
177
+ query: string,
178
+ variables: Record<string, any> = {},
179
+ opts: { token?: string; captcha?: boolean; timeoutMs?: number } = {},
180
+ ): Promise<T> {
181
+ const token = opts.token ?? this.token;
182
+ if (!token) throw new UnboxError("sem token: chame signIn() ou passe opts.token");
183
+ const headers: Record<string, string> = {
184
+ "Content-Type": "application/json",
185
+ Authorization: `Bearer ${token}`,
186
+ };
187
+ // placeOrder, customerOTPRequest, customerPasswordlessSignIn.
188
+ // ⚠️ ORDEM IMPORTA: o header x-captcha-verification foi feito pro UNBOX_CAPTCHA_BYPASS
189
+ // (segredo de 64 chars). Mandar a api key (da2-...) no lugar faz o backend repassá-la ao
190
+ // reCAPTCHA Enterprise, que devolve MALFORMED → o cliente vê CAPTCHA_MALFORMED_ERROR no
191
+ // meio do pagamento (caso real em produção). A key só entra como último recurso se não houver bypass.
192
+ if (opts.captcha) headers["x-captcha-verification"] = this.captchaBypass || this.apiKey;
193
+ let res: Response;
194
+ try {
195
+ res = await fetch(this.gqlUrl, {
196
+ method: "POST",
197
+ headers,
198
+ body: JSON.stringify({ query, variables }),
199
+ signal: AbortSignal.timeout(opts.timeoutMs ?? this.timeoutMs),
200
+ });
201
+ } catch (e: any) {
202
+ // Timeout vira um UnboxError reconhecível (código TIMEOUT). Quem chama decide o que fazer:
203
+ // em leitura, tanto faz; em placeOrder, é a diferença entre "tente de novo" e "NÃO pague de novo".
204
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") throw new UnboxError(UNBOX_TIMEOUT, [{ message: UNBOX_TIMEOUT }]);
205
+ throw e;
206
+ }
207
+ const json = await res.json();
208
+ if (json.errors?.length) {
209
+ throw new UnboxError(json.errors.map((e: any) => e.message).join(" | "), json.errors);
210
+ }
211
+ return json.data as T;
212
+ }
213
+
214
+ // ------------------------------------------------------ GraphQL (partner, low)
215
+ /**
216
+ * Chamada à API de PARCEIROS. Headers: x-api-key (key do parceiro) + Authorization
217
+ * (token do signIn — o gateway repassa ao core, que resolve a loja pelo JWT; por isso
218
+ * nenhuma query de parceiro pede shopId). O formato do Authorization não é documentado:
219
+ * tentamos `Bearer <jwt>` e, se vier erro de auth, refazemos UMA vez com o token cru,
220
+ * memorizando o formato que funcionou para as próximas chamadas.
221
+ */
222
+ async gqlPartner<T = any>(query: string, variables: Record<string, any> = {}): Promise<T> {
223
+ if (!this.token) throw new UnboxError("sem token: chame signIn() ou passe setToken()");
224
+ const attempt = async (scheme: "bearer" | "raw"): Promise<{ json: any }> => {
225
+ const res = await fetch(this.partnerGqlUrl, {
226
+ method: "POST",
227
+ headers: {
228
+ "Content-Type": "application/json",
229
+ "x-api-key": this.partnerApiKey,
230
+ // Doc oficial: token PURO no Authorization (sem "Bearer ").
231
+ Authorization: scheme === "raw" ? String(this.token) : `Bearer ${this.token}`,
232
+ },
233
+ body: JSON.stringify({ query, variables }),
234
+ signal: AbortSignal.timeout(this.timeoutMs),
235
+ });
236
+ return { json: await res.json() };
237
+ };
238
+ let { json } = await attempt(this.partnerAuthScheme);
239
+ if (json.errors?.length && isAuthSchemeError(json.errors)) {
240
+ const other = this.partnerAuthScheme === "raw" ? "bearer" : "raw";
241
+ const retry = await attempt(other);
242
+ if (!retry.json.errors?.length || !isAuthSchemeError(retry.json.errors)) {
243
+ this.partnerAuthScheme = other; // memoriza o formato aceito
244
+ json = retry.json;
245
+ }
246
+ }
247
+ if (json.errors?.length) {
248
+ throw new UnboxError(json.errors.map((e: any) => e.message).join(" | "), json.errors);
249
+ }
250
+ return json.data as T;
251
+ }
252
+
253
+ // ------------------------------------------------------------------- catálogo
254
+ /** Seleção de produto compartilhada entre core e partner (schemas idênticos aqui). */
255
+ private static CATALOG_PRODUCT_FIELDS = `
256
+ _id productId title slug productType isVisible isSoldOut isBackorder isLowQuantity recurrenceAllowed imageUrls
257
+ minOrderQuantity maxOrderQuantity tagIds
258
+ pricing{displayPrice price minPrice maxPrice}
259
+ variants{_id title sku pricing{price displayPrice compareAtPrice{displayAmount}}}`;
260
+
261
+ async getCatalog(opts: {
262
+ first?: number; offset?: number; searchText?: string; tagIds?: string[];
263
+ sortBy?: string; sortOrder?: "asc" | "desc";
264
+ } = {}): Promise<Connection<{ product: CatalogProduct }>> {
265
+ // Partner: sem shopIds (loja vem do JWT). Diferenças do gateway (validadas ao vivo):
266
+ // 1) args opcionais NÃO podem ir nulos (o resolver rejeita sortOrder:null, que o core
267
+ // tolerava) → montamos a query só com os args realmente presentes;
268
+ // 2) o union CatalogItem exige __typename na seleção pra resolver o tipo.
269
+ if (this.usesPartnerApi) {
270
+ const decl = ["$first:Int", "$offset:Int"];
271
+ const args = ["first:$first", "offset:$offset"];
272
+ const vars: Record<string, any> = { first: opts.first ?? 24, offset: opts.offset ?? 0 };
273
+ const opt = (name: string, type: string, value: any) => {
274
+ if (value === undefined || value === null) return;
275
+ decl.push(`$${name}:${type}`); args.push(`${name}:$${name}`); vars[name] = value;
276
+ };
277
+ opt("searchText", "String", opts.searchText);
278
+ opt("tagIds", "[ID]", opts.tagIds);
279
+ opt("sortBy", "CatalogItemSortByField", opts.sortBy);
280
+ opt("sortOrder", "SortOrder", opts.sortOrder);
281
+ const q = `query(${decl.join(",")}){
282
+ catalogItems(${args.join(",")}){
283
+ totalCount pageInfo{hasNextPage endCursor}
284
+ nodes{__typename ... on CatalogItemProduct{ _id shortDescription product{${UnboxClient.CATALOG_PRODUCT_FIELDS}
285
+ }}}
286
+ }}`;
287
+ const d = await this.gqlPartner<{ catalogItems: any }>(q, vars);
288
+ return d.catalogItems;
289
+ }
290
+ const q = `query($s:[ID]!,$first:Int,$offset:Int,$searchText:String,$tagIds:[ID],$sortBy:CatalogItemSortByField,$sortOrder:SortOrder){
291
+ catalogItems(shopIds:$s,first:$first,offset:$offset,searchText:$searchText,tagIds:$tagIds,sortBy:$sortBy,sortOrder:$sortOrder){
292
+ totalCount pageInfo{hasNextPage endCursor}
293
+ nodes{... on CatalogItemProduct{ _id shortDescription product{${UnboxClient.CATALOG_PRODUCT_FIELDS}
294
+ }}}
295
+ }}`;
296
+ const d = await this.gql<{ catalogItems: any }>(q, {
297
+ s: [this.shopId], first: opts.first ?? 24, offset: opts.offset ?? 0,
298
+ searchText: opts.searchText, tagIds: opts.tagIds,
299
+ sortBy: opts.sortBy, sortOrder: opts.sortOrder,
300
+ });
301
+ return d.catalogItems;
302
+ }
303
+
304
+ private static PDP_PRODUCT_FIELDS = `_id productId title pageTitle slug description additionalInformation productType
305
+ sku isVisible isSoldOut isBackorder isLowQuantity recurrenceAllowed
306
+ minOrderQuantity maxOrderQuantity imageUrls videoUrls tagIds metaDescription
307
+ pricing{displayPrice price minPrice maxPrice}
308
+ variants{_id title sku pricing{price displayPrice compareAtPrice{displayAmount}}}`;
309
+
310
+ async getProductBySlug(productSlug: string): Promise<any> {
311
+ // Partner: mesma query sem shopId (loja vem do JWT). Validado ao vivo no gateway.
312
+ if (this.usesPartnerApi) {
313
+ const q = `query($productSlug:String!){
314
+ catalogItemProductBySlug(productSlug:$productSlug,filterSoldOutVariants:false){
315
+ _id shortDescription cardDescription publishedUrl
316
+ product{ ${UnboxClient.PDP_PRODUCT_FIELDS} }}}`;
317
+ const d = await this.gqlPartner<{ catalogItemProductBySlug: any }>(q, { productSlug });
318
+ return d.catalogItemProductBySlug;
319
+ }
320
+ const q = `query($shopId:ID!,$productSlug:String!){
321
+ catalogItemProductBySlug(shopId:$shopId,productSlug:$productSlug,filterSoldOutVariants:false){
322
+ _id shortDescription cardDescription publishedUrl
323
+ product{ ${UnboxClient.PDP_PRODUCT_FIELDS} }}}`;
324
+ const d = await this.gql<{ catalogItemProductBySlug: any }>(q, { shopId: this.shopId, productSlug });
325
+ return d.catalogItemProductBySlug;
326
+ }
327
+
328
+ /** PDP por productId — fallback/deep link (catalogItemProductById). */
329
+ async getProductById(productId: string): Promise<any> {
330
+ // Partner: catalogItemProductById NÃO existe no gateway; equivalente validado ao vivo:
331
+ // catalogItems(productIdsOrERPCodes:[id], first:1) — devolve o mesmo CatalogItemProduct.
332
+ if (this.usesPartnerApi) {
333
+ const q = `query($ids:[String]){
334
+ catalogItems(productIdsOrERPCodes:$ids,first:1){
335
+ nodes{__typename ... on CatalogItemProduct{ _id shortDescription publishedUrl
336
+ product{ ${UnboxClient.PDP_PRODUCT_FIELDS} }}}}}`;
337
+ const d = await this.gqlPartner<{ catalogItems: any }>(q, { ids: [productId] });
338
+ return d.catalogItems?.nodes?.[0] ?? null;
339
+ }
340
+ const q = `query($shopId:ID!,$productId:ID!){
341
+ catalogItemProductById(shopId:$shopId,productId:$productId,filterSoldOutVariants:false){
342
+ _id shortDescription publishedUrl
343
+ product{ ${UnboxClient.PDP_PRODUCT_FIELDS} }}}`;
344
+ const d = await this.gql<{ catalogItemProductById: any }>(q, { shopId: this.shopId, productId });
345
+ return d.catalogItemProductById;
346
+ }
347
+
348
+ // isTopLevel omitido = sem filtro (igual à vitrine padrão da Unbox). Forçar `true` esconde
349
+ // categorias reais e visíveis marcadas isTopLevel:false no admin (ex.: "Acessórios") — 404
350
+ // em /categoria/<slug> mesmo com o produto corretamente vinculado à categoria.
351
+ async getTags(isTopLevel?: boolean): Promise<any[]> {
352
+ // Partner: mesma query, sem shopId (a loja vem do JWT). Campos idênticos (validado
353
+ // por introspecção no schema de parceiros).
354
+ if (this.usesPartnerApi) {
355
+ const q = `query($isTopLevel:Boolean){
356
+ tags(isTopLevel:$isTopLevel,shouldIncludeInvisible:false,shouldIncludeDeleted:false,first:100){
357
+ nodes{_id name displayTitle slug description isTopLevel isVisible position subTagIds featuredProductIds}
358
+ }}`;
359
+ const d = await this.gqlPartner<{ tags: any }>(q, { isTopLevel });
360
+ return d.tags.nodes;
361
+ }
362
+ const q = `query($shopId:ID!,$isTopLevel:Boolean){
363
+ tags(shopId:$shopId,isTopLevel:$isTopLevel,shouldIncludeInvisible:false,shouldIncludeDeleted:false,first:100){
364
+ nodes{_id name displayTitle slug description isTopLevel isVisible position subTagIds featuredProductIds}
365
+ }}`;
366
+ const d = await this.gql<{ tags: any }>(q, { shopId: this.shopId, isTopLevel });
367
+ return d.tags.nodes;
368
+ }
369
+
370
+ // ----------------------------------------------------------------------- loja
371
+ /** Dados da loja: promoções (shopSales), política de assinatura, settings, pagamentos. */
372
+ async getShop(slug: string): Promise<any> {
373
+ // Partner: shopBySlug() SEM argumentos (a loja vem do JWT; o `slug` recebido é ignorado).
374
+ // O gateway não expõe allowGuestCheckout nem settings.maxInstallments — omitidos aqui;
375
+ // o app já tem defaults (maxInstallments ?? 12) e allowGuestCheckout não é consumido.
376
+ if (this.usesPartnerApi) {
377
+ const q = `query{ shopBySlug{
378
+ _id name slug acceptsBoleto acceptsCreditCard
379
+ settings{allowAnonymousRecurringOrders allowLegalPersonSales showOutOfStockCatalogs}
380
+ shopSales{_id code label description discountMethod enabled createdAt
381
+ calculation{__typename ... on CalculationFreeItemByTier { tiers { cartSubtotalGTE catalogProductVariant { _id title } } }}}
382
+ recurringOrdersPolicy{_id enabled keepOrderPricingPolicy
383
+ allowedFrequencies{_id title periodicity interval}
384
+ pricingPolicy{type value}
385
+ customerActions{canSkipCycle canPause canChangeFrequency canChangeAddress canAddProducts canRemoveProducts canChangeProductQuantity}}
386
+ }}`;
387
+ const d = await this.gqlPartner<{ shopBySlug: any }>(q);
388
+ return d.shopBySlug;
389
+ }
390
+ const q = `query($slug:String!){ shopBySlug(slug:$slug){
391
+ _id name slug acceptsBoleto acceptsCreditCard allowGuestCheckout
392
+ settings{maxInstallments allowAnonymousRecurringOrders allowLegalPersonSales showOutOfStockCatalogs}
393
+ shopSales{_id code label description discountMethod enabled createdAt
394
+ calculation{__typename ... on CalculationFreeItemByTier { tiers { cartSubtotalGTE catalogProductVariant { _id title } } }}}
395
+ recurringOrdersPolicy{_id enabled keepOrderPricingPolicy
396
+ allowedFrequencies{_id title periodicity interval}
397
+ pricingPolicy{type value}
398
+ customerActions{canSkipCycle canPause canChangeFrequency canChangeAddress canAddProducts canRemoveProducts canChangeProductQuantity}}
399
+ }}`;
400
+ const d = await this.gql<{ shopBySlug: any }>(q, { slug });
401
+ return d.shopBySlug;
402
+ }
403
+
404
+ async getPaymentMethods(): Promise<any[]> {
405
+ const q = `query($s:ID!){availablePaymentMethods(shopId:$s){name displayName isEnabled canRefund pluginName}}`;
406
+ const d = await this.gql<{ availablePaymentMethods: any[] }>(q, { s: this.shopId });
407
+ return d.availablePaymentMethods;
408
+ }
409
+
410
+ // ------------------------------------------------------------------ promoções
411
+ async listDiscountCodes(first = 50): Promise<Connection<any>> {
412
+ // Partner: discountCodes sem shopId (loja vem do JWT); nodes do tipo Discount, com os
413
+ // mesmos campos que o app consome.
414
+ if (this.usesPartnerApi) {
415
+ const q = `query($first:Int){ discountCodes(first:$first){
416
+ totalCount nodes{_id code label description enabled discountMethod calculation{__typename}} }}`;
417
+ const d = await this.gqlPartner<{ discountCodes: any }>(q, { first });
418
+ return d.discountCodes;
419
+ }
420
+ const q = `query($s:ID!,$first:ConnectionLimitInt){ discountCodes(shopId:$s,first:$first){
421
+ totalCount nodes{_id code label description enabled discountMethod calculation{__typename}} }}`;
422
+ const d = await this.gql<{ discountCodes: any }>(q, { s: this.shopId, first });
423
+ return d.discountCodes;
424
+ }
425
+
426
+ // -------------------------------------------------------------------- carrinho
427
+ // ⚠️ Schema LIVE: `recurringItemsFrequencyId` NÃO existe em CreateCartInput/AddCartItemsInput
428
+ // (diverge da doc — vale o live). Itens de assinatura marcam-se só com `isRecurring:true`;
429
+ // a FREQUÊNCIA vai no placeOrder (orderRecurrence.recurringItemsFrequencyId).
430
+ async createCart(items: CartItemInput[]): Promise<CartResult> {
431
+ const q = `mutation($i:CreateCartInput!){ createCart(input:$i){
432
+ token
433
+ cart{ _id totalItemQuantity expiresAt
434
+ items{edges{node{_id productConfiguration{productId productVariantId} title variantTitle quantity thumbnail price{amount displayAmount} isRecurring isDiscountedBonusItem}}}
435
+ checkout{fulfillmentGroups{_id} summary{itemTotal{displayAmount} total{amount displayAmount}}}}
436
+ incorrectPriceFailures{productConfiguration{productId} providedPrice{amount} currentPrice{amount}}
437
+ minOrderQuantityFailures{minOrderQuantity quantity}
438
+ maxOrderQuantityFailures{maxOrderQuantity quantity} }}`;
439
+ const d = await this.gql<{ createCart: any }>(q, { i: {
440
+ shopId: this.shopId,
441
+ items: items.map((it) => ({
442
+ price: { amount: it.price, currencyCode: it.currencyCode ?? "BRL" },
443
+ productConfiguration: { productId: it.productId, productVariantId: it.productVariantId },
444
+ quantity: it.quantity,
445
+ isRecurring: it.isRecurring ?? false,
446
+ thumbnail: it.thumbnail ?? "",
447
+ })),
448
+ } });
449
+ const r = d.createCart;
450
+ // Obs.: o servidor SOBRESCREVE o preço com o do catálogo; incorrectPriceFailures vem vazio na
451
+ // prática (ver types.ts). Mantido só como aviso defensivo — quantidade min/max sim pode falhar.
452
+ if (r.incorrectPriceFailures?.length) console.warn("[unbox] incorrectPriceFailures:", r.incorrectPriceFailures);
453
+ return {
454
+ cartId: r.cart._id, cartToken: r.token, cart: r.cart,
455
+ incorrectPriceFailures: r.incorrectPriceFailures,
456
+ minOrderQuantityFailures: r.minOrderQuantityFailures,
457
+ maxOrderQuantityFailures: r.maxOrderQuantityFailures,
458
+ };
459
+ }
460
+
461
+ /** Adiciona itens a um carrinho existente (exige cartId + cartToken). isRecurring marca assinatura. */
462
+ async addCartItems(cartId: string, cartToken: string, items: CartItemInput[]): Promise<any> {
463
+ const q = `mutation($i:AddCartItemsInput!){ addCartItems(input:$i){
464
+ cart{ _id totalItemQuantity items{edges{node{_id productConfiguration{productId productVariantId} title variantTitle quantity price{amount displayAmount} isRecurring isDiscountedBonusItem}}} }
465
+ cartEvents{type data}
466
+ minOrderQuantityFailures{minOrderQuantity quantity} maxOrderQuantityFailures{maxOrderQuantity quantity} }}`;
467
+ const d = await this.gql<{ addCartItems: any }>(q, { i: {
468
+ cartId, cartToken,
469
+ items: items.map((it) => ({
470
+ price: { amount: it.price, currencyCode: it.currencyCode ?? "BRL" },
471
+ productConfiguration: { productId: it.productId, productVariantId: it.productVariantId },
472
+ quantity: it.quantity, isRecurring: it.isRecurring ?? false, thumbnail: it.thumbnail ?? "",
473
+ })),
474
+ } });
475
+ return d.addCartItems;
476
+ }
477
+
478
+ /** Altera a quantidade de um item do carrinho (cartItemId = node._id do item). */
479
+ async updateItemQuantity(cartId: string, cartToken: string, cartItemId: string, quantity: number): Promise<any> {
480
+ const q = `mutation($i:UpdateCartItemsQuantityInput!){ updateCartItemsQuantity(input:$i){
481
+ cart{ _id totalItemQuantity } cartEvents{type data} }}`;
482
+ const d = await this.gql<{ updateCartItemsQuantity: any }>(q, { i: { cartId, cartToken, items: [{ cartItemId, quantity }] } });
483
+ return d.updateCartItemsQuantity;
484
+ }
485
+
486
+ /** Remove itens do carrinho (cartItemIds = node._id dos itens). */
487
+ async removeCartItems(cartId: string, cartToken: string, cartItemIds: string[]): Promise<any> {
488
+ const q = `mutation($i:RemoveCartItemsInput!){ removeCartItems(input:$i){
489
+ cart{ _id totalItemQuantity } cartEvents{type data} }}`;
490
+ const d = await this.gql<{ removeCartItems: any }>(q, { i: { cartId, cartToken, cartItemIds } });
491
+ return d.removeCartItems;
492
+ }
493
+
494
+ /**
495
+ * Recarrega um carrinho anônimo (rehidratar a sessão). Retorna o cart completo: itens,
496
+ * brindes, resumo com descontos, e o endereço de entrega já gravado (se houver).
497
+ */
498
+ async getCart(cartId: string, cartToken: string): Promise<any> {
499
+ const q = `query($cartId:ID!,$cartToken:String!){ anonymousCartByCartId(cartId:$cartId,cartToken:$cartToken){
500
+ _id email expiresAt referenceId recurringItemsFrequencyId totalItemQuantity
501
+ items(first:100){ totalCount edges{node{
502
+ _id productConfiguration{productId productVariantId} title variantTitle quantity addedAt
503
+ thumbnail price{amount displayAmount} isRecurring isDiscountedBonusItem }}}
504
+ checkout{
505
+ fulfillmentGroups{_id selectedFulfillmentOption{fulfillmentMethod{_id displayName name} price{displayAmount}}
506
+ data{shippingAddress{fullName taxPayerId phone address1 address2 number neighborhood city region postal}}}
507
+ summary{itemTotal{displayAmount} discountTotal{displayAmount} fulfillmentTotal{displayAmount} taxTotal{displayAmount} total{amount displayAmount}}} }}`;
508
+ const d = await this.gql<{ anonymousCartByCartId: any }>(q, { cartId, cartToken });
509
+ return d.anonymousCartByCartId;
510
+ }
511
+
512
+ /**
513
+ * Grava o e-mail no carrinho anônimo (etapa de contato do checkout, antes do placeOrder).
514
+ * Habilita recuperação de CARRINHO ABANDONADO: o e-mail fica associado ao cart no servidor
515
+ * mesmo se o cliente não finalizar (capturado do storefront oficial — setEmailOnAnonymousCart).
516
+ */
517
+ async setEmailOnCart(cartId: string, cartToken: string, email: string): Promise<any> {
518
+ const q = `mutation($i:SetEmailOnAnonymousCartInput!){ setEmailOnAnonymousCart(input:$i){
519
+ cart{_id email} }}`;
520
+ const d = await this.gql<{ setEmailOnAnonymousCart: any }>(q, { i: { cartId, cartToken, email } });
521
+ return d.setEmailOnAnonymousCart;
522
+ }
523
+
524
+ async setShippingAddress(cartId: string, cartToken: string, address: AddressInput): Promise<string> {
525
+ const q = `mutation($i:SetShippingAddressOnCartInput!){ setShippingAddressOnCart(input:$i){
526
+ cart{checkout{fulfillmentGroups{_id}}} }}`;
527
+ const d = await this.gql<{ setShippingAddressOnCart: any }>(q, {
528
+ i: { cartId, cartToken, address: { country: "BR", ...address } },
529
+ });
530
+ return d.setShippingAddressOnCart.cart.checkout.fulfillmentGroups[0]._id;
531
+ }
532
+
533
+ /** Retorna TODOS os fulfillmentGroups do carrinho (suporte a N grupos de entrega). */
534
+ async getFulfillmentGroupIds(cartId: string, cartToken: string): Promise<string[]> {
535
+ const q = `query($cartId:ID!,$cartToken:String!){ anonymousCartByCartId(cartId:$cartId,cartToken:$cartToken){
536
+ checkout{fulfillmentGroups{_id}} }}`;
537
+ const d = await this.gql<{ anonymousCartByCartId: any }>(q, { cartId, cartToken });
538
+ return (d.anonymousCartByCartId?.checkout?.fulfillmentGroups ?? []).map((g: any) => g._id);
539
+ }
540
+
541
+ /** Cota o frete de UM grupo. OBRIGATÓRIO após setShippingAddress (senão options vem vazio). */
542
+ async quoteShipping(cartId: string, cartToken: string, fulfillmentGroupId: string): Promise<FulfillmentOption[]> {
543
+ const q = `mutation($i:UpdateFulfillmentOptionsForGroupInput!){ updateFulfillmentOptionsForGroup(input:$i){
544
+ cart{checkout{fulfillmentGroups{_id availableFulfillmentOptions{
545
+ price{amount displayAmount} discountPrice{displayAmount}
546
+ fulfillmentMethod{_id name displayName daysToDeliver}}}}} }}`;
547
+ const d = await this.gql<{ updateFulfillmentOptionsForGroup: any }>(q, { i: { cartId, cartToken, fulfillmentGroupId } });
548
+ const groups = d.updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups;
549
+ const fg = groups.find((g: any) => g._id === fulfillmentGroupId) ?? groups[0];
550
+ return fg.availableFulfillmentOptions ?? [];
551
+ }
552
+
553
+ async applyDiscount(cartId: string, cartToken: string, discountCode: string): Promise<any> {
554
+ const q = `mutation($i:ApplyDiscountCodeToCartInput!){ applyDiscountCodeToCart(input:$i){
555
+ cart{_id checkout{summary{discountTotal{displayAmount} total{amount displayAmount}}}} cartEvents{type data} }}`;
556
+ const d = await this.gql<{ applyDiscountCodeToCart: any }>(q, { i: { cartId, token: cartToken, shopId: this.shopId, discountCode } });
557
+ return d.applyDiscountCodeToCart;
558
+ }
559
+
560
+ /**
561
+ * Resolve o discount._id a partir do CÓDIGO do cupom (necessário p/ removeDiscountCodeFromCart,
562
+ * já que o Cart lido não expõe os ids de desconto aplicados). Casa por code (case-insensitive).
563
+ */
564
+ async findDiscountIdByCode(code: string): Promise<string | null> {
565
+ const list = await this.listDiscountCodes(100);
566
+ const match = (list.nodes as any[]).find((d) => (d.code ?? "").toLowerCase() === code.toLowerCase());
567
+ return match?._id ?? null;
568
+ }
569
+
570
+ /** Remove um cupom do carrinho. Usa o discountId (= discount._id), NÃO o code. */
571
+ async removeDiscount(cartId: string, cartToken: string, discountId: string): Promise<any> {
572
+ const q = `mutation($i:RemoveDiscountCodeFromCartInput!){ removeDiscountCodeFromCart(input:$i){
573
+ cart{checkout{summary{discountTotal{displayAmount} total{amount displayAmount}}}} cartEvents{type data} }}`;
574
+ const d = await this.gql<{ removeDiscountCodeFromCart: any }>(q, { i: { cartId, token: cartToken, shopId: this.shopId, discountId } });
575
+ return d.removeDiscountCodeFromCart;
576
+ }
577
+
578
+ /** Seleciona o frete de UM grupo e devolve o estado final (itens + total) p/ o placeOrder. */
579
+ async selectShipping(cartId: string, cartToken: string, fulfillmentGroupId: string, fulfillmentMethodId: string): Promise<any> {
580
+ const q = `mutation($i:SelectFulfillmentOptionForGroupInput!){ selectFulfillmentOptionForGroup(input:$i){
581
+ cart{
582
+ items{edges{node{productConfiguration{productId productVariantId} title variantTitle price{amount} quantity addedAt thumbnail isDiscountedBonusItem}}}
583
+ checkout{summary{itemTotal{displayAmount} discountTotal{displayAmount} fulfillmentTotal{displayAmount} total{amount displayAmount}}}} }}`;
584
+ const d = await this.gql<{ selectFulfillmentOptionForGroup: any }>(q, { i: { cartId, cartToken, fulfillmentGroupId, fulfillmentMethodId } });
585
+ return d.selectFulfillmentOptionForGroup.cart;
586
+ }
587
+
588
+ /**
589
+ * Monta os `items` do placeOrder a partir do carrinho final. INCLUI brindes/itens promocionais:
590
+ * a Unbox exige que os itens do fulfillmentGroup batam EXATAMENTE com os do carrinho no servidor
591
+ * (senão FULFILLMENT_GROUP_AND_CART_ITEMS_DO_NOT_MATCH_ERROR). O valor cobrado é o total
592
+ * autoritativo do servidor (payment.amount = summary.total), então incluir brindes não cobra a mais.
593
+ */
594
+ buildOrderItems(cart: any): PlaceOrderParams["items"] {
595
+ return cart.items.edges
596
+ .map((e: any) => e.node)
597
+ .map((n: any) => ({
598
+ addedAt: n.addedAt,
599
+ price: n.price.amount,
600
+ productConfiguration: n.productConfiguration,
601
+ quantity: n.quantity,
602
+ thumbnail: n.thumbnail ?? "",
603
+ // ESSENCIAIS p/ casar com o carrinho — sem eles a Unbox lança
604
+ // FULFILLMENT_GROUP_AND_CART_ITEMS_DO_NOT_MATCH (validado contra um pedido real).
605
+ isRecurring: n.isRecurring ?? false,
606
+ isDiscountedBonusItem: n.isDiscountedBonusItem ?? false,
607
+ }));
608
+ }
609
+
610
+ /**
611
+ * "Calcule o frete" para a PDP (fora do checkout). Cria um carrinho efêmero com 1 item,
612
+ * grava o endereço (só CEP basta na prática p/ cotar) e cota o frete. Compõe primitivas
613
+ * já validadas ao vivo — não depende de createShipmentQuote (schema não confirmado).
614
+ */
615
+ async quoteShippingForProduct(item: CartItemInput, postal: string): Promise<FulfillmentOption[]> {
616
+ const cart = await this.createCart([item]);
617
+ const addr: AddressInput = {
618
+ fullName: "Cotação", taxPayerId: "", postal, address1: "—", number: "0",
619
+ neighborhood: "—", city: "—", region: "SP", phone: "00000000000",
620
+ };
621
+ const fgId = await this.setShippingAddress(cart.cartId, cart.cartToken, addr);
622
+ return this.quoteShipping(cart.cartId, cart.cartToken, fgId);
623
+ }
624
+
625
+ // -------------------------------------------------------------------- checkout
626
+ /** Opções de parcelamento (sem juros) para um valor — usado no cartão de crédito (não assinatura). */
627
+ async getInstallments(amount: number): Promise<Array<{ installment: number; amount: number }>> {
628
+ // Partner: input sem shopId (loja vem do JWT); resposta com os mesmos campos.
629
+ if (this.usesPartnerApi) {
630
+ const q = `query($amount:Float!){ getInstallments(input:{amount:$amount}){ installments{installment amount} } }`;
631
+ const d = await this.gqlPartner<{ getInstallments: { installments: any[] } }>(q, { amount });
632
+ return (d.getInstallments?.installments ?? []).map((i: any) => ({ installment: i.installment, amount: i.amount }));
633
+ }
634
+ const q = `query($shopId:ID!,$amount:Float!){ getInstallments(input:{shopId:$shopId,amount:$amount}){ installments } }`;
635
+ const d = await this.gql<{ getInstallments: { installments: any[] } }>(q, { shopId: this.shopId, amount });
636
+ return (d.getInstallments?.installments ?? []).map((i: any) => ({ installment: i.installment, amount: i.amount }));
637
+ }
638
+
639
+ /** Cria o pedido (REAL). Envia x-captcha-verification automaticamente. */
640
+ async placeOrder(p: PlaceOrderParams): Promise<any> {
641
+ // country é obrigatório (String!) tanto no shippingAddress quanto no billingAddress.
642
+ const address = { country: "BR", ...p.address };
643
+ const payment = p.payment.type === "pix"
644
+ ? { amount: p.total, method: "unboxpay_pix", data: { paymentType: "pix" }, billingAddress: address }
645
+ : {
646
+ amount: p.total, method: "unboxpay_credit", billingAddress: address,
647
+ data: {
648
+ cardHolder: p.payment.card.cardHolder, cardNumber: p.payment.card.cardNumber,
649
+ expirationMonth: p.payment.card.expirationMonth, expirationYear: p.payment.card.expirationYear,
650
+ securityCode: p.payment.card.securityCode, installments: p.payment.card.installments ?? 1,
651
+ paymentType: "credit",
652
+ },
653
+ };
654
+ // device (antifraude/3DS) é OBRIGATÓRIO e vai no NÍVEL RAIZ do PlaceOrderInput
655
+ // (irmão de order/payments). Sem navegador (scripts), o fallback é { type: "API" }.
656
+ const device: DeviceInput = p.device ?? { type: "API" };
657
+ const input: any = {
658
+ order: {
659
+ cartId: p.cartId, currencyCode: "BRL", email: p.email, shopId: this.shopId,
660
+ fulfillmentGroups: [{
661
+ type: "SHIPPING", shopId: this.shopId, totalPrice: p.total,
662
+ selectedFulfillmentMethodId: p.fulfillmentMethodId,
663
+ data: { shippingAddress: address }, items: p.items,
664
+ }],
665
+ },
666
+ payments: [payment],
667
+ device,
668
+ };
669
+ if (p.recurrence) input.orderRecurrence = { createNewRecurringOrder: true, recurringItemsFrequencyId: p.recurrence.recurringItemsFrequencyId };
670
+
671
+ const q = `mutation($i:PlaceOrderInput!){ placeOrder(input:$i){
672
+ token orders{_id referenceId status summary{total{amount displayAmount}}
673
+ recurringOrderId generatedNewRecurringOrder
674
+ payments{method{name} status{status} captureErrorMessage
675
+ data{... on UnboxPayPaymentData{qrCode paymentRecord redirectUrl numberOfInstallments}}}} }}`;
676
+ const d = await this.gql<{ placeOrder: any }>(q, { i: input }, { captcha: true, timeoutMs: PLACE_ORDER_TIMEOUT_MS });
677
+ return d.placeOrder;
678
+ }
679
+
680
+ /**
681
+ * Acompanhar pedido pelo referenceId.
682
+ * ⚠️ SEGURANÇA: com o token de LOJA, isto retorna QUALQUER pedido só pelo referenceId (validado ao
683
+ * vivo) — referenceId é curto e adivinhável. NUNCA exponha esta chamada ao browser sem o BFF antes
684
+ * verificar a POSSE do pedido: compare o `token` do placeOrder (guardado em cookie httpOnly) ou use
685
+ * o token do cliente logado (`UnboxCustomerClient.order`). Ver docs 09-seguranca.
686
+ */
687
+ async getOrder(referenceId: string, token?: string): Promise<any> {
688
+ // Partner: orderByReferenceId(id) NÃO aceita o token de posse — a verificação de posse
689
+ // do pedido é (e sempre foi) responsabilidade do BFF (lib/orders.ts getOwnedOrder, via
690
+ // cookie httpOnly). Diferença de shape: OrderItem tem imageURLs em vez de thumbnail —
691
+ // normalizamos aqui pra manter o contrato do app.
692
+ if (this.usesPartnerApi) {
693
+ const q = (endereco: string) => `query($id:ID!){
694
+ orderByReferenceId(id:$id){
695
+ _id referenceId status email
696
+ summary{total{amount displayAmount}}
697
+ payments{displayName mode processor isCaptured cardBrand captureErrorMessage amount{amount displayAmount}}
698
+ fulfillmentGroups{
699
+ status type trackingCode
700
+ ${endereco}
701
+ items{nodes{_id title variantTitle quantity imageURLs{thumbnail small medium large original} productSlug price{amount displayAmount} subtotal{displayAmount} productConfiguration{productId productVariantId}}}
702
+ }
703
+ invoiceIssued dispatched delivered
704
+ recurringOrderId }}`;
705
+ const d = await comEnderecoDoGrupo((endereco) => this.gqlPartner<{ orderByReferenceId: any }>(q(endereco), { id: referenceId }));
706
+ const order = d.orderByReferenceId;
707
+ for (const g of order?.fulfillmentGroups ?? []) {
708
+ for (const n of g?.items?.nodes ?? []) {
709
+ // imageURLs é um OBJETO de tamanhos (ImageSizes), não uma lista: `?.[0]` vinha vazio.
710
+ const img = n?.imageURLs;
711
+ if (n && n.thumbnail === undefined) n.thumbnail = img?.thumbnail ?? img?.small ?? img?.medium ?? img?.original ?? "";
712
+ }
713
+ }
714
+ return order;
715
+ }
716
+ // Seleção conservadora: fora displayStatus e payments.data (resolvedores que quebram a
717
+ // consulta inteira) e trackingUrl (não existe neste contexto: use trackingCode). O endereço
718
+ // do grupo entra pelo comEnderecoDoGrupo, que repete sem ele se a união não resolver.
719
+ const q = (endereco: string) => `query($id:ID!,$shopId:ID,$token:String){
720
+ orderByReferenceId(id:$id,shopId:$shopId,token:$token){
721
+ _id referenceId status email
722
+ summary{total{amount displayAmount}}
723
+ payments{displayName mode processor isCaptured cardBrand captureErrorMessage amount{amount displayAmount}}
724
+ fulfillmentGroups{
725
+ status type trackingCode
726
+ ${endereco}
727
+ items{nodes{_id title variantTitle quantity thumbnail productSlug price{amount displayAmount} subtotal{displayAmount} productConfiguration{productId productVariantId}}}
728
+ }
729
+ invoiceIssued dispatched delivered
730
+ recurringOrderId }}`;
731
+ const d = await comEnderecoDoGrupo((endereco) => this.gql<{ orderByReferenceId: any }>(q(endereco), { id: referenceId, shopId: this.shopId, token }));
732
+ return d.orderByReferenceId;
733
+ }
734
+
735
+ // -------------------------------------------------------- conta do cliente (OTP)
736
+ /** Storefront pede OTP por e-mail (contexto de loja). Exige x-captcha-verification (= UNBOX_CAPTCHA_BYPASS). */
737
+ async requestCustomerOtp(email: string): Promise<boolean> {
738
+ const q = `mutation($i:CustomerOTPRequestInput!){ customerOTPRequest(input:$i){success} }`;
739
+ const d = await this.gql<{ customerOTPRequest: { success: boolean } }>(q, { i: { email, shopId: this.shopId } }, { captcha: true });
740
+ return d.customerOTPRequest.success;
741
+ }
742
+
743
+ /** Troca o OTP pelo token DO CLIENTE (use-o no UnboxCustomerClient). */
744
+ async customerSignIn(email: string, otp: string): Promise<{ accessToken: string; firstAccess: boolean }> {
745
+ const q = `mutation($i:CustomerPasswordlessSignInInput){ customerPasswordlessSignIn(input:$i){
746
+ accessToken idToken firstAccess newShopSignIn }}`;
747
+ const d = await this.gql<{ customerPasswordlessSignIn: any }>(q, { i: { email, otp, shopId: this.shopId } }, { captcha: true });
748
+ return { accessToken: d.customerPasswordlessSignIn.accessToken, firstAccess: d.customerPasswordlessSignIn.firstAccess };
749
+ }
750
+
751
+ async customerAccountExists(email: string): Promise<boolean> {
752
+ const q = `query($i:HasCustomerAccountInput){ hasCustomerAccount(input:$i){result} }`;
753
+ const d = await this.gql<{ hasCustomerAccount: { result: boolean } }>(q, { i: { email, shopId: this.shopId } });
754
+ return d.hasCustomerAccount.result;
755
+ }
756
+
757
+ async getAddressByPostalCode(postalCode: string): Promise<any> {
758
+ const q = `query($i:getAddressByPostalCodeInput!){ getAddressByPostalCode(input:$i){
759
+ address1 neighborhood city region cityCode }}`;
760
+ const d = await this.gql<{ getAddressByPostalCode: any }>(q, { i: { shopId: this.shopId, postalCode: postalCode.replace(/\D/g, "") } });
761
+ return d.getAddressByPostalCode;
762
+ }
763
+
764
+ // --------------------------------------------------------------- payment links
765
+ /**
766
+ * Cria um Payment Link hospedado pela Unbox (bom para WhatsApp / compartilhar carrinho).
767
+ * ⚠️ Operação de loja (admin-ish) — derivada da doc 07/10; rode sob demanda. Retorna o
768
+ * PaymentLink; a página pública é montada via getPublicPaymentLink(referenceId).
769
+ */
770
+ async createPaymentLink(input: {
771
+ items: Array<{ productId: string; productVariantId: string; quantity: number }>;
772
+ constraints?: PaymentLinkConstraints;
773
+ customerData?: Record<string, { value: string; editable: boolean }>;
774
+ }): Promise<any> {
775
+ const q = `mutation($i:CreatePaymentLinkInput!){ createPaymentLink(input:$i){
776
+ _id referenceId status constraints{expirationDate usageLimit} }}`;
777
+ const d = await this.gql<{ createPaymentLink: any }>(q, { i: {
778
+ shopId: this.shopId,
779
+ items: input.items,
780
+ constraints: input.constraints,
781
+ metadata: input.customerData ? { customerData: input.customerData } : undefined,
782
+ } });
783
+ return d.createPaymentLink;
784
+ }
785
+
786
+ /** Lê um payment link público (para montar a página de pagamento). discountCode é aplicado aqui. */
787
+ async getPublicPaymentLink(paymentLinkId: string, discountCode?: string): Promise<any> {
788
+ const q = `query($id:ID!,$shopId:ID!,$discountCode:String){ publicPaymentLink(paymentLinkId:$id,shopId:$shopId,discountCode:$discountCode){
789
+ _id referenceId status }}`;
790
+ const d = await this.gql<{ publicPaymentLink: any }>(q, { id: paymentLinkId, shopId: this.shopId, discountCode });
791
+ return d.publicPaymentLink;
792
+ }
793
+
794
+ /** Cria um carrinho real a partir de um cart template (campanhas / "compre de novo"). */
795
+ async createCartByTemplate(cartTemplateId: string): Promise<CartResult> {
796
+ const q = `mutation($i:CreateCartByTemplateInput!){ createCartByTemplate(input:$i){
797
+ token cart{ _id totalItemQuantity checkout{summary{total{amount displayAmount}}} } }}`;
798
+ // Escrita já publicada na API de parceiros (CreateCartPayload tem a mesma shape).
799
+ const d = this.usesPartnerApi
800
+ ? await this.gqlPartner<{ createCartByTemplate: any }>(q, { i: { shopId: this.shopId, cartTemplateId } })
801
+ : await this.gql<{ createCartByTemplate: any }>(q, { i: { shopId: this.shopId, cartTemplateId } });
802
+ const r = d.createCartByTemplate;
803
+ return { cartId: r.cart._id, cartToken: r.token, cart: r.cart };
804
+ }
805
+
806
+ // -------------------------------------------------------------------- webhooks
807
+ /** ⚠️ EFEITO REAL: cria uma assinatura de webhook na loja. Rode sob demanda (script). */
808
+ async subscribeWebhook(eventType: string, endpoint: string): Promise<any> {
809
+ const q = `mutation($i:SubscribeToWebhookInput){ subscribeToWebhook(input:$i){
810
+ _id eventType endpoint secret createdAt }}`;
811
+ // Mesma mutation nos dois endpoints — uma das poucas ESCRITAS já publicadas na API de
812
+ // parceiros (junto com createCartByTemplate e os CRUDs de cupom).
813
+ const d = this.usesPartnerApi
814
+ ? await this.gqlPartner<{ subscribeToWebhook: any }>(q, { i: { eventType, endpoint } })
815
+ : await this.gql<{ subscribeToWebhook: any }>(q, { i: { eventType, endpoint } });
816
+ return d.subscribeToWebhook;
817
+ }
818
+
819
+ // ------------------------------------------------------------------ inventário
820
+ /** Inventário de uma variante (API de parceiros — simpleInventory). Exige partnerApiKey. */
821
+ async getSimpleInventory(productId: string, productVariantId: string): Promise<SimpleInventoryInfo | null> {
822
+ if (!this.usesPartnerApi) throw new UnboxError("getSimpleInventory exige UNBOX_PARTNER_API_KEY (API de parceiros)");
823
+ const q = `query($pc:ProductConfigurationInput!){ simpleInventory(productConfiguration:$pc){
824
+ _id canBackorder inventoryInStock inventoryReserved isEnabled lowInventoryWarningThreshold
825
+ productConfiguration{productId productVariantId} }}`;
826
+ const d = await this.gqlPartner<{ simpleInventory: SimpleInventoryInfo | null }>(q, { pc: { productId, productVariantId } });
827
+ return d.simpleInventory ?? null;
828
+ }
829
+ }