@bacano/sdk 1.1.0 → 1.1.9

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.
package/README.md CHANGED
@@ -66,6 +66,16 @@ in [`STOREFRONT_CONSTRUCTION.md`](./STOREFRONT_CONSTRUCTION.md). It supports
66
66
  static SiteGround exports without rebuilding, responsive Bacano media and
67
67
  signed temporary preview links.
68
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
+
69
79
  #### Instalación y alcance de la skill
70
80
 
71
81
  El paquete incluye `skills/bacano-web`, una skill portátil para que un agente
@@ -107,6 +117,13 @@ profesional (`P1`) y mejoras (`P2`), para no bloquear una web por funciones
107
117
  apagadas. Sus informes usan estados claros con emojis: `✅ APROBADO`,
108
118
  `⚠️ ADVERTENCIA`, `❌ FALLÓ`, `➖ NO APLICA` y `🧪 NO VERIFICADO`.
109
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
+
110
127
  ### Entry points
111
128
 
112
129
  | Import | Contents |
@@ -191,7 +208,8 @@ await client.init();
191
208
 
192
209
  `catalog.getProducts()` only returns products that are active, enabled for the
193
210
  website branch, have a valid `CLIENT` price and have available
194
- 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.
195
213
 
196
214
  ```typescript
197
215
  // List products
@@ -297,18 +315,28 @@ const recommendations = await client.catalog.getProductRecommendations({
297
315
  limit: 16,
298
316
  });
299
317
 
300
- // Related/sibling products configured in Bacano.
301
- // Use these to render product-level alternatives that are NOT real variants.
302
- // Each item is scoped to the current website branch and only includes active
303
- // 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.
304
322
  for (const related of product?.relatedProducts ?? []) {
305
323
  console.log(related.productId);
306
324
  console.log(related.slug);
307
- console.log(related.displayLabel ?? related.name);
325
+ console.log(related.name);
308
326
  console.log(related.sku);
309
327
  console.log(related.media[0]?.url);
310
328
  }
311
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
+
312
340
  // Categories and brands
313
341
  const categories = await client.catalog.getCategories();
314
342
  const brands = await client.catalog.getBrands();
@@ -359,12 +387,19 @@ const availability = await client.catalog.checkAvailability([
359
387
 
360
388
  `maxPurchasableQuantity` is Bacano's current public purchase limit after active
361
389
  reservations, capped by the maximum accepted by public cart and checkout
362
- actions. It is not a raw warehouse inventory field. Use it to limit quantity
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
363
393
  selectors and the total units of that variant already present in the local
364
394
  cart. Refresh the variants in one batch when opening the cart or checkout; do
365
395
  not request availability after every `+` click. This value can change after it
366
396
  is read, so Bacano still revalidates stock when checkout starts and finishes.
367
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
+
368
403
  ### 4. Local Cart And Checkout Start
369
404
 
370
405
  The website checkout remains guest-first. Buyer credentials and email
@@ -427,6 +462,9 @@ await client.cart.updateItem("cart-item-uuid", 5);
427
462
  // Remove by line id.
428
463
  await client.cart.removeItem("cart-item-uuid");
429
464
 
465
+ // Discard the complete private cart and clear its SDK session.
466
+ await client.cart.clear();
467
+
430
468
  // View current cart from the stored guest session.
431
469
  const currentCart = await client.cart.get();
432
470
  console.log(currentCart?.items); // CartItem[]
@@ -435,11 +473,21 @@ console.log(currentCart?.totals); // { subtotal, tax, discount, total }
435
473
 
436
474
  Cart rules:
437
475
 
438
- - 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.
439
482
  - Email, phone or a complete document pair can start a Bacano
440
483
  `sales_channel=WEB`, `PENDING + UNPAID` checkout.
441
484
  - `sales_channel=WEB`, `PENDING + UNPAID` without `order_number` means checkout started or abandoned.
442
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.
443
491
  - The web must collect `fulfillmentFamily` before starting checkout:
444
492
  `SHIPPING` for delivery or `PICKUP` for store pickup.
445
493
  - The cart token is opaque. Bacano stores only its SHA-256 hash.
@@ -653,6 +701,23 @@ until `createPaymentSession` succeeds, then retires it. Payment response and
653
701
  retry pages must continue with the returned `orderId + publicOrderToken`; a
654
702
  second purchase starts a new cart instead of reusing the finalized order.
655
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
+
656
721
  ### 6. Public Order Tracking
657
722
 
658
723
  Confirmation pages should not depend on browser-only snapshots. After a
@@ -785,6 +850,21 @@ createBacanoClient({
785
850
  identifier/token. Buyer access tokens are never persisted by Bacano SDK; the
786
851
  configured identity provider owns their lifecycle.
787
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
+
788
868
  ---
789
869
 
790
870
  ## React Integration
@@ -925,6 +1005,10 @@ await client.checkout.submit({
925
1005
  `order_placed`, `purchase`, and `refund` are server-authoritative events. A
926
1006
  storefront must never emit them from a thank-you page or payment response route.
927
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`.
928
1012
  Google or Meta failures are isolated from catalog, cart, checkout, and payment.
