brainerce 1.23.13 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,219 @@ 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
+ | Product reviews on PDP + JSON-LD aggregateRating | `client.listProductReviews(id)`, `client.submitProductReview(id, …)` | ✅ |
52
+ | Multi-language + RTL (when i18n enabled) | `client.setLocale()` | conditional |
53
+
54
+ ---
55
+
56
+ ## Critical Rules
57
+
58
+ Violating any of these causes production incidents or broken orders. Read them before writing SDK code.
59
+
60
+ ### SDK usage
61
+
62
+ - ALWAYS call SDK client methods. Never reconstruct REST URLs or call `fetch` directly.
63
+ - NEVER invent SDK method names. If it's not in this README or in `get-sdk-docs`, it doesn't exist.
64
+ - NEVER hardcode product data, categories, or store copy — Brainerce is the database.
65
+ - NEVER use `submitGuestOrder()` or `createOrder()` — they bypass payment and produce unpaid orders.
66
+ - ALWAYS use SDK helpers (`getCartTotals`, `formatPrice`, `getProductPriceInfo`, `getCartItemImage`, `getCartItemName`, `getVariantPrice`, `getStockStatus`, `getDescriptionContent`) instead of reading raw fields.
67
+
68
+ ### State management
69
+
70
+ - The SDK manages cart, checkout, and session state. Do NOT duplicate it in your own Redux/context.
71
+ - Product lists, categories, and inventory counts are NOT client state — fetch on demand.
72
+ - Discount rules and coupon validity are evaluated server-side. Never re-implement them client-side.
73
+
74
+ ### Authentication
75
+
76
+ - 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.
77
+ - 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.
78
+ - ALWAYS build OAuth button placeholders and a callback handler even when no OAuth provider is configured.
79
+ - NEVER silently swallow auth errors. Render the specific error (invalid credentials, expired token, rate limited).
80
+
81
+ ### Checkout & orders
82
+
83
+ - The checkout sequence is strict: `setShippingAddress` → pick a shipping rate → `getPaymentProviders` → provider payment → `handlePaymentSuccess` → `waitForOrder`. Never skip or reorder.
84
+ - ALWAYS call `handlePaymentSuccess(checkoutId)` on the confirmation page — clears the cart so users don't see stale items.
85
+ - 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.
86
+ - NEVER use the checkout total as the cart total — they diverge (tax, shipping, discounts). Display `checkout.lineItems` on the summary, not `cart.items`.
87
+ - The reservation timer is a hard guarantee — display the countdown from the cart and let the SDK handle expiry.
88
+
89
+ ### Token handling
90
+
91
+ - Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally.
92
+ - NEVER put the admin API key (`brainerce_*`) in client code. It is a server-only secret.
93
+ - OAuth callbacks arrive with the token in URL params. Extract and apply the token before redirecting.
94
+
95
+ ### i18n
96
+
97
+ - NEVER hardcode currency, locale, or language strings — read them from `getStoreInfo()`.
98
+ - NEVER format prices with `toFixed(2)` — use `formatPrice()` from the SDK.
99
+ - 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.
100
+
101
+ ### Type safety
102
+
103
+ - NEVER use `as any` or `as unknown as`. Fix the type, don't hide it.
104
+ - NEVER write your own copies of SDK types (Cart, Product, Order). Import from `'brainerce'`.
105
+ - All prices are **STRINGS** — always `parseFloat()` before math or comparisons.
106
+ - `CartItem` / `CheckoutLineItem` = **NESTED** (`item.product.name`, `item.unitPrice`). `OrderItem` = **FLAT** (`item.name`, `item.price`). Not interchangeable.
107
+ - `Cart` has no `.total` field — call `getCartTotals(cart)`.
108
+
109
+ ---
110
+
111
+ ## Business Flows
112
+
113
+ These sequences are non-negotiable. The order of SDK calls matters.
114
+
115
+ ### Checkout flow
116
+
117
+ 1. Collect customer email, billing address, shipping address (`line1`, `line2`, `city`, `region`, `postalCode`, `country`). `email` is required.
118
+ 2. Submit address to get shipping rates:
119
+ ```ts
120
+ const { checkout, rates } = await client.setShippingAddress(checkoutId, {
121
+ email,
122
+ firstName,
123
+ lastName,
124
+ line1,
125
+ city,
126
+ region,
127
+ postalCode,
128
+ country,
129
+ });
130
+ // rates = available shipping rates; checkout = updated checkout object
131
+ ```
132
+ 3. Let the customer pick a rate, then persist it:
133
+ ```ts
134
+ await client.selectShippingMethod(checkoutId, rateId);
135
+ ```
136
+ 4. Fetch available payment providers:
137
+ ```ts
138
+ const providers = await client.getPaymentProviders();
139
+ ```
140
+ Each provider has a `renderType` — `'sdk-widget'` (Stripe, PayPal, Grow), `'iframe'` (Cardcom), `'redirect'`, `'sandbox'`. Branch on `renderType`, never on provider name.
141
+ 5. Confirm payment using the provider's flow (Stripe Elements `stripe.confirmCardPayment`, PayPal button, redirect, etc.).
142
+ 6. On the confirmation page, **always call both**:
143
+ ```ts
144
+ await client.handlePaymentSuccess(checkoutId); // clears cart
145
+ const order = await client.waitForOrder(checkoutId); // polls until order exists
146
+ ```
147
+ 7. Display `checkout.lineItems` (not `cart.items`) on the order summary.
148
+
149
+ ### Registration flow
150
+
151
+ 1. Collect email, password, first name, last name.
152
+ 2. Call `registerCustomer`:
153
+ ```ts
154
+ const result = await client.registerCustomer({ email, password, firstName, lastName });
155
+ ```
156
+ 3. Branch on `result.requiresVerification`:
157
+ - `true` → store token temporarily, route to verify-email UI (do NOT set token yet)
158
+ - `false` → `client.setCustomerToken(result.token)`, route to account
159
+ 4. On verify-email: collect 6-digit code → `client.verifyEmail(code)`. Offer resend via `client.resendVerificationEmail()`.
160
+ 5. After `verifyEmail` resolves: `client.setCustomerToken(result.token)`, route to account.
161
+
162
+ > Build the verify-email step even if verification is currently disabled — it auto-hides.
163
+
164
+ ### Login flow
165
+
166
+ 1. Collect email + password.
167
+ 2. Call `loginCustomer`:
168
+ ```ts
169
+ const result = await client.loginCustomer(email, password);
170
+ ```
171
+ 3. Branch on `result.requiresVerification`:
172
+ - `true` → route to verify-email
173
+ - `false` → `client.setCustomerToken(result.token)`, route to previous page or account
174
+ 4. Always offer OAuth buttons from `client.getAvailableOAuthProviders()` — render the region even when empty, it auto-populates when a provider is enabled.
175
+ 5. Render specific errors (bad credentials, rate limited, disabled) — never swallow them.
176
+
177
+ ### Order confirmation flow
178
+
179
+ 1. Read `checkoutId` from URL or session.
180
+ 2. `await client.handlePaymentSuccess(checkoutId)` — mandatory, clears cart so purchased items don't show on next visit.
181
+ 3. `const order = await client.waitForOrder(checkoutId)` — polls until the webhook writes the order.
182
+ 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.
183
+ 5. On success: render order number and line items from the returned `order` object.
184
+
185
+ ### Password reset flow
186
+
187
+ **Forgot password step:** collect email → `client.forgotPassword(email)` → always show a generic success message (prevents account enumeration, regardless of whether the account exists).
188
+
189
+ **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.
190
+
191
+ ### OAuth flow
192
+
193
+ 1. Get the list of available provider names:
194
+ ```ts
195
+ const { providers } = await client.getAvailableOAuthProviders();
196
+ // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects)
197
+ ```
198
+ 2. For each provider, get the authorization URL:
199
+ ```ts
200
+ const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
201
+ redirectUrl: `${window.location.origin}/auth/callback`,
202
+ });
203
+ window.location.href = authorizationUrl; // full-page redirect, NOT a popup
204
+ ```
205
+ 3. On callback, the URL contains `token` + `oauth_success` (or `oauth_error`) query params:
206
+ ```ts
207
+ const token = new URLSearchParams(location.search).get('token');
208
+ if (token) client.setCustomerToken(token); // then redirect to account
209
+ ```
210
+ 4. On `oauth_error` query param: redirect to login with an error message.
211
+
212
+ > Build the OAuth button region AND the callback handler even when no providers are configured.
213
+
214
+ ### Inventory reservation flow
215
+
216
+ - Display the countdown from `cart.reservation?.expiresAt` — refresh once per second (`reservation` is optional; only present when a reservation strategy is active).
217
+ - On expiry: call `client.getCart()` to refresh. Items whose reservation expired are flagged server-side.
218
+ - On the checkout page: if reservation has expired, block payment and show "your cart has expired" with a link back to cart.
219
+ - Do NOT implement your own timer logic — the SDK is the source of truth.
220
+
221
+ ---
222
+
21
223
  ## Quick Reference - Helper Functions
