@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 +98 -12
- package/STOREFRONT_CONSTRUCTION.md +17 -4
- package/STOREFRONT_MARKETING.md +5 -0
- package/STOREFRONT_PERFORMANCE.md +39 -0
- package/STOREFRONT_PREVIEW.md +25 -0
- package/STOREFRONT_PRODUCT_SPECIFICATIONS.md +127 -0
- package/STOREFRONT_VOLUME_PRICING.md +131 -0
- package/client.d.ts +1 -0
- package/graphql.cjs +203 -3
- package/graphql.d.ts +1 -1
- package/graphql.js +203 -3
- package/index.cjs +474 -44
- package/index.d.ts +1 -1
- package/index.js +474 -44
- package/marketing/attribution.d.ts +2 -0
- package/marketing/types.d.ts +2 -0
- package/modules/cart-errors.d.ts +2 -0
- package/modules/cart.d.ts +1 -0
- package/package.json +1 -1
- package/react/hooks.d.ts +1 -0
- package/react/website-gate.d.ts +1 -1
- package/react.cjs +508 -62
- package/react.js +509 -63
- package/skills/bacano-web/SKILL.md +17 -10
- package/skills/bacano-web/references/architecture-runtime.md +109 -0
- package/skills/bacano-web/references/common.md +2 -1
- package/skills/bacano-web/references/report.md +22 -4
- package/skills/bacano-web/scripts/audit-http.mjs +39 -2
- package/transport/documents.d.ts +21 -18
- package/types.d.ts +136 -0
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
|
|
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
|
-
//
|
|
301
|
-
// Use these
|
|
302
|
-
//
|
|
303
|
-
// catalog
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
1673
|
-
falls back to `CLIENT`; wholesale tiers
|
|
1674
|
-
|
|
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
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
package/STOREFRONT_MARKETING.md
CHANGED
|
@@ -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.
|
package/STOREFRONT_PREVIEW.md
CHANGED
|
@@ -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.
|