brainerce 1.23.12 → 1.23.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,17 @@ Official SDK for building e-commerce storefronts with **Brainerce Platform**.
4
4
 
5
5
  This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to Brainerce's unified commerce API.
6
6
 
7
- > **AI Agents / Vibe Coders:** Use the MCP server for AI-powered store building: `npx @brainerce/mcp-server`. It provides docs, code templates, and live store capabilities to any AI tool (Cursor, Lovable, Claude Code).
7
+ > **AI Agents / Vibe Coders (Cursor, Lovable, Claude Code, VS Code):** Use the MCP server for AI-powered store building: `npx @brainerce/mcp-server`. It provides docs, code templates, and live store capabilities directly inside your IDE.
8
+ > Note: the MCP server runs inside your IDE — it is not available in chat-only tools like Google AI Studio or ChatGPT.
9
+
10
+ ## Two SDK modes — choose the right one
11
+
12
+ | Mode | Config key | Use for | Where to run |
13
+ | -------------- | ------------------------ | ---------------------------------- | ----------------------------------- |
14
+ | **Storefront** | `salesChannelId: 'vc_*'` | Building the customer-facing store | Browser / client-side |
15
+ | **Admin** | `apiKey: 'brainerce_*'` | Managing products, team, settings | Server only — never in browser code |
16
+
17
+ > **Building a storefront?** You only need your **Sales Channel ID** (`vc_*`) from the Brainerce dashboard under **Sales Channels**. No API key needed. API keys are a server-side admin secret.
8
18
 
9
19
  ## Installation
10
20
 
@@ -18,27 +28,218 @@ yarn add brainerce
18
28
 
19
29
  ---
20
30
 