22
224
 
23
225
  The SDK exports these utility functions for common UI tasks:
24
226
 
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)` |
227
+ | Function | Purpose | Example |
228
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
229
+ | `formatPrice(amount, { currency?, locale? })` | Format prices for display | `formatPrice("99.99", { currency: 'USD' })` → `$99.99` |
230
+ | `getPriceDisplay(amount, currency?, locale?)` | Alias for `formatPrice` | Same as above |
231
+ | `getDescriptionContent(product)` | Get product description (HTML or text) | `getDescriptionContent(product)` |
232
+ | `isHtmlDescription(product)` | Check if description is HTML | `isHtmlDescription(product)` → `true/false` |
233
+ | `getStockStatus(inventory)` | Get human-readable stock status | `getStockStatus(inventory)` → `"In Stock"` |
234
+ | `getProductPrice(product)` | Get effective price (handles sales) | `getProductPrice(product)` → `29.99` |
235
+ | `getProductPriceInfo(product)` | Get price + sale info + discount % (falls back to `priceMin` when `basePrice=0` on VARIABLE) | `{ price, isOnSale, discountPercent }` |
236
+ | `getVariantPrice(variant, basePrice)` | Get variant price with fallback | `getVariantPrice(variant, '29.99')` → `34.99` |
237
+ | `getCartTotals(cart, shippingPrice?)` | Calculate cart subtotal/discount/total | `{ subtotal, discount, shipping, total }` |
238
+ | `getCartItemName(item)` | Get name from nested cart item | `getCartItemName(item)` → `"Blue T-Shirt"` |
239
+ | `getCartItemImage(item)` | Get image URL from cart item | `getCartItemImage(item)` → `"https://..."` |
240
+ | `getVariantOptions(variant)` | Get variant attributes as array | `[{ name: "Color", value: "Red" }]` |
241
+ | `isCouponApplicableToProduct(coupon, product)` | Check if coupon applies | `isCouponApplicableToProduct(coupon, product)` |
242
+ | `isAllowedPaymentUrl(url, options?)` | Validate a payment URL host | `isAllowedPaymentUrl(intent.clientSecret)` → `true` |
243
+ | `safePaymentRedirect(url, options?)` | Validate then `window.location.href` | `safePaymentRedirect(intent.clientSecret)` |
42
244
 
43
245
  ```typescript
