@classytic/commerce-client 0.1.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-24
4
+
5
+ Add merchant `stock.read`, batched `stock.query` and guarded `stock.setQuantity` over Arc Next, product stock-tracking input and public per-variant availability types. Count retries require an explicit stable key. Browser bundles remain independent of server kernels.
6
+
7
+ ## 0.2.0 — 2026-10-23
8
+
9
+ Add typed merchant collection CRUD and public collection list/detail methods with
10
+ cursor pagination over Arc Next. Add the collections media folder. Browser
11
+ entry points remain independent of React, MongoDB and server domain packages.
12
+
13
+ Dependencies are already at the latest published (`arc-next` 0.18.0, `media-transform` 0.3.0, `primitives` 0.34.0). Dev toolchain: `typescript@7.1.0-beta`, which tsdown 0.23.2 needs to emit declarations with TypeScript 7.
14
+
3
15
  ## 0.1.1 — 2026-10-20
4
16
 
5
17
  Add tenant-scoped media removal through the guarded media resource, and type media folder, size and display metadata for Fluid library bindings. Report unsupported source formats and browser decoding failures before starting storage writes.
package/README.md CHANGED
@@ -128,3 +128,47 @@ and sort. Restart pagination when filters change. Public calls always enforce th
128
128
  resolved store and published/priced visibility, and reject unknown query fields.
129
129
  Merchant `products.getAll({ params })` continues to use Arc's normal allowed query
130
130
  syntax, including tags, collections and base-price bracket filters.
131
+
132
+ ### Managed collections
133
+
134
+ A collection is a store-owned merchandising group, independent of the category
135
+ hierarchy. Enable `modules: { collections: true }` on the catalog engine and mount
136
+ `createStorefrontCollectionResource` from `@spinekit/catalog/collections-storefront`.
137
+ Disabled engines register no collection model or indexes. The SDK stays browser-safe
138
+ and does not import catalog, MongoDB, React or the server modules.
139
+
140
+ ```ts
141
+ const groups = await storefront.collections({ featured: true, limit: 8 });
142
+ const collection = await storefront.collection('eid-picks');
143
+ const products = await storefront.products({
144
+ collection: collection.slug,
145
+ category: 'children', // optional; intersects the collection
146
+ tag: 'cotton', // optional
147
+ sort: 'newest',
148
+ limit: 20,
149
+ });
150
+ // Pass groups.next / products.next as `after`, retaining the same filters.
151
+ ```
152
+
153
+ Only visible collections are public. Hidden or missing collection filters return
154
+ 404. `featured` and `displayOrder` are editorial settings, not sales rankings.
155
+ A collection does not belong to a category: its products can span categories, or a
156
+ frontend can intersect both filters. Storefront sections and page layout stay in
157
+ the frontend.
158
+
159
+ Merchant collection edits use the version returned by the management API:
160
+
161
+ ```ts
162
+ await merchant.collections.update({
163
+ id: collection._id,
164
+ data: { name: 'Summer favourites' },
165
+ version: collection.__v,
166
+ });
167
+ ```
168
+
169
+ Arc Next sends `If-Match` for this version. A 412 response means another editor
170
+ changed the collection: reload and review the latest record before saving again.
171
+ Keep the original version with an open draft; do not silently retry with a newer
172
+ version.
173
+
174
+ Quantity inventory is documented in [management.md](management.md#stock).
@@ -1,4 +1,3 @@
1
- //#region src/category-tree.d.ts
2
1
  export interface CategoryBranch {
3
2
  name: string;
4
3
  slug: string;
@@ -16,5 +15,4 @@ export declare function categoryNavigation(rows: readonly {
16
15
  name: string;
17
16
  slug: string;
18
17
  parent?: string | null;
19
- }[]): CategoryBranch[];
20
- //#endregion
18
+ }[]): CategoryBranch[];
@@ -1,4 +1,4 @@
1
- //#region src/category-tree.ts
1
+ // src/category-tree.ts
2
2
  /** Flatten a category tree into breadcrumb choices, excluding a complete subtree when editing its parent. */
