@bacano/sdk 1.0.1 → 1.1.8

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 (72) hide show
  1. package/README.md +439 -18
  2. package/STOREFRONT_CLARITY.md +140 -0
  3. package/STOREFRONT_CONSTRUCTION.md +105 -0
  4. package/STOREFRONT_LEADS.md +130 -0
  5. package/STOREFRONT_LEGAL.md +66 -0
  6. package/STOREFRONT_MARKETING.md +291 -0
  7. package/STOREFRONT_PERFORMANCE.md +304 -0
  8. package/STOREFRONT_PREVIEW.md +499 -0
  9. package/STOREFRONT_PRODUCT_SPECIFICATIONS.md +127 -0
  10. package/STOREFRONT_VOLUME_PRICING.md +131 -0
  11. package/client.d.ts +8 -1
  12. package/errors.d.ts +9 -1
  13. package/graphql.cjs +266 -3
  14. package/graphql.d.ts +2 -2
  15. package/graphql.js +266 -3
  16. package/index.cjs +2035 -255
  17. package/index.d.ts +8 -6
  18. package/index.js +2035 -255
  19. package/marketing/attribution.d.ts +53 -0
  20. package/marketing/clarity.d.ts +3 -0
  21. package/marketing/domain.d.ts +1 -0
  22. package/marketing/ga4.d.ts +2 -0
  23. package/marketing/index.d.ts +5 -0
  24. package/marketing/runtime.d.ts +2 -0
  25. package/marketing/sanitize.d.ts +2 -0
  26. package/marketing/types.d.ts +111 -0
  27. package/media.d.ts +45 -2
  28. package/modules/auth.d.ts +3 -2
  29. package/modules/availability.d.ts +2 -2
  30. package/modules/cart-errors.d.ts +2 -0
  31. package/modules/cart-session.d.ts +2 -2
  32. package/modules/cart.d.ts +4 -3
  33. package/modules/catalog.d.ts +15 -3
  34. package/modules/content.d.ts +2 -2
  35. package/modules/marketing.d.ts +5 -0
  36. package/modules/orders.d.ts +2 -2
  37. package/modules/profile.d.ts +1 -1
  38. package/modules/static-catalog.d.ts +13 -0
  39. package/modules/website.d.ts +7 -1
  40. package/package.json +14 -3
  41. package/react/context-registry.d.ts +9 -0
  42. package/react/context.d.ts +1 -1
  43. package/react/hooks.d.ts +24 -2
  44. package/react/marketing-context.d.ts +3 -0
  45. package/react/marketing-provider.d.ts +10 -0
  46. package/react/preview/cache.d.ts +23 -0
  47. package/react/preview/context.d.ts +5 -0
  48. package/react/preview/navigation.d.ts +9 -0
  49. package/react/preview/provider.d.ts +3 -0
  50. package/react/preview/shell.d.ts +3 -0
  51. package/react/preview/toolbar.d.ts +3 -0
  52. package/react/preview/types.d.ts +60 -0
  53. package/react/preview.cjs +742 -0
  54. package/react/preview.d.ts +14 -0
  55. package/react/preview.js +730 -0
  56. package/react/website-gate.d.ts +22 -0
  57. package/react.cjs +3893 -1795
  58. package/react.d.ts +8 -4
  59. package/react.js +3891 -1789
  60. package/skills/bacano-web/SKILL.md +116 -0
  61. package/skills/bacano-web/agents/openai.yaml +4 -0
  62. package/skills/bacano-web/references/architecture-runtime.md +109 -0
  63. package/skills/bacano-web/references/common.md +92 -0
  64. package/skills/bacano-web/references/informational.md +39 -0
  65. package/skills/bacano-web/references/report.md +70 -0
  66. package/skills/bacano-web/references/storefront.md +116 -0
  67. package/skills/bacano-web/scripts/audit-http.mjs +284 -0
  68. package/skills/bacano-web/scripts/cli.mjs +222 -0
  69. package/skills/bacano-web/scripts/postinstall.mjs +8 -0
  70. package/transport/documents.d.ts +23 -18
  71. package/transport/graphql.d.ts +13 -1
  72. package/types.d.ts +338 -0
package/README.md CHANGED
@@ -21,13 +21,135 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
21
21
  as a peer dependency. The core client has no runtime dependencies and works
22
22
  without React.
23
23
 