44
246
  import {
@@ -110,9 +312,9 @@ const paypalProvider = providers.find(p => p.provider === 'paypal');
110
312
  ```typescript
111
313
  import { BrainerceClient } from 'brainerce';
112
314
 
113
- // Initialize with your Connection ID
315
+ // salesChannelId (vc_*) is all you need — no API key for storefronts
114
316
  const client = new BrainerceClient({
115
- connectionId: 'vc_YOUR_CONNECTION_ID',
317
+ salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
116
318
  });
117
319
 
118
320
  // Fetch products
@@ -649,7 +851,7 @@ Create a file `lib/brainerce.ts`:
649
851
  import { BrainerceClient } from 'brainerce';
650
852
 
651
853
  export const client = new BrainerceClient({
652
- connectionId: 'vc_YOUR_CONNECTION_ID', // Your Connection ID from Brainerce
854
+ salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
653
855
  });
654
856
 
655
857
  // ----- Cart Helpers -----
@@ -1121,6 +1323,9 @@ interface Product {
1121
1323
  sku: string;
1122
1324
  basePrice: number;
1123
1325
  salePrice?: number | null;
1326
+ priceMin?: string | null; // Lowest variant price (VARIABLE products only)
1327
+ priceMax?: string | null; // Highest variant price (VARIABLE products only)
1328
+ priceVaries?: boolean; // true when range should be shown ("₪49 – ₪199")
1124
1329
  status: 'active' | 'draft';
1125
1330
  type: 'SIMPLE' | 'VARIABLE';
1126
1331
  images?: ProductImage[];
@@ -1471,16 +1676,30 @@ console.log(`${count} items in cart`);
1471
1676
 
1472
1677
  #### Apply Coupon
1473
1678
 
1679
+ **On the cart page** (before checkout session is created):
1680
+
1474
1681
  ```typescript
1475
1682
  const cart = await client.smartGetCart();
1476
1683
  const updated = await client.applyCoupon(cart.id, 'SAVE20');
1477
1684
  console.log(updated.discountAmount); // "10.00"
1478
1685
  console.log(updated.couponCode); // "SAVE20"
1479
1686
 
1480
- // Remove coupon
1481
1687
  await client.removeCoupon(cart.id);
1482
1688
  ```
1483
1689
 
1690
+ **On the checkout page** (checkout session already exists — preferred):
1691
+
1692
+ ```typescript
1693
+ // applyCheckoutCoupon applies to cart AND updates checkout totals atomically
1694
+ const checkout = await client.applyCheckoutCoupon(checkoutId, 'SAVE20');
1695
+ console.log(checkout.discountAmount); // "10.00"
1696
+ console.log(checkout.total); // updated total
1697
+
1698
+ await client.removeCheckoutCoupon(checkoutId);
1699
+ ```
1700
+
1701
+ > **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.
1702
+
1484
1703
  #### Cart Totals
1485
1704
 
1486
1705
  ```typescript
@@ -2332,12 +2551,13 @@ function PaymentForm({ checkoutId }: { checkoutId: string }) {
2332
2551
  }
2333
2552
  ```
2334
2553
 
2335
- #### Complete Order After Payment: `completeGuestCheckout()`
2554
+ #### Complete Order After Payment: `completeGuestCheckout()` (legacy untracked flow only)
2555
+
2556
+ > **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
2557
 
2337
- **CRITICAL:** After payment succeeds, you MUST call `completeGuestCheckout()` to create the order on the server and clear the cart.
2558
+ **CRITICAL (untracked flow only):** After payment succeeds, you MUST call `completeGuestCheckout()` to create the order on the server.
2338
2559
 
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!
2560
+ > **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
2561
 
2342
2562
  ```typescript
2343
2563
  // On your /checkout/success page:
@@ -3014,12 +3234,14 @@ console.log(store.language); // 'en', 'he', etc.
3014
3234
 
3015
3235
  ## Admin API Reference
3016
3236
 
3017
- The Admin API requires an API key (`apiKey`) and provides full access to store configuration and management features.
3237
+ > **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)).
3238
+
3239
+ 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
3240
 
3019
3241
  ```typescript
3020
- // Initialize in Admin mode
3021
- const client = new BrainerceClient({
3022
- apiKey: process.env.BRAINERCE_API_KEY, // 'brainerce_*' prefix
3242
+ // Only in server-side code (Node.js, Edge functions, API routes — never in the browser)
3243
+ const admin = new BrainerceClient({
3244
+ apiKey: process.env.BRAINERCE_API_KEY, // 'brainerce_*' prefix — keep this secret
3023
3245
  });
3024
3246
  ```
3025
3247
 
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';
@@ -317,9 +323,42 @@ interface Product {
317
323
  * (`getProductBySlug`, `getProductById`); omitted from list responses.
318
324
  */
319
325
  modifierGroups?: ModifierGroup[];
326
+ /** Average rating across visible reviews (0 when reviewCount is 0). */
327
+ avgRating?: number;
328
+ /** Count of visible reviews (non-hidden). */
329
+ reviewCount?: number;
320
330
  createdAt: string;
321
331
  updatedAt: string;
322
332
  }
333
+ /**
334
+ * Product review submitted by a customer.
335
+ * Reviews publish immediately (no PENDING state). Merchants hide via the admin
336
+ * surface — hiddenAt is null for visible reviews.
337
+ */
338
+ interface ProductReview {
339
+ id: string;
340
+ productId: string;
341
+ authorName: string;
342
+ rating: number;
343
+ body: string | null;
344
+ verifiedPurchase: boolean;
345
+ /** Only present in admin responses; null on storefront responses. */
346
+ hiddenAt?: string | null;
347
+ createdAt: string;
348
+ }
349
+ /** Admin-mode review with full PII. Returned by `client.adminReviews.*`. */
350
+ interface ProductReviewAdmin extends ProductReview {
351
+ customerId: string | null;
352
+ authorEmail: string | null;
353
+ orderId: string | null;
354
+ updatedAt: string;
355
+ }
356
+ interface SubmitProductReviewInput {
357
+ authorName: string;
358
+ authorEmail?: string;
359
+ rating: number;
360
+ body?: string;
361
+ }
323
362
  interface ProductImage {
324
363
  url: string;
325
364
  position?: number;
@@ -605,7 +644,7 @@ declare function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice
605
644
  * }