929
1013
  This isolation also applies to Bacano's server-side outbox producers: losing a
930
1014
  marketing event must never roll back an order, payment, or refund transition.
@@ -1388,7 +1472,7 @@ function ProfilePage() {
1388
1472
  | `useProductFilters(opts?)` | `{ data, loading, error, refetch }` | No |
1389
1473
  | `useSearchSuggestions(opts?)` | `{ data, loading, error, refetch }` | No |
1390
1474
  | `useAvailability(ids)` | `{ data, loading, error, refetch }` | No |
1391
- | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem }` | No |
1475
+ | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem, clear }` | No |
1392
1476
  | `useCheckout()` | `{ checkout, loading, error }` | No |
1393
1477
  | `useOrders(opts?)` | `{ data, loading, error, refetch }` | Yes |
1394
1478
  | `useOrder(id)` | `{ data, loading, error, refetch }` | Yes |
@@ -1669,9 +1753,11 @@ import type {
1669
1753
  - **Two Hasura roles**: `public` for anonymous catalog and guest checkout, `customer` for optional account/order history features
1670
1754
  - **Only enabled price tiers are exposed** — `CLIENT` is always available;
1671
1755
  `SALE`, `WHOLESALE` and `WHOLESALE_2` appear only when enabled by the company
1672
- - **Checkout remains retail** — Bacano charges a valid lower `SALE` price or
1673
- falls back to `CLIENT`; wholesale tiers are informational until a wholesale
1674
- 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.
1675
1761
  - **Raw inventory is abstracted** — `checkAvailability()` returns boolean + label, never raw quantities
1676
1762
  - **Company-scoped** — all queries are automatically filtered by `company_id` from the website context
1677
1763
  - **Local cart first** — the web keeps cart state locally until the visitor leaves email or phone
@@ -44,12 +44,22 @@ Después del gate se montan `ClerkProvider`, `BacanoProvider`,
44
44
  `BacanoMarketingProvider` y el resto del storefront. Puede entregarse
45
45
  `initialConfig` si el build ya obtuvo `public-config`; el gate vuelve a
46
46
  consultarlo antes de montar el sitio para detectar cambios posteriores al
47
- despliegue y evitar mostrar fugazmente el contenido estático.
47
+ despliegue.
48
+
49
+ Mientras confirma la configuración, el gate serializa `children` dentro de un
50
+ `<noscript>` semántico. El catálogo, los productos, encabezados y enlaces siguen
51
+ presentes en el HTML exportado y son visibles cuando JavaScript está
52
+ deshabilitado, pero React no monta esos descendientes. Así evita cargar Clerk,
53
+ GA4, Meta o Clarity y tampoco muestra fugazmente la tienda si construcción está
54
+ activa. `loadingFallback` es únicamente la UI visible para navegadores con
55
+ JavaScript durante esa consulta; por defecto permanece vacío.
48
56
 
49
57
  La pantalla oficial usa la imagen mobile hasta 767 px y la desktop desde 768
50
- px. En ambos casos emplea las fuentes responsivas generadas por Bacano. El
51
- storefront puede proporcionar `constructionFallback` si requiere una UI
52
- propia, pero no debe alterar la decisión `construction.enforced`.
58
+ px. En ambos casos emplea las fuentes responsivas generadas por Bacano. Si solo
59
+ existe una de las dos imágenes la reutiliza de forma segura; si no existe
60
+ ninguna, conserva el fondo y muestra el título y mensaje configurados. El
61
+ storefront puede proporcionar `constructionFallback` si requiere una UI propia,
62
+ pero no debe alterar la decisión `construction.enforced`.
53
63
 
54
64
  ## Vista previa temporal
55
65
 
@@ -83,10 +93,13 @@ edge. El contrato del SDK y la configuración por website permanecen válidos.
83
93
  ## Checklist del storefront
84
94
 
85
95
  - `BacanoWebsiteGate` está por fuera de auth y marketing.
96
+ - El export contiene el catálogo estático dentro del fallback `<noscript>`.
97
+ - Sin JavaScript, catálogo, productos y enlaces continúan visibles.
86
98
  - La configuración se consulta en runtime con `cache: no-store`.
87
99
  - La pantalla activa no inicializa Clerk, GA4, Meta ni Clarity.
88
100
  - Localhost y dominios de desarrollo siguen mostrando la web.
89
101
  - Las imágenes desktop y mobile usan `srcset` y `sizes`.
90
102
  - El enlace temporal abre el storefront y desaparece de la barra de dirección.
91
103
  - Un token vencido no concede acceso y se elimina de `sessionStorage`.
104
+ - La pantalla oficial sigue siendo útil aunque no tenga imágenes.
92
105
  - Un fallo de `public-config` degrada de forma abierta y no tumba el sitio.
@@ -178,6 +178,11 @@ en [`STOREFRONT_CLARITY.md`](./STOREFRONT_CLARITY.md).
178
178
  autenticación, OAuth o pago en nombres, URLs o parámetros.
179
179
  - La atribución se obtiene con `marketing.getAttribution()` y se entrega al
180
180
  contrato oficial de carrito/checkout.
181
+ - `bacano_origin=<key-publica>` puede identificar un origen personalizado
182
+ configurado en Bacano. El SDK lo conserva durante la navegación y el backend
183
+ lo valida contra la empresa antes de asignarlo a la venta.
184
+ - Si no existe esa key, Bacano normaliza UTM, referidor y click IDs a los
185
+ orígenes base; una venta web sin señal reconocida conserva `Web` como origen.
181
186
  - La autorización publicitaria del checkout es opcional, separada y nunca
182
187
  bloquea la compra.
183
188
  - Los datos de contacto solo pueden normalizarse y hashearse en el backend bajo
@@ -93,6 +93,17 @@ available was reached. Do not poll after every click or persist the limit as
93
93
  catalog data: another checkout may consume stock, and Bacano remains
94
94
  authoritative when checkout starts and finishes.
95
95
 
96
+ For websites configured with multiple inventory sources, the cap is the
97
+ largest quantity fulfillable by a single source, not the sum of all warehouses.
98
+ Bacano assigns each complete order line to the first prioritized warehouse that
99
+ can fulfill it. Storefronts must never choose, expose or combine warehouses.
100
+
101
+ Post-sale inventory follows the same ownership rules. A returned item goes back
102
+ to the warehouse that fulfilled the original order line. An outgoing replacement
103
+ for a web exchange is assigned to the first current website inventory source
104
+ that can fulfill the complete replacement quantity. Pausing the website does not
105
+ block post-sale processing for orders placed while it was active.
106
+
96
107
  ## Responsive media
97
108
 
98
109
  Every SDK product/content media object can expose `responsiveSources`. Use it
@@ -248,6 +259,32 @@ checkout, sign-in and customer account pages. Public catalog and product pages
248
259
  must not download an authentication runtime merely because buyer accounts are
249
260
  enabled for the website.
250
261
 
262
+ ## Runtime traffic audit
263
+
264
+ A Static First review is incomplete without a real browser trace. Capture a
265
+ fresh direct load separately from internal navigation, catalog interaction and
266
+ product navigation. For every GraphQL request record its `operationName`,
267
+ variables, initiator, phase and status.
268
+
269
+ The report must distinguish:
270
+
271
+ - data already present in exported HTML or the static bootstrap;
272
+ - volatile price, availability and purchase-limit refreshes;
273
+ - structural catalog requests caused by an explicit filter or pagination;
274
+ - duplicated operations with equivalent variables and no intervening
275
+ invalidation or user action.
276
+
277
+ The initial public route must not download full products already represented by
278
+ the bootstrap. Live-state requests must group and deduplicate variant IDs,
279
+ respect the documented batch limit and keep a failure-safe cache. A documented
280
+ revalidation is not a duplicate, but its reason and ownership must be visible in
281
+ the trace.
282
+
283
+ Also record the number of `BacanoClient` and `BacanoProvider` instances and
284
+ confirm that server/build `public-config` becomes the provider's
285
+ `initialContext`. If a browser trace cannot be captured, mark this control as
286
+ not verified instead of inferring it from source code.
287
+
251
288
  ## Acceptance checklist for storefront agents
252
289
 
253
290
  - [ ] View source contains real product cards/details, not only skeletons.
@@ -263,3 +300,5 @@ enabled for the website.
263
300
  - [ ] Mobile and desktop Lighthouse runs show no material layout shift.
264
301
  - [ ] A production trace confirms no duplicated catalog, shipping or website
265
302
  bootstrap calls during the first navigation.
303
+ - [ ] The trace lists operation names and separates static, live and
304
+ interaction-driven data.
@@ -451,6 +451,29 @@ Baby Rex is the implementation reference for integration shape, not a source to
451
451
  copy blindly. Other storefronts keep their own route maps, Content keys,
452
452
  mappers, and visual components.
453
453
 
454
+ ## Preview audit contract
455
+
456
+ An audit must verify both the SDK lifecycle and the storefront result model.
457
+ The SDK lifecycle is `idle`, `loading`, `ready` or `error`; `missing` and
458
+ `unsupported` are storefront document/view outcomes and must not be presented
459
+ as extra SDK statuses.
460
+
461
+ At minimum, automate or capture evidence for:
462
+
463
+ - a direct request to the single preview route returning `noindex, nofollow`;
464
+ - the preview route being absent from every sitemap;
465
+ - reuse of the global `BacanoProvider` and initialized `BacanoClient`;
466
+ - a finite route allowlist that rejects account, checkout, payment and order
467
+ routes;
468
+ - absence of GA4, Meta, Clarity and other effective marketing scripts, requests
469
+ and events on a direct preview load;
470
+ - `idle -> loading -> ready`, safe error, missing and unsupported rendering;
471
+ - cache reuse, forced refresh and internal navigation;
472
+ - unchanged static HTML, route generation and sitemap for public pages.
473
+
474
+ The final report must name the requests observed in preview and distinguish an
475
+ unexecuted browser check from a passing zero-request assertion.
476
+
454
477
  ## Short checklist for agents
455
478
 
456
479
  - [ ] Use `@bacano/sdk` `1.0.5` or newer.
@@ -461,6 +484,8 @@ mappers, and visual components.
461
484
  - [ ] Keep preview `noindex`, outside the sitemap, and free of transactional routes.
462
485
  - [ ] Hide root-level commercial UI and verify zero marketing events in preview.
463
486
  - [ ] Test idle, loading, ready, safe error, refresh, cache, and navigation states.
487
+ - [ ] Test storefront-specific missing and unsupported outcomes.
488
+ - [ ] Capture a direct preview trace proving zero effective marketing traffic.
464
489
  - [ ] Verify static public pages and their build output remain unchanged.
465
490
  - [ ] Run typecheck, lint, unit tests, E2E, and the complete production build.
466
491
 
@@ -0,0 +1,127 @@
1
+ # Fichas técnicas de producto en storefronts
2
+
3
+ Este documento define el contrato público de `@bacano/sdk` para mostrar y
4
+ filtrar la información técnica creada por cada comercio en Bacano Content.
5
+
6
+ La capacidad es independiente de los atributos y variantes comerciales. Las
7
+ variantes continúan siendo la fuente de combinaciones, SKU, precio e
8
+ inventario; una ficha técnica no debe usarse para recrearlas ni modificarlas.
9
+
10
+ ## Lectura en detalle y builds estáticos
11
+
12
+ `Product.specification` contiene la ficha técnica efectiva del producto o
13
+ `null` cuando no existe una ficha pública con valores. Está disponible en:
14
+
15
+ - `client.catalog.getProduct()`;
16
+ - `client.catalog.getProductBySlug()`;
17
+ - los productos de `client.catalog.getStaticCatalogBootstrap()`;
18
+ - los productos devueltos por `client.catalog.getCatalogPage()`.
19
+
20
+ Las tiendas estáticas deben consumir el valor que llega en el bootstrap. Un
21
+ cambio público en la definición o los valores requiere **Publicar cambios** y
22
+ un nuevo build del storefront. Stock, precios y descuentos conservan su flujo
23
+ dinámico independiente.
24
+
25
+ ```tsx
26
+ import type { ProductSpecification } from "@bacano/sdk";
27
+
28
+ export function ProductTechnicalSpecification({
29
+ specification,
30
+ }: {
31
+ specification: ProductSpecification | null;
32
+ }) {
33
+ if (!specification) return null;
34
+
35
+ return (
36
+ <section aria-labelledby="technical-specification-title">
37
+ <h2 id="technical-specification-title">Información adicional</h2>
38
+ {specification.sections.map((section) => (
39
+ <div key={section.id ?? section.key}>
40
+ <h3>{section.title}</h3>
41
+ {section.description ? <p>{section.description}</p> : null}
42
+ <dl>
43
+ {section.fields.map((field) => (
44
+ <div key={field.id}>
45
+ <dt>{field.label}</dt>
46
+ <dd>{formatSpecificationValue(field.value)}</dd>
47
+ </div>
48
+ ))}
49
+ </dl>
50
+ </div>
51
+ ))}
52
+ </section>
53
+ );
54
+ }
55
+
56
+ function formatSpecificationValue(value: unknown): string {
57
+ if (typeof value === "boolean") return value ? "Sí" : "No";
58
+ if (typeof value === "string" || typeof value === "number") {
59
+ return String(value);
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return value.map(formatSpecificationValue).join(", ");
63
+ }
64
+ if (value && typeof value === "object") {
65
+ if ("label" in value && typeof value.label === "string") {
66
+ return value.label;
67
+ }
68
+ if ("unit" in value && "value" in value) {
69
+ const measure = value as {
70
+ value: number;
71
+ unit: { symbol: string };
72
+ };
73
+ return `${measure.value} ${measure.unit.symbol}`;
74
+ }
75
+ }
76
+ return "";
77
+ }
78
+ ```
79
+
80
+ Los campos vacíos o internos ya son omitidos por Bacano. El storefront recibe
81
+ la organización por secciones y puede reproducirla o aplicar su propio diseño.
82
+ No debe consultar Hasura, interpretar el JSON editable ni reconstruir unidades
83
+ u opciones por su cuenta.
84
+
85
+ ## Facetas técnicas
86
+
87
+ Solicitar `filters: true` en `getCatalogPage()` devuelve
88
+ `result.filters.technical`. Cada faceta expone un `fieldId` estable, tipo,
89
+ rango, unidad, opciones y conteos relevantes para los productos públicos y
90
+ vendibles del resultado.
91
+
92
+ ```ts
93
+ const page = await client.catalog.getCatalogPage({
94
+ categoryListKey: "categorias",
95
+ attributeListKey: "atributos",
96
+ categorySlug: "vehiculos-electricos",
97
+ technicalByField: {
98
+ "FIELD_UUID_AUTONOMIA": { min: 40, max: 120 },
99
+ "FIELD_UUID_BATERIA": { optionIds: ["OPTION_UUID_LITIO"] },
100
+ },
101
+ include: { products: true, filters: true },
102
+ });
103
+ ```
104
+
105
+ Reglas del filtro:
106
+
107
+ - los valores seleccionados dentro de una misma faceta se combinan con OR;
108
+ - las facetas diferentes se combinan con AND;
109
+ - número y medida aceptan `min` y `max`;
110
+ - fecha acepta `dateFrom` y `dateTo`;
111
+ - booleano acepta `booleanValues`;
112
+ - lista simple o múltiple acepta `optionIds`;
113
+ - el storefront solo debe enviar IDs publicados por `filters.technical`.
114
+
115
+ Bacano rechaza como filtro cualquier campo interno, oculto o no marcado como
116
+ filtrable aunque se intente enviar su UUID manualmente.
117
+
118
+ ## Reglas de integración
119
+
120
+ - No crear GraphQL directo ni consultar el JSON flexible de Content.
121
+ - No persistir una segunda copia de la ficha en el storefront.
122
+ - No convertir fichas técnicas en atributos o variantes.
123
+ - No inventar facetas para campos que Bacano no publica como filtrables.
124
+ - Conservar el orden de secciones, campos y opciones entregado por el SDK.
125
+ - Omitir por completo el bloque cuando `specification` sea `null`.
126
+ - Validar el build estático con productos con y sin ficha antes de publicar.
127
+
@@ -0,0 +1,131 @@
1
+ # Precios por cantidad en storefronts
2
+
3
+ Este documento define el contrato oficial para mayoreo por cantidad en sitios
4
+ web Bacano. El storefront no calcula, copia ni fuerza precios: Bacano resuelve
5
+ el carrito completo en cada cambio y vuelve a validarlo al cerrar la compra.
6
+
7
+ ## Configuración
8
+
9
+ La política pertenece a un `website_id`. El comercio activa un primer umbral
10
+ para `WHOLESALE` y, si tiene habilitado ese tipo de precio, puede configurar un
11
+ segundo umbral para `WHOLESALE_2`.
12
+
13
+ - Solo participan variantes con un precio `WHOLESALE` válido para la sucursal
14
+ del website.
15
+ - La cantidad elegible es la suma de unidades participantes del carrito, no la
16
+ cantidad de una única línea.
17
+ - Al alcanzar el segundo nivel, una variante sin `WHOLESALE_2` conserva su
18
+ precio `WHOLESALE`.
19
+ - Un nivel mayorista nunca reemplaza un precio regular o promocional menor.
20
+ - Las variantes sin precio mayorista siguen comprándose a su mejor precio
21
+ regular, pero no suman para alcanzar el umbral.
22
+
23
+ ## Configuración pública
24
+
25
+ `getWebsitePublicConfig()` expone únicamente los umbrales públicos necesarios
26
+ para comunicar la promoción:
27
+
28
+ ```ts
29
+ const config = await getWebsitePublicConfig({
30
+ apiUrl,
31
+ websiteSlug,
32
+ origin,
33
+ });
34
+
35
+ config.volumePricing;
36
+ // {
37
+ // schemaVersion: 1,
38
+ // enabled: true,
39
+ // tiers: [
40
+ // { priceType: "WHOLESALE", minQuantity: 3 },
41
+ // { priceType: "WHOLESALE_2", minQuantity: 6 }
42
+ // ]
43
+ // }
44
+ ```
45
+
46
+ Este bloque sirve para mensajes generales. No debe usarse para calcular el
47
+ precio final porque la elegibilidad depende de los precios vigentes de cada
48
+ variante y del carrito completo.
49
+
50
+ ## Respuesta del carrito
51
+
52
+ Las operaciones `cart.getOrCreate()`, `cart.get()`, `cart.setItem()`,
53
+ `cart.removeItem()` y `cart.startCheckout()` devuelven:
54
+
55
+ ```ts
56
+ cart.volumePricing;
57
+ // {
58
+ // enabled: true,
59
+ // eligibleQuantity: 4,
60
+ // appliedPriceType: "WHOLESALE",
61
+ // appliedMinQuantity: 3,
62
+ // nextPriceType: "WHOLESALE_2",
63
+ // nextMinQuantity: 6,
64
+ // remainingQuantity: 2,
65
+ // totalSavings: 24000
66
+ // }
67
+
68
+ cart.items[0];
69
+ // {
70
+ // baseUnitPrice: 50000,
71
+ // unitPrice: 42000,
72
+ // lineTotal: 126000,
73
+ // unitSavings: 8000,
74
+ // lineSavings: 24000,
75
+ // volumePricingEligible: true,
76
+ // resolvedPriceType: "WHOLESALE",
77
+ // volumePricingMinQuantity: 3,
78
+ // ...
79
+ // }
80
+ ```
81
+
82
+ El storefront debe pintar `unitPrice`, `lineTotal` y los totales retornados por
83
+ Bacano. `baseUnitPrice`, `unitSavings`, `lineSavings` y `totalSavings` permiten
84
+ explicar el cambio sin reconstruir reglas. `volumePricingEligible` indica si la
85
+ línea participa en los umbrales, incluso antes de alcanzar uno.
86
+
87
+ Al cambiar una cantidad debe esperar la respuesta de `setItem()` antes de
88
+ confirmar visualmente el nuevo total. La experiencia recomendada es mostrar el
89
+ precio anterior tachado cuando exista ahorro, el nivel aplicado y un mensaje
90
+ como “Agrega 2 unidades más para obtener Mayorista 2” usando
91
+ `remainingQuantity`. Si `volumePricing.enabled` es `false`, no debe mostrarse
92
+ ninguna comunicación de mayoreo.
93
+
94
+ ## Sincronización antes del checkout
95
+
96
+ El precio por cantidad no depende del correo ni de la identidad del comprador.
97
+ Para mostrarlo desde el carrito, el storefront debe crear o recuperar la sesión
98
+ privada con `cart.getOrCreate()` al agregar la primera unidad y utilizar
99
+ `cart.setItem()` después de cada cambio. `cart.startCheckout()` incorpora el
100
+ contacto y vuelve a validar los mismos precios de forma autoritativa.
101
+
102
+ El carrito privado puede existir sin correo, teléfono ni identidad. Su creación
103
+ no inicia la logística: Bacano crea o sincroniza la entrega únicamente cuando
104
+ el storefront llama `cart.startCheckout()` con la modalidad elegida.
105
+
106
+ El storefront conserva bajo su responsabilidad los datos visuales que necesite
107
+ (imagen, slug o atributos), pero no precios ni totales autoritativos. Para
108
+ vaciar el carrito debe llamar `cart.clear()`: Bacano lo marca como descartado y
109
+ el SDK elimina exclusivamente su sesión privada. Quitar la última línea produce
110
+ el mismo cierre automáticamente. Ninguna de estas operaciones borra el carrito
111
+ local propio del storefront; ese almacenamiento se limpia después de recibir la
112
+ confirmación del SDK.
113
+
114
+ ## Checkout y concurrencia
115
+
116
+ El checkout vuelve a resolver precios, stock y cupones de forma autoritativa.
117
+ Los valores locales son únicamente una vista optimista. Una modificación de
118
+ precios, inventario o política entre la vista del carrito y el checkout puede
119
+ producir una respuesta actualizada o un error comercial controlado.
120
+
121
+ Los pedidos conservan por línea el tipo de precio resuelto y el umbral aplicado
122
+ como evidencia. La configuración futura del website no cambia el pedido ya
123
+ confirmado.
124
+
125
+ ## Prohibiciones
126
+
127
+ - No calcular umbrales en Baby Rex ni en otro storefront.
128
+ - No seleccionar `WHOLESALE` directamente desde el navegador.
129
+ - No enviar precios en operaciones de carrito o checkout.
130
+ - No consultar Hasura ni tablas de precios mediante GraphQL directo.
131
+ - No aplicar descuentos paralelos para simular mayoreo.
package/client.d.ts CHANGED
@@ -45,6 +45,7 @@ export interface BacanoClient {
45
45
  quantity: number;
46
46
  }>;
47
47
  removeItem(cartItemId: string): Promise<Cart>;
48
+ clear(): Promise<void>;
48
49
  };
49
50
  checkout: {
50
51
  getLocationOptions(): Promise<WebsiteLocationOptions>;