24
+ ### Auditoría del storefront
25
+
26
+ El SDK incluye la skill oficial `bacano-web`. Instalarla al iniciar un
27
+ storefront y ejecutarla antes de publicar o después de un cambio relevante del
28
+ SDK, hosting, autenticación, pagos o medición:
29
+
30
+ ```bash
31
+ pnpm add @bacano/sdk
32
+ pnpm exec bacano-web install --agent all
33
+ pnpm exec bacano-web check
34
+ ```
35
+
36
+ Después invocarla desde el agente elegido:
37
+
38
+ - Codex: `$bacano-web`
39
+ - Claude Code: `/bacano-web`
40
+ - Antigravity: solicitar una auditoría Bacano Web o elegirla desde `/skills`
41
+
42
+ `install` solo instala una copia canónica de la skill y sus enlaces de
43
+ descubrimiento. `check` confirma que está vigente. Ninguno ejecuta la auditoría,
44
+ modifica el storefront ni despliega; esa revisión comienza únicamente cuando
45
+ el usuario o agente invoca la skill.
46
+
47
+ Every storefront must follow [`STOREFRONT_MARKETING.md`](./STOREFRONT_MARKETING.md)
48
+ when instrumenting analytics, advertising signals, contact actions, ecommerce
49
+ events, attribution, or preview behavior. It is the canonical implementation
50
+ contract for storefront agents and ships inside the npm package.
51
+
52
+ Commercial forms and newsletter subscriptions must follow
53
+ [`STOREFRONT_LEADS.md`](./STOREFRONT_LEADS.md). Storefronts own the form UI;
54
+ Bacano owns consent evidence, identity resolution, attribution and idempotent
55
+ lead capture through the SDK.
56
+
57
+ Static and hybrid stores must also follow
58
+ [`STOREFRONT_PERFORMANCE.md`](./STOREFRONT_PERFORMANCE.md). It defines the
59
+ official build bootstrap, live-state refresh, responsive media and request
60
+ ownership rules. It also documents the shared public-config bootstrap, deferred
61
+ marketing scripts and route-scoped authentication required for fast public
62
+ catalog pages.
63
+
64
+ Storefronts that can be hidden temporarily must mount the runtime gate defined
65
+ in [`STOREFRONT_CONSTRUCTION.md`](./STOREFRONT_CONSTRUCTION.md). It supports
66
+ static SiteGround exports without rebuilding, responsive Bacano media and
67
+ signed temporary preview links.
68
+
69
+ Product detail pages and technical catalog filters must follow
70
+ [`STOREFRONT_PRODUCT_SPECIFICATIONS.md`](./STOREFRONT_PRODUCT_SPECIFICATIONS.md).
71
+
72
+ Quantity-based web pricing is documented in
73
+ [`STOREFRONT_VOLUME_PRICING.md`](./STOREFRONT_VOLUME_PRICING.md). Bacano owns
74
+ the authoritative basket calculation; storefronts only render the effective
75
+ prices and tier progress returned by the cart API.
76
+ It documents the additive specification contract, static publication and the
77
+ strict separation from commercial attributes and variants.
78
+
79
+ #### Instalación y alcance de la skill
80
+
81
+ El paquete incluye `skills/bacano-web`, una skill portátil para que un agente
82
+ audite el repositorio, build y dominio antes de producción. Primero confirma si
83
+ el proyecto es una **web informativa** o una **tienda virtual** y aplica un
84
+ checklist distinto; no exige carrito, checkout o pagos a un sitio informativo.
85
+
86
+ Ubicaciones del proyecto después de instalar la skill:
87
+
88
+ | Agente | Ruta de descubrimiento | Invocación |
89
+ | ----------- | --------------------------- | ----------------------------- |
90
+ | Codex | `.codex/skills/bacano-web` | `$bacano-web` |
91
+ | Claude Code | `.claude/skills/bacano-web` | `/bacano-web` |
92
+ | Antigravity | `.agents/skills/bacano-web` | Solicitud natural o `/skills` |
93
+
94
+ Para instalarla solo para un agente puede indicarse explícitamente:
95
+
96
+ ```bash
97
+ pnpm exec bacano-web install --agent antigravity
98
+ ```
99
+
100
+ También puede ejecutarse sin `--agent`; en una terminal interactiva pregunta si
101
+ se usará Antigravity, Claude Code, Codex o todos. Mantiene una sola copia física
102
+ en `.agents/skills/bacano-web` y crea enlaces de descubrimiento para los otros
103
+ agentes seleccionados.
104
+
105
+ ```bash
106
+ pnpm exec bacano-web check
107
+ ```
108
+
109
+ El paquete incluye un aviso `postinstall` como ayuda adicional, aunque algunos
110
+ gestores pueden ocultar su salida. El aviso nunca modifica el repositorio y
111
+ puede desactivarse en entornos controlados con
112
+ `BACANO_SDK_HIDE_SKILL_NOTICE=1`.
113
+
114
+ La skill realiza auditorías de solo lectura por defecto. Primero detecta las
115
+ capacidades realmente activas y separa gates de publicación (`P0`), calidad
116
+ profesional (`P1`) y mejoras (`P2`), para no bloquear una web por funciones
117
+ apagadas. Sus informes usan estados claros con emojis: `✅ APROBADO`,
118
+ `⚠️ ADVERTENCIA`, `❌ FALLÓ`, `➖ NO APLICA` y `🧪 NO VERIFICADO`.
119
+
120
+ Además audita explícitamente la arquitectura **Static First** y la vista previa
121
+ oficial. El informe incluye una sección “Arquitectura SDK y tráfico runtime” con
122
+ instancias del cliente/provider, operaciones GraphQL observadas, duplicados,
123
+ datos estáticos, actualizaciones vivas y evidencia de `noindex`, sitemap y cero
124
+ marketing en preview. Cuando no puede capturar un trace real, lo declara como
125
+ `🧪 NO VERIFICADO` en lugar de asumirlo desde el código.
126
+
24
127
  ### Entry points