606
645
  * ```
607
646
  */
608
- declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount'> | null | undefined): {
647
+ declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'> | null | undefined): {
609
648
  price: number;
610
649
  originalPrice: number;
611
650
  isOnSale: boolean;
@@ -1134,6 +1173,12 @@ interface Coupon {
1134
1173
  * Returns objects with id and name from backend.
1135
1174
  */
1136
1175
  applicableProducts?: EntityRef[] | null;
1176
+ /** Products explicitly excluded from this coupon. */
1177
+ excludedProducts?: EntityRef[] | null;
1178
+ /** Categories this coupon applies to (inclusion list). */
1179
+ applicableCategories?: EntityRef[] | null;
1180
+ /** Categories explicitly excluded from this coupon. */
1181
+ excludedCategories?: EntityRef[] | null;
1137
1182
  combinesWithOther: boolean;
1138
1183
  /** Whether coupon needs sync to platforms */
1139
1184
  needsSync?: boolean;
@@ -1200,6 +1245,12 @@ interface CreateCouponDto {
1200
1245
  conditions?: Record<string, unknown>;
1201
1246
  /** Product IDs this coupon applies to */
1202
1247
  applicableProducts?: string[];
1248
+ /** Product IDs explicitly excluded from this coupon */
1249
+ excludedProducts?: string[];
1250
+ /** Category IDs this coupon applies to */
1251
+ applicableCategories?: string[];
1252
+ /** Category IDs explicitly excluded from this coupon */
1253
+ excludedCategories?: string[];
1203
1254
  combinesWithOther?: boolean;
1204
1255
  /** Platforms to publish this coupon to */
1205
1256
  platforms?: ConnectorPlatform[];
@@ -1222,6 +1273,9 @@ interface UpdateCouponDto {
1222
1273
  maximumDiscount?: number | string | null;
1223
1274
  conditions?: Record<string, unknown> | null;
1224
1275
  applicableProducts?: string[] | null;
1276
+ excludedProducts?: string[] | null;
1277
+ applicableCategories?: string[] | null;
1278
+ excludedCategories?: string[] | null;
1225
1279
  combinesWithOther?: boolean;
1226
1280
  }
1227
1281
  /**
@@ -6060,6 +6114,48 @@ declare class BrainerceClient {
6060
6114
  * ```
6061
6115
  */
6062
6116
  getProductRecommendations(productId: string, type?: ProductRelationType): Promise<ProductRecommendationsResponse>;
6117
+ /**
6118
+ * List visible reviews for a product (storefront / sales-channel modes).
6119
+ * Reviews that the merchant has hidden are excluded.
6120
+ *
6121
+ * @example
6122
+ * ```typescript
6123
+ * const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
6124
+ * data.forEach(r => console.log(r.rating, r.body, r.verifiedPurchase));
6125
+ * ```
6126
+ */
6127
+ listProductReviews(productId: string, params?: {
6128
+ page?: number;
6129
+ limit?: number;
6130
+ }): Promise<PaginatedResponse<ProductReview>>;
6131
+ /**
6132
+ * Submit a customer review for a product (storefront / sales-channel modes).
6133
+ * Publishes immediately. Duplicate submissions from the same email return 409.
6134
+ *
6135
+ * @example
6136
+ * ```typescript
6137
+ * const review = await client.submitProductReview('prod_123', {
6138
+ * authorName: 'Jane Doe',
6139
+ * authorEmail: 'jane@example.com',
6140
+ * rating: 5,
6141
+ * body: 'Loved it!',
6142
+ * });
6143
+ * ```
6144
+ */
6145
+ submitProductReview(productId: string, input: SubmitProductReviewInput): Promise<ProductReview>;
6146
+ /**
6147
+ * Admin: list all reviews for a product (incl. hidden). Requires API key with `reviews:read`.
6148
+ */
6149
+ adminListProductReviews(productId: string, params?: {
6150
+ storeId?: string;
6151
+ page?: number;
6152
+ limit?: number;
6153
+ visibility?: 'visible' | 'hidden' | 'all';
6154
+ }): Promise<PaginatedResponse<ProductReviewAdmin>>;
6155
+ /** Admin: hide a review (sets hiddenAt). */
6156
+ hideProductReview(reviewId: string, storeId?: string): Promise<ProductReviewAdmin>;
6157
+ /** Admin: unhide a previously hidden review. */
6158
+ showProductReview(reviewId: string, storeId?: string): Promise<ProductReviewAdmin>;
6063
6159
  /**
6064
6160
  * Get cross-sell recommendations based on cart contents.
6065
6161
  * Returns products that complement items already in the cart.
@@ -6397,6 +6493,35 @@ declare class BrainerceClient {
6397
6493
  * ```
6398
6494
  */
6399
6495
  getCheckout(checkoutId: string): Promise<Checkout>;
6496
+ /**
6497
+ * Apply a coupon code to an existing checkout session.
6498
+ *
6499
+ * The coupon is validated, applied to the linked cart, and the checkout
6500
+ * totals (discountAmount, totalAmount) are updated atomically.
6501
+ * Use this instead of `applyCoupon()` when the checkout session is already
6502
+ * created — which is the common case when the customer enters a code on the
6503
+ * checkout page.
6504
+ *
6505
+ * @example
6506
+ * ```typescript
6507
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
6508
+ * console.log('New total:', checkout.total);
6509
+ * console.log('Discount:', checkout.discountAmount);
6510
+ * ```
6511
+ */
6512
+ applyCheckoutCoupon(checkoutId: string, code: string): Promise<Checkout>;
6513
+ /**
6514
+ * Remove a coupon from an existing checkout session.
6515
+ *
6516
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
6517
+ *
6518
+ * @example
6519
+ * ```typescript
6520
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
6521
+ * console.log('New total:', checkout.total);
6522
+ * ```
6523
+ */
6524
+ removeCheckoutCoupon(checkoutId: string): Promise<Checkout>;
6400
6525
  /**
6401
6526
  * Set customer information on checkout
6402
6527
  *
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';
@@ -317,9 +323,42 @@ interface Product {
317
323
  * (`getProductBySlug`, `getProductById`); omitted from list responses.
318
324
  */
319
325
  modifierGroups?: ModifierGroup[];
326
+ /** Average rating across visible reviews (0 when reviewCount is 0). */
327
+ avgRating?: number;
328
+ /** Count of visible reviews (non-hidden). */
329
+ reviewCount?: number;
320
330
  createdAt: string;
321
331
  updatedAt: string;