31
+ ## What You Must Build
32
+
33
+ Every Brainerce storefront must include **all mandatory features** below. Features auto-hide when the underlying capability is disabled, so build them all anyway — they'll appear the moment the store owner enables them.
34
+
35
+ | Feature | SDK entry point | Mandatory |
36
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------- |
37
+ | Product list with search, filter, pagination | `client.getProducts()`, `client.getSearchSuggestions(query)` | ✅ |
38
+ | Product detail with variant picker, stock, price | `client.getProductBySlug()` + helpers | ✅ |
39
+ | Buyer customization fields (engraving, uploads, select) | `product.customizationFields`, `client.uploadCustomizationFile()` | ✅ |
40
+ | Cart (add, update, remove, coupon, totals) | `client.addToCart()`, `getCartTotals(cart)` | ✅ |
41
+ | Inventory reservation countdown | Cart expiry timestamp from `client.getCart()` | ✅ |
42
+ | Full checkout end-to-end with payment | `setShippingAddress → selectShippingMethod → getPaymentProviders → pay → handlePaymentSuccess → waitForOrder` | ✅ |
43
+ | Order confirmation (clear cart + wait for real order) | `client.handlePaymentSuccess()`, `client.waitForOrder()` | ✅ |
44
+ | Register + email verification flow | `client.registerCustomer()`, `client.verifyEmail()` | ✅ |
45
+ | Login + verification branch | `client.loginCustomer()` | ✅ |
46
+ | Forgot / reset password | `client.forgotPassword()`, `client.resetPassword()` | ✅ |
47
+ | OAuth sign-in buttons + callback handler | `client.getAvailableOAuthProviders()` | ✅ |
48
+ | Account area (profile + order history) | `client.getMyProfile()`, `client.getMyOrders()` | ✅ |
49
+ | Global header: cart count + search autocomplete | `client.getCart()`, `client.getSearchSuggestions(query)` | ✅ |
50
+ | Discount banners + product badges | `client.getDiscountBanners()`, `client.getProductDiscountBadge(productId)` | ✅ |
51
+ | Multi-language + RTL (when i18n enabled) | `client.setLocale()` | conditional |
52
+
53
+ ---
54
+
55
+ ## Critical Rules
56
+
57
+ Violating any of these causes production incidents or broken orders. Read them before writing SDK code.
58
+
59
+ ### SDK usage
60
+
61
+ - ALWAYS call SDK client methods. Never reconstruct REST URLs or call `fetch` directly.
62
+ - NEVER invent SDK method names. If it's not in this README or in `get-sdk-docs`, it doesn't exist.
63
+ - NEVER hardcode product data, categories, or store copy — Brainerce is the database.
64
+ - NEVER use `submitGuestOrder()` or `createOrder()` — they bypass payment and produce unpaid orders.
65
+ - ALWAYS use SDK helpers (`getCartTotals`, `formatPrice`, `getProductPriceInfo`, `getCartItemImage`, `getCartItemName`, `getVariantPrice`, `getStockStatus`, `getDescriptionContent`) instead of reading raw fields.
66
+
67
+ ### State management
68
+
69
+ - The SDK manages cart, checkout, and session state. Do NOT duplicate it in your own Redux/context.
70
+ - Product lists, categories, and inventory counts are NOT client state — fetch on demand.
71
+ - Discount rules and coupon validity are evaluated server-side. Never re-implement them client-side.
72
+
73
+ ### Authentication
74
+
75
+ - ALWAYS handle the `requiresVerification` flag in `registerCustomer` and `loginCustomer` responses. If true, route to the verify-email step BEFORE treating the user as logged in.
76
+ - ALWAYS build the verify-email, forgot-password, and reset-password flows even when the store currently has email verification disabled. They auto-hide when unused.
77
+ - ALWAYS build OAuth button placeholders and a callback handler even when no OAuth provider is configured.
78
+ - NEVER silently swallow auth errors. Render the specific error (invalid credentials, expired token, rate limited).
79
+
80
+ ### Checkout & orders
81
+
82
+ - The checkout sequence is strict: `setShippingAddress` → pick a shipping rate → `getPaymentProviders` → provider payment → `handlePaymentSuccess` → `waitForOrder`. Never skip or reorder.
83
+ - ALWAYS call `handlePaymentSuccess(checkoutId)` on the confirmation page — clears the cart so users don't see stale items.
84
+ - ALWAYS call `waitForOrder(checkoutId)` to poll for the real order before showing an order number. The payment callback may return before the order record exists.
85
+ - NEVER use the checkout total as the cart total — they diverge (tax, shipping, discounts). Display `checkout.lineItems` on the summary, not `cart.items`.
86
+ - The reservation timer is a hard guarantee — display the countdown from the cart and let the SDK handle expiry.
87
+
88
+ ### Token handling
89
+
90
+ - Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally.
91
+ - NEVER put the admin API key (`brainerce_*`) in client code. It is a server-only secret.
92
+ - OAuth callbacks arrive with the token in URL params. Extract and apply the token before redirecting.
93
+
94
+ ### i18n
95
+
96
+ - NEVER hardcode currency, locale, or language strings — read them from `getStoreInfo()`.
97
+ - NEVER format prices with `toFixed(2)` — use `formatPrice()` from the SDK.
98
+ - When i18n is enabled, call `client.setLocale(locale)` at app init and include a language switcher. For RTL locales (`he`, `ar`), set `<html dir="rtl">` — do NOT add `flex-row-reverse` on top.
99
+
100
+ ### Type safety
101
+
102
+ - NEVER use `as any` or `as unknown as`. Fix the type, don't hide it.
103
+ - NEVER write your own copies of SDK types (Cart, Product, Order). Import from `'brainerce'`.
104
+ - All prices are **STRINGS** — always `parseFloat()` before math or comparisons.
105
+ - `CartItem` / `CheckoutLineItem` = **NESTED** (`item.product.name`, `item.unitPrice`). `OrderItem` = **FLAT** (`item.name`, `item.price`). Not interchangeable.
106
+ - `Cart` has no `.total` field — call `getCartTotals(cart)`.
107
+
108
+ ---
109
+
110
+ ## Business Flows
111
+
112
+ These sequences are non-negotiable. The order of SDK calls matters.
113
+
114
+ ### Checkout flow
115
+
116
+ 1. Collect customer email, billing address, shipping address (`line1`, `line2`, `city`, `region`, `postalCode`, `country`). `email` is required.
117
+ 2. Submit address to get shipping rates:
118
+ ```ts
119
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
120
+ email,
121
+ firstName,
122
+ lastName,
123
+ line1,
124
+ city,
125
+ region,
126
+ postalCode,
127
+ country,
128
+ });
129
+ // rates = available shipping rates; checkout = updated checkout object
130
+ ```
131
+ 3. Let the customer pick a rate, then persist it:
132
+ ```ts
133
+ await client.selectShippingMethod(checkoutId, rateId);
134
+ ```
135
+ 4. Fetch available payment providers:
136
+ ```ts
137
+ const providers = await client.getPaymentProviders();
138
+ ```
139
+ Each provider has a `renderType` — `'sdk-widget'` (Stripe, PayPal, Grow), `'iframe'` (Cardcom), `'redirect'`, `'sandbox'`. Branch on `renderType`, never on provider name.
140
+ 5. Confirm payment using the provider's flow (Stripe Elements `stripe.confirmCardPayment`, PayPal button, redirect, etc.).
141
+ 6. On the confirmation page, **always call both**:
142
+ ```ts
143
+ await client.handlePaymentSuccess(checkoutId); // clears cart
144
+ const order = await client.waitForOrder(checkoutId); // polls until order exists
145
+ ```
146
+ 7. Display `checkout.lineItems` (not `cart.items`) on the order summary.
147
+
148
+ ### Registration flow
149
+
150
+ 1. Collect email, password, first name, last name.
151
+ 2. Call `registerCustomer`:
152
+ ```ts
153
+ const result = await client.registerCustomer({ email, password, firstName, lastName });
154
+ ```
155
+ 3. Branch on `result.requiresVerification`:
156
+ - `true` → store token temporarily, route to verify-email UI (do NOT set token yet)
157
+ - `false` → `client.setCustomerToken(result.token)`, route to account
158
+ 4. On verify-email: collect 6-digit code → `client.verifyEmail(code)`. Offer resend via `client.resendVerificationEmail()`.
159
+ 5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, route to account.
160
+
161
+ > Build the verify-email step even if verification is currently disabled — it auto-hides.
162
+
163
+ ### Login flow
164
+
165
+ 1. Collect email + password.
166
+ 2. Call `loginCustomer`:
167
+ ```ts
168
+ const result = await client.loginCustomer(email, password);
169
+ ```
170
+ 3. Branch on `result.requiresVerification`:
171
+ - `true` → route to verify-email
172
+ - `false` → `client.setCustomerToken(result.token)`, route to previous page or account
173
+ 4. Always offer OAuth buttons from `client.getAvailableOAuthProviders()` — render the region even when empty, it auto-populates when a provider is enabled.
174
+ 5. Render specific errors (bad credentials, rate limited, disabled) — never swallow them.
175
+
176
+ ### Order confirmation flow
177
+
178
+ 1. Read `checkoutId` from URL or session.
179
+ 2. `await client.handlePaymentSuccess(checkoutId)` — mandatory, clears cart so purchased items don't show on next visit.
180
+ 3. `const order = await client.waitForOrder(checkoutId)` — polls until the webhook writes the order.
181
+ 4. Show a spinner during step 3 (webhook may lag). On timeout: show "we're still processing, check your email" with a link to order history — the order WILL appear there.
182
+ 5. On success: render order number and line items from the returned `order` object.
183
+
184
+ ### Password reset flow
185
+
186
+ **Forgot password step:** collect email → `client.forgotPassword(email)` → always show a generic success message (prevents account enumeration, regardless of whether the account exists).
187
+
188
+ **Reset password step:** read `token` from URL query param → if missing, show error with link back → collect new password → `client.resetPassword(token, newPassword)` → on success route to login → on expired/invalid token show the specific error with link back.
189
+
190
+ ### OAuth flow
191
+
192
+ 1. Get the list of available provider names:
193
+ ```ts
194
+ const { providers } = await client.getAvailableOAuthProviders();
195
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects)
196
+ ```
197
+ 2. For each provider, get the authorization URL:
198
+ ```ts
199
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
200
+ redirectUrl: `${window.location.origin}/auth/callback`,
201
+ });
202
+ window.location.href = authorizationUrl; // full-page redirect, NOT a popup
203
+ ```
204
+ 3. On callback, the URL contains `token` + `oauth_success` (or `oauth_error`) query params:
205
+ ```ts
206
+ const token = new URLSearchParams(location.search).get('token');
207
+ if (token) client.setCustomerToken(token); // then redirect to account
208
+ ```
209
+ 4. On `oauth_error` query param: redirect to login with an error message.
210
+
211
+ > Build the OAuth button region AND the callback handler even when no providers are configured.
212
+
213
+ ### Inventory reservation flow
214
+
215
+ - Display the countdown from `cart.reservation?.expiresAt` — refresh once per second (`reservation` is optional; only present when a reservation strategy is active).
216
+ - On expiry: call `client.getCart()` to refresh. Items whose reservation expired are flagged server-side.
217
+ - On the checkout page: if reservation has expired, block payment and show "your cart has expired" with a link back to cart.
218
+ - Do NOT implement your own timer logic — the SDK is the source of truth.
219
+
220
+ ---
221
+
21
222
  ## Quick Reference - Helper Functions