3
3
  function categoryChoices(nodes, excludeSlug) {
4
4
  function visit(items, path, seen) {
@@ -35,5 +35,4 @@ function categoryNavigation(rows) {
35
35
  }
36
36
  return roots;
37
37
  }
38
- //#endregion
39
38
  export { categoryChoices, categoryNavigation };
@@ -1,4 +1,3 @@
1
- //#region src/delivery-types.d.ts
2
1
  export interface DeliveryRules {
3
2
  mode: 'flat' | 'zones';
4
3
  zones: Array<{
@@ -44,5 +43,4 @@ export interface DeliveryQuote {
44
43
  currency: string;
45
44
  zone?: string;
46
45
  freeDelivery?: boolean;
47
- }
48
- //#endregion
46
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { DeliveryArea, DeliveryDistrict, DeliveryQuote, DeliveryQuoteInput } from "./delivery-types.js";
2
- import { a as StorefrontCategory, c as StorefrontProductPage, d as StorefrontRequestOptions, i as GuestOrderAccess, l as StorefrontProductQuery, n as GuestCheckoutOptions, o as StorefrontOrder, r as GuestCheckoutResult, s as StorefrontProduct, t as GuestCheckoutInput, u as StorefrontProfile } from "./types-C0cSi6Pk.js";
2
+ import { a as StorefrontCategory, c as StorefrontCollectionQuery, d as StorefrontProductPage, f as StorefrontProductQuery, i as GuestOrderAccess, l as StorefrontOrder, m as StorefrontRequestOptions, n as GuestCheckoutOptions, o as StorefrontCollection, p as StorefrontProfile, r as GuestCheckoutResult, s as StorefrontCollectionPage, t as GuestCheckoutInput, u as StorefrontProduct } from "./types-Bz-eFZb2.js";
3
3
  import { ArcClientConfig } from "@classytic/arc-next/client";
4
- //#region src/index.d.ts
5
4
  export interface StorefrontClientOptions {
6
5
  /** Full API mount, for example https://api.example.com/api/v1. */
7
6
  apiUrl: string;
@@ -14,6 +13,8 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
14
13
  profile: (request?: StorefrontRequestOptions) => Promise<StorefrontProfile>;
15
14
  products: (query?: StorefrontProductQuery, request?: StorefrontRequestOptions) => Promise<StorefrontProductPage>;
16
15
  product: (slug: string, request?: StorefrontRequestOptions) => Promise<StorefrontProduct>;
16
+ collections: (query?: StorefrontCollectionQuery, request?: StorefrontRequestOptions) => Promise<StorefrontCollectionPage>;
17
+ collection: (slug: string, request?: StorefrontRequestOptions) => Promise<StorefrontCollection>;
17
18
  categories: (request?: StorefrontRequestOptions) => Promise<StorefrontCategory[]>;
18
19
  deliveryDistricts: (request?: StorefrontRequestOptions) => Promise<{
19
20
  requiresArea: boolean;
@@ -26,5 +27,4 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
26
27
  checkout: (input: GuestCheckoutInput, request: GuestCheckoutOptions) => Promise<GuestCheckoutResult>;
27
28
  orderStatus: (access: GuestOrderAccess, request?: StorefrontRequestOptions) => Promise<StorefrontOrder>;
28
29
  };
29
- export type StorefrontClient = ReturnType<typeof createStorefrontClient>;
30
- //#endregion
30
+ export type StorefrontClient = ReturnType<typeof createStorefrontClient>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BaseApi } from "@classytic/arc-next/api";
2
2
  import { createClient } from "@classytic/arc-next/client";
3
- //#region src/index.ts
3
+ // src/index.ts
4
4
  function segment(value) {
5
5
  if (!value || value === "." || value === "..") throw new Error("A nonempty resource identifier is required.");
6
6
  return encodeURIComponent(value);
@@ -33,6 +33,11 @@ function createStorefrontClient(options) {
33
33
  options: request
34
34
  }),
35
35
  product: (slug, request = {}) => catalog.request("GET", `${catalog.baseUrl}/products/${segment(slug)}`, { options: request }),
36
+ collections: (query = {}, request = {}) => catalog.request("GET", `${catalog.baseUrl}/collections`, {
37
+ params: { ...query },
38
+ options: request
39
+ }),
40
+ collection: (slug, request = {}) => catalog.request("GET", `${catalog.baseUrl}/collections/${segment(slug)}`, { options: request }),
36
41
  categories: async (request = {}) => {
37
42
  return (await catalog.request("GET", `${catalog.baseUrl}/categories`, { options: request })).data;
38
43
  },
@@ -62,5 +67,4 @@ function createStorefrontClient(options) {
62
67
  } })
63
68
  };