322
332
  }
333
+ /**
334
+ * Product review submitted by a customer.
335
+ * Reviews publish immediately (no PENDING state). Merchants hide via the admin
336
+ * surface — hiddenAt is null for visible reviews.
337
+ */
338
+ interface ProductReview {
339
+ id: string;
340
+ productId: string;
341
+ authorName: string;
342
+ rating: number;
343
+ body: string | null;
344
+ verifiedPurchase: boolean;
345
+ /** Only present in admin responses; null on storefront responses. */
346
+ hiddenAt?: string | null;
347
+ createdAt: string;
348
+ }
349
+ /** Admin-mode review with full PII. Returned by `client.adminReviews.*`. */
350
+ interface ProductReviewAdmin extends ProductReview {
351
+ customerId: string | null;
352
+ authorEmail: string | null;
353
+ orderId: string | null;
354
+ updatedAt: string;
355
+ }
356
+ interface SubmitProductReviewInput {
357
+ authorName: string;
358
+ authorEmail?: string;
359
+ rating: number;
360
+ body?: string;
361
+ }
323
362
  interface ProductImage {
324
363
  url: string;
325
364
  position?: number;
@@ -605,7 +644,7 @@ declare function getProductPrice(product: Pick<Product, 'basePrice' | 'salePrice
605
644
  * }
606
645
  * ```
607
646
  */
608
- declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount'> | null | undefined): {
647
+ declare function getProductPriceInfo(product: Pick<Product, 'basePrice' | 'salePrice' | 'discount' | 'priceMin' | 'priceVaries'> | null | undefined): {
609
648
  price: number;
610
649
  originalPrice: number;
611
650
  isOnSale: boolean;
@@ -1134,6 +1173,12 @@ interface Coupon {
1134
1173
  * Returns objects with id and name from backend.
1135
1174
  */
1136
1175
  applicableProducts?: EntityRef[] | null;
1176
+ /** Products explicitly excluded from this coupon. */
1177
+ excludedProducts?: EntityRef[] | null;
1178
+ /** Categories this coupon applies to (inclusion list). */
1179
+ applicableCategories?: EntityRef[] | null;
1180
+ /** Categories explicitly excluded from this coupon. */
1181
+ excludedCategories?: EntityRef[] | null;
1137
1182
  combinesWithOther: boolean;
1138
1183
  /** Whether coupon needs sync to platforms */
1139
1184
  needsSync?: boolean;
@@ -1200,6 +1245,12 @@ interface CreateCouponDto {
1200
1245
  conditions?: Record<string, unknown>;
1201
1246
  /** Product IDs this coupon applies to */
1202
1247
  applicableProducts?: string[];
1248
+ /** Product IDs explicitly excluded from this coupon */
1249
+ excludedProducts?: string[];
1250
+ /** Category IDs this coupon applies to */
1251
+ applicableCategories?: string[];
1252
+ /** Category IDs explicitly excluded from this coupon */
1253
+ excludedCategories?: string[];
1203
1254
  combinesWithOther?: boolean;
1204
1255
  /** Platforms to publish this coupon to */
1205
1256
  platforms?: ConnectorPlatform[];
@@ -1222,6 +1273,9 @@ interface UpdateCouponDto {
1222
1273
  maximumDiscount?: number | string | null;
1223
1274
  conditions?: Record<string, unknown> | null;
1224
1275
  applicableProducts?: string[] | null;
1276
+ excludedProducts?: string[] | null;
1277
+ applicableCategories?: string[] | null;
1278
+ excludedCategories?: string[] | null;
1225
1279
  combinesWithOther?: boolean;
1226
1280
  }
1227
1281
  /**
@@ -6060,6 +6114,48 @@ declare class BrainerceClient {
6060
6114
  * ```
6061
6115
  */
6062
6116
  getProductRecommendations(productId: string, type?: ProductRelationType): Promise<ProductRecommendationsResponse>;
6117
+ /**
6118
+ * List visible reviews for a product (storefront / sales-channel modes).
6119
+ * Reviews that the merchant has hidden are excluded.
6120
+ *
6121
+ * @example
6122
+ * ```typescript
6123
+ * const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
6124
+ * data.forEach(r => console.log(r.rating, r.body, r.verifiedPurchase));
6125
+ * ```
6126
+ */
6127
+ listProductReviews(productId: string, params?: {
6128
+ page?: number;
6129
+ limit?: number;
6130
+ }): Promise<PaginatedResponse<ProductReview>>;
6131
+ /**
6132
+ * Submit a customer review for a product (storefront / sales-channel modes).
6133
+ * Publishes immediately. Duplicate submissions from the same email return 409.
6134
+ *
6135
+ * @example
6136
+ * ```typescript
6137
+ * const review = await client.submitProductReview('prod_123', {
6138
+ * authorName: 'Jane Doe',
6139
+ * authorEmail: 'jane@example.com',
6140
+ * rating: 5,
6141
+ * body: 'Loved it!',
6142
+ * });
6143
+ * ```
6144
+ */
6145
+ submitProductReview(productId: string, input: SubmitProductReviewInput): Promise<ProductReview>;
6146
+ /**
6147
+ * Admin: list all reviews for a product (incl. hidden). Requires API key with `reviews:read`.
6148
+ */
6149
+ adminListProductReviews(productId: string, params?: {
6150
+ storeId?: string;
6151
+ page?: number;
6152
+ limit?: number;
6153
+ visibility?: 'visible' | 'hidden' | 'all';
6154
+ }): Promise<PaginatedResponse<ProductReviewAdmin>>;
6155
+ /** Admin: hide a review (sets hiddenAt). */
6156
+ hideProductReview(reviewId: string, storeId?: string): Promise<ProductReviewAdmin>;
6157
+ /** Admin: unhide a previously hidden review. */
6158
+ showProductReview(reviewId: string, storeId?: string): Promise<ProductReviewAdmin>;
6063
6159
  /**
6064
6160
  * Get cross-sell recommendations based on cart contents.
6065
6161
  * Returns products that complement items already in the cart.
@@ -6397,6 +6493,35 @@ declare class BrainerceClient {
6397
6493
  * ```
6398
6494
  */
6399
6495
  getCheckout(checkoutId: string): Promise<Checkout>;
6496
+ /**
6497
+ * Apply a coupon code to an existing checkout session.
6498
+ *
6499
+ * The coupon is validated, applied to the linked cart, and the checkout
6500
+ * totals (discountAmount, totalAmount) are updated atomically.
6501
+ * Use this instead of `applyCoupon()` when the checkout session is already
6502
+ * created — which is the common case when the customer enters a code on the
6503
+ * checkout page.
6504
+ *
6505
+ * @example
6506
+ * ```typescript
6507
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
6508
+ * console.log('New total:', checkout.total);
6509
+ * console.log('Discount:', checkout.discountAmount);
6510
+ * ```
6511
+ */
6512
+ applyCheckoutCoupon(checkoutId: string, code: string): Promise<Checkout>;
6513
+ /**
6514
+ * Remove a coupon from an existing checkout session.
6515
+ *
6516
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
6517
+ *
6518
+ * @example
6519
+ * ```typescript
6520
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
6521
+ * console.log('New total:', checkout.total);
6522
+ * ```
6523
+ */
6524
+ removeCheckoutCoupon(checkoutId: string): Promise<Checkout>;
6400
6525
  /**
6401
6526
  * Set customer information on checkout
6402
6527
  *
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_"');
@@ -2792,6 +2790,107 @@ var BrainerceClient = class {
2792
2790
  400
2793
2791
  );
2794
2792
  }
2793
+ // ===================================================
2794
+ // Product Reviews
2795
+ // ===================================================
2796
+ /**
2797
+ * List visible reviews for a product (storefront / sales-channel modes).
2798
+ * Reviews that the merchant has hidden are excluded.
2799
+ *
2800
+ * @example
2801
+ * ```typescript
2802
+ * const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
2803
+ * data.forEach(r => console.log(r.rating, r.body, r.verifiedPurchase));
2804
+ * ```
2805
+ */
2806
+ async listProductReviews(productId, params) {
2807
+ const queryParams = {};
2808
+ if (params?.page) queryParams.page = params.page;
2809
+ if (params?.limit) queryParams.limit = params.limit;
2810
+ if (this.isVibeCodedMode()) {
2811
+ return this.vibeCodedRequest(
2812
+ "GET",
2813
+ `/products/${productId}/reviews`,
2814
+ void 0,
2815
+ queryParams
2816
+ );
2817
+ }
2818
+ if (this.storeId && !this.apiKey) {
2819
+ return this.storefrontRequest(
2820
+ "GET",
2821
+ `/products/${productId}/reviews`,
2822
+ void 0,
2823
+ queryParams
2824
+ );
2825
+ }
2826
+ throw new BrainerceError("listProductReviews() requires vibe-coded or storefront mode", 400);
2827
+ }
2828
+ /**
2829
+ * Submit a customer review for a product (storefront / sales-channel modes).
2830
+ * Publishes immediately. Duplicate submissions from the same email return 409.
2831
+ *
2832
+ * @example
2833
+ * ```typescript
2834
+ * const review = await client.submitProductReview('prod_123', {
2835
+ * authorName: 'Jane Doe',
2836
+ * authorEmail: 'jane@example.com',
2837
+ * rating: 5,
2838
+ * body: 'Loved it!',
2839
+ * });
2840
+ * ```
2841
+ */
2842
+ async submitProductReview(productId, input) {
2843
+ if (this.isVibeCodedMode()) {
2844
+ return this.vibeCodedRequest("POST", `/products/${productId}/reviews`, input);
2845
+ }
2846
+ if (this.storeId && !this.apiKey) {
2847
+ return this.storefrontRequest("POST", `/products/${productId}/reviews`, input);
2848
+ }
2849
+ throw new BrainerceError("submitProductReview() requires vibe-coded or storefront mode", 400);
2850
+ }
2851
+ /**
2852
+ * Admin: list all reviews for a product (incl. hidden). Requires API key with `reviews:read`.
2853
+ */
2854
+ async adminListProductReviews(productId, params) {
2855
+ if (!this.apiKey) {
2856
+ throw new BrainerceError("adminListProductReviews() requires admin (API key) mode", 400);
2857
+ }
2858
+ const queryParams = {};
2859
+ if (params?.storeId) queryParams.storeId = params.storeId;
2860
+ if (params?.page) queryParams.page = params.page;
2861
+ if (params?.limit) queryParams.limit = params.limit;
2862
+ if (params?.visibility) queryParams.visibility = params.visibility;
2863
+ return this.adminRequest(
2864
+ "GET",
2865
+ `/api/v1/products/${productId}/reviews`,
2866
+ void 0,
2867
+ queryParams
2868
+ );
2869
+ }
2870
+ /** Admin: hide a review (sets hiddenAt). */
2871
+ async hideProductReview(reviewId, storeId) {
2872
+ if (!this.apiKey) {
2873
+ throw new BrainerceError("hideProductReview() requires admin (API key) mode", 400);
2874
+ }
2875
+ return this.adminRequest(
2876
+ "PATCH",
2877
+ `/api/v1/reviews/${reviewId}/hide`,
2878
+ void 0,
2879
+ storeId ? { storeId } : void 0
2880
+ );
2881
+ }
2882
+ /** Admin: unhide a previously hidden review. */
2883
+ async showProductReview(reviewId, storeId) {
2884
+ if (!this.apiKey) {
2885
+ throw new BrainerceError("showProductReview() requires admin (API key) mode", 400);
2886
+ }
2887
+ return this.adminRequest(
2888
+ "PATCH",
2889
+ `/api/v1/reviews/${reviewId}/show`,
2890
+ void 0,
2891
+ storeId ? { storeId } : void 0
2892
+ );
2893
+ }
2795
2894
  /**
2796
2895
  * Get cross-sell recommendations based on cart contents.
2797
2896
  * Returns products that complement items already in the cart.
@@ -3575,6 +3674,69 @@ var BrainerceClient = class {
3575
3674
  "checkout"
3576
3675
  );
3577
3676
  }
3677
+ /**
3678
+ * Apply a coupon code to an existing checkout session.
3679
+ *
3680
+ * The coupon is validated, applied to the linked cart, and the checkout
3681
+ * totals (discountAmount, totalAmount) are updated atomically.
3682
+ * Use this instead of `applyCoupon()` when the checkout session is already
3683
+ * created — which is the common case when the customer enters a code on the
3684
+ * checkout page.
3685
+ *
3686
+ * @example
3687
+ * ```typescript
3688
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
3689
+ * console.log('New total:', checkout.total);
3690
+ * console.log('Discount:', checkout.discountAmount);
3691
+ * ```
3692
+ */
3693
+ async applyCheckoutCoupon(checkoutId, code) {
3694
+ if (this.isVibeCodedMode()) {
3695
+ return this.withGuards(
3696
+ this.vibeCodedRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3697
+ "checkout"
3698
+ );
3699
+ }
3700
+ if (this.storeId && !this.apiKey) {
3701
+ return this.withGuards(
3702
+ this.storefrontRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3703
+ "checkout"
3704
+ );
3705
+ }
3706
+ return this.withGuards(
3707
+ this.adminRequest("POST", `/api/v1/checkout/${checkoutId}/coupon`, { code }),
3708
+ "checkout"
3709
+ );
3710
+ }
3711
+ /**
3712
+ * Remove a coupon from an existing checkout session.
3713
+ *
3714
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
3715
+ *
3716
+ * @example
3717
+ * ```typescript
3718
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
3719
+ * console.log('New total:', checkout.total);
3720
+ * ```
3721
+ */
3722
+ async removeCheckoutCoupon(checkoutId) {
3723
+ if (this.isVibeCodedMode()) {
3724
+ return this.withGuards(
3725
+ this.vibeCodedRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3726
+ "checkout"
3727
+ );
3728
+ }
3729
+ if (this.storeId && !this.apiKey) {
3730
+ return this.withGuards(
3731
+ this.storefrontRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3732
+ "checkout"
3733
+ );
3734
+ }
3735
+ return this.withGuards(
3736
+ this.adminRequest("DELETE", `/api/v1/checkout/${checkoutId}/coupon`),
3737
+ "checkout"
3738
+ );
3739
+ }
3578
3740
  /**
3579
3741
  * Set customer information on checkout
3580
3742
  *
@@ -6972,6 +7134,7 @@ function getProductPriceInfo(product) {
6972
7134
  if (!product) {
6973
7135
  return { price: 0, originalPrice: 0, isOnSale: false, discountAmount: 0, discountPercent: 0 };
6974
7136
  }
7137
+ const resolvedBasePrice = parseFloat(product.basePrice) === 0 && product.priceMin ? product.priceMin : product.basePrice;
6975
7138
  if (product.discount) {
6976
7139
  const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
6977
7140
  const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
@@ -6985,7 +7148,7 @@ function getProductPriceInfo(product) {
6985
7148
  discountPercent: rulePercent
6986
7149
  };
6987
7150
  }
6988
- const basePrice = parseFloat(product.basePrice) || 0;
7151
+ const basePrice = parseFloat(resolvedBasePrice) || 0;
6989
7152
  const salePrice = product.salePrice ? parseFloat(product.salePrice) : null;
6990
7153
  const isOnSale = salePrice !== null && salePrice < basePrice;
6991
7154
  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_"');
@@ -2729,6 +2727,107 @@ var BrainerceClient = class {
2729
2727
  400
2730
2728
  );
2731
2729
  }
2730
+ // ===================================================
2731
+ // Product Reviews
2732
+ // ===================================================
2733
+ /**
2734
+ * List visible reviews for a product (storefront / sales-channel modes).
2735
+ * Reviews that the merchant has hidden are excluded.
2736
+ *
2737
+ * @example
2738
+ * ```typescript
2739
+ * const { data, meta } = await client.listProductReviews('prod_123', { page: 1, limit: 20 });
2740
+ * data.forEach(r => console.log(r.rating, r.body, r.verifiedPurchase));
2741
+ * ```
2742
+ */
2743
+ async listProductReviews(productId, params) {
2744
+ const queryParams = {};
2745
+ if (params?.page) queryParams.page = params.page;
2746
+ if (params?.limit) queryParams.limit = params.limit;
2747
+ if (this.isVibeCodedMode()) {
2748
+ return this.vibeCodedRequest(
2749
+ "GET",
2750
+ `/products/${productId}/reviews`,
2751
+ void 0,
2752
+ queryParams
2753
+ );
2754
+ }
2755
+ if (this.storeId && !this.apiKey) {
2756
+ return this.storefrontRequest(
2757
+ "GET",
2758
+ `/products/${productId}/reviews`,
2759
+ void 0,
2760
+ queryParams
2761
+ );
2762
+ }
2763
+ throw new BrainerceError("listProductReviews() requires vibe-coded or storefront mode", 400);
2764
+ }
2765
+ /**
2766
+ * Submit a customer review for a product (storefront / sales-channel modes).
2767
+ * Publishes immediately. Duplicate submissions from the same email return 409.
2768
+ *
2769
+ * @example
2770
+ * ```typescript
2771
+ * const review = await client.submitProductReview('prod_123', {
2772
+ * authorName: 'Jane Doe',
2773
+ * authorEmail: 'jane@example.com',
2774
+ * rating: 5,
2775
+ * body: 'Loved it!',
2776
+ * });
2777
+ * ```
2778
+ */
2779
+ async submitProductReview(productId, input) {
2780
+ if (this.isVibeCodedMode()) {
2781
+ return this.vibeCodedRequest("POST", `/products/${productId}/reviews`, input);
2782
+ }
2783
+ if (this.storeId && !this.apiKey) {
2784
+ return this.storefrontRequest("POST", `/products/${productId}/reviews`, input);
2785
+ }
2786
+ throw new BrainerceError("submitProductReview() requires vibe-coded or storefront mode", 400);
2787
+ }
2788
+ /**
2789
+ * Admin: list all reviews for a product (incl. hidden). Requires API key with `reviews:read`.
2790
+ */
2791
+ async adminListProductReviews(productId, params) {
2792
+ if (!this.apiKey) {
2793
+ throw new BrainerceError("adminListProductReviews() requires admin (API key) mode", 400);
2794
+ }
2795
+ const queryParams = {};
2796
+ if (params?.storeId) queryParams.storeId = params.storeId;
2797
+ if (params?.page) queryParams.page = params.page;
2798
+ if (params?.limit) queryParams.limit = params.limit;
2799
+ if (params?.visibility) queryParams.visibility = params.visibility;
2800
+ return this.adminRequest(
2801
+ "GET",
2802
+ `/api/v1/products/${productId}/reviews`,
2803
+ void 0,
2804
+ queryParams
2805
+ );
2806
+ }
2807
+ /** Admin: hide a review (sets hiddenAt). */
2808
+ async hideProductReview(reviewId, storeId) {
2809
+ if (!this.apiKey) {
2810
+ throw new BrainerceError("hideProductReview() requires admin (API key) mode", 400);
2811
+ }
2812
+ return this.adminRequest(
2813
+ "PATCH",
2814
+ `/api/v1/reviews/${reviewId}/hide`,
2815
+ void 0,
2816
+ storeId ? { storeId } : void 0
2817
+ );
2818
+ }
2819
+ /** Admin: unhide a previously hidden review. */
2820
+ async showProductReview(reviewId, storeId) {
2821
+ if (!this.apiKey) {
2822
+ throw new BrainerceError("showProductReview() requires admin (API key) mode", 400);
2823
+ }
2824
+ return this.adminRequest(
2825
+ "PATCH",
2826
+ `/api/v1/reviews/${reviewId}/show`,
2827
+ void 0,
2828
+ storeId ? { storeId } : void 0
2829
+ );
2830
+ }
2732
2831
  /**
2733
2832
  * Get cross-sell recommendations based on cart contents.
2734
2833
  * Returns products that complement items already in the cart.
@@ -3512,6 +3611,69 @@ var BrainerceClient = class {
3512
3611
  "checkout"
3513
3612
  );
3514
3613
  }
3614
+ /**
3615
+ * Apply a coupon code to an existing checkout session.
3616
+ *
3617
+ * The coupon is validated, applied to the linked cart, and the checkout
3618
+ * totals (discountAmount, totalAmount) are updated atomically.
3619
+ * Use this instead of `applyCoupon()` when the checkout session is already
3620
+ * created — which is the common case when the customer enters a code on the
3621
+ * checkout page.
3622
+ *
3623
+ * @example
3624
+ * ```typescript
3625
+ * const checkout = await client.applyCheckoutCoupon('checkout_123', 'SAVE20');
3626
+ * console.log('New total:', checkout.total);
3627
+ * console.log('Discount:', checkout.discountAmount);
3628
+ * ```
3629
+ */
3630
+ async applyCheckoutCoupon(checkoutId, code) {
3631
+ if (this.isVibeCodedMode()) {
3632
+ return this.withGuards(
3633
+ this.vibeCodedRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3634
+ "checkout"
3635
+ );
3636
+ }
3637
+ if (this.storeId && !this.apiKey) {
3638
+ return this.withGuards(
3639
+ this.storefrontRequest("POST", `/checkout/${checkoutId}/coupon`, { code }),
3640
+ "checkout"
3641
+ );
3642
+ }
3643
+ return this.withGuards(
3644
+ this.adminRequest("POST", `/api/v1/checkout/${checkoutId}/coupon`, { code }),
3645
+ "checkout"
3646
+ );
3647
+ }
3648
+ /**
3649
+ * Remove a coupon from an existing checkout session.
3650
+ *
3651
+ * Clears the coupon from the linked cart and recalculates the checkout totals.
3652
+ *
3653
+ * @example
3654
+ * ```typescript
3655
+ * const checkout = await client.removeCheckoutCoupon('checkout_123');
3656
+ * console.log('New total:', checkout.total);
3657
+ * ```
3658
+ */
3659
+ async removeCheckoutCoupon(checkoutId) {
3660
+ if (this.isVibeCodedMode()) {
3661
+ return this.withGuards(
3662
+ this.vibeCodedRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3663
+ "checkout"
3664
+ );
3665
+ }
3666
+ if (this.storeId && !this.apiKey) {
3667
+ return this.withGuards(
3668
+ this.storefrontRequest("DELETE", `/checkout/${checkoutId}/coupon`),
3669
+ "checkout"
3670
+ );
3671
+ }
3672
+ return this.withGuards(
3673
+ this.adminRequest("DELETE", `/api/v1/checkout/${checkoutId}/coupon`),
3674
+ "checkout"
3675
+ );
3676
+ }
3515
3677
  /**
3516
3678
  * Set customer information on checkout
3517
3679
  *
@@ -6909,6 +7071,7 @@ function getProductPriceInfo(product) {
6909
7071
  if (!product) {
6910
7072
  return { price: 0, originalPrice: 0, isOnSale: false, discountAmount: 0, discountPercent: 0 };
6911
7073
  }
7074
+ const resolvedBasePrice = parseFloat(product.basePrice) === 0 && product.priceMin ? product.priceMin : product.basePrice;
6912
7075
  if (product.discount) {
6913
7076
  const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
6914
7077
  const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
@@ -6922,7 +7085,7 @@ function getProductPriceInfo(product) {
6922
7085
  discountPercent: rulePercent
6923
7086
  };
6924
7087
  }
6925
- const basePrice = parseFloat(product.basePrice) || 0;
7088
+ const basePrice = parseFloat(resolvedBasePrice) || 0;
6926
7089
  const salePrice = product.salePrice ? parseFloat(product.salePrice) : null;
6927
7090
  const isOnSale = salePrice !== null && salePrice < basePrice;
6928
7091
  const effectivePrice = isOnSale ? salePrice : basePrice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainerce",
3
- "version": "1.23.13",
3
+ "version": "1.24.0",
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",