brainerce 1.27.0 → 1.28.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
@@ -93,7 +93,7 @@ Violating any of these causes production incidents or broken orders. Read them b
93
93
 
94
94
  - Customer auth tokens (`result.token` from `loginCustomer`/`registerCustomer`) should be passed to `client.setCustomerToken(token)`. The SDK stores session state internally.
95
95
  - NEVER put the admin API key (`brainerce_*`) in client code. It is a server-only secret.
96
- - OAuth callbacks arrive with the token in URL params. Extract and apply the token before redirecting.
96
+ - OAuth callbacks arrive with a one-time `auth_code` URL param. Call `client.exchangeOAuthCode(authCode)` to swap it for the JWT and apply via `setCustomerToken`. (The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release.)
97
97
 
98
98
  ### i18n
99
99
 
@@ -205,11 +205,16 @@ These sequences are non-negotiable. The order of SDK calls matters.
205
205
  });
206
206
  window.location.href = authorizationUrl; // full-page redirect, NOT a popup
207
207
  ```
208
- 3. On callback, the URL contains `token` + `oauth_success` (or `oauth_error`) query params:
208
+ 3. On callback, the URL contains `auth_code` + `oauth_success` (or `oauth_error`) query params. Exchange the single-use code for the JWT:
209
209
  ```ts
210
- const token = new URLSearchParams(location.search).get('token');
211
- if (token) client.setCustomerToken(token); // then redirect to account
210
+ const params = new URLSearchParams(location.search);
211
+ const code = params.get('auth_code');
212
+ if (code) {
213
+ const result = await client.exchangeOAuthCode(code);
214
+ client.setCustomerToken(result.token); // then redirect to account
215
+ }
212
216
  ```
217
+ The legacy `?token=` URL param is still emitted for backward compatibility but will be removed in the next major release — migrate to `auth_code` now.
213
218
  4. On `oauth_error` query param: redirect to login with an error message.
214
219
 
215
220
  > Build the OAuth button region AND the callback handler even when no providers are configured.
@@ -1936,6 +1941,11 @@ console.log(cart.discountAmount); // Discount applied
1936
1941
  console.log(cart.couponCode); // 'SAVE20'
1937
1942
  ```
1938
1943
 
1944
+ > **Region-restricted coupons:** a coupon may be limited to specific regions via
1945
+ > its `regionIds`. If the buyer's checkout region isn't in that list, redemption is
1946
+ > rejected even when the code is otherwise valid. An empty/omitted `regionIds`
1947
+ > applies in all regions. See [`/docs/concepts/region-coupons`](/docs/concepts/region-coupons).
1948
+
1939
1949
  #### Remove Coupon
1940
1950
 
1941
1951
  ```typescript
@@ -1994,6 +2004,52 @@ const checkout = await client.createCheckout({
1994
2004
  });
1995
2005
  ```
1996
2006
 