25
128
 
26
- | Import | Contents |
27
- | --- | --- |
28
- | `@bacano/sdk` | Core client, catalog, cart, checkout, orders, profile |
29
- | `@bacano/sdk/react` | `BacanoProvider` and React hooks |
30
- | `@bacano/sdk/graphql` | Raw GraphQL documents, for advanced use |
129
+ | Import | Contents |
130
+ | --------------------------- | ----------------------------------------------------- |
131
+ | `@bacano/sdk` | Core client, catalog, cart, checkout, orders, profile |
132
+ | `@bacano/sdk/react` | `BacanoProvider` and React hooks |
133
+ | `@bacano/sdk/react/preview` | Optional live preview controller and toolbar |
134
+ | `@bacano/sdk/graphql` | Raw GraphQL documents, for advanced use |
135
+
136
+ ---
137
+
138
+ ## Live Content Preview
139
+
140
+ Static storefronts can show current published Bacano Content without rebuilding
141
+ their public pages. Import the isolated preview entrypoint only from the
142
+ storefront preview route:
143
+
144
+ ```tsx
145
+ import { BacanoPreviewShell } from "@bacano/sdk/react/preview";
146
+ ```
147
+
148
+ The public storefront remains static. Preview V1 never exposes drafts and
149
+ automatically disables the SDK marketing runtime inside its tree. See
150
+ [`STOREFRONT_PREVIEW.md`](./STOREFRONT_PREVIEW.md), the canonical contract for
151
+ provider hierarchy, responsibilities, security, agent checklist, and the
152
+ validated Baby Rex reference implementation.
31
153
 
32
154
  ---
33
155
 
@@ -86,7 +208,8 @@ await client.init();
86
208
 
87
209
  `catalog.getProducts()` only returns products that are active, enabled for the
88
210
  website branch, have a valid `CLIENT` price and have available
89
- inventory in that branch warehouse.
211
+ inventory in one of the inventory sources configured for the website. Websites
212
+ without custom sources continue using the branch's default warehouse.
90
213
 
