@bacano/sdk 1.0.0

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 ADDED
@@ -0,0 +1,1357 @@
1
+ # @bacano/sdk
2
+
3
+ TypeScript SDK for building e-commerce websites on the Bacano ERP. Zero runtime dependencies, optional React hooks, and full type safety.
4
+
5
+ Buyer credentials are owned by each storefront's Clerk application. Read
6
+ [`AUTHENTICATION.md`](./AUTHENTICATION.md) before implementing registration,
7
+ sign-in, customer profiles, authenticated checkout, or order history. The guide
8
+ ships inside the npm package.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @bacano/sdk
14
+ ```
15
+
16
+ ```bash
17
+ pnpm add @bacano/sdk
18
+ ```
19
+
20
+ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
21
+ as a peer dependency. The core client has no runtime dependencies and works
22
+ without React.
23
+
24
+ ### Entry points
25
+
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 |
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Create a Website in Bacano
37
+
38
+ Before using the SDK, an active website must exist in Bacano. Create it from
39
+ the **Websites** section of the Bacano platform, or ask your Bacano contact to
40
+ set one up.
41
+
42
+ Required fields:
43
+
44
+ - **Name**: display name.
45
+ - **Slug**: SDK identifier, for example `my-store`.
46
+ - **Branch / chain**: required for stock availability and cart creation.
47
+ - **Domain**: optional custom domain, without `https://`.
48
+ - **Advanced config**: optional JSON object consumed by the external web.
49
+
50
+ The branch matters: `checkAvailability()` and checkout/cart sync actions need a
51
+ `chain_id`. A website without a branch can be resolved, but shopping actions
52
+ will fail with `NO_CHAIN`.
53
+
54
+ ### 2. Initialize the Client
55
+
56
+ ```typescript
57
+ import { createBacanoClient } from "@bacano/sdk";
58
+
59
+ const client = createBacanoClient({
60
+ apiUrl: "https://api.bacanoerp.com",
61
+ websiteSlug: "my-store",
62
+ });
63
+
64
+ // Must be called once — resolves the website context.
65
+ await client.init();
66
+ ```
67
+
68
+ Next.js example:
69
+
70
+ ```env
71
+ NEXT_PUBLIC_BACANO_API_URL=https://api.bacanoerp.com
72
+ NEXT_PUBLIC_BACANO_WEBSITE_SLUG=my-store
73
+ ```
74
+
75
+ ```typescript
76
+ const client = createBacanoClient({
77
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
78
+ websiteSlug: process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!,
79
+ tokenStorage: "memory",
80
+ });
81
+
82
+ await client.init();
83
+ ```
84
+
85
+ ### 3. Browse Products (No Auth Required)
86
+
87
+ `catalog.getProducts()` only returns products that are active, enabled for the
88
+ website branch, have a valid `CLIENT` price and have available
89
+ inventory in that branch warehouse.
90
+
91
+ ```typescript
92
+ // List products
93
+ const products = await client.catalog.getProducts({ limit: 10 });
94
+
95
+ // Search across product names, descriptions, brands, and attributes
96
+ // Results are ranked by relevance when no explicit sortBy is set
97
+ const results = await client.catalog.getProducts({ search: "cotton" });
98
+
99
+ // Search suggestions (lightweight autocomplete)
100
+ const suggestions = await client.catalog.getSearchSuggestions({
101
+ query: "shi",
102
+ limit: 5,
103
+ });
104
+ // → [{ type: 'product', text: 'Shirt', id: '...' }, { type: 'brand', text: 'Shimano', id: '...' }]
105
+
106
+ // Filter by category
107
+ const clothing = await client.catalog.getProducts({ categoryName: "CLOTHES" });
108
+
109
+ // Filter by price range
110
+ const affordable = await client.catalog.getProducts({
111
+ minPrice: 10,
112
+ maxPrice: 50,
113
+ });
114
+
115
+ // In-stock only. This is already the website default, kept for UI clarity.
116
+ const available = await client.catalog.getProducts({ inStock: true });
117
+
118
+ // Filter by color
119
+ const redItems = await client.catalog.getProducts({
120
+ colors: ["Red", "Burgundy"],
121
+ });
122
+
123
+ // Filter by reusable content/catalog tags
124
+ const babyGirl = await client.catalog.getProducts({
125
+ tags: ["bebe-nina"],
126
+ });
127
+
128
+ // Filter by dynamic attributes (category-specific)
129
+ const mediumCotton = await client.catalog.getProducts({
130
+ categoryName: "CLOTHES",
131
+ attributes: { Size: ["M"], Material: ["Cotton"] },
132
+ });
133
+
134
+ // Sort products
135
+ const newest = await client.catalog.getProducts({ sortBy: "newest" });
136
+ const cheapest = await client.catalog.getProducts({ sortBy: "price_asc" });
137
+
138
+ // Combine multiple filters
139
+ const filtered = await client.catalog.getProducts({
140
+ categoryName: "CLOTHES",
141
+ colors: ["Red"],
142
+ minPrice: 20,
143
+ inStock: true,
144
+ sortBy: "price_asc",
145
+ limit: 20,
146
+ });
147
+
148
+ // Single product
149
+ const product = await client.catalog.getProduct("product-uuid");
150
+
151
+ // Automatic recommendations for the product detail. This is an independent,
152
+ // lightweight request; Bacano resolves ranking, availability and public price.
153
+ const recommendations = await client.catalog.getProductRecommendations({
154
+ productId: product!.id,
155
+ limit: 16,
156
+ });
157
+
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.
162
+ for (const related of product?.relatedProducts ?? []) {
163
+ console.log(related.productId);
164
+ console.log(related.slug);
165
+ console.log(related.displayLabel ?? related.name);
166
+ console.log(related.sku);
167
+ console.log(related.media[0]?.url);
168
+ }
169
+
170
+ // Categories and brands
171
+ const categories = await client.catalog.getCategories();
172
+ const brands = await client.catalog.getBrands();
173
+
174
+ // Product filters / facets (for building filter UIs)
175
+ const filters = await client.catalog.getProductFilters();
176
+ // → { categories, brands, colors, priceRange: { min, max }, attributes: [] }
177
+
178
+ // Category-aware facets (includes dynamic attributes for that category)
179
+ const clothingFilters = await client.catalog.getProductFilters({
180
+ categoryName: "CLOTHES",
181
+ });
182
+ // → { ..., attributes: [{ id, name: 'Size', values: [{ value: 'S', count: 5 }, ...] }] }
183
+
184
+ // Public attribute list configured in Bacano.
185
+ // Without filters it returns the full published list for the company.
186
+ const attributeList = await client.catalog.getAttributeList({
187
+ key: "filtros-catalogo",
188
+ });
189
+
190
+ // Without category/search filters, but still limited to real available products.
191
+ const applicableAttributeList = await client.catalog.getAttributeList({
192
+ key: "filtros-catalogo",
193
+ onlyApplicable: true,
194
+ });
195
+
196
+ // With filters, Bacano resolves only attributes and values that apply to
197
+ // active, priced and in-stock products in the current website context.
198
+ const contextualAttributeList = await client.catalog.getAttributeList({
199
+ key: "filtros-catalogo",
200
+ onlyApplicable: true,
201
+ filters: {
202
+ categoryName: "CLOTHES",
203
+ tags: ["bebe-nina"],
204
+ minPrice: 10000,
205
+ maxPrice: 120000,
206
+ },
207
+ });
208
+
209
+ // Stock availability
210
+ const availability = await client.catalog.checkAvailability([
211
+ "variant-uuid-1",
212
+ "variant-uuid-2",
213
+ ]);
214
+ // → [{ productVariantId, inStock: true, label: "In Stock" }]
215
+ ```
216
+
217
+ ### 4. Local Cart And Checkout Start
218
+
219
+ The website checkout remains guest-first. Buyer credentials and email
220
+ verification are owned by the website's Clerk application; passwords never
221
+ pass through this SDK or Bacano.
222
+
223
+ While a visitor is only browsing and adding products, keep the cart in the web
224
+ app local state. Do not create a Bacano order yet.
225
+
226
+ When the visitor starts checkout and provides an identifier allowed by the
227
+ company policy, send the local cart to Bacano in one operation:
228
+
229
+ ```typescript
230
+ const checkoutCart = await client.cart.startCheckout({
231
+ fulfillmentFamily: "SHIPPING", // use 'PICKUP' for store pickup
232
+ contact: {
233
+ name: "Jane Doe",
234
+ email: "jane@example.com",
235
+ phone: "+573001112233",
236
+ address: {
237
+ city: "Bucaramanga",
238
+ line1: "Cra 1 # 2-3",
239
+ notes: "Porteria azul",
240
+ },
241
+ },
242
+ items: [
243
+ { productVariantId: "variant-uuid-1", quantity: 2 },
244
+ { productVariantId: "variant-uuid-2", quantity: 1 },
245
+ ],
246
+ });
247
+
248
+ console.log(checkoutCart.id); // WEB + PENDING + UNPAID checkout order id
249
+ console.log(checkoutCart.cartToken); // opaque cart token stored by the SDK
250
+ ```
251
+
252
+ This creates or recovers a `sales_channel=WEB`, `PENDING + UNPAID` order with
253
+ `orders.website_contact_snapshot` and the received items. From this point on,
254
+ the checkout can be recovered in Bacano as an abandoned checkout if the customer
255
+ does not pay or confirm. Bacano also stores `orders.sales_channel = WEB` for
256
+ these started web checkouts.
257
+
258
+ `fulfillmentFamily` tells Bacano how the physical products will be fulfilled:
259
+ `SHIPPING` creates/synchronizes the normal delivery flow and `PICKUP`
260
+ creates/synchronizes the pickup flow. Bacano stores this in
261
+ `orders.fulfillment_family`.
262
+
263
+ After checkout has started, cart edits go through Bacano and are guarded by
264
+ `order_id + cart_token`:
265
+
266
+ ```typescript
267
+ // Set variant quantity after startCheckout has created a Bacano cart session.
268
+ await client.cart.setItem("variant-uuid", 2);
269
+
270
+ // Compatibility alias for adding one variant quantity.
271
+ await client.cart.addItem("variant-uuid", 2);
272
+
273
+ // Update by line id. Internally resolves the variant and calls setItem.
274
+ await client.cart.updateItem("cart-item-uuid", 5);
275
+
276
+ // Remove by line id.
277
+ await client.cart.removeItem("cart-item-uuid");
278
+
279
+ // View current cart from the stored guest session.
280
+ const currentCart = await client.cart.get();
281
+ console.log(currentCart?.items); // CartItem[]
282
+ console.log(currentCart?.totals); // { subtotal, tax, discount, total }
283
+ ```
284
+
285
+ Cart rules:
286
+
287
+ - No contact yet means local web cart only.
288
+ - Email, phone or a complete document pair can start a Bacano
289
+ `sales_channel=WEB`, `PENDING + UNPAID` checkout.
290
+ - `sales_channel=WEB`, `PENDING + UNPAID` without `order_number` means checkout started or abandoned.
291
+ - 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.
292
+ - The web must collect `fulfillmentFamily` before starting checkout:
293
+ `SHIPPING` for delivery or `PICKUP` for store pickup.
294
+ - The cart token is opaque. Bacano stores only its SHA-256 hash.
295
+ - Cart edits go through Hasura Actions, not direct public table mutations.
296
+ - Filling checkout does not create a formal `client` yet; it stores only
297
+ `website_contact_snapshot` on the order.
298
+ - A pending unpaid web checkout does not reserve stock by itself, deduct
299
+ inventory, create payments or post accounting. Creating an external gateway
300
+ session reserves stock until the attempt finishes or expires.
301
+
302
+ ### 5. Guest Checkout Confirmation
303
+
304
+ Before confirming a shipping checkout, ask Bacano for location, shipping,
305
+ payment and coupon options:
306
+
307
+ ```typescript
308
+ const deliveryOptions = await client.checkout.getDeliveryOptions();
309
+ const locations = await client.checkout.getLocationOptions();
310
+ const shippingPolicy = await client.checkout.getShippingPolicy();
311
+ const shippingProgress = await client.checkout.getFreeShippingProgress({
312
+ items: cartItems.map((item) => ({
313
+ productVariantId: item.productVariantId,
314
+ quantity: item.quantity,
315
+ })),
316
+ });
317
+ const quotes = await client.checkout.getShippingQuotes({
318
+ address,
319
+ items: cartItems.map((item) => ({
320
+ productVariantId: item.productVariantId,
321
+ quantity: item.quantity,
322
+ })),
323
+ });
324
+ const paymentOptions = await client.checkout.getPaymentOptions({
325
+ fulfillmentFamily: "SHIPPING",
326
+ });
327
+ await client.checkout.applyCoupon({ code: "BEBE10" });
328
+ ```
329
+
330
+ `getDeliveryOptions` can run before contact, cart or address exists. Use it to
331
+ show options such as `SHIPPING` and `PICKUP`. Call `getShippingQuotes` only when
332
+ the selected option is `SHIPPING` and the customer has provided an address.
333
+
334
+ Use `getShippingPolicy()` for general storefront messages before a cart
335
+ exists. Use `getFreeShippingProgress({ items })` to show cart progress before
336
+ collecting contact or address data. Bacano resolves current prices, stock,
337
+ website and shipping policy; the storefront must format the structured values
338
+ instead of hardcoding or recalculating them.
339
+
340
+ `getShippingQuotes` returns the final shipping price for an address. Before a
341
+ guest checkout exists, pass `items`. Once an active cart session exists, the
342
+ SDK intentionally prefers the persisted cart and ignores loose items so
343
+ coupons and server-side totals remain authoritative.
344
+
345
+ Each quote exposes `baseAmount` and the final `amount`. When Bacano's
346
+ website-level free-shipping policy applies, `amount` is zero and
347
+ `freeShippingApplied` is `true`. Use `freeShippingRemaining` to show progress
348
+ before the threshold is reached. Storefronts must not hardcode or recalculate
349
+ the threshold; Bacano revalidates it during `checkout.submit`.
350
+
351
+ Use `getPaymentOptions({ fulfillmentFamily })` to render payment modes valid
352
+ for the selected delivery option. For example, `PICKUP` may return "Pagar
353
+ ahora" and "Pagar al recoger", while `SHIPPING` may return "Pagar ahora" and
354
+ "Pagar contraentrega".
355
+
356
+ Only payment methods marked as **Disponible para web** in Bacano and enabled in
357
+ the website payment settings are returned. Gateway methods such as Wompi expose
358
+ `gatewayCode`, `requiresPaymentSession` and public configuration only; secrets
359
+ never leave Bacano.
360
+
361
+ Each payment option also exposes `initialPaymentPolicy` (`none`, `optional` or
362
+ `required`), `requiresInitialPayment`, `minInitialAmount` and
363
+ `suggestedInitialAmount`. When required, submit the selected option with:
364
+
365
+ ```typescript
366
+ initialPayment: {
367
+ amount: selectedAmount,
368
+ paymentMethodId: selectedInitialMethod.paymentMethodId,
369
+ }
370
+ ```
371
+
372
+ The API enforces the policy, minimum and order total. Do not infer deposits from
373
+ labels or delivery mode.
374
+
375
+ ```typescript
376
+ const payNow = paymentOptions.find((option) => option.mode === "pay_now");
377
+
378
+ const result = await client.checkout.submit({
379
+ fulfillmentFamily: "SHIPPING",
380
+ contact: {
381
+ firstName: "Jane",
382
+ lastName: "Doe",
383
+ email: "jane@example.com",
384
+ cellphone: "+573001112233",
385
+ documentType: "13",
386
+ documentNumber: "123456789",
387
+ },
388
+ address,
389
+ shippingQuoteId: quotes[0].quoteId,
390
+ paymentMode: "pay_now",
391
+ paymentMethodId: payNow?.methods[0]?.paymentMethodId,
392
+ buyerAccountOptIn: true,
393
+ });
394
+
395
+ const publicOrderUrl =
396
+ result.orderId && result.publicOrderToken
397
+ ? `/pedido/${result.orderId}?token=${result.publicOrderToken}`
398
+ : null;
399
+
400
+ if (result.requiresPaymentSession && result.paymentMethodId) {
401
+ const session = await client.checkout.createPaymentSession({
402
+ orderId: result.orderId ?? undefined,
403
+ publicOrderToken: result.publicOrderToken ?? undefined,
404
+ paymentMethodId: result.paymentMethodId,
405
+ returnUrl:
406
+ `${window.location.origin}/pago/respuesta` +
407
+ `?orderId=${encodeURIComponent(result.orderId ?? "")}` +
408
+ `&token=${encodeURIComponent(result.publicOrderToken ?? "")}`,
409
+ });
410
+
411
+ if (
412
+ session.success &&
413
+ session.integrationMode === "redirect" &&
414
+ session.redirectUrl
415
+ ) {
416
+ window.location.assign(session.redirectUrl);
417
+ }
418
+ }
419
+
420
+ if (result.success) {
421
+ console.log(`Order ${result.orderNumber} placed!`);
422
+ console.log(publicOrderUrl);
423
+ } else {
424
+ console.error("Checkout failed:", result.errors);
425
+ }
426
+ ```
427
+
428
+ `buyerAccountOptIn` must reflect an explicit, unchecked-by-default buyer
429
+ choice. For guests, Bacano queues a Clerk activation invitation as soon as a
430
+ valid order is persisted, even when payment remains pending. Invalid attempts
431
+ that do not create an order send nothing, and checkout never waits for Clerk.
432
+ `buyerAccountInvitationRequested` reports whether Bacano durably accepted the
433
+ request. Authenticated buyers do not receive an invitation; their verified
434
+ Clerk token owns the order automatically. See [AUTHENTICATION.md](./AUTHENTICATION.md)
435
+ for the invitation landing-page and secure order-claim contract.
436
+
437
+ `returnUrl` is the storefront response page. `session.redirectUrl` is the
438
+ provider-hosted checkout and is the only URL the storefront opens. The
439
+ storefront never renders gateway widgets or constructs Wompi/Addi/ePayco URLs.
440
+ On the response page, send the provider's public transaction ID back to Bacano
441
+ when available and wait for the public order:
442
+
443
+ ```typescript
444
+ await client.checkout.reconcilePaymentSession({
445
+ orderId,
446
+ publicOrderToken,
447
+ paymentSessionId,
448
+ gatewayTransactionId,
449
+ });
450
+
451
+ const order = await client.orders.waitForPayment({
452
+ orderId,
453
+ token: publicOrderToken,
454
+ intervalMs: 1_500,
455
+ timeoutMs: 45_000,
456
+ });
457
+ ```
458
+
459
+ Load `getWebsitePublicConfig()` before rendering contact fields.
460
+ Pass the same required `websiteSlug` used to construct the client. Bacano
461
+ selects that website explicitly and then verifies that the request Origin
462
+ belongs to it; Origin alone never chooses a storefront.
463
+ `customerIdentification` exposes the company-level policy configured in
464
+ `Settings > Sales > Checkout`: the primary identifier, additional required
465
+ fields and valid document types. The storefront renders that contract; Bacano
466
+ enforces it again and uses the configured primary identifier to recognize a
467
+ returning guest customer. `AUTO` preserves the legacy email-first, phone-second
468
+ behavior.
469
+
470
+ The storefront never approves a payment from query parameters. Bacano verifies
471
+ the transaction directly with the provider; the signed webhook remains the
472
+ primary confirmation path.
473
+
474
+ After invitation acceptance, Bacano fills only missing commercial profile
475
+ fields from its persisted order snapshots: name, mobile and primary shipping
476
+ address. Existing profile data is never overwritten, and the authenticated
477
+ email always comes from Clerk verification. `name` and `phone` remain
478
+ supported for older storefronts; prefer `firstName`, `lastName` and
479
+ `cellphone` in new checkouts.
480
+
481
+ Checkout submission is idempotent for the active SDK cart session. If Bacano
482
+ returns `ORDER_CONFIRMATION_RETRYABLE`, keep the current cart session and call
483
+ `checkout.submit` again with the same data. Do not call `cart.startCheckout`
484
+ again: Bacano resumes the same `orderId + cartToken`, pending attempts do not
485
+ consume a sales number, and a replay after confirmation returns the existing
486
+ order result. For gateway checkouts, the SDK retains the private cart session
487
+ until `createPaymentSession` succeeds, then retires it. Payment response and
488
+ retry pages must continue with the returned `orderId + publicOrderToken`; a
489
+ second purchase starts a new cart instead of reusing the finalized order.
490
+
491
+ ### 6. Public Order Tracking
492
+
493
+ Confirmation pages should not depend on browser-only snapshots. After a
494
+ successful checkout, `submit` returns `publicOrderToken`; use it with the order
495
+ id to build a durable customer link:
496
+
497
+ ```typescript
498
+ const publicOrder = await client.orders.getPublicOrder({
499
+ orderId: result.orderId!,
500
+ token: result.publicOrderToken!,
501
+ });
502
+
503
+ console.log(publicOrder?.orderNumber);
504
+ console.log(publicOrder?.status);
505
+ console.log(publicOrder?.paymentStatus);
506
+ console.log(publicOrder?.payment.requiresPaymentSession);
507
+ console.log(publicOrder?.payment.nextAction);
508
+ console.log(publicOrder?.items);
509
+ console.log(publicOrder?.nextSteps);
510
+ ```
511
+
512
+ `getPublicOrder` does not require login. Bacano validates the current website,
513
+ the order id and the opaque public token. The response is safe for customers:
514
+ order state, payment state, delivery summary, payment summary, totals and item
515
+ summary. It does not expose private credentials, internal snapshots or editable
516
+ cart permissions.
517
+
518
+ `payment.nextAction` is the structured UI contract:
519
+ `CREATE_PAYMENT_SESSION`, `WAIT_FOR_PAYMENT`, `RETRY_PAYMENT`,
520
+ `PAY_ON_DELIVERY`, `PAY_AT_PICKUP`, `WAIT_FOR_ORDER_CONFIRMATION` or `NONE`.
521
+ `nextSteps` is display copy only. A missing `payment.session` does not by itself
522
+ mean that no gateway is required.
523
+
524
+ For payment gateways, the redirect page should be visual only. The gateway
525
+ webhook updates Bacano; `/pago/respuesta` can use `orderId + publicOrderToken`
526
+ to redirect or link back to `/pedido/[id]`.
527
+
528
+ The checkout validates:
529
+
530
+ - Cart ownership through `order_id + cart_token`
531
+ - Order has `sales_channel=WEB`, `PENDING + UNPAID` and no `order_number`
532
+ - Cart is not empty
533
+ - Products are active and enabled for the website branch through `chain_products`
534
+ - Products are enabled for the website branch
535
+ - Sufficient stock for all items
536
+ - All items have a valid positive `CLIENT` price
537
+
538
+ It also **recalculates totals** from current prices at checkout time to prevent
539
+ stale-price exploits. Shipping and coupons are recalculated in Bacano as well;
540
+ the web only sends the selected IDs and address. On success the order keeps `sales_channel=WEB`, `PENDING + UNPAID`,
541
+ receives an order number, and Bacano creates or reuses the formal `client` by
542
+ email first, then phone. The selected fulfillment family is synchronized through
543
+ the existing delivery machinery so the ERP can continue pickup/shipping actions
544
+ from the sale detail.
545
+
546
+ Checkout intentionally does **not** create accounting entries, payments, stock
547
+ reservations or inventory deductions in this iteration. Those effects happen in a
548
+ future payment-confirmation flow.
549
+
550
+ ### 6. Optional Auth Module
551
+
552
+ The SDK still exposes `client.auth` for future logged-in customer flows, but it
553
+ is not required for catalog, cart or guest checkout.
554
+
555
+ ### 7. Order History
556
+
557
+ ```typescript
558
+ const orders = await client.orders.list({ limit: 10 });
559
+
560
+ const order = await client.orders.get("order-uuid");
561
+ console.log(order?.orderNumber); // "WEB-000001"
562
+ console.log(order?.status); // "PENDING", "PAYED", "SENT", "COMPLETED"
563
+ console.log(order?.items); // OrderItem[]
564
+ ```
565
+
566
+ ### 8. Customer Profile
567
+
568
+ ```typescript
569
+ const profile = await client.profile.get();
570
+ console.log(profile.name, profile.email);
571
+
572
+ const addresses = await client.profile.getAddresses();
573
+ const preferredShippingAddress = addresses.find((address) => address.isPrimary);
574
+
575
+ await client.profile.update({
576
+ name: "Jane",
577
+ lastname: "Smith",
578
+ cellphone: "+573001234567",
579
+ address: "123 Main St",
580
+ addressComplement: "Apto 401",
581
+ countryCode: "CO",
582
+ departmentId: "68",
583
+ municipalityId: "68001",
584
+ });
585
+ ```
586
+
587
+ `profile.email` is read-only and comes from the buyer's verified Clerk
588
+ identity. Use `cellphone` for the mobile/WhatsApp number in E.164 format;
589
+ `phone` is an optional secondary telephone number. Never send passwords,
590
+ verification codes or Clerk secrets through this module.
591
+
592
+ `profile.getAddresses()` also requires an authenticated Clerk session. It
593
+ returns only checkout-ready addresses with canonical department and
594
+ municipality data. A complete commercial profile address is `PRIMARY`; saved
595
+ delivery addresses are `SHIPPING`. `departmentId` and `municipalityId` are the
596
+ canonical geographic codes accepted by `checkout.getShippingQuotes()`. An
597
+ incomplete legacy address is intentionally omitted so the storefront can show
598
+ the location form instead of guessing by display text. The storefront must not
599
+ send a `clientId`.
600
+
601
+ ---
602
+
603
+ ## Configuration Reference
604
+
605
+ ```typescript
606
+ createBacanoClient({
607
+ // Required
608
+ apiUrl: string; // Bacano public API URL
609
+ websiteSlug: string; // URL-friendly identifier (e.g., "my-store")
610
+
611
+ // Optional
612
+ tokenStorage?: 'localStorage' | 'cookie' | 'memory'; // Guest cart session only
613
+ getAccessToken?: () => string | null | Promise<string | null>;
614
+ });
615
+ ```
616
+
617
+ ### Cart Session Storage
618
+
619
+ `tokenStorage` is retained for compatibility but stores only the guest cart
620
+ identifier/token. Buyer access tokens are never persisted by Bacano SDK; the
621
+ configured identity provider owns their lifecycle.
622
+
623
+ ---
624
+
625
+ ## React Integration
626
+
627
+ ### Setup
628
+
629
+ Wrap your app with `BacanoProvider`:
630
+
631
+ ```tsx
632
+ import { BacanoProvider } from "@bacano/sdk/react";
633
+
634
+ function App() {
635
+ return (
636
+ <BacanoProvider
637
+ config={{
638
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
639
+ websiteSlug: "my-store",
640
+ }}
641
+ >
642
+ <Shop />
643
+ </BacanoProvider>
644
+ );
645
+ }
646
+ ```
647
+
648
+ ### Initialization State
649
+
650
+ ```tsx
651
+ import { useBacanoState } from "@bacano/sdk/react";
652
+
653
+ function Shop() {
654
+ const { loading, error } = useBacanoState();
655
+
656
+ if (loading) return <p>Loading website...</p>;
657
+ if (error) return <p>Error: {error.message}</p>;
658
+
659
+ return <ProductList />;
660
+ }
661
+ ```
662
+
663
+ ### Available Hooks
664
+
665
+ #### Buyer authentication with Clerk
666
+
667
+ ```tsx
668
+ import { ClerkProvider, useAuth } from "@clerk/nextjs";
669
+ import { BacanoProvider } from "@bacano/sdk/react";
670
+
671
+ function StorefrontSdk({ children }: { children: React.ReactNode }) {
672
+ const { getToken } = useAuth();
673
+ return (
674
+ <BacanoProvider
675
+ config={{
676
+ apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
677
+ websiteSlug: "my-store",
678
+ getAccessToken: () => getToken(),
679
+ }}
680
+ >
681
+ {children}
682
+ </BacanoProvider>
683
+ );
684
+ }
685
+ ```
686
+
687
+ Mount `StorefrontSdk` inside the website-specific `ClerkProvider`. Use Clerk's
688
+ components or hooks for sign-in, sign-up, verification and password recovery.
689
+ The SDK obtains a fresh token for each protected request and never receives
690
+ the buyer password.
691
+
692
+ #### `useProducts(opts?)`
693
+
694
+ ```tsx
695
+ import { getPrimaryProductImage, useProducts } from "@bacano/sdk/react";
696
+
697
+ function ProductList() {
698
+ const {
699
+ data: products,
700
+ loading,
701
+ error,
702
+ refetch,
703
+ } = useProducts({
704
+ categoryName: "Clothing",
705
+ limit: 20,
706
+ });
707
+
708
+ if (loading) return <p>Loading...</p>;
709
+ if (error) return <p>Error: {error.message}</p>;
710
+
711
+ return (
712
+ <ul>
713
+ {products?.map((p) => {
714
+ const image = getPrimaryProductImage(p.media);
715
+
716
+ return (
717
+ <li key={p.id}>
718
+ {image && <img src={image.url} alt={image.alt ?? p.name} />}
719
+ <h3>{p.name}</h3>
720
+ <p>{p.brand?.name}</p>
721
+ {p.variants.map((v) => (
722
+ <span key={v.id}>
723
+ {v.sku}: ${v.prices[0]?.price}
724
+ </span>
725
+ ))}
726
+ </li>
727
+ );
728
+ })}
729
+ </ul>
730
+ );
731
+ }
732
+ ```
733
+
734
+ #### `useProduct(id)`
735
+
736
+ ```tsx
737
+ import { useProduct } from "@bacano/sdk/react";
738
+
739
+ function ProductDetail({ productId }: { productId: string }) {
740
+ const { data: product, loading } = useProduct(productId);
741
+
742
+ if (loading || !product) return <p>Loading...</p>;
743
+
744
+ return (
745
+ <div>
746
+ <h1>{product.name}</h1>
747
+ <p>{product.description}</p>
748
+ {product.attributes.map((attr) => (
749
+ <span key={attr.id}>
750
+ {attr.name}: {attr.value}
751
+ </span>
752
+ ))}
753
+ </div>
754
+ );
755
+ }
756
+ ```
757
+
758
+ #### `useCategories()` and `useBrands()`
759
+
760
+ ```tsx
761
+ import { useCategories, useBrands } from "@bacano/sdk/react";
762
+
763
+ function Sidebar() {
764
+ const { data: categories } = useCategories();
765
+ const { data: brands } = useBrands();
766
+
767
+ return (
768
+ <nav>
769
+ <h4>Categories</h4>
770
+ {categories?.map((cat) => (
771
+ <div key={cat.name}>
772
+ <strong>{cat.name}</strong>
773
+ <ul>
774
+ {cat.subcategories.map((sub) => (
775
+ <li key={sub.id}>{sub.name}</li>
776
+ ))}
777
+ </ul>
778
+ </div>
779
+ ))}
780
+
781
+ <h4>Brands</h4>
782
+ {brands?.map((b) => (
783
+ <span key={b.id}>{b.name}</span>
784
+ ))}
785
+ </nav>
786
+ );
787
+ }
788
+ ```
789
+
790
+ #### `useProductFilters(opts?)`
791
+
792
+ ```tsx
793
+ import { useProducts, useProductFilters } from "@bacano/sdk/react";
794
+ import { useState } from "react";
795
+
796
+ function FilteredProductList() {
797
+ const [category, setCategory] = useState("CLOTHES");
798
+ const [selectedColors, setSelectedColors] = useState<string[]>([]);
799
+ const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
800
+ const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string[]>>(
801
+ {},
802
+ );
803
+
804
+ // Fetch available filter options for the selected category
805
+ const { data: facets } = useProductFilters({ categoryName: category });
806
+
807
+ // Fetch products with active filters applied
808
+ const { data: products, loading } = useProducts({
809
+ categoryName: category,
810
+ colors: selectedColors.length > 0 ? selectedColors : undefined,
811
+ minPrice: priceRange[0],
812
+ maxPrice: priceRange[1],
813
+ attributes:
814
+ Object.keys(selectedAttrs).length > 0 ? selectedAttrs : undefined,
815
+ inStock: true,
816
+ sortBy: "price_asc",
817
+ });
818
+
819
+ return (
820
+ <div style={{ display: "flex", gap: "2rem" }}>
821
+ {/* Filter sidebar built from facets */}
822
+ <aside>
823
+ <h4>
824
+ Price: ${facets?.priceRange.min} – ${facets?.priceRange.max}
825
+ </h4>
826
+
827
+ <h4>Colors</h4>
828
+ {facets?.colors.map((c) => (
829
+ <label key={c.value}>
830
+ <input
831
+ type="checkbox"
832
+ onChange={() => {
833
+ /* toggle color */
834
+ }}
835
+ />
836
+ {c.value} ({c.count})
837
+ </label>
838
+ ))}
839
+
840
+ <h4>Dynamic Attributes</h4>
841
+ {facets?.attributes.map((attr) => (
842
+ <div key={attr.id}>
843
+ <strong>{attr.name}</strong>
844
+ {attr.values.map((v) => (
845
+ <label key={v.value}>
846
+ <input
847
+ type="checkbox"
848
+ onChange={() => {
849
+ /* toggle attr */
850
+ }}
851
+ />
852
+ {v.value} ({v.count})
853
+ </label>
854
+ ))}
855
+ </div>
856
+ ))}
857
+ </aside>
858
+
859
+ {/* Product grid */}
860
+ <main>
861
+ {products?.map((p) => (
862
+ <div key={p.id}>{p.name}</div>
863
+ ))}
864
+ </main>
865
+ </div>
866
+ );
867
+ }
868
+ ```
869
+
870
+ #### `useSearchSuggestions(opts?)`
871
+
872
+ ```tsx
873
+ import { useSearchSuggestions } from "@bacano/sdk/react";
874
+ import { useState } from "react";
875
+
876
+ function SearchBar() {
877
+ const [query, setQuery] = useState("");
878
+ const { data: suggestions, loading } = useSearchSuggestions({
879
+ query,
880
+ limit: 8,
881
+ });
882
+
883
+ return (
884
+ <div>
885
+ <input
886
+ type="text"
887
+ placeholder="Search products..."
888
+ value={query}
889
+ onChange={(e) => setQuery(e.target.value)}
890
+ />
891
+ {query && suggestions && suggestions.length > 0 && (
892
+ <ul>
893
+ {suggestions.map((s, i) => (
894
+ <li key={i}>
895
+ <span>{s.type === "brand" ? "🏷️" : "📦"}</span>
896
+ {s.text}
897
+ </li>
898
+ ))}
899
+ </ul>
900
+ )}
901
+ </div>
902
+ );
903
+ }
904
+ ```
905
+
906
+ #### `useAvailability(variantIds)`
907
+
908
+ ```tsx
909
+ import { useAvailability } from "@bacano/sdk/react";
910
+
911
+ function StockBadge({ variantIds }: { variantIds: string[] }) {
912
+ const { data: availability } = useAvailability(variantIds);
913
+
914
+ return (
915
+ <>
916
+ {availability?.map((a) => (
917
+ <span
918
+ key={a.productVariantId}
919
+ style={{ color: a.inStock ? "green" : "red" }}
920
+ >
921
+ {a.label}
922
+ </span>
923
+ ))}
924
+ </>
925
+ );
926
+ }
927
+ ```
928
+
929
+ #### `useCart()`
930
+
931
+ ```tsx
932
+ import { useCart } from "@bacano/sdk/react";
933
+
934
+ function CartPage() {
935
+ const { cart, loading, startCheckout, updateItem, removeItem } = useCart();
936
+
937
+ async function handleStartCheckout() {
938
+ await startCheckout({
939
+ contact: { email: "buyer@example.com", phone: "+573001112233" },
940
+ items: [{ productVariantId: "variant-uuid", quantity: 1 }],
941
+ });
942
+ }
943
+
944
+ if (loading) return <p>Loading cart...</p>;
945
+ if (!cart || cart.items.length === 0) return <p>Cart is empty</p>;
946
+
947
+ return (
948
+ <div>
949
+ {cart.items.map((item) => (
950
+ <div key={item.id}>
951
+ <span>{item.productVariant.product.name}</span>
952
+ <span>SKU: {item.productVariant.sku}</span>
953
+ <span>Qty: {item.quantity}</span>
954
+ <button onClick={() => updateItem(item.id, item.quantity + 1)}>
955
+ +
956
+ </button>
957
+ <button onClick={() => updateItem(item.id, item.quantity - 1)}>
958
+ -
959
+ </button>
960
+ <button onClick={() => removeItem(item.id)}>Remove</button>
961
+ </div>
962
+ ))}
963
+ <p>Total: ${cart.totals.total}</p>
964
+ <button onClick={handleStartCheckout}>Start checkout</button>
965
+ </div>
966
+ );
967
+ }
968
+ ```
969
+
970
+ #### `useCheckout()`
971
+
972
+ ```tsx
973
+ import { useCheckout, useCart } from "@bacano/sdk/react";
974
+
975
+ function CheckoutButton() {
976
+ const { cart } = useCart();
977
+ const { checkout, loading, error } = useCheckout();
978
+
979
+ async function handleCheckout() {
980
+ if (!cart) return;
981
+ try {
982
+ const result = await checkout({
983
+ contact: { email: "buyer@example.com", phone: "+573001112233" },
984
+ });
985
+ alert(`Order ${result.orderNumber} placed!`);
986
+ } catch (err) {
987
+ // error state is also updated automatically
988
+ }
989
+ }
990
+
991
+ return (
992
+ <div>
993
+ <button onClick={handleCheckout} disabled={loading || !cart}>
994
+ {loading ? "Processing..." : "Place Order"}
995
+ </button>
996
+ {error && <p style={{ color: "red" }}>{error.message}</p>}
997
+ </div>
998
+ );
999
+ }
1000
+ ```
1001
+
1002
+ #### `useOrders(opts?)` and `useOrder(id)`
1003
+
1004
+ ```tsx
1005
+ import { useOrders } from "@bacano/sdk/react";
1006
+
1007
+ function OrderHistory() {
1008
+ const { data: orders, loading } = useOrders({ limit: 10 });
1009
+
1010
+ if (loading) return <p>Loading...</p>;
1011
+
1012
+ return (
1013
+ <ul>
1014
+ {orders?.map((order) => (
1015
+ <li key={order.id}>
1016
+ {order.orderNumber} — {order.status} — ${order.total}
1017
+ </li>
1018
+ ))}
1019
+ </ul>
1020
+ );
1021
+ }
1022
+ ```
1023
+
1024
+ #### `useProfile()`
1025
+
1026
+ ```tsx
1027
+ import { useProfile } from "@bacano/sdk/react";
1028
+
1029
+ function ProfilePage() {
1030
+ const { profile, loading, updateProfile } = useProfile();
1031
+
1032
+ if (loading || !profile) return <p>Loading...</p>;
1033
+
1034
+ return (
1035
+ <form
1036
+ onSubmit={async (e) => {
1037
+ e.preventDefault();
1038
+ const formData = new FormData(e.currentTarget);
1039
+ await updateProfile({
1040
+ name: formData.get("name") as string,
1041
+ address: formData.get("address") as string,
1042
+ });
1043
+ }}
1044
+ >
1045
+ <input name="name" defaultValue={profile.name ?? ""} />
1046
+ <input name="address" defaultValue={profile.address ?? ""} />
1047
+ <button type="submit">Save</button>
1048
+ </form>
1049
+ );
1050
+ }
1051
+ ```
1052
+
1053
+ ### Hooks Summary
1054
+
1055
+ | Hook | Returns | Auth Required |
1056
+ | ----------------------------- | --------------------------------------------------------------------------------------- | ------------- |
1057
+ | `useBacano()` | `BacanoClient` | No |
1058
+ | `useBacanoState()` | `{ client, loading, error }` | No |
1059
+ | `useProducts(opts?)` | `{ data, loading, error, refetch }` | No |
1060
+ | `useProduct(id)` | `{ data, loading, error, refetch }` | No |
1061
+ | `useCategories()` | `{ data, loading, error, refetch }` | No |
1062
+ | `useBrands()` | `{ data, loading, error, refetch }` | No |
1063
+ | `useProductFilters(opts?)` | `{ data, loading, error, refetch }` | No |
1064
+ | `useSearchSuggestions(opts?)` | `{ data, loading, error, refetch }` | No |
1065
+ | `useAvailability(ids)` | `{ data, loading, error, refetch }` | No |
1066
+ | `useCart()` | `{ cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem }` | No |
1067
+ | `useCheckout()` | `{ checkout, loading, error }` | No |
1068
+ | `useOrders(opts?)` | `{ data, loading, error, refetch }` | Yes |
1069
+ | `useOrder(id)` | `{ data, loading, error, refetch }` | Yes |
1070
+ | `useProfile()` | `{ profile, loading, error, updateProfile }` | Yes |
1071
+
1072
+ ---
1073
+
1074
+ ## Advanced: Bring Your Own GraphQL Client
1075
+
1076
+ If you use Apollo, urql, TanStack Query, or any other GraphQL client, you can import the raw typed documents instead:
1077
+
1078
+ ```typescript
1079
+ import { documents, createAuthHeaders } from "@bacano/sdk/graphql";
1080
+ import type { Product, Cart } from "@bacano/sdk/graphql";
1081
+ ```
1082
+
1083
+ ### With Apollo Client
1084
+
1085
+ ```tsx
1086
+ import { documents } from "@bacano/sdk/graphql";
1087
+ import { useQuery, useMutation } from "@apollo/client";
1088
+
1089
+ function ProductList({ companyId }: { companyId: string }) {
1090
+ const { data, loading } = useQuery(documents.GET_PRODUCTS, {
1091
+ variables: { companyId, limit: 20 },
1092
+ });
1093
+
1094
+ // data.products is the raw Hasura response (snake_case)
1095
+ }
1096
+
1097
+ function StartCheckoutButton({ variantId }: { variantId: string }) {
1098
+ const [startCheckout] = useMutation(documents.START_GUEST_CHECKOUT);
1099
+
1100
+ return (
1101
+ <button
1102
+ onClick={() =>
1103
+ startCheckout({
1104
+ variables: {
1105
+ websiteId: "website-uuid",
1106
+ contact: { email: "buyer@example.com" },
1107
+ items: [{ product_variant_id: variantId, quantity: 1 }],
1108
+ },
1109
+ })
1110
+ }
1111
+ >
1112
+ Start checkout
1113
+ </button>
1114
+ );
1115
+ }
1116
+ ```
1117
+
1118
+ ### With fetch (SSR / Server Components)
1119
+
1120
+ ```typescript
1121
+ import { documents, createAuthHeaders } from "@bacano/sdk/graphql";
1122
+
1123
+ // In a Next.js Server Component or API route:
1124
+ async function getProducts(apiUrl: string, companyId: string) {
1125
+ const res = await fetch(`${apiUrl}/api/v1/website/graphql`, {
1126
+ method: "POST",
1127
+ headers: {
1128
+ "Content-Type": "application/json",
1129
+ // No auth headers = public role (anonymous catalog browsing)
1130
+ },
1131
+ body: JSON.stringify({
1132
+ query: documents.GET_PRODUCTS,
1133
+ variables: { companyId, limit: 20 },
1134
+ }),
1135
+ });
1136
+
1137
+ const { data } = await res.json();
1138
+ return data.products;
1139
+ }
1140
+
1141
+ // Authenticated request:
1142
+ async function getOrders(
1143
+ apiUrl: string,
1144
+ getToken: () => Promise<string | null>,
1145
+ ) {
1146
+ const token = await getToken();
1147
+ const res = await fetch(`${apiUrl}/api/v1/website/graphql`, {
1148
+ method: "POST",
1149
+ headers: {
1150
+ "Content-Type": "application/json",
1151
+ ...createAuthHeaders(token),
1152
+ },
1153
+ body: JSON.stringify({
1154
+ query: documents.GET_ORDERS,
1155
+ variables: { limit: 10 },
1156
+ }),
1157
+ });
1158
+
1159
+ const { data } = await res.json();
1160
+ return data.orders;
1161
+ }
1162
+ ```
1163
+
1164
+ ### Available Documents
1165
+
1166
+ | Document | Type | Description |
1167
+ | ---------------------------- | -------- | ---------------------------------------------------------------------- |
1168
+ | `RESOLVE_WEBSITE_BY_SLUG` | Query | Resolve website by slug |
1169
+ | `GET_PRODUCTS` | Query | List products with variants, prices, media, attributes |
1170
+ | `GET_PRODUCT_BY_PK` | Query | Single product with full details |
1171
+ | `GET_CATEGORIES` | Query | All categories with subcategories |
1172
+ | `GET_BRANDS` | Query | Company brands |
1173
+ | `CHECK_AVAILABILITY` | Query | Stock availability (boolean + label) |
1174
+ | `GET_PRODUCT_FILTERS` | Query | Universal facets (price range, brands with counts, categories/colors) |
1175
+ | `GET_ATTRIBUTE_FACETS` | Query | Category-specific attribute facets with value counts |
1176
+ | `GET_SEARCH_SUGGESTIONS` | Query | Lightweight product/brand name search for autocomplete |
1177
+ | `CREATE_CART` | Mutation | Compatibility cart creation |
1178
+ | `GET_CART` | Mutation | Current `WEB + PENDING + UNPAID` checkout with line items |
1179
+ | `START_GUEST_CHECKOUT` | Mutation | Create/update `WEB + PENDING + UNPAID` from contact + local cart items |
1180
+ | `SET_CART_ITEM` | Mutation | Update item quantity after checkout has started |
1181
+ | `REMOVE_CART_ITEM` | Mutation | Remove item after checkout has started |
1182
+ | `GET_SHIPPING_POLICY` | Mutation | Public website free-shipping policy without a cart |
1183
+ | `GET_FREE_SHIPPING_PROGRESS` | Mutation | Server-calculated progress from current cart items |
1184
+ | `GET_SHIPPING_QUOTES` | Mutation | Final shipping quotes for an address |
1185
+ | `CHECKOUT` | Mutation | Confirm checkout, assign client and order number |
1186
+ | `GET_ORDERS` | Query | Order history |
1187
+ | `GET_ORDER_BY_PK` | Query | Single order with items |
1188
+ | `GET_PROFILE` | Query | Customer profile |
1189
+ | `UPDATE_PROFILE` | Mutation | Update customer profile |
1190
+
1191
+ ---
1192
+
1193
+ ## SSR / Next.js Server-Side Usage
1194
+
1195
+ For server-side rendering or Server Components, use `memory` cart storage and
1196
+ provide a server-side Clerk token provider when the request is authenticated:
1197
+
1198
+ ```typescript
1199
+ import { createBacanoClient } from "@bacano/sdk";
1200
+
1201
+ // In getServerSideProps or a Server Component:
1202
+ const client = createBacanoClient({
1203
+ apiUrl: process.env.BACANO_API_URL!,
1204
+ websiteSlug: "my-store",
1205
+ tokenStorage: "memory",
1206
+ getAccessToken: async () => tokenFromClerkServerAuth ?? null,
1207
+ });
1208
+
1209
+ await client.init();
1210
+ const orders = await client.orders.list();
1211
+ ```
1212
+
1213
+ Protected server-side requests must also reach Bacano with the storefront's
1214
+ exact configured `Origin`. Browser requests add it automatically; server
1215
+ proxies must preserve it.
1216
+
1217
+ ---
1218
+
1219
+ ## Error Handling
1220
+
1221
+ The SDK throws typed errors you can catch and handle:
1222
+
1223
+ ```typescript
1224
+ import {
1225
+ WebsiteError,
1226
+ AuthError,
1227
+ CheckoutError,
1228
+ NotInitializedError,
1229
+ GraphQLRequestError,
1230
+ } from "@bacano/sdk";
1231
+
1232
+ try {
1233
+ await client.checkout.submit({
1234
+ contact: { email: "buyer@example.com", phone: "+573001112233" },
1235
+ });
1236
+ } catch (err) {
1237
+ if (err instanceof CheckoutError) {
1238
+ // Checkout validation failed
1239
+ console.log("Validation errors:", err.errors);
1240
+ // e.g., ["Insufficient stock for SKU-001", "Product XYZ is no longer available"]
1241
+ // ORDER_CONFIRMATION_RETRYABLE can be retried with checkout.submit;
1242
+ // the SDK intentionally preserves the current cart session.
1243
+ } else if (err instanceof AuthError) {
1244
+ // Not logged in or session expired
1245
+ console.log("Please log in again");
1246
+ } else if (err instanceof GraphQLRequestError) {
1247
+ // Hasura returned GraphQL errors
1248
+ console.log("GraphQL errors:", err.graphqlErrors);
1249
+ } else if (err instanceof NotInitializedError) {
1250
+ // client.init() was not called
1251
+ } else if (err instanceof WebsiteError) {
1252
+ // Other SDK error (network, rate limit, etc.)
1253
+ console.log("Error code:", err.code);
1254
+ // Codes: NETWORK_ERROR, RATE_LIMITED, INVALID_RESPONSE,
1255
+ // WEBSITE_NOT_FOUND, NO_CHAIN, etc.
1256
+ }
1257
+ }
1258
+ ```
1259
+
1260
+ Common setup errors:
1261
+
1262
+ | Code / message | Meaning | Fix |
1263
+ | ------------------- | ---------------------------------------------- | -------------------------------------------------------------- |
1264
+ | `WEBSITE_NOT_FOUND` | No active website matches slug/domain. | Check slug/domain and active status in Bacano. |
1265
+ | `NO_CHAIN` | Website has no branch. | Select a branch for the website in the Bacano platform. |
1266
+ | `NETWORK_ERROR` | Bacano API is unreachable. | Check `apiUrl`, deployment and network access. |
1267
+ | No images | Bacano Storage proxy works but Next blocks it. | Add Bacano API host to `next.config.ts` image remote patterns. |
1268
+
1269
+ ---
1270
+
1271
+ ## Type Reference
1272
+
1273
+ All types are exported from the main entry point:
1274
+
1275
+ ```typescript
1276
+ import type {
1277
+ // Config
1278
+ BacanoClientConfig,
1279
+ BacanoClientContext,
1280
+
1281
+ // Buyer auth integration
1282
+ AccessTokenProvider,
1283
+ WebsitePublicConfig,
1284
+ GetWebsitePublicConfigOptions,
1285
+
1286
+ // Catalog
1287
+ Product,
1288
+ ProductVariant,
1289
+ ProductPrice, // CLIENT, SALE, WHOLESALE or WHOLESALE_2 when enabled
1290
+ ProductVariantPricing, // storefront-ready current/regular/sale/wholesale values
1291
+ ProductMedia, // { fileId, order, url, mediaKind, mimeType, fileName, alt }
1292
+ ProductMediaKind, // 'image' | 'video' | 'document' | 'file'
1293
+ ContentMedia, // media item shape stored inside contentJson fields
1294
+ ProductAttribute,
1295
+ VariantAttribute,
1296
+ ProductCategory,
1297
+ ProductSubcategory,
1298
+ Brand,
1299
+ ProductAvailability, // { productVariantId, inStock, label }
1300
+ GetProductsOptions, // { categoryName, search, colors, tags, minPrice, maxPrice, inStock, attributes, sortBy, ... }
1301
+ GetProductFiltersOptions,
1302
+ ProductFilters, // { categories, brands, colors, priceRange, attributes }
1303
+ AttributeFilter, // { id, name, values: [{ value, count }] }
1304
+ ProductSortBy, // 'name_asc' | 'name_desc' | 'price_asc' | 'price_desc' | 'newest'
1305
+ SearchSuggestion, // { type: 'product' | 'brand' | 'category', text, id? }
1306
+ GetSearchSuggestionsOptions,
1307
+
1308
+ // Cart
1309
+ StartGuestCheckoutInput,
1310
+ WebsiteGuestContact,
1311
+ WebsiteCheckoutItem,
1312
+ Cart, // { id, cartToken, items, totals: { subtotal, tax, discount, total } }
1313
+ CartItem,
1314
+
1315
+ // Checkout
1316
+ CheckoutResult, // { success, orderId, orderNumber, errors }
1317
+
1318
+ // Orders
1319
+ Order, // { id, orderNumber, status, items, totals, ... }
1320
+ OrderItem,
1321
+ GetOrdersOptions,
1322
+
1323
+ // Profile
1324
+ CustomerProfile,
1325
+ CustomerProfileUpdate,
1326
+ } from "@bacano/sdk";
1327
+ ```
1328
+
1329
+ ---
1330
+
1331
+ ## Architecture Notes
1332
+
1333
+ - **All website traffic goes through Bacano API** — GraphQL, Auth and Storage are proxied behind the public `apiUrl`
1334
+ - **Two Hasura roles**: `public` for anonymous catalog and guest checkout, `customer` for optional account/order history features
1335
+ - **Only enabled price tiers are exposed** — `CLIENT` is always available;
1336
+ `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
1340
+ - **Raw inventory is abstracted** — `checkAvailability()` returns boolean + label, never raw quantities
1341
+ - **Company-scoped** — all queries are automatically filtered by `company_id` from the website context
1342
+ - **Local cart first** — the web keeps cart state locally until the visitor leaves email or phone
1343
+ - **Started checkouts = pending unpaid web orders** — `sales_channel: WEB`, `status: PENDING`, `payment_status: UNPAID`, with `website_contact_snapshot`
1344
+ - **No stock/accounting on abandoned checkout** — abandoned checkout records do not reserve stock, deduct inventory, create payments or post accounting
1345
+ - **Token auto-refresh** — JWT is refreshed 30 seconds before expiry
1346
+ - **Website resolution is cached** — `init()` resolves once, subsequent calls use the cached context
1347
+
1348
+ ---
1349
+
1350
+ ## Support
1351
+
1352
+ For access to the Bacano platform, website setup, or questions about this SDK,
1353
+ contact your Bacano representative.
1354
+
1355
+ ## License
1356
+
1357
+ MIT — see [`LICENSE`](./LICENSE).