@classytic/commerce-client 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-20
4
+
5
+ First publish.
6
+
7
+ Expose storefront tag/collection/sale/price/sort queries and public taxonomy/compare-at prices; type merchant taxonomy and compare-at writes.
8
+
9
+ Add typed category trees, hierarchy choices, parent/description/image fields and category image uploads. Category updates exclude immutable slugs.
10
+
11
+ API-key lifetime options expose optional non-expiry support; creation accepts explicit `expiresInDays: null`.
12
+
13
+ Delivery quote inputs include destination country; the storefront sends it when requesting prices.
14
+
15
+ Add a bounded store sales overview and tenant-scoped delivery settings. Public storefront clients can load delivery districts/areas and request server-priced delivery quotes without merchant credentials.
16
+
17
+ Typed store profiles, catalog reads, guest checkout with explicit retry keys, and private order tracking.
18
+
19
+ The isolated `/management` entry supports merchant catalog/media, order commands
20
+ and cash collection, store settings, Telegram connections and delivery history,
21
+ plus API-key options, creation, rotation and revocation.
package/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Classytic Source-Available License (Community & Commercial), Version 1.0
2
+
3
+ Copyright (c) 2026 Classytic LLC. All rights reserved.
4
+
5
+ This software is source-available, not open source. Please read this License
6
+ before using it.
7
+
8
+ 1. DEFINITIONS
9
+ "Software" means the contents of this package and any accompanying source
10
+ code, object code, or documentation, in any form.
11
+ "Licensor" means Classytic LLC, a limited liability company organized in the
12
+ United States.
13
+ "You" means the individual or legal entity exercising rights under this
14
+ License.
15
+ "Evaluation Use" means use for evaluation, development, testing,
16
+ experimentation, or internal non-commercial purposes only.
17
+ "Production Use" means any use of the Software that is live, commercial,
18
+ revenue-generating, or customer-facing, or any use other than Evaluation Use.
19
+ "Commercial License" means a separate written agreement executed with
20
+ Licensor that grants Production Use rights.
21
+
22
+ 2. OWNERSHIP
23
+ The Software is licensed, not sold. Licensor retains all right, title, and
24
+ interest in and to the Software, including all intellectual property rights.
25
+ All rights not expressly granted in this License are reserved by Licensor.
26
+
27
+ 3. EVALUATION GRANT
28
+ Subject to Your compliance with this License, Licensor grants You a
29
+ worldwide, royalty-free, non-exclusive, non-transferable, revocable license
30
+ to download, install, run, and modify the Software solely for Evaluation Use.
31
+
32
+ 4. PRODUCTION USE REQUIRES A COMMERCIAL LICENSE
33
+ Production Use of the Software is NOT permitted under this License and
34
+ requires a Commercial License from Licensor. A Commercial License may be
35
+ granted at Licensor's discretion, with or without a fee, and may confer
36
+ perpetual, irrevocable, and transferable Production Use rights as set out in
37
+ that agreement. Contact licensing@classytic.com to obtain one.
38
+
39
+ 5. RESTRICTIONS
40
+ You may not:
41
+ (a) make any Production Use of the Software without a Commercial License;
42
+ (b) redistribute, resell, rent, sublicense, or otherwise make the Software
43
+ (in whole or in part, original or modified) available to third parties as
44
+ a stand-alone product, framework, library, or hosted/managed service that
45
+ competes with the Software or with Licensor;
46
+ (c) remove, obscure, or alter any copyright, license, or attribution notice.
47
+
48
+ 6. FEEDBACK AND CONTRIBUTIONS
49
+ Any feedback, suggestions, or contributions You provide relating to the
50
+ Software may be used by Licensor for any purpose without obligation to You.
51
+
52
+ 7. TERMINATION
53
+ This License and the Evaluation Grant terminate automatically if You breach
54
+ any term. Upon termination You must cease all use of the Software and destroy
55
+ all copies in Your possession. Rights under a Commercial License are governed
56
+ by that agreement's own terms and survive termination of this License to the
57
+ extent stated there.
58
+
59
+ 8. DISCLAIMER OF WARRANTY
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
63
+
64
+ 9. LIMITATION OF LIABILITY
65
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE
66
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
68
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69
+
70
+ 10. GOVERNING LAW
71
+ This License is governed by the laws of the State of Delaware, United States,
72
+ without regard to its conflict-of-laws principles. (Adjust to Your LLC's state
73
+ of formation.)
74
+
75
+ For commercial licensing, contact: licensing@classytic.com
package/README.md CHANGED
@@ -1,3 +1,130 @@
1
- # Temporary Holding Version
1
+ # @classytic/commerce-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@classytic/commerce-client` calls the shared Spine storefront resources.
4
+ It exposes profile, catalog, guest checkout and order tracking. The backend must
5
+ compose those resources, as Classytic Commerce does; the ERP's older checkout endpoints have
6
+ a different contract.
7
+
8
+ Install with `npm install @classytic/commerce-client`.
9
+
10
+ Use Arc Next 0.17 or later. This entry uses its server-safe client/API subpaths;
11
+ it imports no React hooks, Next.js APIs, merchant credentials or server kernels.
12
+
13
+ ```ts
14
+ import { createStorefrontClient } from '@classytic/commerce-client';
15
+ import type { GuestCheckoutInput } from '@classytic/commerce-client/types';
16
+
17
+ const shop = createStorefrontClient({
18
+ apiUrl: 'http://localhost:4140/api/v1', // Full API mount, including its prefix.
19
+ store: 'store-alpha',
20
+ });
21
+ const profile = await shop.profile();
22
+ const page = await shop.products({ limit: 20, category: 'mugs' });
23
+ const next = page.next ? await shop.products({ after: page.next, limit: 20, category: 'mugs' }) : null;
24
+ const categories = await shop.categories();
25
+ // category=clothing includes products assigned to its descendants.
26
+ const product = await shop.product('blue-mug');
27
+ ```
28
+
29
+ Each instance captures its API mount and store. Dashboard `configureClient` and
30
+ `configureAuth` settings cannot change its routing or add credentials. Cookies
31
+ are omitted. Construct separate instances for separate stores or SSR requests.
32
+
33
+ ## Checkout from a frontend cart
34
+
35
+ ```ts
36
+ const input: GuestCheckoutInput = {
37
+ lines: [{ productId: product.id, quantity: 2 }],
38
+ customer: { name: 'Buyer', phone: '01712345678' },
39
+ shippingAddress: { line1: 'Road 4', city: 'Dhaka', country: 'BD' },
40
+ };
41
+ // Create once for this attempt, outside any retry handler.
42
+ const idempotencyKey = crypto.randomUUID();
43
+ const placed = await shop.checkout(input, { idempotencyKey });
44
+ const status = await shop.orderStatus(placed);
45
+ ```
46
+
47
+ Keep the input and key unchanged when retrying a lost response. Use a new key
48
+ when the buyer intentionally starts another order. The SDK does not generate a
49
+ fallback key. The server resolves prices, publication, variants, delivery charges
50
+ and totals. Multiple active variants require `variantSku` on each relevant line.
51
+ Do not send prices or lifecycle/payment state.
52
+
53
+ Money stays in integer minor units. Display it using the primitives money helpers;
54
+ do not assume every currency has two decimal places. This client does no pricing
55
+ arithmetic and stores no cart.
56
+
57
+ The checkout result includes an access token bound to that store and order. Keep
58
+ it privately for the buyer; the SDK puts it in `x-order-access`, never the URL.
59
+ It does not persist tokens or place them in logs. Status reads use `no-store` and
60
+ omit buyer contact/address. Shipment and delivery are fulfillment facts; delivery
61
+ alone does not establish COD collection.
62
+
63
+ ## Requests and errors
64
+
65
+ Read methods accept an optional `{ signal }`; checkout takes it alongside its
66
+ required `idempotencyKey`. The client timeout defaults to 15 seconds and can be
67
+ configured with `timeoutMs`. A custom `fetch` transport is available for embedding
68
+ and tests; ordinary browser/Node consumers use the runtime's fetch.
69
+
70
+ ```ts
71
+ import { isArcApiError } from '@classytic/arc-next/client';
72
+
73
+ try {
74
+ await shop.checkout(input, { idempotencyKey });
75
+ } catch (error) {
76
+ if (isArcApiError(error)) {
77
+ // Inspect status/code/fieldErrors; preserve the attempt key for a retry.
78
+ console.error(error.code, error.fieldErrors);
79
+ }
80
+ }
81
+ ```
82
+
83
+ Arc Next owns HTTP parsing, timeouts and the canonical error contract. Category
84
+ reads return the data array; product pages retain their cursor metadata.
85
+ The server remains the authority for validation;
86
+ unknown filters and extra checkout fields fail there.
87
+
88
+ Products use bounded cursor pages. Categories currently return at most 100 active
89
+ entries. Merchant catalog/order CRUD, integrations, key management and React hooks
90
+ are outside this public client.
91
+
92
+ `products({ q, category, limit, after })` searches product names literally,
93
+ case-insensitively, inside the selected store’s published catalog. Search terms
94
+ are limited to 100 characters. Pagination preserves the same search/category
95
+ filters; `next: null` marks the final page.
96
+
97
+ ## Merchant dashboard and backend clients
98
+
99
+ Use the separate [`/management` entry](management.md) for authenticated products,
100
+ categories, media, order commands, store settings, Telegram connections,
101
+ notification delivery history and API keys. The root entry remains isolated for
102
+ public storefronts.
103
+
104
+ Delivery quotes accept `country` alongside `lines` and `areaId`. Send the destination
105
+ country when known so the preview applies the same country restriction as checkout.
106
+
107
+ `/category-tree` exports framework-independent `categoryNavigation` and
108
+ `categoryChoices` helpers. Public categories include `parent`, `parentPath`,
109
+ `description`, `image` and `displayOrder`; navigation omits children whose parents
110
+ are absent from the public category list.
111
+
112
+ ## Storefront product filters
113
+
114
+ ```ts
115
+ const featured = await shop.products({ collection: 'featured', limit: 8 });
116
+ const summerSale = await shop.products({ tag: 'summer', onSale: true, limit: 8 });
117
+ const affordable = await shop.products({ category: 'clothing', minPrice: 0, maxPrice: 100000, sort: 'price-asc' });
118
+ ```
119
+
120
+ Filters combine with AND. A tag or collection matches its exact stored name.
121
+ `category` includes descendants; `q` searches product names literally. Price bounds
122
+ are inclusive integer minor units on the product's base price; variant adjustments
123
+ are not part of that range. Sort values are `newest` (default), `price-asc` and
124
+ `price-desc`. Price sorting includes products with a numeric base price.
125
+
126
+ `next` is an opaque Mongokit cursor: pass it back as `after` with the same filters
127
+ and sort. Restart pagination when filters change. Public calls always enforce the
128
+ resolved store and published/priced visibility, and reject unknown query fields.
129
+ Merchant `products.getAll({ params })` continues to use Arc's normal allowed query
130
+ syntax, including tags, collections and base-price bracket filters.
@@ -0,0 +1,20 @@
1
+ //#region src/category-tree.d.ts
2
+ export interface CategoryBranch {
3
+ name: string;
4
+ slug: string;
5
+ children?: readonly CategoryBranch[];
6
+ }
7
+ export interface CategoryChoice {
8
+ value: string;
9
+ label: string;
10
+ depth: number;
11
+ }
12
+ /** Flatten a category tree into breadcrumb choices, excluding a complete subtree when editing its parent. */
13
+ export declare function categoryChoices(nodes: readonly CategoryBranch[], excludeSlug?: string): CategoryChoice[];
14
+ /** Group the public flat projection into a navigation tree; children of missing parents stay unlisted. */
15
+ export declare function categoryNavigation(rows: readonly {
16
+ name: string;
17
+ slug: string;
18
+ parent?: string | null;
19
+ }[]): CategoryBranch[];
20
+ //#endregion
@@ -0,0 +1,39 @@
1
+ //#region src/category-tree.ts
2
+ /** Flatten a category tree into breadcrumb choices, excluding a complete subtree when editing its parent. */
3
+ function categoryChoices(nodes, excludeSlug) {
4
+ function visit(items, path, seen) {
5
+ return items.flatMap((item) => {
6
+ if (item.slug === excludeSlug) return [];
7
+ if (seen.has(item.slug) || path.length > 20) throw new Error("Invalid category hierarchy.");
8
+ const branch = [...path, item.name];
9
+ return [{
10
+ value: item.slug,
11
+ label: branch.join(" / "),
12
+ depth: path.length
13
+ }, ...visit(item.children ?? [], branch, /* @__PURE__ */ new Set([...seen, item.slug]))];
14
+ });
15
+ }
16
+ return visit(nodes, [], /* @__PURE__ */ new Set());
17
+ }
18
+ /** Group the public flat projection into a navigation tree; children of missing parents stay unlisted. */
19
+ function categoryNavigation(rows) {
20
+ const nodes = /* @__PURE__ */ new Map();
21
+ for (const row of rows) {
22
+ if (nodes.has(row.slug)) throw new Error("Duplicate category slug.");
23
+ nodes.set(row.slug, {
24
+ name: row.name,
25
+ slug: row.slug,
26
+ children: []
27
+ });
28
+ }
29
+ const roots = [];
30
+ for (const row of rows) {
31
+ const node = nodes.get(row.slug);
32
+ if (!node) throw new Error("Category navigation is incomplete.");
33
+ if (row.parent) nodes.get(row.parent)?.children.push(node);
34
+ else roots.push(node);
35
+ }
36
+ return roots;
37
+ }
38
+ //#endregion
39
+ export { categoryChoices, categoryNavigation };
@@ -0,0 +1,48 @@
1
+ //#region src/delivery-types.d.ts
2
+ export interface DeliveryRules {
3
+ mode: 'flat' | 'zones';
4
+ zones: Array<{
5
+ name: string;
6
+ chargeMinor: number;
7
+ districts: string[];
8
+ }>;
9
+ freeOverMinor: number | null;
10
+ }
11
+ export interface DeliverySettings {
12
+ deliveryChargeMinor: number;
13
+ deliveryRules: DeliveryRules;
14
+ version: number | null;
15
+ }
16
+ export interface DeliverySettingsUpdate {
17
+ deliveryChargeMinor: number;
18
+ deliveryRules: DeliveryRules;
19
+ expectedVersion: number | null;
20
+ }
21
+ export interface DeliveryDistrict {
22
+ id: string;
23
+ name: string;
24
+ divisionId: string;
25
+ divisionName: string;
26
+ }
27
+ export interface DeliveryArea {
28
+ id: number;
29
+ name: string;
30
+ districtName: string;
31
+ }
32
+ export interface DeliveryQuoteInput {
33
+ lines: Array<{
34
+ productId: string;
35
+ variantSku?: string;
36
+ quantity: number;
37
+ }>;
38
+ areaId?: number;
39
+ country?: string;
40
+ }
41
+ export interface DeliveryQuote {
42
+ subtotalMinor: number;
43
+ deliveryChargeMinor: number;
44
+ currency: string;
45
+ zone?: string;
46
+ freeDelivery?: boolean;
47
+ }
48
+ //#endregion
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
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";
3
+ import { ArcClientConfig } from "@classytic/arc-next/client";
4
+ //#region src/index.d.ts
5
+ export interface StorefrontClientOptions {
6
+ /** Full API mount, for example https://api.example.com/api/v1. */
7
+ apiUrl: string;
8
+ store: string;
9
+ timeoutMs?: number;
10
+ fetch?: ArcClientConfig['fetch'];
11
+ }
12
+ /** Build an isolated guest client; never inherit dashboard credentials or global routing. */
13
+ export declare function createStorefrontClient(options: StorefrontClientOptions): {
14
+ profile: (request?: StorefrontRequestOptions) => Promise<StorefrontProfile>;
15
+ products: (query?: StorefrontProductQuery, request?: StorefrontRequestOptions) => Promise<StorefrontProductPage>;
16
+ product: (slug: string, request?: StorefrontRequestOptions) => Promise<StorefrontProduct>;
17
+ categories: (request?: StorefrontRequestOptions) => Promise<StorefrontCategory[]>;
18
+ deliveryDistricts: (request?: StorefrontRequestOptions) => Promise<{
19
+ requiresArea: boolean;
20
+ data: DeliveryDistrict[];
21
+ }>;
22
+ deliveryAreas: (district: string, request?: StorefrontRequestOptions) => Promise<{
23
+ data: DeliveryArea[];
24
+ }>;
25
+ quoteDelivery: (input: DeliveryQuoteInput, request?: StorefrontRequestOptions) => Promise<DeliveryQuote>;
26
+ checkout: (input: GuestCheckoutInput, request: GuestCheckoutOptions) => Promise<GuestCheckoutResult>;
27
+ orderStatus: (access: GuestOrderAccess, request?: StorefrontRequestOptions) => Promise<StorefrontOrder>;
28
+ };
29
+ export type StorefrontClient = ReturnType<typeof createStorefrontClient>;
30
+ //#endregion
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ import { BaseApi } from "@classytic/arc-next/api";
2
+ import { createClient } from "@classytic/arc-next/client";
3
+ //#region src/index.ts
4
+ function segment(value) {
5
+ if (!value || value === "." || value === "..") throw new Error("A nonempty resource identifier is required.");
6
+ return encodeURIComponent(value);
7
+ }
8
+ /** Build an isolated guest client; never inherit dashboard credentials or global routing. */
9
+ function createStorefrontClient(options) {
10
+ const store = options.store;
11
+ const url = new URL(options.apiUrl);
12
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("apiUrl must be an HTTP(S) API mount without credentials, query or fragment.");
13
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(store)) throw new Error("A valid store slug is required.");
14
+ const config = {
15
+ client: createClient({
16
+ baseUrl: url.href.replace(/\/+$/, ""),
17
+ getToken: () => null,
18
+ getOrgId: () => null,
19
+ credentials: "omit",
20
+ autoIdempotency: false,
21
+ timeoutMs: options.timeoutMs ?? 15e3,
22
+ ...options.fetch ? { fetch: options.fetch } : {}
23
+ }),
24
+ basePath: "",
25
+ cache: "no-store"
26
+ };
27
+ const catalog = new BaseApi(`storefront/${segment(store)}`, config);
28
+ const profiles = new BaseApi("stores", config);
29
+ return {
30
+ profile: (request = {}) => profiles.request("GET", `${profiles.baseUrl}/${segment(store)}`, { options: request }),
31
+ products: (query = {}, request = {}) => catalog.request("GET", `${catalog.baseUrl}/products`, {
32
+ params: { ...query },
33
+ options: request
34
+ }),
35
+ product: (slug, request = {}) => catalog.request("GET", `${catalog.baseUrl}/products/${segment(slug)}`, { options: request }),
36
+ categories: async (request = {}) => {
37
+ return (await catalog.request("GET", `${catalog.baseUrl}/categories`, { options: request })).data;
38
+ },
39
+ deliveryDistricts: (request = {}) => catalog.request("GET", `${catalog.baseUrl}/delivery/districts`, { options: request }),
40
+ deliveryAreas: (district, request = {}) => catalog.request("GET", `${catalog.baseUrl}/delivery/areas`, {
41
+ params: { district },
42
+ options: request
43
+ }),
44
+ quoteDelivery: (input, request = {}) => catalog.request("POST", `${catalog.baseUrl}/orders/quote`, {
45
+ data: input,
46
+ options: request
47
+ }),
48
+ checkout: (input, request) => {
49
+ if (!request?.idempotencyKey?.trim()) throw new Error("idempotencyKey is required; preserve it for retries.");
50
+ return catalog.request("POST", `${catalog.baseUrl}/orders`, {
51
+ data: input,
52
+ options: {
53
+ signal: request.signal,
54
+ headerOptions: { "Idempotency-Key": request.idempotencyKey }
55
+ }
56
+ });
57
+ },
58
+ orderStatus: (access, request = {}) => catalog.request("GET", `${catalog.baseUrl}/orders/${segment(access.orderNumber)}/status`, { options: {
59
+ signal: request.signal,
60
+ cache: "no-store",
61
+ headerOptions: { "x-order-access": access.accessToken }
62
+ } })
63
+ };
64
+ }
65
+ //#endregion
66
+ export { createStorefrontClient };
@@ -0,0 +1,225 @@
1
+ import { o as StorefrontOrder, s as StorefrontProduct } from "./types-C0cSi6Pk.js";
2
+ import { Address } from "@classytic/primitives/address";
3
+ import { Money } from "@classytic/primitives/money";
4
+ //#region src/management-types.d.ts
5
+ export interface ProductInput {
6
+ name: string;
7
+ slug: string;
8
+ productType: 'physical';
9
+ status: 'draft' | 'active' | 'archived';
10
+ description?: string;
11
+ categorySlug?: string;
12
+ tags?: string[];
13
+ collections?: string[];
14
+ images?: StorefrontProduct['images'];
15
+ defaultMonetization: {
16
+ type: 'one_time';
17
+ pricing: {
18
+ basePrice: Money;
19
+ currency: string;
20
+ compareAtPrice?: Money;
21
+ };
22
+ };
23
+ variationAttributes?: Array<{
24
+ code: string;
25
+ name: string;
26
+ values: Array<{
27
+ code: string;
28
+ label: string;
29
+ }>;
30
+ }>;
31
+ variants?: Array<{
32
+ sku: string;
33
+ attributes: Record<string, string>;
34
+ priceModifier: number;
35
+ isActive: boolean;
36
+ }>;
37
+ }
38
+ export interface MerchantProduct extends ProductInput {
39
+ _id: string;
40
+ updatedAt: string;
41
+ }
42
+ export interface CategoryInput {
43
+ name: string;
44
+ /** Omit on create to generate from the name. Slugs are immutable after creation. */
45
+ slug?: string;
46
+ parent?: string | null;
47
+ description?: string;
48
+ image?: {
49
+ url: string;
50
+ alt?: string;
51
+ } | null;
52
+ isActive?: boolean;
53
+ displayOrder?: number;
54
+ }
55
+ export type CategoryUpdate = Partial<Omit<CategoryInput, 'slug'>>;
56
+ export interface MerchantCategory extends CategoryInput {
57
+ _id: string;
58
+ slug: string;
59
+ parentPath?: string;
60
+ productCount?: number;
61
+ }
62
+ export interface MerchantCategoryTree extends MerchantCategory {
63
+ children: MerchantCategoryTree[];
64
+ totalProductCount: number;
65
+ }
66
+ export interface MerchantOrder extends StorefrontOrder {
67
+ _id: string;
68
+ customerSnapshot?: {
69
+ name: string;
70
+ phone?: string;
71
+ email?: string;
72
+ };
73
+ shippingAddress?: Address;
74
+ lines: Array<{
75
+ lineId: string;
76
+ quantity: number;
77
+ snapshot: {
78
+ name?: string;
79
+ sku?: string;
80
+ };
81
+ lineTotal?: Money;
82
+ }>;
83
+ paymentState?: {
84
+ chargeStatus: string;
85
+ totalCharged?: Money;
86
+ };
87
+ }
88
+ export type OrderCommand = {
89
+ action: 'confirm' | 'start_processing' | 'ship' | 'deliver';
90
+ } | {
91
+ action: 'cancel';
92
+ reason: string;
93
+ };
94
+ export interface StoreSettings {
95
+ version: number | null;
96
+ currency: string;
97
+ deliveryChargeMinor: number;
98
+ }
99
+ export interface MerchantMedia {
100
+ _id: string;
101
+ url: string;
102
+ filename: string;
103
+ mimeType?: string;
104
+ width?: number;
105
+ height?: number;
106
+ variants?: Array<{
107
+ name: string;
108
+ url: string;
109
+ width: number;
110
+ height: number;
111
+ mimeType: string;
112
+ size: number;
113
+ }>;
114
+ }
115
+ export interface IntegrationProvider {
116
+ service: string;
117
+ provider: string;
118
+ label: string;
119
+ oauth: boolean;
120
+ credentialFields: Array<{
121
+ name: string;
122
+ label: string;
123
+ type: 'text' | 'password' | 'number' | 'boolean' | 'select';
124
+ required: boolean;
125
+ secret?: boolean;
126
+ placeholder?: string;
127
+ help?: string;
128
+ options?: Array<{
129
+ label: string;
130
+ value: string;
131
+ }>;
132
+ }>;
133
+ }
134
+ export interface MerchantIntegration {
135
+ _id: string;
136
+ service: string;
137
+ provider: string;
138
+ label?: string;
139
+ enabled: boolean;
140
+ status: 'connected' | 'disabled' | 'error';
141
+ config: Record<string, unknown>;
142
+ secretsSet: Record<string, boolean>;
143
+ lastTestedAt?: string | null;
144
+ lastTestStatus?: 'ok' | 'failed' | 'skipped';
145
+ }
146
+ export interface NotificationDelivery {
147
+ _id: string;
148
+ timestamp: string;
149
+ event: string;
150
+ status: 'delivered' | 'partial' | 'failed' | 'skipped';
151
+ subject?: {
152
+ sourceModel: string;
153
+ sourceId: string;
154
+ };
155
+ }
156
+ export interface MerchantApiKey {
157
+ id: string;
158
+ kind: 'user' | 'service';
159
+ name: string;
160
+ prefix: string;
161
+ ownerId: string;
162
+ scopes: string[];
163
+ status: 'active' | 'expired' | 'revoked';
164
+ expiresAt: string | null;
165
+ lastUsedAt: string | null;
166
+ ceilingRole: string | null;
167
+ }
168
+ export interface ApiKeyOptions {
169
+ scopes: Array<{
170
+ scope: string;
171
+ requires: string[];
172
+ grantable: boolean;
173
+ }>;
174
+ ceilingRoles: Array<{
175
+ role: string;
176
+ scopes: string[];
177
+ }>;
178
+ can: {
179
+ own: boolean;
180
+ createService: boolean;
181
+ readBranch: boolean;
182
+ updateService: boolean;
183
+ deleteOthers: boolean;
184
+ };
185
+ lifetime: {
186
+ defaultDays: number;
187
+ maxDays: number;
188
+ allowNeverExpires?: boolean;
189
+ };
190
+ }
191
+ export interface ApiKeyInput {
192
+ kind: 'user' | 'service';
193
+ name: string;
194
+ scopes: string[];
195
+ /** Null explicitly requests a key that does not expire. */
196
+ expiresInDays: number | null;
197
+ ceilingRole?: string;
198
+ }
199
+ export interface IssuedApiKey {
200
+ key: string;
201
+ record: MerchantApiKey;
202
+ }
203
+ /** Inclusive start and exclusive end as ISO timestamps, at most 92 days apart. */
204
+ export interface AnalyticsWindow {
205
+ from: string;
206
+ to: string;
207
+ }
208
+ /** Order values include delivery charges and are independent of payment collection. */
209
+ export interface MerchantOverview {
210
+ summary: {
211
+ totalOrders: number;
212
+ totalOrderValue: number;
213
+ fulfilledOrders: number;
214
+ fulfilledOrderValue: number;
215
+ canceledOrders: number;
216
+ canceledOrderValue: number;
217
+ averageOrderValue: number;
218
+ };
219
+ statuses: Array<{
220
+ status: string;
221
+ count: number;
222
+ orderValue: number;
223
+ }>;
224
+ }
225
+ //#endregion
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
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";
3
+ import { BaseApi } from "@classytic/arc-next/api";
4
+ import { ArcClientConfig } from "@classytic/arc-next/client";
5
+ //#region src/management.d.ts
6
+ export interface MerchantClientOptions {
7
+ apiUrl: string;
8
+ token: string;
9
+ organizationId: string;
10
+ fetch?: ArcClientConfig['fetch'];
11
+ }
12
+ /** Capture one authenticated store; callers create a new instance when switching stores. */
13
+ export declare function createMerchantClient(options: MerchantClientOptions): {
14
+ integrations: {
15
+ catalog: (signal?: AbortSignal) => Promise<IntegrationProvider[]>;
16
+ list: (signal?: AbortSignal) => Promise<MerchantIntegration[]>;
17
+ connect: (data: {
18
+ service: string;
19
+ provider: string;
20
+ label: string;
21
+ credentials: Record<string, unknown>;
22
+ enable: boolean;
23
+ }) => Promise<MerchantIntegration>;
24
+ command: (id: string, action: 'test' | 'enable' | 'disable') => Promise<MerchantIntegration>;
25
+ remove: (id: string) => Promise<null>;
26
+ };
27
+ notifications: {
28
+ list: ({ token, organizationId, params, options }?: {
29
+ token?: string | null;
30
+ organizationId?: string | null;
31
+ params?: import("@classytic/arc-next/api").QueryParams;
32
+ options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
33
+ }) => Promise<import("@classytic/repo-core/pagination").PaginatedResult<NotificationDelivery>>;
34
+ };
35
+ apiKeys: {
36
+ options: (signal?: AbortSignal) => Promise<ApiKeyOptions>;
37
+ list: (owner: 'me' | 'branch', signal?: AbortSignal) => Promise<MerchantApiKey[]>;
38
+ create: (data: ApiKeyInput) => Promise<IssuedApiKey>;
39
+ rotate: (id: string) => Promise<IssuedApiKey>;
40
+ revoke: (id: string) => Promise<MerchantApiKey>;
41
+ };
42
+ delivery: {
43
+ read: (signal?: AbortSignal) => Promise<DeliverySettings>;
44
+ update: (data: DeliverySettingsUpdate) => Promise<DeliverySettings>;
45
+ districts(signal?: AbortSignal): Promise<DeliveryDistrict[]>;
46
+ };
47
+ products: BaseApi<MerchantProduct, ProductInput, Partial<ProductInput>>;
48
+ categories: BaseApi<MerchantCategory, CategoryInput, Partial<Omit<CategoryInput, "slug">>> & {
49
+ tree: (options?: {
50
+ signal?: AbortSignal;
51
+ }) => Promise<MerchantCategoryTree[]>;
52
+ };
53
+ analytics: {
54
+ overview(window: AnalyticsWindow, options?: {
55
+ signal?: AbortSignal;
56
+ }): Promise<MerchantOverview>;
57
+ };
58
+ orders: {
59
+ collect(id: string, amount: number, idempotencyKey: string): Promise<{
60
+ outstanding: number;
61
+ settled: boolean;
62
+ transactionId?: string;
63
+ }>;
64
+ list: ({ token, organizationId, params, options }?: {
65
+ token?: string | null;
66
+ organizationId?: string | null;
67
+ params?: import("@classytic/arc-next/api").QueryParams;
68
+ options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
69
+ }) => Promise<import("@classytic/repo-core/pagination").PaginatedResult<MerchantOrder>>;
70
+ detail: (orderNumber: string, options?: {
71
+ signal?: AbortSignal;
72
+ }) => Promise<MerchantOrder>;
73
+ command(id: string, command: OrderCommand, idempotencyKey: string): Promise<MerchantOrder>;
74
+ };
75
+ settings: {
76
+ get: () => Promise<StoreSettings>;
77
+ update: (data: {
78
+ expectedVersion: number | null;
79
+ deliveryChargeMinor: number;
80
+ }) => Promise<StoreSettings>;
81
+ };
82
+ media: {
83
+ list: ({ token, organizationId, params, options }?: {
84
+ token?: string | null;
85
+ organizationId?: string | null;
86
+ params?: import("@classytic/arc-next/api").QueryParams;
87
+ options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
88
+ }) => Promise<import("@classytic/repo-core/pagination").PaginatedResult<MerchantMedia>>;
89
+ upload: (file: File, folder?: 'products' | 'categories') => Promise<MerchantMedia>;
90
+ uploadImage(file: File, folder?: 'products' | 'categories'): Promise<MerchantMedia>;
91
+ };
92
+ };
93
+ export type MerchantClient = ReturnType<typeof createMerchantClient>;
94
+ //#endregion
@@ -0,0 +1,261 @@
1
+ import { BaseApi } from "@classytic/arc-next/api";
2
+ import { createClient } from "@classytic/arc-next/client";
3
+ //#region src/management-delivery.ts
4
+ function createMerchantDelivery(client) {
5
+ const api = new BaseApi("stores/delivery", {
6
+ client,
7
+ basePath: "",
8
+ cache: "no-store"
9
+ });
10
+ return { delivery: {
11
+ read: (signal) => api.request("GET", `${api.baseUrl}/current`, { options: { signal } }),
12
+ update: (data) => api.request("PATCH", `${api.baseUrl}/current`, { data }),
13
+ async districts(signal) {
14
+ return (await api.request("GET", `${api.baseUrl}/districts`, { options: { signal } })).data;
15
+ }
16
+ } };
17
+ }
18
+ //#endregion
19
+ //#region src/management-access.ts
20
+ function key(value) {
21
+ if (!value || value === "." || value === "..") throw new Error("An identifier is required.");
22
+ return encodeURIComponent(value);
23
+ }
24
+ function createMerchantAccess(client) {
25
+ const config = {
26
+ client,
27
+ basePath: "",
28
+ cache: "no-store"
29
+ };
30
+ const integrations = new BaseApi("integrations", config);
31
+ const apiKeys = new BaseApi("auth/api-keys", config);
32
+ const deliveries = new BaseApi("notifications/delivery-log", config);
33
+ return {
34
+ integrations: {
35
+ catalog: (signal) => integrations.request("GET", `${integrations.baseUrl}/catalog`, { options: { signal } }),
36
+ list: (signal) => integrations.request("GET", integrations.baseUrl, { options: { signal } }),
37
+ connect: (data) => integrations.request("POST", integrations.baseUrl, { data }),
38
+ command: (id, action) => integrations.dispatchAction({
39
+ id: key(id),
40
+ action
41
+ }),
42
+ remove: (id) => integrations.request("DELETE", `${integrations.baseUrl}/${key(id)}`)
43
+ },
44
+ notifications: { list: deliveries.getAll.bind(deliveries) },
45
+ apiKeys: {
46
+ options: (signal) => apiKeys.request("GET", `${apiKeys.baseUrl}/options`, { options: { signal } }),
47
+ list: (owner, signal) => apiKeys.request("GET", apiKeys.baseUrl, {
48
+ params: { owner },
49
+ options: { signal }
50
+ }),
51
+ create: (data) => apiKeys.request("POST", apiKeys.baseUrl, { data }),
52
+ rotate: (id) => apiKeys.dispatchAction({
53
+ id: key(id),
54
+ action: "rotate"
55
+ }),
56
+ revoke: (id) => apiKeys.dispatchAction({
57
+ id: key(id),
58
+ action: "revoke"
59
+ })
60
+ }
61
+ };
62
+ }
63
+ //#endregion
64
+ //#region src/management-media.ts
65
+ function createMerchantMedia(client, fetcher) {
66
+ const media = new BaseApi("media", {
67
+ client,
68
+ basePath: "",
69
+ cache: "no-store"
70
+ });
71
+ async function put(target, blob) {
72
+ const headers = new Headers(target.headers);
73
+ if (!headers.has("content-type")) headers.set("content-type", blob.type);
74
+ if (!(await (fetcher ?? fetch)(target.uploadUrl, {
75
+ method: "PUT",
76
+ credentials: "omit",
77
+ headers,
78
+ signal: AbortSignal.timeout(6e4),
79
+ body: blob
80
+ })).ok) throw new Error("Image upload failed. Please retry.");
81
+ }
82
+ async function upload(file, folder, processed, originalFilename) {
83
+ const derivatives = processed?.derivatives ?? [];
84
+ const started = await media.request("POST", `${media.baseUrl}/start-write`, { data: {
85
+ filename: file.name,
86
+ contentType: file.type,
87
+ size: file.size,
88
+ folder,
89
+ ...derivatives.length ? { derivatives: derivatives.map((item) => ({
90
+ name: item.name,
91
+ contentType: item.blob.type,
92
+ size: item.bytes
93
+ })) } : {}
94
+ } });
95
+ if (started.kind === "dedup") return started.media;
96
+ if (started.kind !== "presigned") throw new Error("Choose a smaller product image for direct upload.");
97
+ const targets = derivatives.map((derivative) => {
98
+ const target = started.derivativeUploads?.find((item) => item.name === derivative.name)?.upload;
99
+ if (!target) throw new Error("The server did not provide every requested image size. Please retry.");
100
+ return {
101
+ derivative,
102
+ target
103
+ };
104
+ });
105
+ await put(started.upload, file);
106
+ await Promise.all(targets.map(({ derivative, target }) => put(target, derivative.blob)));
107
+ const hints = processed ? (await import("@classytic/media-transform/image/uploadHints")).toUploadHints(processed) : {};
108
+ return media.request("POST", `${media.baseUrl}/complete-write`, { data: {
109
+ key: started.upload.key,
110
+ filename: file.name,
111
+ mimeType: file.type,
112
+ size: file.size,
113
+ ...originalFilename ? { originalFilename } : {},
114
+ ...hints,
115
+ ...targets.length ? { variants: targets.map(({ derivative, target }) => ({
116
+ name: derivative.name,
117
+ key: target.key,
118
+ mimeType: derivative.blob.type,
119
+ size: derivative.bytes,
120
+ width: derivative.width,
121
+ height: derivative.height
122
+ })) } : {}
123
+ } });
124
+ }
125
+ return {
126
+ list: media.getAll.bind(media),
127
+ upload: (file, folder = "products") => upload(file, folder),
128
+ async uploadImage(file, folder = "products") {
129
+ const policy = await media.request("GET", `${media.baseUrl}/transform-policy`);
130
+ const { resolveTransformPlan } = await import("@classytic/media-transform/policy");
131
+ const plan = resolveTransformPlan({
132
+ folder,
133
+ mimeType: file.type,
134
+ bytes: file.size
135
+ }, policy);
136
+ if (!plan.transform) {
137
+ if (plan.outcome === "preserve") return upload(file, folder);
138
+ throw new Error("The image processing policy is unsupported. Refresh the dashboard before uploading.");
139
+ }
140
+ const [{ createBrowserImageAdapter }, { compressImageSet }, { storedFilename }] = await Promise.all([
141
+ import("@classytic/media-transform/image/BrowserImageAdapter"),
142
+ import("@classytic/media-transform/image/compressImageSet"),
143
+ import("@classytic/media-transform/image/uploadHints")
144
+ ]);
145
+ const result = await compressImageSet(await createBrowserImageAdapter(), file, plan.options);
146
+ return upload(new File([result.blob], storedFilename(file.name, result.format), { type: result.blob.type }), folder, result, file.name);
147
+ }
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region src/management.ts
152
+ function orderKey(value) {
153
+ if (!value || value === "." || value === "..") throw new Error("An order number is required.");
154
+ return encodeURIComponent(value);
155
+ }
156
+ /** Capture one authenticated store; callers create a new instance when switching stores. */
157
+ function createMerchantClient(options) {
158
+ const { token, organizationId, fetch: fetcher } = options;
159
+ const url = new URL(options.apiUrl);
160
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("A valid HTTP(S) API mount is required.");
161
+ if (!token.trim() || !organizationId.trim()) throw new Error("A bearer token and organizationId are required.");
162
+ const client = createClient({
163
+ baseUrl: url.href.replace(/\/+$/, ""),
164
+ getToken: () => token,
165
+ getOrgId: () => organizationId,
166
+ credentials: "omit",
167
+ autoIdempotency: false,
168
+ timeoutMs: 15e3,
169
+ ...fetcher ? { fetch: fetcher } : {}
170
+ });
171
+ const config = {
172
+ client,
173
+ basePath: "",
174
+ cache: "no-store"
175
+ };
176
+ const products = new BaseApi("products", config);
177
+ const categories = new BaseApi("categories", config);
178
+ const orders = new BaseApi("orders", config);
179
+ const stores = new BaseApi("stores", config);
180
+ return {
181
+ ...createMerchantAccess(client),
182
+ ...createMerchantDelivery(client),
183
+ products,
184
+ categories: Object.assign(categories, { tree: (options = {}) => categories.request("GET", `${categories.baseUrl}/tree`, { options }) }),
185
+ analytics: { async overview(window, options = {}) {
186
+ const from = new Date(window.from);
187
+ const to = new Date(window.to);
188
+ if (!Number.isFinite(from.getTime()) || !Number.isFinite(to.getTime()) || to <= from || to.getTime() - from.getTime() > 79488e5) throw new Error("Choose a valid analytics window of at most 92 days.");
189
+ const statuses = (await orders.request("GET", `${orders.baseUrl}/aggregations/revenueByStatus`, {
190
+ params: {
191
+ "createdAt[gte]": from.toISOString(),
192
+ "createdAt[lt]": to.toISOString()
193
+ },
194
+ options
195
+ })).rows.map(({ status, count, revenue }) => ({
196
+ status,
197
+ count,
198
+ orderValue: revenue
199
+ }));
200
+ const summary = {
201
+ totalOrders: 0,
202
+ totalOrderValue: 0,
203
+ fulfilledOrders: 0,
204
+ fulfilledOrderValue: 0,
205
+ canceledOrders: 0,
206
+ canceledOrderValue: 0,
207
+ averageOrderValue: 0
208
+ };
209
+ for (const row of statuses) {
210
+ summary.totalOrders += row.count;
211
+ summary.totalOrderValue += row.orderValue;
212
+ if (row.status === "fulfilled" || row.status === "completed") {
213
+ summary.fulfilledOrders += row.count;
214
+ summary.fulfilledOrderValue += row.orderValue;
215
+ }
216
+ if (row.status === "canceled") {
217
+ summary.canceledOrders += row.count;
218
+ summary.canceledOrderValue += row.orderValue;
219
+ }
220
+ }
221
+ summary.averageOrderValue = summary.totalOrders ? Math.round(summary.totalOrderValue / summary.totalOrders) : 0;
222
+ return {
223
+ summary,
224
+ statuses
225
+ };
226
+ } },
227
+ orders: {
228
+ collect(id, amount, idempotencyKey) {
229
+ if (!idempotencyKey.trim()) throw new Error("Preserve an explicit idempotencyKey for collection retries.");
230
+ return orders.dispatchAction({
231
+ id: orderKey(id),
232
+ action: "collect_payment",
233
+ data: { amount },
234
+ options: { headerOptions: { "Idempotency-Key": idempotencyKey } }
235
+ });
236
+ },
237
+ list: orders.getAll.bind(orders),
238
+ detail: (orderNumber, options = {}) => orders.getById({
239
+ id: orderKey(orderNumber),
240
+ options
241
+ }),
242
+ command(id, command, idempotencyKey) {
243
+ if (!idempotencyKey.trim()) throw new Error("Preserve an explicit idempotencyKey for command retries.");
244
+ const { action, ...data } = command;
245
+ return orders.dispatchAction({
246
+ id: orderKey(id),
247
+ action,
248
+ data,
249
+ options: { headerOptions: { "Idempotency-Key": idempotencyKey } }
250
+ });
251
+ }
252
+ },
253
+ settings: {
254
+ get: () => stores.request("GET", `${stores.baseUrl}/settings/current`),
255
+ update: (data) => stores.request("PATCH", `${stores.baseUrl}/settings/current`, { data })
256
+ },
257
+ media: createMerchantMedia(client, fetcher)
258
+ };
259
+ }
260
+ //#endregion
261
+ export { createMerchantClient };
@@ -0,0 +1,128 @@
1
+ import { Address } from "@classytic/primitives/address";
2
+ import { Money } from "@classytic/primitives/money";
3
+ //#region src/types.d.ts
4
+ /** Public wire projections; merchant-only catalog and buyer fields are absent. */
5
+ interface StorefrontProduct {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ description?: string;
10
+ categorySlug?: string;
11
+ tags?: string[];
12
+ collections?: string[];
13
+ onSale?: boolean;
14
+ images?: Array<{
15
+ url: string;
16
+ alt?: string;
17
+ order?: number;
18
+ isFeatured?: boolean;
19
+ variants?: {
20
+ thumbnail?: string;
21
+ medium?: string;
22
+ };
23
+ }>;
24
+ monetization?: {
25
+ type: 'one_time';
26
+ basePrice: Money;
27
+ compareAtPrice?: Money;
28
+ };
29
+ variants?: Array<{
30
+ sku: string;
31
+ attributes: Record<string, string>;
32
+ priceModifier: number;
33
+ }>;
34
+ }
35
+ interface StorefrontCategory {
36
+ id: string;
37
+ name: string;
38
+ slug: string;
39
+ parent?: string | null;
40
+ parentPath?: string;
41
+ description?: string;
42
+ image?: {
43
+ url: string;
44
+ alt?: string;
45
+ } | null;
46
+ displayOrder?: number;
47
+ }
48
+ interface StorefrontProductPage {
49
+ data: StorefrontProduct[];
50
+ next: string | null;
51
+ }
52
+ interface StorefrontProductQuery {
53
+ limit?: number;
54
+ /** Opaque cursor returned by the preceding page; keep filters and sort unchanged. */
55
+ after?: string;
56
+ category?: string;
57
+ q?: string;
58
+ tag?: string;
59
+ collection?: string;
60
+ onSale?: boolean;
61
+ /** Inclusive base-price bounds in the store currency's integer minor units. */
62
+ minPrice?: number;
63
+ maxPrice?: number;
64
+ sort?: 'newest' | 'price-asc' | 'price-desc';
65
+ }
66
+ interface StorefrontProfile {
67
+ organizationId: string;
68
+ name: string;
69
+ slug: string;
70
+ status: 'active';
71
+ currency: string;
72
+ deliveryChargeMinor: number;
73
+ }
74
+ interface GuestCheckoutInput {
75
+ lines: Array<{
76
+ productId: string;
77
+ variantSku?: string;
78
+ quantity: number;
79
+ }>;
80
+ customer: {
81
+ name: string;
82
+ phone: string;
83
+ email?: string;
84
+ };
85
+ shippingAddress: Address & {
86
+ areaId?: number;
87
+ };
88
+ }
89
+ interface StorefrontOrder {
90
+ orderNumber: string;
91
+ status: string;
92
+ fulfillmentStatus?: 'unfulfilled' | 'partial' | 'fulfilled' | 'returned';
93
+ fulfillmentSummary?: {
94
+ total: number;
95
+ fulfilled: number;
96
+ shipped: number;
97
+ delivered: number;
98
+ returned: number;
99
+ };
100
+ paymentTerm?: 'prepaid' | 'collect_on_delivery';
101
+ currency: string;
102
+ totals: {
103
+ subtotal: Money;
104
+ discount: Money;
105
+ tax: Money;
106
+ shipping: Money;
107
+ grandTotal: Money;
108
+ taxBasis?: 'inclusive' | 'exclusive';
109
+ };
110
+ createdAt: string;
111
+ updatedAt: string;
112
+ }
113
+ type GuestCheckoutResult = StorefrontOrder & {
114
+ accessToken: string;
115
+ };
116
+ interface GuestOrderAccess {
117
+ orderNumber: string;
118
+ accessToken: string;
119
+ }
120
+ interface StorefrontRequestOptions {
121
+ signal?: AbortSignal;
122
+ }
123
+ interface GuestCheckoutOptions extends StorefrontRequestOptions {
124
+ /** Create once per checkout attempt; preserve across retries and lost responses. */
125
+ idempotencyKey: string;
126
+ }
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 };
@@ -0,0 +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 };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/management.md ADDED
@@ -0,0 +1,88 @@
1
+ # Merchant client
2
+
3
+ Import `createMerchantClient` from `@classytic/commerce-client/management`.
4
+ Its contracts live in `/management/types`. The guest-only root entry imports
5
+ none of this authenticated surface.
6
+
7
+ ```ts
8
+ import { createMerchantClient } from '@classytic/commerce-client/management';
9
+
10
+ const merchant = createMerchantClient({
11
+ apiUrl: 'https://api.example.com/api/v1',
12
+ token: merchantSessionToken,
13
+ organizationId: selectedStoreId,
14
+ });
15
+ const products = await merchant.products.getAll({ params: { page: 1, limit: 20 } });
16
+ const categories = await merchant.categories.tree();
17
+ const orders = await merchant.orders.list();
18
+ ```
19
+
20
+ Each instance captures one bearer credential and store. Create another instance
21
+ on session or store changes. Cookies and automatic command keys are disabled.
22
+ The server authorizes every operation; choosing a store ID grants no access.
23
+
24
+ Product/category APIs use Arc Next's typed CRUD methods. Order writes expose
25
+ only supported commands (`confirm`, `start_processing`, `ship`, `deliver`,
26
+ `cancel`) and cash collection. Generate a key once per command attempt and keep
27
+ it, its amount and its body unchanged across retries.
28
+
29
+ ```ts
30
+ const key = crypto.randomUUID();
31
+ await merchant.orders.command(orderNumber, { action: 'confirm' }, key);
32
+
33
+ // After complete delivery and actual receipt of cash, with the collect permission:
34
+ const collectionKey = crypto.randomUUID();
35
+ await merchant.orders.collect(orderNumber, amountMinor, collectionKey);
36
+ ```
37
+
38
+ `media.upload(file)` uses the shared start-write → storage PUT → complete-write
39
+ protocol. Merchant authorization goes to the API; the storage PUT receives only
40
+ its presigned headers and file bytes. A failed PUT does not confirm an upload.
41
+ Settings updates carry `expectedVersion`; refetch after a conflict before saving.
42
+
43
+ `integrations.catalog()` describes enabled providers and credential fields.
44
+ `integrations.connect()` stores/tests a connection for the authenticated store;
45
+ `command(id, 'test' | 'enable' | 'disable')` controls it and `remove(id)` disconnects
46
+ it. `notifications.list()` reads paginated delivery evidence. These routes may be
47
+ absent when the deployment has no notification credential encryption key.
48
+
49
+ `apiKeys.options()` returns live scope grants, ownership permissions and lifetime
50
+ bounds. Use them to construct `apiKeys.create(input)`. `apiKeys.list('me' | 'branch')`
51
+ returns safe records; `rotate(id)` returns a replacement secret and `revoke(id)`
52
+ disables one. Creation/rotation require a fresh bearer session. Store the returned
53
+ secret on your server: it is shown once, cannot be read later and should never be
54
+ placed in query caches or browser storage. These mutations have no automatic
55
+ retry; refresh the key list after an uncertain outcome before retrying.
56
+
57
+ `analytics.overview({ from, to }, { signal })` reads one store-scoped order
58
+ aggregation. Supply ISO timestamps for an inclusive start and exclusive end,
59
+ at most 92 days apart. Read the configured currency through `settings.get()`.
60
+ Amounts are integer minor units; the rounded average uses the same unit.
61
+
62
+ The summary reports order count/value, fulfilled count/value, canceled
63
+ count/value and average order value, alongside the individual status rows.
64
+ Total order value includes every status and delivery charges. Fulfilled combines
65
+ `fulfilled` and `completed`: shipment can fulfill an order before delivery.
66
+ Refunded remains a separate status. These metrics do not measure received cash
67
+ or prove delivery. Order-read permission is required, including `read:orders`
68
+ for API keys; organization IDs in query parameters cannot widen the store scope.
69
+
70
+ ## Category hierarchy
71
+
72
+ `categories.tree({ signal })` returns nested categories with `children` and
73
+ `totalProductCount`. `parent` is the parent category's slug; `null` moves a category
74
+ to the top level. Slugs are permanent after creation. Omit a blank create slug to
75
+ generate it from the name; update names without resending a slug.
76
+
77
+ Import `categoryChoices` from `@classytic/commerce-client/category-tree` for
78
+ breadcrumb labels. Pass the edited slug as its second argument to exclude that
79
+ category and all its descendants from parent choices.
80
+
81
+ `media.uploadImage(file, 'categories')` optimizes a category image using the
82
+ category-folder policy. Send `image: null` to clear its reference; detached-media
83
+ cleanup follows the server's normal governance policy.
84
+
85
+ Product writes also accept `tags`, `collections` and
86
+ `defaultMonetization.pricing.compareAtPrice`. The catalog derives `onSale`; clients
87
+ never set it. Tags and collections use the existing catalog arrays, with no
88
+ additional collection-management service.
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@classytic/commerce-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Typed storefront and merchant clients for the Classytic Commerce API",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./types": {
15
+ "types": "./dist/types.d.ts",
16
+ "default": "./dist/types.js"
17
+ },
18
+ "./management": {
19
+ "types": "./dist/management.d.ts",
20
+ "default": "./dist/management.js"
21
+ },
22
+ "./management/types": {
23
+ "types": "./dist/management-types.d.ts",
24
+ "default": "./dist/management-types.js"
25
+ },
26
+ "./delivery-types": {
27
+ "types": "./dist/delivery-types.d.ts",
28
+ "default": "./dist/delivery-types.js"
29
+ },
30
+ "./category-tree": {
31
+ "types": "./dist/category-tree.d.ts",
32
+ "default": "./dist/category-tree.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "CHANGELOG.md",
39
+ "LICENSE",
40
+ "management.md"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
47
+ "push": "classytic-push",
48
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
49
+ "release": "npm run push -- main && npm run release:tag && npm publish"
50
+ },
51
+ "dependencies": {
52
+ "@classytic/arc-next": "^0.18.0",
53
+ "@classytic/media-transform": "^0.3.0",
54
+ "@classytic/primitives": "^0.34.0"
55
+ },
56
+ "devDependencies": {
57
+ "@classytic/dev-tools": "^0.2.1",
58
+ "@types/node": "^26.6.5",
59
+ "esbuild": "^0.28.1",
60
+ "publint": "^0.3.25",
61
+ "tsdown": "^0.23.1",
62
+ "typescript": "^7.0.2",
63
+ "vitest": "^5.0.5"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "license": "SEE LICENSE IN LICENSE",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/classytic/commerce-client.git"
72
+ },
73
+ "bugs": {
74
+ "url": "https://github.com/classytic/commerce-client/issues"
75
+ },
76
+ "homepage": "https://github.com/classytic/commerce-client#readme"
77
+ }