91
214
  ```typescript
92
215
  // List products
@@ -148,6 +271,43 @@ const filtered = await client.catalog.getProducts({
148
271
  // Single product
149
272
  const product = await client.catalog.getProduct("product-uuid");
150
273
 
274
+ // Static builds: fetch the complete sellable catalog and its first renderable
275
+ // catalog page. This is the canonical source for initial HTML, routes,
276
+ // metadata, sitemap and product page data.
277
+ const bootstrap = await client.catalog.getStaticCatalogBootstrap({
278
+ categoryListKey: "categorias-web",
279
+ attributeListKey: "atributos",
280
+ pageSize: 100,
281
+ initialPageSize: 24,
282
+ maxProducts: 10_000,
283
+ sortBy: "name_asc",
284
+ });
285
+
286
+ const productByRoute = new Map(
287
+ bootstrap.products.map((item) => [item.slug || item.id, item]),
288
+ );
289
+
290
+ // Cards and filters use product.variants. Detail selectors use the official
291
+ // helper so sold-out sizes remain visible and older snapshots stay compatible.
292
+ const detailVariants = getProductDetailVariants(product!);
293
+
294
+ // Do not call getProductBySlug() once per route during the same build.
295
+ // Reuse productByRoute from the snapshot instead.
296
+ // Render bootstrap.initialPage in the initial catalog HTML instead of
297
+ // publishing an empty skeleton that waits for a browser request.
298
+
299
+ // After hydration, refresh only volatile state for visible variants.
300
+ const liveState = await client.catalog.getProductLiveState(
301
+ detailVariants.map((variant) => variant.id),
302
+ );
303
+ // Each item includes maxPurchasableQuantity for product and cart controls.
304
+
305
+ // Responsive storage candidates are provided by the SDK.
306
+ const srcSet = formatMediaSrcSet(product.media[0]?.responsiveSources ?? []);
307
+
308
+ // Editorial ContentMedia uses heroUrl plus intrinsic-width-aware
309
+ // responsiveSources. See STOREFRONT_PERFORMANCE.md for the <picture> contract.
310
+
151
311
  // Automatic recommendations for the product detail. This is an independent,
152
312
  // lightweight request; Bacano resolves ranking, availability and public price.
153
313
  const recommendations = await client.catalog.getProductRecommendations({
@@ -155,18 +315,28 @@ const recommendations = await client.catalog.getProductRecommendations({
155
315
  limit: 16,
156
316
  });
157
317
 
158
- // Related/sibling products configured in Bacano.
159
- // Use these to render product-level alternatives that are NOT real variants.
160
- // Each item is scoped to the current website branch and only includes active
161
- // catalog products.
318
+ // Manually curated related products configured in Bacano.
319
+ // Use these for recommendations, comparisons or complementary products.
320
+ // Their public name always comes from the related product itself. Each item is
321
+ // scoped to the current website branch and only includes active catalog items.
162
322
  for (const related of product?.relatedProducts ?? []) {
163
323
  console.log(related.productId);
164
324
  console.log(related.slug);
165
- console.log(related.displayLabel ?? related.name);
325
+ console.log(related.name);
166
326
  console.log(related.sku);
167
327
  console.log(related.media[0]?.url);
168
328
  }
169
329
 
330
+ // Customer-facing alternatives configured as one central option group.
331
+ // The current product is included in members. Use displayLabel for the
332
+ // selector and slug for navigation; keep relatedProducts for recommendations.
333
+ if (product?.optionGroup) {
334
+ console.log(product.optionGroup.title);
335
+ for (const option of product.optionGroup.members) {
336
+ console.log(option.displayLabel ?? option.name, option.slug);
337
+ }
338
+ }
339
+
170
340
  // Categories and brands
171
341
  const categories = await client.catalog.getCategories();
172
342
  const brands = await client.catalog.getBrands();
@@ -211,9 +381,25 @@ const availability = await client.catalog.checkAvailability([
211
381
  "variant-uuid-1",
212
382
  "variant-uuid-2",
213
383
  ]);
214
- // → [{ productVariantId, inStock: true, label: "In Stock" }]
384
+ // → [{ productVariantId, inStock: true, label: "In Stock",
385
+ // maxPurchasableQuantity: 8 }]
215
386
  ```
216
387
 
388
+ `maxPurchasableQuantity` is Bacano's current public purchase limit after active
389
+ reservations, capped by the maximum accepted by public cart and checkout
390
+ actions. When a website uses multiple warehouses, this is the maximum that one
391
+ source can fulfill; Bacano intentionally does not add stock from different
392
+ warehouses. It is not a raw warehouse inventory field. Use it to limit quantity
393
+ selectors and the total units of that variant already present in the local
394
+ cart. Refresh the variants in one batch when opening the cart or checkout; do
395
+ not request availability after every `+` click. This value can change after it
396
+ is read, so Bacano still revalidates stock when checkout starts and finishes.
397
+
398
+ At checkout, each order line is reserved in the first configured warehouse,
399
+ by priority, that can fulfill that complete line. Different lines may use
400
+ different warehouses, but Bacano does not split a single line between sources.
401
+ The storefront does not implement this allocation or send warehouse IDs.
402
+
217
403
  ### 4. Local Cart And Checkout Start
218
404
 
219
405
  The website checkout remains guest-first. Buyer credentials and email
@@ -276,6 +462,9 @@ await client.cart.updateItem("cart-item-uuid", 5);
276
462
  // Remove by line id.
277
463
  await client.cart.removeItem("cart-item-uuid");