64
69
  }
65
- //#endregion
66
70
  export { createStorefrontClient };
@@ -1,8 +1,10 @@
1
- import { o as StorefrontOrder, s as StorefrontProduct } from "./types-C0cSi6Pk.js";
1
+ import { l as StorefrontOrder, u as StorefrontProduct } from "./types-Bz-eFZb2.js";
2
2
  import { Address } from "@classytic/primitives/address";
3
3
  import { Money } from "@classytic/primitives/money";
4
- //#region src/management-types.d.ts
5
4
  export interface ProductInput {
5
+ inventory?: {
6
+ stockTracked: boolean;
7
+ };
6
8
  name: string;
7
9
  slug: string;
8
10
  productType: 'physical';
@@ -36,6 +38,7 @@ export interface ProductInput {
36
38
  }>;
37
39
  }
38
40
  export interface MerchantProduct extends ProductInput {
41
+ stock?: ProductStock;
39
42
  _id: string;
40
43
  updatedAt: string;
41
44
  }
@@ -63,6 +66,26 @@ export interface MerchantCategoryTree extends MerchantCategory {
63
66
  children: MerchantCategoryTree[];
64
67
  totalProductCount: number;
65
68
  }
69
+ export interface CollectionInput {
70
+ name: string;
71
+ /** Omit to derive from the name. Permanent after creation. */
72
+ slug?: string;
73
+ description?: string;
74
+ image?: {
75
+ url: string;
76
+ alt?: string;
77
+ } | null;
78
+ isActive?: boolean;
79
+ featured?: boolean;
80
+ displayOrder?: number;
81
+ }
82
+ export type CollectionUpdate = Partial<Omit<CollectionInput, 'slug'>>;
83
+ export interface MerchantCollection extends CollectionInput {
84
+ _id: string;
85
+ /** Pass the version read by the editor to collections.update({ version }). */
86
+ __v: number;
87
+ slug: string;
88
+ }
66
89
  export interface MerchantOrder extends StorefrontOrder {
67
90
  _id: string;
68
91
  customerSnapshot?: {
@@ -227,4 +250,17 @@ export interface MerchantOverview {
227
250
  orderValue: number;
228
251
  }>;
229
252
  }
230
- //#endregion
253
+ export interface ProductStock {
254
+ productId: string;
255
+ tracked: boolean;
256
+ items: Array<{
257
+ variantSku?: string;
258
+ attributes: Record<string, string>;
259
+ quantity: number;
260
+ }>;
261
+ }
262
+ export interface StockCountInput {
263
+ variantSku?: string;
264
+ quantity: number;
265
+ expectedQuantity: number;
266
+ }
@@ -1,8 +1,7 @@
1
1
  import { DeliveryDistrict, DeliverySettings, DeliverySettingsUpdate } from "./delivery-types.js";
2
- import { AnalyticsWindow, ApiKeyInput, ApiKeyOptions, CategoryInput, IntegrationProvider, IssuedApiKey, MerchantApiKey, MerchantCategory, MerchantCategoryTree, MerchantIntegration, MerchantMedia, MerchantOrder, MerchantOverview, MerchantProduct, NotificationDelivery, OrderCommand, ProductInput, StoreSettings } from "./management-types.js";
2
+ import { AnalyticsWindow, ApiKeyInput, ApiKeyOptions, CategoryInput, CollectionInput, IntegrationProvider, IssuedApiKey, MerchantApiKey, MerchantCategory, MerchantCategoryTree, MerchantCollection, MerchantIntegration, MerchantMedia, MerchantOrder, MerchantOverview, MerchantProduct, NotificationDelivery, OrderCommand, ProductInput, ProductStock, StockCountInput, StoreSettings } from "./management-types.js";
3
3
  import { BaseApi } from "@classytic/arc-next/api";
4
4
  import { ArcClientConfig } from "@classytic/arc-next/client";
5
- //#region src/management.d.ts
6
5
  export interface MerchantClientOptions {
7
6
  apiUrl: string;
8
7
  token: string;
@@ -44,7 +43,16 @@ export declare function createMerchantClient(options: MerchantClientOptions): {
44
43
  update: (data: DeliverySettingsUpdate) => Promise<DeliverySettings>;
45
44
  districts(signal?: AbortSignal): Promise<DeliveryDistrict[]>;
46
45
  };
46
+ stock: {
47
+ read: (productId: string, signal?: AbortSignal) => Promise<ProductStock>;
48
+ query(productIds: string[], signal?: AbortSignal): Promise<ProductStock[]>;
49
+ setQuantity(productId: string, data: StockCountInput, idempotencyKey: string): Promise<{
50
+ productId: string;
51
+ quantity: number;
52
+ }>;
53
+ };
47
54
  products: BaseApi<MerchantProduct, ProductInput, Partial<ProductInput>>;
55
+ collections: BaseApi<MerchantCollection, CollectionInput, Partial<Omit<CollectionInput, "slug">>>;
48
56
  categories: BaseApi<MerchantCategory, CategoryInput, Partial<Omit<CategoryInput, "slug">>> & {
49
57
  tree: (options?: {
50
58
  signal?: AbortSignal;
@@ -92,9 +100,8 @@ export declare function createMerchantClient(options: MerchantClientOptions): {
92
100
  id: string;
93
101
  options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
94
102
  }) => Promise<import("@classytic/repo-core/repository").DeleteResult>;
95
- upload: (file: File, folder?: 'products' | 'categories') => Promise<MerchantMedia>;
96
- uploadImage(file: File, folder?: 'products' | 'categories'): Promise<MerchantMedia>;
103
+ upload: (file: File, folder?: 'products' | 'categories' | 'collections') => Promise<MerchantMedia>;
104
+ uploadImage(file: File, folder?: 'products' | 'categories' | 'collections'): Promise<MerchantMedia>;
97
105
  };
98
106
  };
99
- export type MerchantClient = ReturnType<typeof createMerchantClient>;
100
- //#endregion
107
+ export type MerchantClient = ReturnType<typeof createMerchantClient>;
@@ -1,6 +1,39 @@
1
1
  import { BaseApi } from "@classytic/arc-next/api";
2
2
  import { createClient } from "@classytic/arc-next/client";
3
- //#region src/management-delivery.ts
3
+ // src/management-stock.ts
4
+ function createMerchantStock(client) {
5
+ const api = new BaseApi("inventory/products", {
6
+ client,
7
+ basePath: "",
8
+ cache: "no-store"
9
+ });
10
+ const id = (value) => {
11
+ if (!/^[a-f\d]{24}$/i.test(value)) throw new Error("A product ID is required.");
12
+ return value;
13
+ };
14
+ return { stock: {
15
+ read: (productId, signal) => api.request("GET", `${api.baseUrl}/${id(productId)}`, { options: { signal } }),
16
+ async query(productIds, signal) {
17
+ if (!productIds.length) return [];
18
+ if (productIds.length > 50) throw new Error("Read stock for at most 50 products at once.");
19
+ return (await api.request("POST", `${api.baseUrl}/query`, {
20
+ data: { productIds: productIds.map(id) },
21
+ options: { signal }
22
+ })).data;
23
+ },
24
+ setQuantity(productId, data, idempotencyKey) {
25
+ if (!idempotencyKey.trim()) throw new Error("Preserve an explicit idempotency key for stock retries.");
26
+ return api.request("POST", `${api.baseUrl}/${id(productId)}/action`, {
27
+ data: {
28
+ ...data,
29
+ action: "set_quantity"
30
+ },
31
+ options: { headerOptions: { "Idempotency-Key": idempotencyKey } }
32
+ });
33
+ }
34
+ } };
35
+ }
36
+ // src/management-delivery.ts
4
37
  function createMerchantDelivery(client) {
5
38
  const api = new BaseApi("stores/delivery", {
6
39
  client,
@@ -15,8 +48,7 @@ function createMerchantDelivery(client) {
15
48
  }
16
49
  } };
17
50
  }
18
- //#endregion
19
- //#region src/management-access.ts
51
+ // src/management-access.ts
20
52
  function key(value) {
21
53
  if (!value || value === "." || value === "..") throw new Error("An identifier is required.");
22
54
  return encodeURIComponent(value);
@@ -60,8 +92,7 @@ function createMerchantAccess(client) {
60
92
  }
61
93
  };
62
94
  }
63
- //#endregion
64
- //#region src/management-media.ts
95
+ // src/management-media.ts
65
96
  function createMerchantMedia(client, fetcher) {
66
97
  const media = new BaseApi("media", {
67
98
  client,
@@ -151,8 +182,7 @@ function createMerchantMedia(client, fetcher) {
151
182
  }
152
183
  };
153
184
  }
154
- //#endregion
155
- //#region src/management.ts
185
+ // src/management.ts
156
186
  function orderKey(value) {
157
187
  if (!value || value === "." || value === "..") throw new Error("An order number is required.");
158
188
  return encodeURIComponent(value);
@@ -183,8 +213,10 @@ function createMerchantClient(options) {
183
213
  const stores = new BaseApi("stores", config);
184
214
  return {
185
215
  ...createMerchantAccess(client),
216
+ ...createMerchantStock(client),
186
217
  ...createMerchantDelivery(client),
187
218
  products,
219
+ collections: new BaseApi("collections", config),
188
220
  categories: Object.assign(categories, { tree: (options = {}) => categories.request("GET", `${categories.baseUrl}/tree`, { options }) }),
189
221
  analytics: { async overview(window, options = {}) {
190
222
  const from = new Date(window.from);
@@ -261,5 +293,4 @@ function createMerchantClient(options) {
261
293
  media: createMerchantMedia(client, fetcher)
262
294
  };
263
295
  }
264
- //#endregion
265
296
  export { createMerchantClient };
@@ -1,4 +1,3 @@
1
- //#region src/platform.d.ts
2
1
  export type OrganizationStatus = 'pending' | 'active' | 'rejected' | 'suspended';
3
2
  export type OrganizationAction = 'approve' | 'reject' | 'suspend' | 'reactivate';
4
3
  export interface PlatformOrganization {
@@ -32,5 +31,4 @@ export declare function createPlatformClient({ apiUrl, token }: {
32
31
  note: string;
33
32
  }): Promise<PlatformOrganization>;
34
33
  };
35
- };
36
- //#endregion
34
+ };
package/dist/platform.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { BaseApi } from "@classytic/arc-next/api";
2
2
  import { createClient } from "@classytic/arc-next/client";
3
- //#region src/platform.ts
3
+ // src/platform.ts
4
4
  /** Platform administration uses a bearer identity without selecting a merchant organization. */
5
5
  function createPlatformClient({ apiUrl, token }) {
6
6
  if (!token.trim()) throw new Error("A platform bearer token is required.");
@@ -25,5 +25,4 @@ function createPlatformClient({ apiUrl, token }) {
25
25
  }
26
26
  } };
27
27
  }
28
- //#endregion
29
28
  export { createPlatformClient };
@@ -1,8 +1,15 @@
1
1
  import { Address } from "@classytic/primitives/address";
2
2
  import { Money } from "@classytic/primitives/money";
3
- //#region src/types.d.ts
4
3
  /** Public wire projections; merchant-only catalog and buyer fields are absent. */
5
4
  interface StorefrontProduct {
5
+ stock?: {
6
+ tracked: boolean;
7
+ quantity?: number;
8
+ variants?: Array<{
9
+ sku?: string;
10
+ quantity: number;
11
+ }>;
12
+ };
6
13
  id: string;
7
14
  name: string;
8
15
  slug: string;
@@ -45,6 +52,28 @@ interface StorefrontCategory {
45
52
  } | null;
46
53
  displayOrder?: number;
47
54
  }
55
+ interface StorefrontCollection {
56
+ id: string;
57
+ slug: string;
58
+ name: string;
59
+ description?: string;
60
+ image?: {
61
+ url: string;
62
+ alt?: string;
63
+ } | null;
64
+ featured: boolean;
65
+ displayOrder: number;
66
+ }
67
+ interface StorefrontCollectionQuery {
68
+ limit?: number;
69
+ /** Opaque cursor; retain the same featured filter between pages. */
70
+ after?: string;
71
+ featured?: boolean;
72
+ }
73
+ interface StorefrontCollectionPage {
74
+ data: StorefrontCollection[];
75
+ next: string | null;
76
+ }
48
77
  interface StorefrontProductPage {
49
78
  data: StorefrontProduct[];
50
79
  next: string | null;
@@ -124,5 +153,4 @@ interface GuestCheckoutOptions extends StorefrontRequestOptions {
124
153
  /** Create once per checkout attempt; preserve across retries and lost responses. */
125
154
  idempotencyKey: string;
126
155
  }
127
- //#endregion
128
- export { StorefrontCategory as a, StorefrontProductPage as c, StorefrontRequestOptions as d, GuestOrderAccess as i, StorefrontProductQuery as l, GuestCheckoutOptions as n, StorefrontOrder as o, GuestCheckoutResult as r, StorefrontProduct as s, GuestCheckoutInput as t, StorefrontProfile as u };
156
+ export { StorefrontCategory as a, StorefrontCollectionQuery as c, StorefrontProductPage as d, StorefrontProductQuery as f, GuestOrderAccess as i, StorefrontOrder as l, StorefrontRequestOptions as m, GuestCheckoutOptions as n, StorefrontCollection as o, StorefrontProfile as p, GuestCheckoutResult as r, StorefrontCollectionPage as s, GuestCheckoutInput as t, StorefrontProduct as u };
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as StorefrontCategory, c as StorefrontProductPage, d as StorefrontRequestOptions, i as GuestOrderAccess, l as StorefrontProductQuery, n as GuestCheckoutOptions, o as StorefrontOrder, r as GuestCheckoutResult, s as StorefrontProduct, t as GuestCheckoutInput, u as StorefrontProfile } from "./types-C0cSi6Pk.js";
2
- export { GuestCheckoutInput, GuestCheckoutOptions, GuestCheckoutResult, GuestOrderAccess, StorefrontCategory, StorefrontOrder, StorefrontProduct, StorefrontProductPage, StorefrontProductQuery, StorefrontProfile, StorefrontRequestOptions };
1
+ import { a as StorefrontCategory, c as StorefrontCollectionQuery, d as StorefrontProductPage, f as StorefrontProductQuery, i as GuestOrderAccess, l as StorefrontOrder, m as StorefrontRequestOptions, n as GuestCheckoutOptions, o as StorefrontCollection, p as StorefrontProfile, r as GuestCheckoutResult, s as StorefrontCollectionPage, t as GuestCheckoutInput, u as StorefrontProduct } from "./types-Bz-eFZb2.js";
2
+ export { GuestCheckoutInput, GuestCheckoutOptions, GuestCheckoutResult, GuestOrderAccess, StorefrontCategory, StorefrontCollection, StorefrontCollectionPage, StorefrontCollectionQuery, StorefrontOrder, StorefrontProduct, StorefrontProductPage, StorefrontProductQuery, StorefrontProfile, StorefrontRequestOptions };
package/management.md CHANGED
@@ -98,3 +98,60 @@ the media resource's guarded delete; an in-use image is refused by the server.
98
98
  through media-transform before requesting storage writes. The server must accept
99
99
  every output format in its policy, including the browser encoder's fallback.
100
100
  Passthrough thresholds and dimensions belong to that policy, rather than the UI.
101
+
102
+ ## Managed collections
103
+
104
+ ```ts
105
+ const collection = await merchant.collections.create({ data: {
106
+ name: 'Eid picks', // slug is generated when omitted
107
+ description: 'Pieces for the celebrations.',
108
+ isActive: true,
109
+ featured: true,
110
+ displayOrder: 10,
111
+ } });
112
+ await merchant.products.update({ id: productId, data: {
113
+ collections: [collection.slug], // replaces this product's memberships
114
+ } });
115
+ await merchant.collections.update({ id: collection._id, data: {
116
+ name: 'Our Eid edit', // preserves its permanent slug
117
+ } });
118
+ ```
119
+
120
+ Collections use the same store-bound bearer client and Arc Next CRUD/pagination
121
+ contract as products. Create a new client when switching stores. Slug uniqueness
122
+ is per store. An unknown or foreign collection cannot be assigned. A collection
123
+ cannot be deleted until products release its slug; hide it instead when keeping
124
+ memberships. Upload cover images with `merchant.media.uploadImage(file,
125
+ 'collections')` and pass `{ url, alt }` as `image`.
126
+
127
+ Existing string labels require the Brihot server's `collections:migrate` script
128
+ before switching an existing storefront to managed collection navigation. It is a
129
+ per-store dry run by default; it creates metadata without rewriting memberships.
130
+
131
+ ## Stock
132
+
133
+ Brihot uses available quantities per product/variant. New products default to
134
+ tracked; existing products keep their saved setting. Untracked products have no
135
+ quantity limit. Set counts before enabling tracking on an existing product.
136
+
137
+ ```ts
138
+ const stock = await merchant.stock.read(productId);
139
+ const item = stock.items[0];
140
+ await merchant.stock.setQuantity(productId, {
141
+ variantSku: item.variantSku, // omit for a simple product
142
+ quantity: 12,
143
+ expectedQuantity: item.quantity,
144
+ }, crypto.randomUUID()); // create once; reuse on a transport retry
145
+ await merchant.products.update({ id: productId, data: {
146
+ inventory: { stockTracked: true },
147
+ } });
148
+ ```
149
+
150
+ Use `stock.query(productIds)` for a table page (maximum 50 products), rather than
151
+ one request per row. A stale count returns 409; reload before submitting a new
152
+ count. Checkout deducts atomically and refuses insufficient stock. Canceling an
153
+ unshipped order restores its quantity once; shipping does not deduct again.
154
+
155
+ Storefront products optionally expose `stock: { tracked, quantity, variants }`.
156
+ Use a selected variant's quantity, disable sold-out options and cap the quantity
157
+ input. These values inform the UI; checkout always validates current stock.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/commerce-client",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Typed storefront and merchant clients for the Classytic Commerce API",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -63,7 +63,7 @@
63
63
  "esbuild": "^0.28.1",
64
64
  "publint": "^0.3.25",
65
65
  "tsdown": "^0.23.1",
66
- "typescript": "^7.0.2",
66
+ "typescript": "7.1.0-beta",
67
67
  "vitest": "^5.0.5"
68
68
  },
69
69
  "publishConfig": {