@classytic/commerce-client 0.0.0-stage → 0.1.1
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 +31 -0
- package/LICENSE +75 -0
- package/README.md +129 -2
- package/dist/category-tree.d.ts +20 -0
- package/dist/category-tree.js +39 -0
- package/dist/delivery-types.d.ts +48 -0
- package/dist/delivery-types.js +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +66 -0
- package/dist/management-types.d.ts +230 -0
- package/dist/management-types.js +1 -0
- package/dist/management.d.ts +100 -0
- package/dist/management.js +265 -0
- package/dist/platform.d.ts +36 -0
- package/dist/platform.js +29 -0
- package/dist/types-C0cSi6Pk.d.ts +128 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/management.md +100 -0
- package/package.json +79 -4
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
remove: ({ token, organizationId, id, options }: {
|
|
90
|
+
token?: string | null;
|
|
91
|
+
organizationId?: string | null;
|
|
92
|
+
id: string;
|
|
93
|
+
options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
|
|
94
|
+
}) => 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>;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export type MerchantClient = ReturnType<typeof createMerchantClient>;
|
|
100
|
+
//#endregion
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
remove: media.delete.bind(media),
|
|
128
|
+
upload: (file, folder = "products") => upload(file, folder),
|
|
129
|
+
async uploadImage(file, folder = "products") {
|
|
130
|
+
const policy = await media.request("GET", `${media.baseUrl}/transform-policy`);
|
|
131
|
+
const { resolveTransformPlan, isTransformableImage } = await import("@classytic/media-transform/policy");
|
|
132
|
+
if (!isTransformableImage(file.type)) throw new Error("Choose a JPEG, PNG, WebP, AVIF or BMP image that your browser can open.");
|
|
133
|
+
const plan = resolveTransformPlan({
|
|
134
|
+
folder,
|
|
135
|
+
mimeType: file.type,
|
|
136
|
+
bytes: file.size
|
|
137
|
+
}, policy);
|
|
138
|
+
if (!plan.transform) {
|
|
139
|
+
if (plan.outcome === "preserve") return upload(file, folder);
|
|
140
|
+
throw new Error("The image processing policy is unsupported. Refresh the dashboard before uploading.");
|
|
141
|
+
}
|
|
142
|
+
const [{ createBrowserImageAdapter }, { compressImageSet }, { storedFilename }] = await Promise.all([
|
|
143
|
+
import("@classytic/media-transform/image/BrowserImageAdapter"),
|
|
144
|
+
import("@classytic/media-transform/image/compressImageSet"),
|
|
145
|
+
import("@classytic/media-transform/image/uploadHints")
|
|
146
|
+
]);
|
|
147
|
+
const result = await compressImageSet(await createBrowserImageAdapter(), file, plan.options).catch(() => {
|
|
148
|
+
throw new Error("Your browser could not process this image. Try a JPEG or PNG copy.");
|
|
149
|
+
});
|
|
150
|
+
return upload(new File([result.blob], storedFilename(file.name, result.format), { type: result.blob.type }), folder, result, file.name);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/management.ts
|
|
156
|
+
function orderKey(value) {
|
|
157
|
+
if (!value || value === "." || value === "..") throw new Error("An order number is required.");
|
|
158
|
+
return encodeURIComponent(value);
|
|
159
|
+
}
|
|
160
|
+
/** Capture one authenticated store; callers create a new instance when switching stores. */
|
|
161
|
+
function createMerchantClient(options) {
|
|
162
|
+
const { token, organizationId, fetch: fetcher } = options;
|
|
163
|
+
const url = new URL(options.apiUrl);
|
|
164
|
+
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.");
|
|
165
|
+
if (!token.trim() || !organizationId.trim()) throw new Error("A bearer token and organizationId are required.");
|
|
166
|
+
const client = createClient({
|
|
167
|
+
baseUrl: url.href.replace(/\/+$/, ""),
|
|
168
|
+
getToken: () => token,
|
|
169
|
+
getOrgId: () => organizationId,
|
|
170
|
+
credentials: "omit",
|
|
171
|
+
autoIdempotency: false,
|
|
172
|
+
timeoutMs: 15e3,
|
|
173
|
+
...fetcher ? { fetch: fetcher } : {}
|
|
174
|
+
});
|
|
175
|
+
const config = {
|
|
176
|
+
client,
|
|
177
|
+
basePath: "",
|
|
178
|
+
cache: "no-store"
|
|
179
|
+
};
|
|
180
|
+
const products = new BaseApi("products", config);
|
|
181
|
+
const categories = new BaseApi("categories", config);
|
|
182
|
+
const orders = new BaseApi("orders", config);
|
|
183
|
+
const stores = new BaseApi("stores", config);
|
|
184
|
+
return {
|
|
185
|
+
...createMerchantAccess(client),
|
|
186
|
+
...createMerchantDelivery(client),
|
|
187
|
+
products,
|
|
188
|
+
categories: Object.assign(categories, { tree: (options = {}) => categories.request("GET", `${categories.baseUrl}/tree`, { options }) }),
|
|
189
|
+
analytics: { async overview(window, options = {}) {
|
|
190
|
+
const from = new Date(window.from);
|
|
191
|
+
const to = new Date(window.to);
|
|
192
|
+
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.");
|
|
193
|
+
const statuses = (await orders.request("GET", `${orders.baseUrl}/aggregations/revenueByStatus`, {
|
|
194
|
+
params: {
|
|
195
|
+
"createdAt[gte]": from.toISOString(),
|
|
196
|
+
"createdAt[lt]": to.toISOString()
|
|
197
|
+
},
|
|
198
|
+
options
|
|
199
|
+
})).rows.map(({ status, count, revenue }) => ({
|
|
200
|
+
status,
|
|
201
|
+
count,
|
|
202
|
+
orderValue: revenue
|
|
203
|
+
}));
|
|
204
|
+
const summary = {
|
|
205
|
+
totalOrders: 0,
|
|
206
|
+
totalOrderValue: 0,
|
|
207
|
+
fulfilledOrders: 0,
|
|
208
|
+
fulfilledOrderValue: 0,
|
|
209
|
+
canceledOrders: 0,
|
|
210
|
+
canceledOrderValue: 0,
|
|
211
|
+
averageOrderValue: 0
|
|
212
|
+
};
|
|
213
|
+
for (const row of statuses) {
|
|
214
|
+
summary.totalOrders += row.count;
|
|
215
|
+
summary.totalOrderValue += row.orderValue;
|
|
216
|
+
if (row.status === "fulfilled" || row.status === "completed") {
|
|
217
|
+
summary.fulfilledOrders += row.count;
|
|
218
|
+
summary.fulfilledOrderValue += row.orderValue;
|
|
219
|
+
}
|
|
220
|
+
if (row.status === "canceled") {
|
|
221
|
+
summary.canceledOrders += row.count;
|
|
222
|
+
summary.canceledOrderValue += row.orderValue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
summary.averageOrderValue = summary.totalOrders ? Math.round(summary.totalOrderValue / summary.totalOrders) : 0;
|
|
226
|
+
return {
|
|
227
|
+
summary,
|
|
228
|
+
statuses
|
|
229
|
+
};
|
|
230
|
+
} },
|
|
231
|
+
orders: {
|
|
232
|
+
collect(id, amount, idempotencyKey) {
|
|
233
|
+
if (!idempotencyKey.trim()) throw new Error("Preserve an explicit idempotencyKey for collection retries.");
|
|
234
|
+
return orders.dispatchAction({
|
|
235
|
+
id: orderKey(id),
|
|
236
|
+
action: "collect_payment",
|
|
237
|
+
data: { amount },
|
|
238
|
+
options: { headerOptions: { "Idempotency-Key": idempotencyKey } }
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
list: orders.getAll.bind(orders),
|
|
242
|
+
detail: (orderNumber, options = {}) => orders.getById({
|
|
243
|
+
id: orderKey(orderNumber),
|
|
244
|
+
options
|
|
245
|
+
}),
|
|
246
|
+
command(id, command, idempotencyKey) {
|
|
247
|
+
if (!idempotencyKey.trim()) throw new Error("Preserve an explicit idempotencyKey for command retries.");
|
|
248
|
+
const { action, ...data } = command;
|
|
249
|
+
return orders.dispatchAction({
|
|
250
|
+
id: orderKey(id),
|
|
251
|
+
action,
|
|
252
|
+
data,
|
|
253
|
+
options: { headerOptions: { "Idempotency-Key": idempotencyKey } }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
settings: {
|
|
258
|
+
get: () => stores.request("GET", `${stores.baseUrl}/settings/current`),
|
|
259
|
+
update: (data) => stores.request("PATCH", `${stores.baseUrl}/settings/current`, { data })
|
|
260
|
+
},
|
|
261
|
+
media: createMerchantMedia(client, fetcher)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { createMerchantClient };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/platform.d.ts
|
|
2
|
+
export type OrganizationStatus = 'pending' | 'active' | 'rejected' | 'suspended';
|
|
3
|
+
export type OrganizationAction = 'approve' | 'reject' | 'suspend' | 'reactivate';
|
|
4
|
+
export interface PlatformOrganization {
|
|
5
|
+
_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
slug: string;
|
|
8
|
+
status: OrganizationStatus;
|
|
9
|
+
createdAt?: string;
|
|
10
|
+
statusHistory?: Array<{
|
|
11
|
+
status: OrganizationStatus;
|
|
12
|
+
occurredAt: string;
|
|
13
|
+
by?: string;
|
|
14
|
+
note?: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** Platform administration uses a bearer identity without selecting a merchant organization. */
|
|
18
|
+
export declare function createPlatformClient({ apiUrl, token }: {
|
|
19
|
+
apiUrl: string;
|
|
20
|
+
token: string;
|
|
21
|
+
}): {
|
|
22
|
+
organizations: {
|
|
23
|
+
list: ({ token, organizationId, params, options }?: {
|
|
24
|
+
token?: string | null;
|
|
25
|
+
organizationId?: string | null;
|
|
26
|
+
params?: import("@classytic/arc-next/api").QueryParams;
|
|
27
|
+
options?: Omit<import("@classytic/arc-next/api").RequestOptions, "token" | "organizationId">;
|
|
28
|
+
}) => Promise<import("@classytic/repo-core/pagination").PaginatedResult<PlatformOrganization>>;
|
|
29
|
+
decide(id: string, data: {
|
|
30
|
+
action: OrganizationAction;
|
|
31
|
+
expectedStatus: OrganizationStatus;
|
|
32
|
+
note: string;
|
|
33
|
+
}): Promise<PlatformOrganization>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseApi } from "@classytic/arc-next/api";
|
|
2
|
+
import { createClient } from "@classytic/arc-next/client";
|
|
3
|
+
//#region src/platform.ts
|
|
4
|
+
/** Platform administration uses a bearer identity without selecting a merchant organization. */
|
|
5
|
+
function createPlatformClient({ apiUrl, token }) {
|
|
6
|
+
if (!token.trim()) throw new Error("A platform bearer token is required.");
|
|
7
|
+
const url = new URL(apiUrl);
|
|
8
|
+
if (url.username || url.password || url.search || url.hash) throw new Error("Use a plain API URL.");
|
|
9
|
+
const client = createClient({
|
|
10
|
+
baseUrl: url.href.replace(/\/+$/, ""),
|
|
11
|
+
getToken: () => token,
|
|
12
|
+
credentials: "omit",
|
|
13
|
+
autoIdempotency: false,
|
|
14
|
+
timeoutMs: 15e3
|
|
15
|
+
});
|
|
16
|
+
const organizations = new BaseApi("admin/organizations", {
|
|
17
|
+
client,
|
|
18
|
+
basePath: "",
|
|
19
|
+
cache: "no-store"
|
|
20
|
+
});
|
|
21
|
+
return { organizations: {
|
|
22
|
+
list: organizations.getAll.bind(organizations),
|
|
23
|
+
decide(id, data) {
|
|
24
|
+
return organizations.request("POST", `${organizations.baseUrl}/${encodeURIComponent(id)}/action`, { data });
|
|
25
|
+
}
|
|
26
|
+
} };
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { createPlatformClient };
|
|
@@ -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 };
|
package/dist/types.d.ts
ADDED
|
@@ -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,100 @@
|
|
|
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.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
### Media library
|
|
92
|
+
|
|
93
|
+
`client.media.list({ params: { page: 1, limit: 20, folder: 'products' } })`
|
|
94
|
+
returns tenant-scoped media and pagination. `client.media.remove({ id })` uses
|
|
95
|
+
the media resource's guarded delete; an in-use image is refused by the server.
|
|
96
|
+
|
|
97
|
+
`client.media.uploadImage(file, folder)` resolves the server's transform policy
|
|
98
|
+
through media-transform before requesting storage writes. The server must accept
|
|
99
|
+
every output format in its policy, including the browser encoder's fallback.
|
|
100
|
+
Passthrough thresholds and dimensions belong to that policy, rather than the UI.
|