@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/AUTHENTICATION.md +365 -0
- package/LICENSE +21 -0
- package/README.md +1357 -0
- package/client.d.ts +96 -0
- package/errors.d.ts +24 -0
- package/graphql.cjs +2057 -0
- package/graphql.d.ts +17 -0
- package/graphql.js +2034 -0
- package/index.cjs +4956 -0
- package/index.d.ts +8 -0
- package/index.js +4933 -0
- package/media.d.ts +9 -0
- package/modules/auth.d.ts +9 -0
- package/modules/availability.d.ts +6 -0
- package/modules/cart-session.d.ts +13 -0
- package/modules/cart.d.ts +16 -0
- package/modules/catalog.d.ts +24 -0
- package/modules/checkout.d.ts +42 -0
- package/modules/content.d.ts +9 -0
- package/modules/orders.d.ts +9 -0
- package/modules/profile.d.ts +8 -0
- package/modules/website.d.ts +3 -0
- package/package.json +52 -0
- package/react/context.d.ts +7 -0
- package/react/hooks.d.ts +74 -0
- package/react/provider.d.ts +7 -0
- package/react.cjs +5232 -0
- package/react.d.ts +23 -0
- package/react.js +5209 -0
- package/transport/documents.d.ts +83 -0
- package/transport/graphql.d.ts +7 -0
- package/types.d.ts +991 -0
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).
|