brainerce 2.4.0 → 2.5.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 +229 -30
- package/dist/index.d.mts +405 -82
- package/dist/index.d.ts +405 -82
- package/dist/index.js +386 -112
- package/dist/index.mjs +386 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -596,6 +596,26 @@ if (!page) notFound();
|
|
|
596
596
|
|
|
597
597
|
All `get` / `getBySlug` return `null` on 404. Render a hard-coded fallback so the page never crashes when the merchant hasn't seeded yet.
|
|
598
598
|
|
|
599
|
+
**Admin mode (API key) — every call needs an explicit `storeId`.** The reads above (`get`, `list`, `getBySlug`) are storefront APIs; in admin mode they throw and point you at the admin pair below. Admin mode has no ambient store — `storeId` is only set in storefront mode — and the admin content routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant.
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
// Read (drafts included)
|
|
603
|
+
const rows = await client.content.listAdmin({ storeId, type: 'FAQ', status: 'DRAFT' });
|
|
604
|
+
const row = await client.content.findById('cnt_123', storeId);
|
|
605
|
+
|
|
606
|
+
// Write
|
|
607
|
+
const created = await client.content.faq.create(
|
|
608
|
+
{ key: 'shipping', name: 'Shipping FAQ', data: { items: [{ question: '…', answer: '…' }] } },
|
|
609
|
+
storeId
|
|
610
|
+
);
|
|
611
|
+
await client.content.update('cnt_123', { name: 'Shipping FAQ' }, storeId);
|
|
612
|
+
await client.content.publish('cnt_123', storeId);
|
|
613
|
+
await client.content.unpublish('cnt_123', storeId);
|
|
614
|
+
await client.content.remove('cnt_123', storeId);
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Your API key needs the `content:read` scope for the reads and `content:write` for the writes.
|
|
618
|
+
|
|
599
619
|
**Security**: `FAQ.items[i].answer`, `RICH_TEXT.html`, `PAGE.html`, and `Product.description` are **merchant-authored HTML**. The server does NOT pre-sanitize FAQ/RICH_TEXT/PAGE (merchants may embed iframes); `Product.description` is server-sanitized on write but you still sanitize on render. `Product.description` may contain `<video>` and host-locked YouTube/Vimeo `<iframe>` embeds, so allow those tags (iframe restricted to `www.youtube.com` / `www.youtube-nocookie.com` / `player.vimeo.com`) and add those hosts to your CSP `frame-src`. ALWAYS sanitize before injecting:
|
|
600
620
|
|
|
601
621
|
```typescript
|
|
@@ -612,18 +632,35 @@ Full guide: [Core Integration "Content"](https://brainerce.com/docs/integration/
|
|
|
612
632
|
Merchants publish blog posts in **Content → Blog**. Storefronts choose their own URL scheme: render posts at `/blog/[slug]`, `/articles/[slug]`, or whatever fits the brand.
|
|
613
633
|
|
|
614
634
|
```typescript
|
|
615
|
-
// List published posts (
|
|
635
|
+
// List published posts (storefront / vibe-coded mode)
|
|
616
636
|
const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });
|
|
617
637
|
|
|
618
638
|
// Filter by category or tag
|
|
619
639
|
const { data: news } = await client.blog.getPosts({ category: 'news' });
|
|
620
640
|
const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });
|
|
621
641
|
|
|
622
|
-
// Fetch one by slug — returns null on 404
|
|
642
|
+
// Fetch one by slug — returns null on 404 (storefront / vibe-coded mode)
|
|
623
643
|
const post = await client.blog.getPost(params.slug);
|
|
624
644
|
if (!post) notFound();
|
|
625
645
|
```
|
|
626
646
|
|
|
647
|
+
**Admin mode (API key) — every call needs an explicit `storeId`.** Admin mode has no ambient store (`storeId` is only set in storefront mode) and the admin blog routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (`403 STORE_SCOPE_REQUIRED`). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant. Admin lookups are **by id, not by slug** — `getPost(slug)` is a storefront read and throws in admin mode.
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
// Read (drafts included)
|
|
651
|
+
const { data, meta } = await client.blog.getPosts({ page: 1, limit: 10 }, storeId);
|
|
652
|
+
const post = await client.blog.findById('post_123', storeId); // null on 404
|
|
653
|
+
|
|
654
|
+
// Write
|
|
655
|
+
const draft = await client.blog.create({ title: 'Hello World' }, storeId);
|
|
656
|
+
await client.blog.update('post_123', { title: 'Renamed' }, storeId);
|
|
657
|
+
await client.blog.publish('post_123', storeId);
|
|
658
|
+
await client.blog.unpublish('post_123', storeId);
|
|
659
|
+
await client.blog.remove('post_123', storeId);
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Your API key needs the `blog:read` scope for the reads and `blog:write` for the writes.
|
|
663
|
+
|
|
627
664
|
**Security**: `post.content` is merchant-authored HTML. Always sanitize before rendering:
|
|
628
665
|
|
|
629
666
|
```tsx
|
|
@@ -638,7 +675,7 @@ return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;
|
|
|
638
675
|
|
|
639
676
|
### SEO: JSON-LD builders, sitemap helpers, IndexNow key, llms.txt + agents.md
|
|
640
677
|
|
|
641
|
-
The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on `reviewCount > 0` with explicit `bestRating`/`worstRating`, AggregateOffer with `offerCount` for VARIABLE products, XSS-safe serialization, and the full availability mapping: `InStock` from the backend's pre-computed `inventory.inStock`, `BackOrder` for purchasable-while-out-of-stock products, `OutOfStock` otherwise). `buildProductJsonLd`'s Offer also always includes `itemCondition` (hardcoded `NewCondition`, for a first-party new-goods catalog), `priceValidUntil` when the product has an active sale-price window (`salePriceEndsAt`), and `shippingDetails` when you pass `shipping` (real flat-rate/free zones from `storeInfo.shipping`, omitted entirely and never fabricated if you don't pass it). A `'KIT'` product is emitted with **no `offers` block at all
|
|
678
|
+
The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on `reviewCount > 0` with explicit `bestRating`/`worstRating`, AggregateOffer with `offerCount` for VARIABLE products, XSS-safe serialization, and the full availability mapping: `InStock` from the backend's pre-computed `inventory.inStock`, `BackOrder` for purchasable-while-out-of-stock products, `OutOfStock` otherwise). `buildProductJsonLd`'s Offer also always includes `itemCondition` (hardcoded `NewCondition`, for a first-party new-goods catalog), `priceValidUntil` when the product has an active sale-price window (`salePriceEndsAt`), and `shippingDetails` when you pass `shipping` (real flat-rate/free zones from `storeInfo.shipping`, omitted entirely and never fabricated if you don't pass it). A `'KIT'` product is emitted with **no `offers` block at all**, which is deliberate: kits are not published to Google or Meta shopping feeds, so the builder does not advertise a price for one. Do not hand-roll an Offer to add it. Prefer these builders over hand-rolled JSON-LD:
|
|
642
679
|
|
|
643
680
|
```tsx
|
|
644
681
|
import {
|
|
@@ -1740,6 +1777,39 @@ const { data } = await client.getProducts({
|
|
|
1740
1777
|
|
|
1741
1778
|
### Products
|
|
1742
1779
|
|
|
1780
|
+
#### Kits (`type: 'KIT'`)
|
|
1781
|
+
|
|
1782
|
+
A kit is one purchasable product assembled from other catalog products. It is
|
|
1783
|
+
bought as a SINGLE cart line: pass the kit's own `productId` to `addToCart` and
|
|
1784
|
+
the server reserves each component behind that line. Adding components
|
|
1785
|
+
separately charges twice and reserves twice.
|
|
1786
|
+
|
|
1787
|
+
A kit has **no `inventory` block**. Read `kitAvailable` on the product (`null` =
|
|
1788
|
+
unlimited, `0` = not sellable), or `available` from `getKitComponents`. Outside
|
|
1789
|
+
`FIXED` pricing the product row's `basePrice` is a placeholder; use the resolved
|
|
1790
|
+
price.
|
|
1791
|
+
|
|
1792
|
+
```typescript
|
|
1793
|
+
// Read what's in a kit (admin API key required)
|
|
1794
|
+
const kit = await client.getKitComponents('prod_123');
|
|
1795
|
+
console.log(kit.price, kit.available, kit.pricingMode);
|
|
1796
|
+
kit.components.forEach((c) => console.log(c.name, 'x' + c.quantity));
|
|
1797
|
+
|
|
1798
|
+
// Replace its contents, and how it's priced
|
|
1799
|
+
await client.setKitComponents('prod_123', {
|
|
1800
|
+
components: [
|
|
1801
|
+
{ componentProductId: 'prod_bottle', quantity: 1 },
|
|
1802
|
+
// A VARIABLE component must have one variant pinned
|
|
1803
|
+
{ componentProductId: 'prod_glass', componentVariantId: 'pv_300ml', quantity: 2 },
|
|
1804
|
+
],
|
|
1805
|
+
pricingMode: 'SUM_MINUS_PERCENT', // or 'FIXED' | 'SUM'
|
|
1806
|
+
discountValue: 10,
|
|
1807
|
+
});
|
|
1808
|
+
```
|
|
1809
|
+
|
|
1810
|
+
On the storefront, `getProductBySlug` returns `kitComponents` for display so a
|
|
1811
|
+
shopper can see what is in the box. Those rows are display only.
|
|
1812
|
+
|
|
1743
1813
|
#### Get Products (with pagination)
|
|
1744
1814
|
|
|
1745
1815
|
```typescript
|
|
@@ -1888,10 +1958,35 @@ interface Product {
|
|
|
1888
1958
|
displayCurrency?: string; // ISO 4217 of the display* fields
|
|
1889
1959
|
status: string; // e.g. "active" | "draft"
|
|
1890
1960
|
type: 'SIMPLE' | 'VARIABLE' | 'KIT';
|
|
1961
|
+
// ── KIT only ──────────────────────────────────────────────────────────────
|
|
1962
|
+
// What the kit contains. Returned on the single-product (by slug) read ONLY,
|
|
1963
|
+
// never on list responses. DISPLAY ONLY — render it so the shopper sees what
|
|
1964
|
+
// is in the box; never turn these rows into cart lines.
|
|
1965
|
+
kitComponents?: Array<{
|
|
1966
|
+
productId: string;
|
|
1967
|
+
variantId: string | null;
|
|
1968
|
+
name: string;
|
|
1969
|
+
sku: string | null;
|
|
1970
|
+
quantity: number;
|
|
1971
|
+
image: string | null;
|
|
1972
|
+
}>;
|
|
1973
|
+
// How many kits can be sold: whichever component runs out first decides.
|
|
1974
|
+
// null = unlimited, 0 = NOT SELLABLE (including a kit with no components).
|
|
1975
|
+
// A kit has NO `inventory` block, so this is the ONLY stock signal. Reading
|
|
1976
|
+
// a missing `inventory` as "in stock" renders a sold-out kit as buyable.
|
|
1977
|
+
kitAvailable?: number | null;
|
|
1978
|
+
// How the kit is priced. FIXED (default) keeps the merchant's own basePrice.
|
|
1979
|
+
// SUM = exactly what the components cost, recomputed on every read.
|
|
1980
|
+
// SUM_MINUS_PERCENT = that sum less kitDiscountValue percent (0-100).
|
|
1981
|
+
// Outside FIXED the stored basePrice is a PLACEHOLDER — but the storefront
|
|
1982
|
+
// reads overlay `basePrice` with the resolved price, so render basePrice.
|
|
1983
|
+
kitPricingMode?: 'FIXED' | 'SUM' | 'SUM_MINUS_PERCENT';
|
|
1984
|
+
kitDiscountValue?: number;
|
|
1985
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1891
1986
|
isDownloadable?: boolean;
|
|
1892
1987
|
downloads?: DownloadFile[] | null; // when isDownloadable
|
|
1893
1988
|
images?: ProductImage[];
|
|
1894
|
-
inventory?: InventoryInfo | null;
|
|
1989
|
+
inventory?: InventoryInfo | null; // null/absent on a KIT — use kitAvailable
|
|
1895
1990
|
variants?: ProductVariant[];
|
|
1896
1991
|
categories?: Array<{ id: string; name: string; slug?: string | null }>; // NOT string[] — use slug to link to /category/{slug}
|
|
1897
1992
|
brands?: Array<{ id: string; name: string }>; // objects, not string[]
|
|
@@ -2222,19 +2317,41 @@ function ProductPrice({ product }: { product: Product }) {
|
|
|
2222
2317
|
|
|
2223
2318
|
**When the product is a `'KIT'`:**
|
|
2224
2319
|
|
|
2225
|
-
A kit is
|
|
2226
|
-
not a Bundle offer and not a discount rule
|
|
2227
|
-
slug and SEO.
|
|
2228
|
-
|
|
2229
|
-
- Show `basePrice`
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
`
|
|
2320
|
+
A kit is one purchasable product assembled from other catalog products, sold as
|
|
2321
|
+
one line. It is not a Bundle offer and not a discount rule: it is a product with
|
|
2322
|
+
its own page, slug and SEO.
|
|
2323
|
+
|
|
2324
|
+
- Show `basePrice` as you would for a `'SIMPLE'` product. The storefront reads
|
|
2325
|
+
overlay it with the **resolved** kit price, so do NOT sum the components
|
|
2326
|
+
yourself.
|
|
2327
|
+
- ⛔ **`salePrice` is only meaningful on a `FIXED` kit.** On `SUM` and
|
|
2328
|
+
`SUM_MINUS_PERCENT` the discount is already expressed by the pricing mode and
|
|
2329
|
+
the kit's own sale price is ignored by the engine, so rendering a strikethrough
|
|
2330
|
+
from it shows the shopper a discount that is not being applied. Both reads
|
|
2331
|
+
behave the same way: outside `FIXED` they null the kit's `salePrice`, and on
|
|
2332
|
+
`FIXED` they leave `basePrice`/`salePrice` untouched so the normal was/now
|
|
2333
|
+
logic works. Gate any strikethrough on `kitPricingMode === 'FIXED'`.
|
|
2334
|
+
- **Do not tell the shopper the price is fixed.** There are three pricing modes
|
|
2335
|
+
and two of them move on their own. `kitPricingMode: 'FIXED'` (the default) is
|
|
2336
|
+
the number the merchant typed and stays put. `'SUM'` makes the kit cost
|
|
2337
|
+
exactly what its contents cost, **recomputed on every read**, so a component
|
|
2338
|
+
going on sale lowers the kit price by itself. `'SUM_MINUS_PERCENT'` is that
|
|
2339
|
+
sum less `kitDiscountValue` percent. Outside `FIXED` the stored `basePrice`
|
|
2340
|
+
on the raw product row is a **placeholder** — never cache it, and never
|
|
2341
|
+
compute from a value you fetched earlier.
|
|
2342
|
+
- A kit carries **no `inventory` of its own**. Read **`kitAvailable`**:
|
|
2343
|
+
`null` = unlimited, `0` = **not sellable** (including a kit with no
|
|
2344
|
+
components), any other number = how many kits can be sold. "Don't render
|
|
2345
|
+
out-of-stock just because `inventory` is missing" is only half the rule:
|
|
2346
|
+
without `kitAvailable` a sold-out kit renders as buyable, which is worse.
|
|
2347
|
+
- Render **`kitComponents`** so the shopper can see what is in the box. It comes
|
|
2348
|
+
back on the single-product (by slug) read only, never on list responses, as
|
|
2349
|
+
`{ productId, variantId, name, sku, quantity, image }` rows. They are
|
|
2350
|
+
**display only**.
|
|
2351
|
+
- Add it to the cart by the kit's own `productId` with **no `variantId`** — a
|
|
2352
|
+
kit has no variants and sending one is rejected. It takes no modifier
|
|
2353
|
+
`selections` either (HTTP 400). ⛔ **Never add its components as separate
|
|
2354
|
+
lines**: that charges the customer twice and reserves the stock twice.
|
|
2238
2355
|
|
|
2239
2356
|
#### Rendering Product Descriptions
|
|
2240
2357
|
|
|
@@ -3196,9 +3313,15 @@ interface Checkout {
|
|
|
3196
3313
|
discountAmount: string;
|
|
3197
3314
|
shippingAmount: string;
|
|
3198
3315
|
taxAmount: string; // "0" in inclusive (VAT) mode — see taxBreakdown.totalTax
|
|
3199
|
-
taxBreakdown?: TaxBreakdown | null; // { totalTax, pricesIncludeTax, breakdown[] }
|
|
3316
|
+
taxBreakdown?: TaxBreakdown | null; // { subtotal, shippingNet, totalTax, total, pricesIncludeTax, breakdown[] }
|
|
3200
3317
|
// breakdown[] is ONE ROW PER TAX and is often more than one row (Canada charges
|
|
3201
3318
|
// GST + PST/QST). Loop it; never read breakdown[0].
|
|
3319
|
+
// taxBreakdown.subtotal is NOT the `subtotal` above: it is the net of every
|
|
3320
|
+
// taxed line and it INCLUDES the shipping net, so subtotal + shipping + tax
|
|
3321
|
+
// counts shipping twice. Use taxBreakdown.subtotal - taxBreakdown.shippingNet
|
|
3322
|
+
// for a goods-only row, and subtract shippingNet rather than the gross
|
|
3323
|
+
// shippingAmount — they are equal only while shipping is untaxed. The
|
|
3324
|
+
// top-level `subtotal` is goods-only and needs no adjustment.
|
|
3202
3325
|
total: string;
|
|
3203
3326
|
couponCode?: string | null;
|
|
3204
3327
|
// Gift cards held against this checkout, oldest first. Read these to re-render
|
|
@@ -5083,6 +5206,12 @@ const product: Product = await client.createProduct({
|
|
|
5083
5206
|
costPrice: 12.0, // Optional, internal — never synced to platforms
|
|
5084
5207
|
status: 'active', // 'active' | 'draft' (default: 'active')
|
|
5085
5208
|
type: 'SIMPLE', // 'SIMPLE' | 'VARIABLE' | 'KIT' (default: 'SIMPLE')
|
|
5209
|
+
// KIT only. FIXED (default) means you set basePrice and it stays put. SUM
|
|
5210
|
+
// means the kit costs exactly what its components cost, recomputed on every
|
|
5211
|
+
// read, so a component going on sale lowers the kit price on its own.
|
|
5212
|
+
// SUM_MINUS_PERCENT is that sum less kitDiscountValue percent.
|
|
5213
|
+
// kitPricingMode: 'SUM_MINUS_PERCENT',
|
|
5214
|
+
// kitDiscountValue: 10, // 0-100, required with SUM_MINUS_PERCENT
|
|
5086
5215
|
categories: ['cat_id'], // Existing category IDs
|
|
5087
5216
|
categoryNames: ['Apparel'], // Or assign/auto-create by name — merged with `categories`
|
|
5088
5217
|
brands: ['brand_id'],
|
|
@@ -5155,6 +5284,69 @@ await client.bulkSaveVariants(variableProduct.id, {
|
|
|
5155
5284
|
|
|
5156
5285
|
**GTIN vs MPN:** these are two different identifiers, not interchangeable: GTIN (EAN/UPC/ISBN) is a universal barcode; MPN is manufacturer-specific and only meaningful paired with a brand. Provide GTIN when the product has one; otherwise brand + MPN. A product typically needs one or the other, not both.
|
|
5157
5286
|
|
|
5287
|
+
### Kit Management (`type: 'KIT'`)
|
|
5288
|
+
|
|
5289
|
+
⛔ **`createProduct({ type: 'KIT' })` gives you an EMPTY kit, and an empty kit
|
|
5290
|
+
cannot be bought.** A kit is defined by its component list, so creating the
|
|
5291
|
+
product is only half the job: until you call `setKitComponents`, the kit has no
|
|
5292
|
+
price, `kitAvailable` is `0`, and `addToCart` refuses it as unavailable. It is
|
|
5293
|
+
not an error state you will see in the response — the create succeeds and the
|
|
5294
|
+
product looks fine. Create, then set components, in that order.
|
|
5295
|
+
|
|
5296
|
+
```typescript
|
|
5297
|
+
import type { KitDetail, KitComponentWriteInput } from 'brainerce';
|
|
5298
|
+
|
|
5299
|
+
// 1. Create the kit product (empty and unbuyable at this point)
|
|
5300
|
+
const kit = await client.createProduct({
|
|
5301
|
+
name: 'Wine Gift Box',
|
|
5302
|
+
basePrice: 199, // required to save; only charged on FIXED pricing
|
|
5303
|
+
type: 'KIT',
|
|
5304
|
+
kitPricingMode: 'SUM_MINUS_PERCENT', // also settable on updateProduct
|
|
5305
|
+
kitDiscountValue: 10, // 0-100, required with SUM_MINUS_PERCENT
|
|
5306
|
+
});
|
|
5307
|
+
|
|
5308
|
+
// 2. Give it contents — this is what makes it purchasable
|
|
5309
|
+
const detail: KitDetail = await client.setKitComponents(kit.id, {
|
|
5310
|
+
components: [
|
|
5311
|
+
{ componentProductId: 'prod_bottle', quantity: 1 },
|
|
5312
|
+
// A VARIABLE component MUST have exactly one variant pinned
|
|
5313
|
+
{ componentProductId: 'prod_glass', componentVariantId: 'pv_300ml', quantity: 2 },
|
|
5314
|
+
],
|
|
5315
|
+
pricingMode: 'SUM_MINUS_PERCENT', // optional — omit to leave the mode alone
|
|
5316
|
+
discountValue: 10,
|
|
5317
|
+
});
|
|
5318
|
+
|
|
5319
|
+
// 3. Read it back any time
|
|
5320
|
+
const read: KitDetail = await client.getKitComponents(kit.id);
|
|
5321
|
+
console.log(read.price, read.available, read.pricingMode, read.discountValue);
|
|
5322
|
+
read.components.forEach((c) => console.log(c.name, 'x' + c.quantity, c.unitPrice, c.available));
|
|
5323
|
+
```
|
|
5324
|
+
|
|
5325
|
+
`setKitComponents` is a **full replace**, not a patch: send the list the kit
|
|
5326
|
+
should end up with. An empty `components` array clears the kit and makes it
|
|
5327
|
+
unbuyable again. Max **30** components; `quantity` is 1-2000 per slot.
|
|
5328
|
+
|
|
5329
|
+
`getKitComponents` is safe to call on any product id — a non-KIT returns an
|
|
5330
|
+
empty, unsellable shape rather than throwing, so callers need not branch on
|
|
5331
|
+
product type first.
|
|
5332
|
+
|
|
5333
|
+
**Pricing modes:** `FIXED` (default) charges the kit's own `basePrice` /
|
|
5334
|
+
`salePrice`. `SUM` charges exactly what the components cost, **recomputed on
|
|
5335
|
+
every read**, so a component going on sale lowers the kit price on its own.
|
|
5336
|
+
`SUM_MINUS_PERCENT` is that sum less `kitDiscountValue` percent. Outside `FIXED`
|
|
5337
|
+
the stored `basePrice` is a **placeholder** and the kit's own `salePrice` is
|
|
5338
|
+
ignored. Use `KitDetail.price`, or the `basePrice` on a storefront read, which
|
|
5339
|
+
outside `FIXED` is already overlaid with the resolved figure (on `FIXED` the
|
|
5340
|
+
stored prices are returned as they are, sale included).
|
|
5341
|
+
|
|
5342
|
+
**Refused on write:** a product that is not a `KIT`; a component from another
|
|
5343
|
+
store; a component that is itself a `KIT` (kits do not nest); a `VARIABLE`
|
|
5344
|
+
component with no `componentVariantId`; a variant that does not belong to its
|
|
5345
|
+
product; the same slot listed twice; and the kit itself. You also cannot delete
|
|
5346
|
+
a product while it sits inside a kit — remove it from the kit first. `SIMPLE` ↔
|
|
5347
|
+
`KIT` conversion is allowed (`KIT` → `SIMPLE` only once the kit is empty);
|
|
5348
|
+
`VARIABLE` ↔ `KIT` is refused outright.
|
|
5349
|
+
|
|
5158
5350
|
### Bulk Product Creation (catalog import)
|
|
5159
5351
|
|
|
5160
5352
|
Importing a catalog (a supplier feed, a CSV/Excel export, a store migration)
|
|
@@ -5897,8 +6089,10 @@ Each store has its own team with roles (`OWNER`, `MANAGER`, `STAFF`, `VIEWER`) a
|
|
|
5897
6089
|
granular permissions, including per-sales-channel scoping. **Managing it is a dashboard
|
|
5898
6090
|
operation. There is no SDK path to it, and this is deliberate.**
|
|
5899
6091
|
|
|
5900
|
-
This section previously showed store-level team calls. **Since SDK 2.
|
|
5901
|
-
`BrainerceError` that says so, instead of returning a bare 404.**
|
|
6092
|
+
This section previously showed store-level team calls. **Since SDK 2.2.0 they throw a
|
|
6093
|
+
`BrainerceError` that says so, instead of returning a bare 404.** (The change is dated
|
|
6094
|
+
2.1.1 in the changelog, but that version was never published: npm went 2.1.0 straight to
|
|
6095
|
+
2.2.0, so 2.2.0 is the first release you can actually install it from.) Ten methods are
|
|
5902
6096
|
affected: `getStoreTeam`, `inviteStoreMember`, `updateStoreMember`,
|
|
5903
6097
|
`updateStoreMemberSalesChannels`, `removeStoreMember`, `resendStoreInvitation`,
|
|
5904
6098
|
`revokeStoreInvitation`, `acceptStoreInvitation`, `getMyStores` and
|
|
@@ -5919,17 +6113,22 @@ route is `@Public()`, and it was only ever failing because the SDK asked for
|
|
|
5919
6113
|
Use it to render an invitation-acceptance page; the acceptance itself must happen in the
|
|
5920
6114
|
dashboard, because it is matched against the invited user's own email address.
|
|
5921
6115
|
|
|
5922
|
-
> **The older account-level methods are not a substitute
|
|
5923
|
-
>
|
|
5924
|
-
> `
|
|
5925
|
-
>
|
|
5926
|
-
>
|
|
6116
|
+
> ⛔ **The older account-level methods are not a substitute, and they do not work
|
|
6117
|
+
> either.** `getTeamMembers`, `getTeamInvitations`, `inviteTeamMember`,
|
|
6118
|
+
> `resendTeamInvitation`, `revokeTeamInvitation`, `updateTeamMemberRole` and
|
|
6119
|
+
> `removeTeamMember` now **throw** instead of calling `/api/v1/team/…`. An earlier
|
|
6120
|
+
> version of this page said they remained the supported call for the account team.
|
|
6121
|
+
> That was wrong: those routes hand the team service a synthetic `api-key-user`
|
|
6122
|
+
> principal that has no account membership row, so every one of them answered
|
|
6123
|
+
> `Access denied to this account`. The SDK raises that locally now, naming the cause,
|
|
6124
|
+
> rather than sending a request that cannot succeed.
|
|
5927
6125
|
>
|
|
5928
|
-
> **
|
|
5929
|
-
>
|
|
5930
|
-
>
|
|
5931
|
-
>
|
|
5932
|
-
>
|
|
6126
|
+
> **There is no API-key path to team management, at either level, and there should
|
|
6127
|
+
> not be.** Team membership is account-scoped and an invite can grant ownership of
|
|
6128
|
+
> every store on the account, while an API key is bound to exactly one store.
|
|
6129
|
+
> Admitting the key would turn a single-store credential into account ownership.
|
|
6130
|
+
> Manage members in the dashboard, or use the store-scoped `team:*` tools on the
|
|
6131
|
+
> admin MCP server, which are OAuth-authenticated and bound to one store.
|
|
5933
6132
|
|
|
5934
6133
|
### Email Settings & Templates
|
|
5935
6134
|
|