22
223
 
23
224
  The SDK exports these utility functions for common UI tasks:
24
225
 
25
- | Function | Purpose | Example |
26
- | ---------------------------------------------- | -------------------------------------- | ------------------------------------------------------ |
27
- | `formatPrice(amount, { currency?, locale? })` | Format prices for display | `formatPrice("99.99", { currency: 'USD' })` → `$99.99` |
28
- | `getPriceDisplay(amount, currency?, locale?)` | Alias for `formatPrice` | Same as above |
29
- | `getDescriptionContent(product)` | Get product description (HTML or text) | `getDescriptionContent(product)` |
30
- | `isHtmlDescription(product)` | Check if description is HTML | `isHtmlDescription(product)` → `true/false` |
31
- | `getStockStatus(inventory)` | Get human-readable stock status | `getStockStatus(inventory)` → `"In Stock"` |
32
- | `getProductPrice(product)` | Get effective price (handles sales) | `getProductPrice(product)` → `29.99` |
33
- | `getProductPriceInfo(product)` | Get price + sale info + discount % | `{ price, isOnSale, discountPercent }` |
34
- | `getVariantPrice(variant, basePrice)` | Get variant price with fallback | `getVariantPrice(variant, '29.99')` → `34.99` |
35
- | `getCartTotals(cart, shippingPrice?)` | Calculate cart subtotal/discount/total | `{ subtotal, discount, shipping, total }` |
36
- | `getCartItemName(item)` | Get name from nested cart item | `getCartItemName(item)` → `"Blue T-Shirt"` |
37
- | `getCartItemImage(item)` | Get image URL from cart item | `getCartItemImage(item)` → `"https://..."` |
38
- | `getVariantOptions(variant)` | Get variant attributes as array | `[{ name: "Color", value: "Red" }]` |
39
- | `isCouponApplicableToProduct(coupon, product)` | Check if coupon applies | `isCouponApplicableToProduct(coupon, product)` |
40
- | `isAllowedPaymentUrl(url, options?)` | Validate a payment URL host | `isAllowedPaymentUrl(intent.clientSecret)` → `true` |
41
- | `safePaymentRedirect(url, options?)` | Validate then `window.location.href` | `safePaymentRedirect(intent.clientSecret)` |
226
+ | Function | Purpose | Example |
227
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
228
+ | `formatPrice(amount, { currency?, locale? })` | Format prices for display | `formatPrice("99.99", { currency: 'USD' })` → `$99.99` |
229
+ | `getPriceDisplay(amount, currency?, locale?)` | Alias for `formatPrice` | Same as above |
230
+ | `getDescriptionContent(product)` | Get product description (HTML or text) | `getDescriptionContent(product)` |
231
+ | `isHtmlDescription(product)` | Check if description is HTML | `isHtmlDescription(product)` → `true/false` |
232
+ | `getStockStatus(inventory)` | Get human-readable stock status | `getStockStatus(inventory)` → `"In Stock"` |
233
+ | `getProductPrice(product)` | Get effective price (handles sales) | `getProductPrice(product)` → `29.99` |
234
+ | `getProductPriceInfo(product)` | Get price + sale info + discount % (falls back to `priceMin` when `basePrice=0` on VARIABLE) | `{ price, isOnSale, discountPercent }` |
235
+ | `getVariantPrice(variant, basePrice)` | Get variant price with fallback | `getVariantPrice(variant, '29.99')` → `34.99` |
236
+ | `getCartTotals(cart, shippingPrice?)` | Calculate cart subtotal/discount/total | `{ subtotal, discount, shipping, total }` |
237
+ | `getCartItemName(item)` | Get name from nested cart item | `getCartItemName(item)` → `"Blue T-Shirt"` |
238
+ | `getCartItemImage(item)` | Get image URL from cart item | `getCartItemImage(item)` → `"https://..."` |
239
+ | `getVariantOptions(variant)` | Get variant attributes as array | `[{ name: "Color", value: "Red" }]` |
240
+ | `isCouponApplicableToProduct(coupon, product)` | Check if coupon applies | `isCouponApplicableToProduct(coupon, product)` |
241
+ | `isAllowedPaymentUrl(url, options?)` | Validate a payment URL host | `isAllowedPaymentUrl(intent.clientSecret)` → `true` |
242
+ | `safePaymentRedirect(url, options?)` | Validate then `window.location.href` | `safePaymentRedirect(intent.clientSecret)` |
42
243
 