278
464
 
465
+ // Discard the complete private cart and clear its SDK session.
466
+ await client.cart.clear();
467
+
279
468
  // View current cart from the stored guest session.
280
469
  const currentCart = await client.cart.get();
281
470
  console.log(currentCart?.items); // CartItem[]
@@ -284,11 +473,21 @@ console.log(currentCart?.totals); // { subtotal, tax, discount, total }
284
473
 
285
474
  Cart rules:
286
475
 
287
- - No contact yet means local web cart only.
476
+ - `cart.getOrCreate()` may create a private server cart without contact. This
477
+ is the supported path when the storefront needs authoritative stock, totals
478
+ or volume pricing before checkout.
479
+ - Creating this browsing cart does not create delivery logistics. Delivery is
480
+ synchronized only by `cart.startCheckout()` after the customer selects the
481
+ fulfillment family.
288
482
  - Email, phone or a complete document pair can start a Bacano
289
483
  `sales_channel=WEB`, `PENDING + UNPAID` checkout.
290
484
  - `sales_channel=WEB`, `PENDING + UNPAID` without `order_number` means checkout started or abandoned.
291
485
  - These started checkouts are tracked by `orders.website_cart_status` and should be managed from Bacano's abandoned-cart view, not mixed into the normal sales list.
486
+ - `cart.clear()` marks the private cart as `discarded`, preserves its audit
487
+ history and removes only `@bacano/sdk:cart:{websiteSlug}`. It never deletes
488
+ storage owned by the storefront.
489
+ - Removing the final line also discards the server cart and clears its private
490
+ SDK session automatically.
292
491
  - The web must collect `fulfillmentFamily` before starting checkout:
293
492
  `SHIPPING` for delivery or `PICKUP` for store pickup.
294
493
  - The cart token is opaque. Bacano stores only its SHA-256 hash.
@@ -337,6 +536,20 @@ collecting contact or address data. Bacano resolves current prices, stock,
337
536
  website and shipping policy; the storefront must format the structured values
338
537
  instead of hardcoding or recalculating them.
339
538
 
539
+ For the policy to be enabled, the website must have delivery enabled, a
540
+ positive free-shipping threshold, and at least one active shipping service
541
+ enabled for that website whose collection mode charges shipping with the sale
542
+ (`with_sale`). Shipping collected directly by the carrier is intentionally not
543
+ overridden by this promotion. These prerequisites are configured in Bacano;
544
+ the storefront must not infer or reproduce them.
545
+
546
+ `getShippingPolicy()` is cached for five minutes per SDK client. This keeps
547
+ catalog navigation from repeatedly requesting the same configuration. After
548
+ changing the policy in Bacano, recreate the client or reload the storefront to
549
+ validate it immediately. `getFreeShippingProgress()` always validates the
550
+ submitted variants and quantities against Bacano's current prices and returns
551
+ `enabled: false` with nullable amounts when the policy does not apply.
552
+
340
553
  `getShippingQuotes` returns the final shipping price for an address. Before a
341
554
  guest checkout exists, pass `items`. Once an active cart session exists, the
342
555
  SDK intentionally prefers the persisted cart and ignores loose items so
@@ -488,6 +701,23 @@ until `createPaymentSession` succeeds, then retires it. Payment response and
488
701
  retry pages must continue with the returned `orderId + publicOrderToken`; a
489
702
  second purchase starts a new cart instead of reusing the finalized order.
490
703
 
704
+ The private `orderId + cartToken` session belongs exclusively to the SDK. A
705
+ storefront must never read, remove or rewrite the
706
+ `@bacano/sdk:cart:{websiteSlug}` key. When `cart.startCheckout()` discovers
707
+ that a persisted session is no longer usable, Bacano returns the stable
708
+ `INVALID_CART_TOKEN` or `CART_NOT_FOUND` code. The SDK then removes only that
709
+ private session and retries the same start operation exactly once without a
710
+ token. Storefront-owned product state is not touched, `checkout.submit()` is
711
+ never repeated, and the failed token request creates no order. Other business
712
+ errors preserve the private session and are returned with their stable code.
713
+
714
+ Signing in after starting as a guest does not rotate the cart token. Bacano
715
+ continues an unowned guest cart and can continue a cart already linked to the
716
+ same authenticated buyer. Signing out, changing buyers, completing an order,
717
+ or changing the website chain makes a non-matching private session
718
+ non-recoverable for that browser identity, so the next start creates one fresh
719
+ cart through the bounded recovery above.
720
+
491
721
  ### 6. Public Order Tracking