2007
+ #### Multi-Region Checkout
2008
+
2009
+ If your store uses [regions](#regions), pass a `regionId` to associate the
2010
+ checkout with one (the region must belong to the store). Resolve the buyer's
2011
+ country to a region client-side with `detectRegion`:
2012
+
2013
+ ```typescript
2014
+ const { data: regions } = await client.getRegions(); // or a storefront region list
2015
+ const region = client.detectRegion(buyerCountry, regions);
2016
+
2017
+ const checkout = await client.createCheckout({
2018
+ cartId: cart.id,
2019
+ regionId: region?.id, // omit to associate no region
2020
+ });
2021
+ ```
2022
+
2023
+ > The region is recorded on the checkout and used for payment-provider scoping.
2024
+ > Currency continues to follow the cart until FX price conversion lands, so
2025
+ > passing a `regionId` does not re-price the cart.
2026
+
2027
+ #### Region display pricing (`getProducts({ regionId })`)
2028
+
2029
+ Product reads accept an optional `regionId`. When set, each product/variant that
2030
+ a region [price list](#price-lists) resolves gains additive fields —
2031
+ `resolvedPrice`, `resolvedCurrency`, `resolvedCompareAtPrice`, and `priceSource`
2032
+ (`'variant-entry'` | `'product-entry'`). Your existing `basePrice` / `salePrice`
2033
+ stay in the store currency; the fields appear **only** when a region entry
2034
+ actually applies, so an omitted/invalid region (or a catalog-price fallback)
2035
+ leaves the response byte-identical.
2036
+
2037
+ ```typescript
2038
+ const { data: products } = await client.getProducts({ regionId: 'region_eu' });
2039
+ const p = products[0];
2040
+ const display = p.resolvedPrice
2041
+ ? `${p.resolvedPrice} ${p.resolvedCurrency}` // e.g. "27.00 EUR"
2042
+ : p.basePrice; // store-currency catalog price
2043
+
2044
+ // Single product (id or slug) takes the same option:
2045
+ await client.getProduct('prod_tshirt', { regionId: 'region_eu' });
2046
+ ```
2047
+
2048
+ > **Display-only** — like checkout, `regionId` here does not charge the region
2049
+ > price; it only changes what you render until currency-lock ships. **Mode note:**
2050
+ > region resolution applies on storefront (`storeId`) and admin (`apiKey`) reads;
2051
+ > it is currently ignored in vibe-coded (`vc_*`/`salesChannelId`) mode.
2052
+
1997
2053
  #### Partial Checkout (AliExpress-style)
1998
2054
 
1999
2055
  Allow customers to select which items to checkout from their cart. Only selected items are purchased - remaining items stay in the cart for later.
@@ -3394,6 +3450,14 @@ const zone = await client.createShippingZone({
3394
3450
  countries: ['US'],
3395
3451
  });
3396
3452
 
3453
+ // Restrict a zone to specific regions (PRD §25). Empty/omitted = any region;
3454
+ // a non-empty list hides the zone unless the checkout resolved to one of them.
3455
+ const euZone = await client.createShippingZone({
3456
+ name: 'EU Express',
3457
+ countries: ['DE', 'FR', 'IT'],
3458
+ regionIds: ['reg_eu'],
3459
+ });
3460
+
3397
3461
  // Shipping Rates
3398
3462
  const rates = await client.getZoneShippingRates('zone_id');
3399
3463
  await client.createZoneShippingRate('zone_id', {
@@ -3406,14 +3470,168 @@ await client.createZoneShippingRate('zone_id', {
3406
3470
 
3407
3471
  ### Tax Configuration
3408
3472
 
3473
+ `rate` is a **whole percentage** (`7.25` = 7.25%, not `0.0725`). A rate may target
3474
+ a [tax class](#tax-classes) via `taxClassId`; a rate with `taxClassId: null` (the
3475
+ default) is the **Standard fallback** applied to any line whose class has no
3476
+ class-specific rate.
3477
+
3409
3478
  ```typescript
3410
3479
  const rates = await client.getTaxRates();
3480
+
3481
+ // Standard rate (applies to everything without a class-specific rate)
3411
3482
  await client.createTaxRate({
3412
3483
  name: 'CA Sales Tax',
3413
3484
  rate: 7.25,
3414
3485
  country: 'US',
3415
3486
  region: 'CA',
3416
3487
  });
3488
+
3489
+ // Class-specific rate — only lines assigned to this tax class use it.
3490
+ // Create the class in the dashboard (Settings → Tax) to get its id.
3491
+ await client.createTaxRate({
3492
+ name: 'CA Reduced (Food)',
3493
+ rate: 0,
3494
+ country: 'US',
3495
+ region: 'CA',
3496
+ taxClassId: 'taxclass_food_id',
3497
+ });
3498
+ ```
3499
+
3500
+ ### Tax Classes
3501
+
3502
+ Tax classes let you charge different rates for different product types (e.g.
3503
+ Standard / Reduced / Zero-rated / Food). Assign a class to a product, variant, or
3504
+ category; checkout resolves each line's class **variant → product → category →
3505
+ store default → null** and picks the matching `TaxRate`, falling back to the
3506
+ Standard (null-class) rate. See [`/docs/concepts/tax-classes`](/docs/concepts/tax-classes)
3507
+ for the full resolution order. `storeId` is derived from the API key. Requires
3508
+ the `tax-classes:read` / `tax-classes:write` scopes.
3509
+
3510
+ ```typescript
3511
+ // List / inspect
3512
+ const { data: classes } = await client.getTaxClasses();
3513
+ const detail = await client.getTaxClass('taxclass_id');
3514
+ detail.dependents; // { productCount, variantCount, categoryCount, taxRateCount }
3515
+
3516
+ // Create (slug is kebab-case, unique per store)
3517
+ const food = await client.createTaxClass({
3518
+ name: 'Food',
3519
+ slug: 'food',
3520
+ description: 'Zero / reduced-rate groceries',
3521
+ });
3522
+
3523
+ await client.updateTaxClass(food.id, { description: 'Reduced VAT' });
3524
+ await client.setDefaultTaxClass(food.id); // the class auto-applied to unclassified products
3525
+
3526
+ // Bulk-assign a class to products / variants / categories
3527
+ const { updated } = await client.assignTaxClass(food.id, {
3528
+ productIds: ['prod_1', 'prod_2'],
3529
+ categoryIds: ['cat_groceries'],
3530
+ });
3531
+
3532
+ // Merge moves every product/variant/category/rate FK onto the target, then
3533
+ // deletes this class. Delete is blocked (409) while dependents exist — merge first.
3534
+ await client.mergeTaxClasses(food.id, 'taxclass_standard_id');
3535
+ await client.deleteTaxClass(food.id);
3536
+ ```
3537
+
3538
+ **Storefront (public, no API key).** A storefront lists classes in `storeId`
3539
+ mode — storefront-safe fields only (for a "9% VAT" transparency badge):
3540
+
3541
+ ```typescript
3542
+ const store = new BrainerceClient({ storeId: 'store_123' });
3543
+ const { data: classes } = await store.getStoreTaxClasses();
3544
+ ```
3545
+
3546
+ ### Regions
3547
+
3548
+ A **region** binds a set of countries to a currency, a tax-display mode
3549
+ (`taxInclusive`), and the payment providers enabled there. `storeId` is derived
3550
+ from the API key. Requires the `regions:read` / `regions:write` scopes. See
3551
+ [`/docs/concepts/regions`](/docs/concepts/regions).
3552
+
3553
+ ```typescript
3554
+ // List / inspect
3555
+ const { data: regions } = await client.getRegions();
3556
+ const region = await client.getRegion('region_id');
3557
+
3558
+ // Create — paymentProviderIds are AppInstallation IDs to enable in this region
3559
+ const eu = await client.createRegion({
3560
+ name: 'European Union',
3561
+ currency: 'EUR',
3562
+ countries: ['DE', 'FR', 'IT', 'ES'],
3563
+ taxInclusive: true,
3564
+ paymentProviderIds: ['app_inst_stripe'],
3565
+ });
3566
+
3567
+ await client.updateRegion(eu.id, { isActive: true });
3568
+ await client.setDefaultRegion(eu.id);
3569
+
3570
+ // Manage the country set
3571
+ await client.addRegionCountries(eu.id, ['NL', 'BE']);
3572
+ await client.removeRegionCountry(eu.id, 'BE');
3573
+
3574
+ // Manage payment providers (replaces the full set)
3575
+ await client.updateRegionPaymentProviders(eu.id, ['app_inst_stripe', 'app_inst_paypal']);
3576
+
3577
+ // Which installed providers can serve this region's countries?
3578
+ const compatible = await client.getRegionCompatibleProviders(eu.id);
3579
+
3580
+ // Pure client-side helper (no network) — pick a region for a country code
3581
+ const dest = client.detectRegion('DE', regions); // → the EU region, or the default, or null
3582
+
3583
+ await client.deleteRegion(eu.id);
3584
+ ```
3585
+
3586
+ **Storefront (public, no API key).** A storefront fetches regions in `storeId`
3587
+ mode — only active regions, only storefront-safe fields:
3588
+
3589
+ ```typescript
3590
+ const store = new BrainerceClient({ storeId: 'store_123' });
3591
+
3592
+ const { data: regions } = await store.getStoreRegions();
3593
+ const region = await store.getStoreRegion(regions[0].id); // + paymentProviders
3594
+ const dest = store.detectRegion('DE', regions); // pick by country, client-side
3595
+ ```
3596
+
3597
+ > **Multi-region checkout:** pass a `regionId` to `createCheckout` to associate the
3598
+ > checkout with a region (for reporting + payment-provider scoping). The region
3599
+ > must belong to the store. Currency still follows the cart until FX price
3600
+ > conversion lands. See the Checkout section.
3601
+
3602
+ ### Price Lists (per-region pricing)
3603
+
3604
+ Each region has a **price list** — entries that override a product's or variant's
3605
+ catalog price for buyers in that region. Requires `price-lists:read` /
3606
+ `price-lists:write`. Prices are in the region's currency.
3607
+
3608
+ > ⚠️ **Data-entry only for now.** Region prices are **not yet applied at checkout** —
3609
+ > charging a region-currency price through the store-currency cart needs FX
3610
+ > conversion (a later phase). These methods let you populate the price book ahead of
3611
+ > activation; until then checkout uses the catalog price.
3612
+
3613
+ ```typescript
3614
+ // One entry per product OR variant (variant wins). price > 0; compareAtPrice optional.
3615
+ const res = await client.upsertPriceListEntries('region_eu', [
3616
+ { productId: 'prod_tshirt', price: 27, compareAtPrice: 32 },
3617
+ { variantId: 'var_tshirt_l', price: 29 },
3618
+ ]);
3619
+ res.upserted; // 2
3620
+ res.warnings; // e.g. ["PL-004: compareAtPrice not greater than price …"] (non-blocking)
3621
+
3622
+ // Inspect
3623
+ const meta = await client.getPriceList('region_eu'); // { currency, entryCount, … }
3624
+ const { data: entries } = await client.listPriceListEntries('region_eu', {
3625
+ productId: 'prod_tshirt',
3626
+ });
3627
+
3628
+ // Bulk CSV (columns: productSku,variantSku?,price,compareAtPrice?). Unknown SKUs /
3629
+ // bad prices are skipped + reported, not fatal.
3630
+ const imported = await client.importPrices('region_eu', csvString);
3631
+ imported.skipped; // ['row 4: product SKU "NOPE" not found', …]
3632
+ const csv = await client.exportPrices('region_eu'); // round-trips with importPrices
3633
+
3634
+ await client.deletePriceListEntry('region_eu', 'entry_id'); // line falls back to catalog
3417
3635
  ```
3418
3636
 
3419
3637
  ### Metafield Definitions
@@ -3495,6 +3713,7 @@ const { members, invitations } = await client.getStoreTeam('store_id');
3495
3713
  const invitation = await client.inviteStoreMember('store_id', {
3496
3714
  email: 'newmember@example.com',
3497
3715
  role: 'MANAGER', // 'MANAGER' | 'STAFF' | 'VIEWER'
3716
+ salesChannelIds: ['vc_abc123'], // optional: restrict to vibe-coded channels (connectionId); omit/[] = all channels
3498
3717
  });
3499
3718
 
3500
3719
  // Update member role or set custom permissions
@@ -3503,6 +3722,11 @@ await client.updateStoreMember('store_id', 'member_id', {
3503
3722
  permissions: ['VIEW_PRODUCTS', 'VIEW_ORDERS', 'FULFILL_ORDERS'], // overrides role defaults
3504
3723
  });
3505
3724
 
3725
+ // Replace a member's vibe-coded sales-channel scope ([] clears all restrictions)
3726
+ await client.updateStoreMemberSalesChannels('store_id', 'member_id', {
3727
+ salesChannelIds: ['vc_abc123', 'vc_def456'],
3728
+ });
3729
+
3506
3730
  // Remove a member
3507
3731
  await client.removeStoreMember('store_id', 'member_id');
3508
3732