43
244
  ```typescript
44
245
  import {
@@ -110,9 +311,9 @@ const paypalProvider = providers.find(p => p.provider === 'paypal');
110
311
  ```typescript
111
312
  import { BrainerceClient } from 'brainerce';
112
313
 
113
- // Initialize with your Connection ID
314
+ // salesChannelId (vc_*) is all you need — no API key for storefronts
114
315
  const client = new BrainerceClient({
115
- connectionId: 'vc_YOUR_CONNECTION_ID',
316
+ salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
116
317
  });
117
318
 
118
319
  // Fetch products
@@ -649,7 +850,7 @@ Create a file `lib/brainerce.ts`:
649
850
  import { BrainerceClient } from 'brainerce';
650
851
 
651
852
  export const client = new BrainerceClient({
652
- connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from Brainerce
853
+ salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
653
854
  });
654
855
 
655
856
  // ----- Cart Helpers -----
@@ -1121,6 +1322,9 @@ interface Product {
1121
1322
  sku: string;
1122
1323
  basePrice: number;
1123
1324
  salePrice?: number | null;
1325
+ priceMin?: string | null; // Lowest variant price (VARIABLE products only)
1326
+ priceMax?: string | null; // Highest variant price (VARIABLE products only)
1327
+ priceVaries?: boolean; // true when range should be shown ("₪49 – ₪199")
1124
1328
  status: 'active' | 'draft';
1125
1329
  type: 'SIMPLE' | 'VARIABLE';
1126
1330
  images?: ProductImage[];
@@ -1471,16 +1675,30 @@ console.log(`${count} items in cart`);
1471
1675
 
1472
1676
  #### Apply Coupon
1473
1677
 
1678
+ **On the cart page** (before checkout session is created):
1679
+
1474
1680
  ```typescript
1475
1681
  const cart = await client.smartGetCart();
1476
1682
  const updated = await client.applyCoupon(cart.id, 'SAVE20');
1477
1683
  console.log(updated.discountAmount); // "10.00"
1478
1684
  console.log(updated.couponCode); // "SAVE20"
1479
1685
 
1480
- // Remove coupon
1481
1686
  await client.removeCoupon(cart.id);
1482
1687
  ```
1483
1688
 
1689
+ **On the checkout page** (checkout session already exists — preferred):
1690
+
1691
+ ```typescript
1692
+ // applyCheckoutCoupon applies to cart AND updates checkout totals atomically
1693
+ const checkout = await client.applyCheckoutCoupon(checkoutId, 'SAVE20');
1694
+ console.log(checkout.discountAmount); // "10.00"
1695
+ console.log(checkout.total); // updated total
1696
+
1697
+ await client.removeCheckoutCoupon(checkoutId);
1698
+ ```
1699
+
1700
+ > **Important:** if a checkout session already exists, always use `applyCheckoutCoupon(checkoutId, code)` — not `applyCoupon(cartId, code)`. Applying to the cart after checkout is created does not update the checkout total, so payment will charge the original amount.
1701
+
1484
1702
  #### Cart Totals
1485
1703
 
1486
1704
  ```typescript
@@ -2332,12 +2550,13 @@ function PaymentForm({ checkoutId }: { checkoutId: string }) {
2332
2550
  }
2333
2551
  ```
2334
2552
 
2335
- #### Complete Order After Payment: `completeGuestCheckout()`
2553
+ #### Complete Order After Payment: `completeGuestCheckout()` (legacy untracked flow only)
2554
+
2555
+ > **Context:** This section describes the **legacy untracked guest checkout** flow where the client must explicitly create the order. In the modern tracked flow (`startGuestCheckout()` → webhook creates the order), you use `handlePaymentSuccess()` + `waitForOrder()` instead (see [Business Flows — Checkout](#checkout-flow) above).
2336
2556
 
2337
- **CRITICAL:** After payment succeeds, you MUST call `completeGuestCheckout()` to create the order on the server and clear the cart.
2557
+ **CRITICAL (untracked flow only):** After payment succeeds, you MUST call `completeGuestCheckout()` to create the order on the server.
2338
2558
 
2339
- > **WARNING:** Do NOT use `handlePaymentSuccess()` - it only clears cart state locally
2340
- > and does NOT send the order to the server. Your customer will pay but no order will be created!
2559
+ > **WARNING (untracked flow only):** Do NOT use `handlePaymentSuccess()` here it only clears cart state locally and does NOT create the order on the server. The tracked flow uses `handlePaymentSuccess` + `waitForOrder`; the untracked flow uses `completeGuestCheckout` directly.
2341
2560
 
2342
2561
  ```typescript
2343
2562
  // On your /checkout/success page:
@@ -3014,12 +3233,14 @@ console.log(store.language); // 'en', 'he', etc.
3014
3233
 
3015
3234
  ## Admin API Reference
3016
3235
 
3017
- The Admin API requires an API key (`apiKey`) and provides full access to store configuration and management features.
3236
+ > **Server-side only.** The `apiKey` (`brainerce_*`) is a privileged secret — NEVER put it in browser code, client bundles, or any code that ships to the user's machine. It belongs in an environment variable on your server. For building the customer-facing storefront, use `salesChannelId` instead (see [Quick Start](#quick-start)).
3237
+
3238
+ The Admin API provides full access to store configuration and management features (taxonomy, shipping, team, metafields, etc.) and runs only in server-side code.
3018
3239
 
3019
3240
  ```typescript
3020
- // Initialize in Admin mode
3021
- const client = new BrainerceClient({
3022
- apiKey: process.env.BRAINERCE_API_KEY, // 'brainerce_*' prefix
3241
+ // Only in server-side code (Node.js, Edge functions, API routes — never in the browser)
3242
+ const admin = new BrainerceClient({
3243
+ apiKey: process.env.BRAINERCE_API_KEY, // 'brainerce_*' prefix — keep this secret
3023
3244
  });
3024
3245
  ```
3025
3246
 
@@ -3047,7 +3268,7 @@ const attributes = await client.listAttributes();
3047
3268
  // Create a color swatch attribute
3048
3269
  const colorAttr = await client.createAttribute({
3049
3270
  name: 'Color',
3050
- displayType: 'COLOR_SWATCH', // Options: 'DEFAULT' | 'COLOR_SWATCH' | 'IMAGE_SWATCH'
3271
+ displayType: 'COLOR_SWATCH', // Options: 'DEFAULT' | 'COLOR_SWATCH' | 'IMAGE_SWATCH' | 'MIXED_SWATCH'
3051
3272
  source: 'GLOBAL',
3052
3273
  });
3053
3274
 
@@ -3081,6 +3302,25 @@ await client.createAttributeOption(materialAttr.id, {
3081
3302
  source: 'GLOBAL',
3082
3303
  });
3083
3304
 
3305
+ // Mixed swatch attribute — each option independently uses color or image (image takes priority)
3306
+ const finishAttr = await client.createAttribute({
3307
+ name: 'Finish',
3308
+ displayType: 'MIXED_SWATCH',
3309
+ source: 'GLOBAL',
3310
+ });
3311
+ await client.createAttributeOption(finishAttr.id, {
3312
+ name: 'Gold',
3313
+ value: 'gold',
3314
+ swatchColor: '#FFD700',
3315
+ source: 'GLOBAL',
3316
+ });
3317
+ await client.createAttributeOption(finishAttr.id, {
3318
+ name: 'Marble',
3319
+ value: 'marble',
3320
+ swatchImageUrl: 'https://example.com/marble.jpg',
3321
+ source: 'GLOBAL',
3322
+ });
3323
+
3084
3324
  // Default attribute (text buttons/dropdown)
3085
3325
  const sizeAttr = await client.createAttribute({
3086
3326
  name: 'Size',
package/dist/index.d.mts CHANGED
@@ -218,6 +218,12 @@ interface Product {
218
218
  salePrice?: string | null;
219
219
  /** Cost price as string. Use parseFloat() for calculations. */
220
220
  costPrice?: string | null;
221
+ /** Lowest effective variant price (VARIABLE products only). String Decimal, e.g. "19.99". */
222
+ priceMin?: string | null;
223
+ /** Highest effective variant price (VARIABLE products only). String Decimal, e.g. "99.99". */
224
+ priceMax?: string | null;
225
+ /** True when variant prices differ (VARIABLE products only). Use for "From X – Y" display. */
226
+ priceVaries?: boolean;
221
227
  /** Product status (active, draft). Always returned by backend. */
222
228
  status: string;
223
229
  type: 'SIMPLE' | 'VARIABLE';
@@ -605,7 +611,7 @@ declare function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice
605
611
  * }
606
612
  * ```
607
613
  */
608
- declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount'> | null | undefined): {
614
+ declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'> | null | undefined): {
609
615
  price: number;
610
616
  originalPrice: number;
611
617
  isOnSale: boolean;
@@ -1134,6 +1140,12 @@ interface Coupon {
1134
1140
  * Returns objects with id and name from backend.
1135
1141
  */
1136
1142
  applicableProducts?: EntityRef[] | null;
1143
+ /** Products explicitly excluded from this coupon. */
1144
+ excludedProducts?: EntityRef[] | null;
1145
+ /** Categories this coupon applies to (inclusion list). */
1146
+ applicableCategories?: EntityRef[] | null;
1147
+ /** Categories explicitly excluded from this coupon. */
1148
+ excludedCategories?: EntityRef[] | null;
1137
1149
  combinesWithOther: boolean;
1138
1150
  /** Whether coupon needs sync to platforms */
1139
1151
  needsSync?: boolean;
@@ -1200,6 +1212,12 @@ interface CreateCouponDto {
1200
1212
  conditions?: Record<string, unknown>;
1201
1213
  /** Product IDs this coupon applies to */
1202
1214
  applicableProducts?: string[];
1215
+ /** Product IDs explicitly excluded from this coupon */
1216
+ excludedProducts?: string[];
1217
+ /** Category IDs this coupon applies to */
1218
+ applicableCategories?: string[];
1219
+ /** Category IDs explicitly excluded from this coupon */
1220
+ excludedCategories?: string[];
1203
1221
  combinesWithOther?: boolean;
1204
1222
  /** Platforms to publish this coupon to */
1205
1223
  platforms?: ConnectorPlatform[];
@@ -1222,6 +1240,9 @@ interface UpdateCouponDto {
1222
1240
  maximumDiscount?: number | string | null;
1223
1241
  conditions?: Record<string, unknown> | null;
1224
1242
  applicableProducts?: string[] | null;
1243
+ excludedProducts?: string[] | null;
1244
+ applicableCategories?: string[] | null;
1245
+ excludedCategories?: string[] | null;
1225
1246
  combinesWithOther?: boolean;
1226
1247
  }
1227
1248
  /**
@@ -4354,6 +4375,8 @@ interface Modifier {
4354
4375
  isDefault: boolean;
4355
4376
  /** Sold-out toggle: `false` = out of stock, hidden as selectable in storefronts. */
4356
4377
  available: boolean;
4378
+ /** When `true`, this modifier is never allocated as a free selection — customers always pay its `priceDelta` even when `freeQuantity > 0` on the group. */
4379
+ excludeFromFree?: boolean;
4357
4380
  /**
4358
4381
  * Nested-combo target. When present, picking this modifier opens the
4359
4382
  * referenced product's own modifier groups (depth ≤ 3).
@@ -4524,6 +4547,8 @@ interface CreateModifierInput {
4524
4547
  position?: number;
4525
4548
  isDefault?: boolean;
4526
4549
  available?: boolean;
4550
+ /** When `true`, never allocated as a free selection — customers always pay this modifier's `priceDelta`. */
4551
+ excludeFromFree?: boolean;
4527
4552
  referencedProductId?: string;
4528
4553
  }
4529
4554
  interface UpdateModifierInput extends Partial<CreateModifierInput> {
@@ -6393,6 +6418,35 @@ declare class BrainerceClient {
6393
6418
  * ```
6394
6419
  */
6395
6420
  getCheckout(checkoutId: string): Promise<Checkout>;
6421
+ /**
6422
+ * Apply a coupon code to an existing checkout session.
6423
+ *
6424
+ * The coupon is validated, applied to the linked cart, and the checkout
6425
+ * totals (discountAmount, totalAmount) are updated atomically.
6426
+ * Use this instead of `applyCoupon()` when the checkout session is already
6427
+ * created — which is the common case when the customer enters a code on the
6428
+ * checkout page.
6429
+ *
6430
+ * @example
6431
+ * ```typescript
6432
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
6433
+ * console.log('New total:', checkout.total);
6434
+ * console.log('Discount:', checkout.discountAmount);
6435
+ * ```
6436
+ */
6437
+ applyCheckoutCoupon(checkoutId: string, code: string): Promise<Checkout>;
6438
+ /**
6439
+ * Remove a coupon from an existing checkout session.
6440
+ *
6441
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
6442
+ *
6443
+ * @example
6444
+ * ```typescript
6445
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
6446
+ * console.log('New total:', checkout.total);
6447
+ * ```
6448
+ */
6449
+ removeCheckoutCoupon(checkoutId: string): Promise<Checkout>;
6396
6450
  /**
6397
6451
  * Set customer information on checkout
6398
6452
  *
package/dist/index.d.ts CHANGED
@@ -218,6 +218,12 @@ interface Product {
218
218
  salePrice?: string | null;
219
219
  /** Cost price as string. Use parseFloat() for calculations. */
220
220
  costPrice?: string | null;
221
+ /** Lowest effective variant price (VARIABLE products only). String Decimal, e.g. "19.99". */
222
+ priceMin?: string | null;
223
+ /** Highest effective variant price (VARIABLE products only). String Decimal, e.g. "99.99". */
224
+ priceMax?: string | null;
225
+ /** True when variant prices differ (VARIABLE products only). Use for "From X – Y" display. */
226
+ priceVaries?: boolean;
221
227
  /** Product status (active, draft). Always returned by backend. */
222
228
  status: string;
223
229
  type: 'SIMPLE' | 'VARIABLE';
@@ -605,7 +611,7 @@ declare function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice
605
611
  * }
606
612
  * ```
607
613
  */
608
- declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount'> | null | undefined): {
614
+ declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'> | null | undefined): {
609
615
  price: number;
610
616
  originalPrice: number;
611
617
  isOnSale: boolean;
@@ -1134,6 +1140,12 @@ interface Coupon {
1134
1140
  * Returns objects with id and name from backend.
1135
1141
  */
1136
1142
  applicableProducts?: EntityRef[] | null;
1143
+ /** Products explicitly excluded from this coupon. */
1144
+ excludedProducts?: EntityRef[] | null;
1145
+ /** Categories this coupon applies to (inclusion list). */
1146
+ applicableCategories?: EntityRef[] | null;
1147
+ /** Categories explicitly excluded from this coupon. */
1148
+ excludedCategories?: EntityRef[] | null;
1137
1149
  combinesWithOther: boolean;
1138
1150
  /** Whether coupon needs sync to platforms */
1139
1151
  needsSync?: boolean;
@@ -1200,6 +1212,12 @@ interface CreateCouponDto {
1200
1212
  conditions?: Record<string, unknown>;
1201
1213
  /** Product IDs this coupon applies to */
1202
1214
  applicableProducts?: string[];
1215
+ /** Product IDs explicitly excluded from this coupon */
1216
+ excludedProducts?: string[];
1217
+ /** Category IDs this coupon applies to */
1218
+ applicableCategories?: string[];
1219
+ /** Category IDs explicitly excluded from this coupon */
1220
+ excludedCategories?: string[];
1203
1221
  combinesWithOther?: boolean;
1204
1222
  /** Platforms to publish this coupon to */
1205
1223
  platforms?: ConnectorPlatform[];
@@ -1222,6 +1240,9 @@ interface UpdateCouponDto {
1222
1240
  maximumDiscount?: number | string | null;
1223
1241
  conditions?: Record<string, unknown> | null;
1224
1242
  applicableProducts?: string[] | null;
1243
+ excludedProducts?: string[] | null;
1244
+ applicableCategories?: string[] | null;
1245
+ excludedCategories?: string[] | null;
1225
1246
  combinesWithOther?: boolean;
1226
1247
  }
1227
1248
  /**
@@ -4354,6 +4375,8 @@ interface Modifier {
4354
4375
  isDefault: boolean;
4355
4376
  /** Sold-out toggle: `false` = out of stock, hidden as selectable in storefronts. */
4356
4377
  available: boolean;
4378
+ /** When `true`, this modifier is never allocated as a free selection — customers always pay its `priceDelta` even when `freeQuantity > 0` on the group. */
4379
+ excludeFromFree?: boolean;
4357
4380
  /**
4358
4381
  * Nested-combo target. When present, picking this modifier opens the
4359
4382
  * referenced product's own modifier groups (depth ≤ 3).
@@ -4524,6 +4547,8 @@ interface CreateModifierInput {
4524
4547
  position?: number;
4525
4548
  isDefault?: boolean;
4526
4549
  available?: boolean;
4550
+ /** When `true`, never allocated as a free selection — customers always pay this modifier's `priceDelta`. */
4551
+ excludeFromFree?: boolean;
4527
4552
  referencedProductId?: string;
4528
4553
  }
4529
4554
  interface UpdateModifierInput extends Partial<CreateModifierInput> {
@@ -6393,6 +6418,35 @@ declare class BrainerceClient {
6393
6418
  * ```
6394
6419
  */
6395
6420
  getCheckout(checkoutId: string): Promise<Checkout>;
6421
+ /**
6422
+ * Apply a coupon code to an existing checkout session.
6423
+ *
6424
+ * The coupon is validated, applied to the linked cart, and the checkout
6425
+ * totals (discountAmount, totalAmount) are updated atomically.
6426
+ * Use this instead of `applyCoupon()` when the checkout session is already
6427
+ * created — which is the common case when the customer enters a code on the
6428
+ * checkout page.
6429
+ *
6430
+ * @example
6431
+ * ```typescript
6432
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
6433
+ * console.log('New total:', checkout.total);
6434
+ * console.log('Discount:', checkout.discountAmount);
6435
+ * ```
6436
+ */
6437
+ applyCheckoutCoupon(checkoutId: string, code: string): Promise<Checkout>;
6438
+ /**
6439
+ * Remove a coupon from an existing checkout session.
6440
+ *
6441
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
6442
+ *
6443
+ * @example
6444
+ * ```typescript
6445
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
6446
+ * console.log('New total:', checkout.total);
6447
+ * ```
6448
+ */
6449
+ removeCheckoutCoupon(checkoutId: string): Promise<Checkout>;
6396
6450
  /**
6397
6451
  * Set customer information on checkout
6398
6452
  *
package/dist/index.js CHANGED
@@ -262,9 +262,7 @@ var BrainerceClient = class {
262
262
  this.LOCAL_CART_KEY = "brainerce_cart";
263
263
  const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
264
264
  if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
265
- throw new Error(
266
- "BrainerceClient: either salesChannelId, apiKey, or storeId is required"
267
- );
265
+ throw new Error("BrainerceClient: either salesChannelId, apiKey, or storeId is required");
268
266
  }
269
267
  if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
270
268
  console.warn('BrainerceClient: apiKey should start with "brainerce_"');
@@ -3575,6 +3573,69 @@ var BrainerceClient = class {
3575
3573
  "checkout"
3576
3574
  );
3577
3575
  }
3576
+ /**
3577
+ * Apply a coupon code to an existing checkout session.
3578
+ *
3579
+ * The coupon is validated, applied to the linked cart, and the checkout
3580
+ * totals (discountAmount, totalAmount) are updated atomically.
3581
+ * Use this instead of `applyCoupon()` when the checkout session is already
3582
+ * created — which is the common case when the customer enters a code on the
3583
+ * checkout page.
3584
+ *
3585
+ * @example
3586
+ * ```typescript
3587
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
3588
+ * console.log('New total:', checkout.total);
3589
+ * console.log('Discount:', checkout.discountAmount);
3590
+ * ```
3591
+ */
3592
+ async applyCheckoutCoupon(checkoutId, code) {
3593
+ if (this.isVibeCodedMode()) {
3594
+ return this.withGuards(
3595
+ this.vibeCodedRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3596
+ "checkout"
3597
+ );
3598
+ }
3599
+ if (this.storeId && !this.apiKey) {
3600
+ return this.withGuards(
3601
+ this.storefrontRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3602
+ "checkout"
3603
+ );
3604
+ }
3605
+ return this.withGuards(
3606
+ this.adminRequest("POST", `/api/v1/checkout/${checkoutId}/coupon`, { code }),
3607
+ "checkout"
3608
+ );
3609
+ }
3610
+ /**
3611
+ * Remove a coupon from an existing checkout session.
3612
+ *
3613
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
3614
+ *
3615
+ * @example
3616
+ * ```typescript
3617
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
3618
+ * console.log('New total:', checkout.total);
3619
+ * ```
3620
+ */
3621
+ async removeCheckoutCoupon(checkoutId) {
3622
+ if (this.isVibeCodedMode()) {
3623
+ return this.withGuards(
3624
+ this.vibeCodedRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3625
+ "checkout"
3626
+ );
3627
+ }
3628
+ if (this.storeId && !this.apiKey) {
3629
+ return this.withGuards(
3630
+ this.storefrontRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3631
+ "checkout"
3632
+ );
3633
+ }
3634
+ return this.withGuards(
3635
+ this.adminRequest("DELETE", `/api/v1/checkout/${checkoutId}/coupon`),
3636
+ "checkout"
3637
+ );
3638
+ }
3578
3639
  /**
3579
3640
  * Set customer information on checkout
3580
3641
  *
@@ -6972,6 +7033,7 @@ function getProductPriceInfo(product) {
6972
7033
  if (!product) {
6973
7034
  return { price: 0, originalPrice: 0, isOnSale: false, discountAmount: 0, discountPercent: 0 };
6974
7035
  }
7036
+ const resolvedBasePrice = parseFloat(product.basePrice) === 0 && product.priceMin ? product.priceMin : product.basePrice;
6975
7037
  if (product.discount) {
6976
7038
  const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
6977
7039
  const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
@@ -6985,7 +7047,7 @@ function getProductPriceInfo(product) {
6985
7047
  discountPercent: rulePercent
6986
7048
  };
6987
7049
  }
6988
- const basePrice = parseFloat(product.basePrice) || 0;
7050
+ const basePrice = parseFloat(resolvedBasePrice) || 0;
6989
7051
  const salePrice = product.salePrice ? parseFloat(product.salePrice) : null;
6990
7052
  const isOnSale = salePrice !== null && salePrice < basePrice;
6991
7053
  const effectivePrice = isOnSale ? salePrice : basePrice;
package/dist/index.mjs CHANGED
@@ -199,9 +199,7 @@ var BrainerceClient = class {
199
199
  this.LOCAL_CART_KEY = "brainerce_cart";
200
200
  const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
201
201
  if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
202
- throw new Error(
203
- "BrainerceClient: either salesChannelId, apiKey, or storeId is required"
204
- );
202
+ throw new Error("BrainerceClient: either salesChannelId, apiKey, or storeId is required");
205
203
  }
206
204
  if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
207
205
  console.warn('BrainerceClient: apiKey should start with "brainerce_"');
@@ -3512,6 +3510,69 @@ var BrainerceClient = class {
3512
3510
  "checkout"
3513
3511
  );
3514
3512
  }
3513
+ /**
3514
+ * Apply a coupon code to an existing checkout session.
3515
+ *
3516
+ * The coupon is validated, applied to the linked cart, and the checkout
3517
+ * totals (discountAmount, totalAmount) are updated atomically.
3518
+ * Use this instead of `applyCoupon()` when the checkout session is already
3519
+ * created — which is the common case when the customer enters a code on the
3520
+ * checkout page.
3521
+ *
3522
+ * @example
3523
+ * ```typescript
3524
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
3525
+ * console.log('New total:', checkout.total);
3526
+ * console.log('Discount:', checkout.discountAmount);
3527
+ * ```
3528
+ */
3529
+ async applyCheckoutCoupon(checkoutId, code) {
3530
+ if (this.isVibeCodedMode()) {
3531
+ return this.withGuards(
3532
+ this.vibeCodedRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3533
+ "checkout"
3534
+ );
3535
+ }
3536
+ if (this.storeId && !this.apiKey) {
3537
+ return this.withGuards(
3538
+ this.storefrontRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3539
+ "checkout"
3540
+ );
3541
+ }
3542
+ return this.withGuards(
3543
+ this.adminRequest("POST", `/api/v1/checkout/${checkoutId}/coupon`, { code }),
3544
+ "checkout"
3545
+ );
3546
+ }
3547
+ /**
3548
+ * Remove a coupon from an existing checkout session.
3549
+ *
3550
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
3551
+ *
3552
+ * @example
3553
+ * ```typescript
3554
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
3555
+ * console.log('New total:', checkout.total);
3556
+ * ```
3557
+ */
3558
+ async removeCheckoutCoupon(checkoutId) {
3559
+ if (this.isVibeCodedMode()) {
3560
+ return this.withGuards(
3561
+ this.vibeCodedRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3562
+ "checkout"
3563
+ );
3564
+ }
3565
+ if (this.storeId && !this.apiKey) {
3566
+ return this.withGuards(
3567
+ this.storefrontRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3568
+ "checkout"
3569
+ );
3570
+ }
3571
+ return this.withGuards(
3572
+ this.adminRequest("DELETE", `/api/v1/checkout/${checkoutId}/coupon`),
3573
+ "checkout"
3574
+ );
3575
+ }
3515
3576
  /**
3516
3577
  * Set customer information on checkout
3517
3578
  *
@@ -6909,6 +6970,7 @@ function getProductPriceInfo(product) {
6909
6970
  if (!product) {
6910
6971
  return { price: 0, originalPrice: 0, isOnSale: false, discountAmount: 0, discountPercent: 0 };
6911
6972
  }
6973
+ const resolvedBasePrice = parseFloat(product.basePrice) === 0 && product.priceMin ? product.priceMin : product.basePrice;
6912
6974
  if (product.discount) {
6913
6975
  const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
6914
6976
  const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
@@ -6922,7 +6984,7 @@ function getProductPriceInfo(product) {
6922
6984
  discountPercent: rulePercent
6923
6985
  };
6924
6986
  }
6925
- const basePrice = parseFloat(product.basePrice) || 0;
6987
+ const basePrice = parseFloat(resolvedBasePrice) || 0;
6926
6988
  const salePrice = product.salePrice ? parseFloat(product.salePrice) : null;
6927
6989
  const isOnSale = salePrice !== null && salePrice < basePrice;
6928
6990
  const effectivePrice = isOnSale ? salePrice : basePrice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.23.12",
3
+ "version": "1.23.14",
4
4
  "description": "Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",