492
722
 
493
723
  Confirmation pages should not depend on browser-only snapshots. After a
@@ -620,10 +850,189 @@ createBacanoClient({
620
850
  identifier/token. Buyer access tokens are never persisted by Bacano SDK; the
621
851
  configured identity provider owns their lifecycle.
622
852
 
853
+ ### Website favicon
854
+
855
+ The website's General settings can define one canonical favicon from Bacano's
856
+ media library. It is returned by the existing public-config request, so a
857
+ storefront does not need another API call or direct Storage integration:
858
+
859
+ ```tsx
860
+ const publicConfig = await getWebsitePublicConfig({ apiUrl, websiteSlug });
861
+ const faviconUrl = publicConfig.branding.favicon?.originalUrl;
862
+ ```
863
+
864
+ Use `faviconUrl` in the framework's metadata or document head. When it is
865
+ `null`, keep the storefront's packaged fallback icon. The public contract
866
+ contains only browser-safe media metadata and a public Storage URL.
867
+
623
868
  ---
624
869
 
625
870
  ## React Integration
626
871
 
872
+ ### Storefront marketing and measurement
873
+
874
+ GA4, Meta, and Microsoft Clarity are configured per website in Bacano and are
875
+ disabled by default.
876
+ The existing public-config request returns only active public destination IDs;
877
+ provider secrets never reach the SDK. The runtime remains a no-op outside the
878
+ website's canonical production domain.
879
+
880
+ Mount one provider at the storefront root, using the same public config already
881
+ loaded for buyer accounts:
882
+
883
+ ```tsx
884
+ import { getWebsitePublicConfig } from "@bacano/sdk";
885
+ import { BacanoMarketingProvider, useMarketing } from "@bacano/sdk/react";
886
+
887
+ const publicConfig = await getWebsitePublicConfig({ apiUrl, websiteSlug });
888
+
889
+ <BacanoMarketingProvider publicConfig={publicConfig}>
890
+ <Storefront />
891
+ </BacanoMarketingProvider>;
892
+
893
+ function AddToCartButton({ variant }) {
894
+ const marketing = useMarketing();
895
+
896
+ async function add() {
897
+ await updateCartSuccessfully(variant.id);
898
+ marketing.track({
899
+ eventName: "add_to_cart",
900
+ value: variant.price,
901
+ items: [
902
+ {
903
+ itemId: variant.id, // always product_variant_id
904
+ itemName: variant.productName,
905
+ sku: variant.sku,
906
+ price: variant.price,
907
+ quantity: 1,
908
+ },
909
+ ],
910
+ });
911
+ }
912
+
913
+ return <button onClick={add}>Add to cart</button>;
914
+ }
915
+ ```
916
+
917
+ The browser contract is versioned and accepts `page_view`, `search`,
918
+ `view_item_list`, `select_item`, `view_item`, `add_to_cart`,
919
+ `remove_from_cart`, `view_cart`, `begin_checkout`, `add_shipping_info`,
920
+ `add_payment_info`, and `contact`. Emit events only after the related operation
921
+ has succeeded. Use one manual `page_view` source for SPA navigation because the
922
+ GA4 adapter disables automatic page views.
923
+
924
+ When Microsoft Clarity is active, this same provider loads it automatically and
925
+ forwards canonical browser events as Clarity custom events. Do not install a
926
+ second Clarity snippet or package in the storefront. Setup, privacy, and
927
+ verification are documented in [`STOREFRONT_CLARITY.md`](./STOREFRONT_CLARITY.md).
928
+
929
+ Contact actions can identify their stable storefront location with the optional
930
+ `contactPlacement` field. It accepts only `product_detail`, `floating`,
931
+ `header`, `footer`, or `contact_page`; visible labels and arbitrary strings are
932
+ not part of the public contract. The runtime ignores the field on events other
933
+ than `contact`.
934
+
935
+ ```tsx
936
+ marketing.track({
937
+ eventName: "contact",
938
+ contactMethod: "email",
939
+ contactPlacement: "footer",
940
+ });
941
+ ```
942
+
943
+ A product contact keeps the same variant identity, value, and item payload used
944
+ by the ecommerce events:
945
+
946
+ ```tsx
947
+ marketing.track({
948
+ eventName: "contact",
949
+ contactMethod: "whatsapp",
950
+ contactPlacement: "product_detail",
951
+ value: price * quantity,
952
+ items: [
953
+ {
954
+ itemId: variant.id,
955
+ itemName: product.name,
956
+ sku: variant.sku,
957
+ price,
958
+ quantity,
959
+ itemBrand: product.brand?.name,
960
+ itemCategory: product.category,
961
+ itemVariant: "Talla: 12 meses / Color: Rosado",
962
+ },
963
+ ],
964
+ });
965
+ ```
966
+
967
+ GA4 receives this field as `contact_placement`; Meta receives the same
968
+ `contact_placement` key while preserving `contact_method`, `content_ids`, and
969
+ `contents`.
970
+
971
+ For checkout selectors, an automatic default is not yet a customer choice.
972
+ Emit `add_shipping_info` and `add_payment_info` immediately for an explicit,
973
+ valid selection, or once after a successful checkout submission when the
974
+ customer kept an automatic default. Deduplicate both paths with the selected
975
+ delivery/payment identity. On product lists, `itemId` and `price` must come
976
+ from the same public, price-bearing variant; the advertised variant is the
977
+ lowest-price variant with stable `order` and ID tie-breakers.
978
+
979
+ Transfer the runtime attribution snapshot when checkout starts and again when
980
+ it is submitted. Bacano freezes the touchpoints on the order; the second
981
+ snapshot may only fill missing GA4 `client_id` and `session_id` values that the
982
+ Google tag resolved asynchronously:
983
+
984
+ ```typescript
985
+ await client.cart.startCheckout({
986
+ contact,
987
+ items,
988
+ marketingAttribution: marketing.getAttribution() ?? undefined,
989
+ });
990
+
991
+ await client.checkout.submit({
992
+ contact,
993
+ marketingAttribution: marketing.getAttribution() ?? undefined,
994
+ advertisingConsent: publicConfig.marketing.consentPolicy
995
+ ? {
996
+ policyId: publicConfig.marketing.consentPolicy.id,
997
+ policyVersion: publicConfig.marketing.consentPolicy.version,
998
+ statementHash: publicConfig.marketing.consentPolicy.statementHash,
999
+ granted: optionalAdvertisingCheckbox,
1000
+ }
1001
+ : undefined,
1002
+ });
1003
+ ```
1004
+
1005
+ `order_placed`, `purchase`, and `refund` are server-authoritative events. A
1006
+ storefront must never emit them from a thank-you page or payment response route.
1007
+ URLs are centrally sanitized to retain only approved UTM and click identifiers.
1008
+ Links and QR codes may additionally use `bacano_origin=<public-key>` to select a
1009
+ company-defined sales origin. The SDK preserves it with the attribution
1010
+ snapshot; Bacano validates it server-side and otherwise falls back to the
1011
+ normalized UTM/click-ID origin or `Web`.
1012
+ Google or Meta failures are isolated from catalog, cart, checkout, and payment.
1013
+ This isolation also applies to Bacano's server-side outbox producers: losing a
1014
+ marketing event must never roll back an order, payment, or refund transition.
1015
+ The runtime captures GA4 browser identity and Meta browser context (`fbp`,
1016
+ `fbc`, and user agent) internally, so storefronts must not read cookies or
1017
+ construct provider identifiers themselves. Contact information remains governed
1018
+ by the website's explicit advertising-consent policy.
1019
+
1020
+ An active, validated integration can enable a temporary diagnostic window from
1021
+ Bacano. GA4 events receive `debug_mode`; Meta server events receive its encrypted
1022
+ Test Events code. The mode is off by default, expires automatically, is cleared
1023
+ when an integration is paused or reconfigured, and never exposes the Meta code
1024
+ through public config or the SDK.
1025
+
1026
+ The canonical contract and acceptance checklist live in
1027
+ `docs/websites/MARKETING_MEASUREMENT_STOREFRONTS_CHECKLIST.md` in the Bacano ERP
1028
+ repository.
1029
+
1030
+ Website legal documents and the optional informational cookie notice are
1031
+ available in `publicConfig.legal`. Their canonical contract lives in
1032
+ [`STOREFRONT_LEGAL.md`](./STOREFRONT_LEGAL.md), which ships inside this package.
1033
+ The notice does not gate the marketing runtime; storefronts must not reinterpret
1034
+ it as a consent manager.
1035
+
627
1036
  ### Setup
628
1037
 
629
1038
  Wrap your app with `BacanoProvider`:
@@ -1063,7 +1472,7 @@ function ProfilePage() {
1063
1472
  | `useProductFilters(opts?)` | `{ data, loading, error, refetch }` | No |
1064
1473
  | `useSearchSuggestions(opts?)` | `{ data, loading, error, refetch }` | No |
1065
1474
  | `useAvailability(ids)` | `{ data, loading, error, refetch }` | No |
1066
- | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem }` | No |
1475
+ | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem, clear }` | No |
1067
1476
  | `useCheckout()` | `{ checkout, loading, error }` | No |
1068
1477
  | `useOrders(opts?)` | `{ data, loading, error, refetch }` | Yes |
1069
1478
  | `useOrder(id)` | `{ data, loading, error, refetch }` | Yes |
@@ -1223,6 +1632,7 @@ The SDK throws typed errors you can catch and handle:
1223
1632
  ```typescript
1224
1633
  import {
1225
1634
  WebsiteError,
1635
+ RateLimitError,
1226
1636
  AuthError,
1227
1637
  CheckoutError,
1228
1638
  NotInitializedError,
@@ -1248,6 +1658,8 @@ try {
1248
1658
  console.log("GraphQL errors:", err.graphqlErrors);
1249
1659
  } else if (err instanceof NotInitializedError) {
1250
1660
  // client.init() was not called
1661
+ } else if (err instanceof RateLimitError) {
1662
+ console.log("Retry after seconds:", err.retryAfterSeconds);
1251
1663
  } else if (err instanceof WebsiteError) {
1252
1664
  // Other SDK error (network, rate limit, etc.)
1253
1665
  console.log("Error code:", err.code);
@@ -1257,6 +1669,11 @@ try {
1257
1669
  }
1258
1670
  ```
1259
1671
 
1672
+ `RATE_LIMITED` errors expose `status = 429` and `retryAfterSeconds`. Normal SDK
1673
+ operations do not retry mutations automatically. The static catalog snapshot
1674
+ is a read-only build contract and safely retries up to four times, never before
1675
+ the server's `Retry-After` window and with a three-minute cumulative wait cap.
1676
+
1260
1677
  Common setup errors:
1261
1678
 
1262
1679
  | Code / message | Meaning | Fix |
@@ -1296,7 +1713,7 @@ import type {
1296
1713
  ProductCategory,
1297
1714
  ProductSubcategory,
1298
1715
  Brand,
1299
- ProductAvailability, // { productVariantId, inStock, label }
1716
+ ProductAvailability, // { productVariantId, inStock, label, maxPurchasableQuantity }
1300
1717
  GetProductsOptions, // { categoryName, search, colors, tags, minPrice, maxPrice, inStock, attributes, sortBy, ... }
1301
1718
  GetProductFiltersOptions,
1302
1719
  ProductFilters, // { categories, brands, colors, priceRange, attributes }
@@ -1304,6 +1721,8 @@ import type {
1304
1721
  ProductSortBy, // 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'newest'
1305
1722
  SearchSuggestion, // { type: 'product' | 'brand' | 'category', text, id? }
1306
1723
  GetSearchSuggestionsOptions,
1724
+ GetStaticCatalogSnapshotOptions,
1725
+ StaticCatalogSnapshot,
1307
1726
 
1308
1727
  // Cart
1309
1728
  StartGuestCheckoutInput,
@@ -1334,9 +1753,11 @@ import type {
1334
1753
  - **Two Hasura roles**: `public` for anonymous catalog and guest checkout, `customer` for optional account/order history features
1335
1754
  - **Only enabled price tiers are exposed** — `CLIENT` is always available;
1336
1755
  `SALE`, `WHOLESALE` and `WHOLESALE_2` appear only when enabled by the company
1337
- - **Checkout remains retail** — Bacano charges a valid lower `SALE` price or
1338
- falls back to `CLIENT`; wholesale tiers are informational until a wholesale
1339
- checkout policy is explicitly implemented
1756
+ - **Checkout owns the final price** — Bacano charges a valid lower `SALE` price or
1757
+ falls back to `CLIENT`; wholesale tiers become authoritative only when a
1758
+ website enables the quantity-pricing policy described in
1759
+ [`STOREFRONT_VOLUME_PRICING.md`](./STOREFRONT_VOLUME_PRICING.md). Otherwise
1760
+ they remain informational.
1340
1761
  - **Raw inventory is abstracted** — `checkAvailability()` returns boolean + label, never raw quantities
1341
1762
  - **Company-scoped** — all queries are automatically filtered by `company_id` from the website context
1342
1763
  - **Local cart first** — the web keeps cart state locally until the visitor leaves email or phone