@vencav/shoptet-client 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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.cjs +271 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +244 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { Result } from '@vencav/result';
|
|
4
|
+
export { Result } from '@vencav/result';
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
var SHOPTET_API_BATCH_LIMIT = 300;
|
|
8
|
+
var SHOPTET_API_TIMEOUT_MS = 3e4;
|
|
9
|
+
var SUPPRESS_FLAGS = [
|
|
10
|
+
"suppressDocumentGeneration",
|
|
11
|
+
"suppressEmailSending",
|
|
12
|
+
"suppressProductChecking",
|
|
13
|
+
"suppressStockMovements",
|
|
14
|
+
"suppressHistoricalMandatoryFields",
|
|
15
|
+
"suppressHistoricalPaymentChecking",
|
|
16
|
+
"suppressHistoricalShippingChecking"
|
|
17
|
+
];
|
|
18
|
+
var requireNonEmpty = (ctx, value, name) => {
|
|
19
|
+
if (!value) throw new Error(`${ctx}: ${name} must not be empty`);
|
|
20
|
+
};
|
|
21
|
+
var resolvePositiveInt = (ctx, value, fallback, name) => {
|
|
22
|
+
const resolved = value ?? fallback;
|
|
23
|
+
if (!Number.isInteger(resolved) || resolved < 1) {
|
|
24
|
+
throw new Error(`${ctx}: ${name} must be a positive integer`);
|
|
25
|
+
}
|
|
26
|
+
return resolved;
|
|
27
|
+
};
|
|
28
|
+
var validateHttpsUrl = (ctx, raw, fieldName = "url") => {
|
|
29
|
+
let url;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`${ctx}: ${fieldName} is not a valid URL`);
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== "https:") throw new Error(`${ctx}: ${fieldName} must use HTTPS`);
|
|
36
|
+
if (url.username || url.password) throw new Error(`${ctx}: ${fieldName} must not contain credentials`);
|
|
37
|
+
};
|
|
38
|
+
var apiCall = async (promise) => {
|
|
39
|
+
try {
|
|
40
|
+
return Result.ok((await promise).data);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (axios.isAxiosError(error)) {
|
|
43
|
+
const apiErrors = error.response?.data?.errors;
|
|
44
|
+
if (Array.isArray(apiErrors) && apiErrors.length > 0) {
|
|
45
|
+
const msg = joinMessages(apiErrors);
|
|
46
|
+
return Result.error(new Error(`Shoptet API error (${error.response?.status}): ${msg}`));
|
|
47
|
+
}
|
|
48
|
+
return Result.error(new Error(error.message || "Unknown error"));
|
|
49
|
+
}
|
|
50
|
+
return Result.error(error instanceof Error ? error : new Error(String(error)));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var joinMessages = (errors) => errors.map((e) => e.message).join(", ");
|
|
54
|
+
var parseResponseErrors = (raw) => (raw ?? []).map((e) => ({ code: e.instance ?? "unknown", error: e.message ?? "Unknown error" }));
|
|
55
|
+
var buildSuppressParams = (options) => {
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
for (const flag of SUPPRESS_FLAGS) {
|
|
58
|
+
if (options[flag]) params.set(flag, "true");
|
|
59
|
+
}
|
|
60
|
+
return params;
|
|
61
|
+
};
|
|
62
|
+
var scrubSensitiveHeaders = (headers) => {
|
|
63
|
+
if (!headers || typeof headers !== "object") return;
|
|
64
|
+
const axiosHeaders = headers;
|
|
65
|
+
if (typeof axiosHeaders.delete === "function") {
|
|
66
|
+
axiosHeaders.delete("Shoptet-Private-API-Token");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const headerEntries = Object.keys(headers);
|
|
70
|
+
for (const key of headerEntries) {
|
|
71
|
+
if (key.toLowerCase() === "shoptet-private-api-token") {
|
|
72
|
+
delete headers[key];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var checkWebhookSignature = (secret, payload, signature) => {
|
|
77
|
+
const expected = crypto.createHmac("sha1", secret).update(payload).digest("hex");
|
|
78
|
+
try {
|
|
79
|
+
return crypto.timingSafeEqual(
|
|
80
|
+
Uint8Array.from(Buffer.from(signature, "hex")),
|
|
81
|
+
Uint8Array.from(Buffer.from(expected, "hex"))
|
|
82
|
+
);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var fetchOrder = async (client, orderCode) => {
|
|
88
|
+
const result = await apiCall(
|
|
89
|
+
client.get(`/api/orders/${encodeURIComponent(orderCode)}`)
|
|
90
|
+
);
|
|
91
|
+
if (Result.isError(result)) return result;
|
|
92
|
+
if (result.data.errors?.length) {
|
|
93
|
+
const msg = joinMessages(result.data.errors);
|
|
94
|
+
return Result.error(new Error(`Shoptet API error: ${msg}`));
|
|
95
|
+
}
|
|
96
|
+
const order = result.data.data?.order;
|
|
97
|
+
if (!order) {
|
|
98
|
+
return Result.error(new Error("Shoptet API returned no order data"));
|
|
99
|
+
}
|
|
100
|
+
return Result.ok(order);
|
|
101
|
+
};
|
|
102
|
+
var fetchStockId = async (client) => {
|
|
103
|
+
const result = await apiCall(
|
|
104
|
+
client.get("/api/stocks")
|
|
105
|
+
);
|
|
106
|
+
if (Result.isError(result)) return result;
|
|
107
|
+
const stocks = result.data.data?.stocks;
|
|
108
|
+
if (!stocks || stocks.length === 0) {
|
|
109
|
+
return Result.error(new Error("No stocks (warehouses) found in Shoptet"));
|
|
110
|
+
}
|
|
111
|
+
return Result.ok(stocks[0].id);
|
|
112
|
+
};
|
|
113
|
+
var fetchPricelistId = async (client) => {
|
|
114
|
+
const result = await apiCall(
|
|
115
|
+
client.get("/api/pricelists")
|
|
116
|
+
);
|
|
117
|
+
if (Result.isError(result)) return result;
|
|
118
|
+
const pricelists = result.data.data?.priceLists ?? result.data.data?.pricelists;
|
|
119
|
+
if (!pricelists || pricelists.length === 0) {
|
|
120
|
+
return Result.error(new Error("No pricelists found in Shoptet"));
|
|
121
|
+
}
|
|
122
|
+
return Result.ok(pricelists[0].id);
|
|
123
|
+
};
|
|
124
|
+
var updateStock = async (client, getStockId, maxBatchSize, items) => {
|
|
125
|
+
if (items.length === 0) return Result.ok({ success: 0, failed: 0, errors: [] });
|
|
126
|
+
if (items.length > maxBatchSize) {
|
|
127
|
+
return Result.error(new Error(`batchUpdateStock: too many items (${items.length} > ${maxBatchSize})`));
|
|
128
|
+
}
|
|
129
|
+
const invalidQuantity = items.find((item) => !Number.isFinite(item.quantity));
|
|
130
|
+
if (invalidQuantity) {
|
|
131
|
+
return Result.error(
|
|
132
|
+
new Error(`batchUpdateStock: invalid quantity for "${invalidQuantity.productCode}" (${invalidQuantity.quantity})`)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const stockIdResult = await getStockId();
|
|
136
|
+
if (Result.isError(stockIdResult)) {
|
|
137
|
+
return Result.ok({
|
|
138
|
+
success: 0,
|
|
139
|
+
failed: items.length,
|
|
140
|
+
errors: items.map((item) => ({ code: item.productCode, error: stockIdResult.error.message }))
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const result = await apiCall(
|
|
144
|
+
client.patch(
|
|
145
|
+
`/api/stocks/${stockIdResult.data}/movements`,
|
|
146
|
+
{ data: items.map((item) => ({ productCode: item.productCode, quantity: item.quantity })) }
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
if (Result.isError(result)) {
|
|
150
|
+
return Result.ok({
|
|
151
|
+
success: 0,
|
|
152
|
+
failed: items.length,
|
|
153
|
+
errors: items.map((item) => ({ code: item.productCode, error: result.error.message }))
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const errors = parseResponseErrors(result.data.errors);
|
|
157
|
+
return Result.ok({ success: items.length - errors.length, failed: errors.length, errors });
|
|
158
|
+
};
|
|
159
|
+
var updatePrices = async (client, getPricelistId, maxBatchSize, items) => {
|
|
160
|
+
if (items.length === 0) return Result.ok({ success: 0, failed: 0, errors: [] });
|
|
161
|
+
if (items.length > maxBatchSize) {
|
|
162
|
+
return Result.error(new Error(`batchUpdatePrices: too many items (${items.length} > ${maxBatchSize})`));
|
|
163
|
+
}
|
|
164
|
+
const invalidPrice = items.find((item) => !Number.isFinite(item.price) || item.price < 0);
|
|
165
|
+
if (invalidPrice) {
|
|
166
|
+
return Result.error(
|
|
167
|
+
new Error(`batchUpdatePrices: invalid price for "${invalidPrice.code}" (${invalidPrice.price})`)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const pricelistIdResult = await getPricelistId();
|
|
171
|
+
if (Result.isError(pricelistIdResult)) {
|
|
172
|
+
return Result.ok({
|
|
173
|
+
success: 0,
|
|
174
|
+
failed: items.length,
|
|
175
|
+
errors: items.map((item) => ({ code: item.code, error: pricelistIdResult.error.message }))
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const result = await apiCall(
|
|
179
|
+
client.patch(
|
|
180
|
+
`/api/pricelists/${pricelistIdResult.data}`,
|
|
181
|
+
{
|
|
182
|
+
data: items.map((item) => ({
|
|
183
|
+
code: item.code,
|
|
184
|
+
includingVat: true,
|
|
185
|
+
price: { price: item.price.toFixed(2) }
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
if (Result.isError(result)) {
|
|
191
|
+
return Result.ok({
|
|
192
|
+
success: 0,
|
|
193
|
+
failed: items.length,
|
|
194
|
+
errors: items.map((item) => ({ code: item.code, error: result.error.message }))
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const errors = parseResponseErrors(result.data.errors);
|
|
198
|
+
return Result.ok({ success: items.length - errors.length, failed: errors.length, errors });
|
|
199
|
+
};
|
|
200
|
+
var postOrder = async (client, order, orderOptions) => {
|
|
201
|
+
const qs = buildSuppressParams(orderOptions).toString();
|
|
202
|
+
const url = `/api/orders${qs ? `?${qs}` : ""}`;
|
|
203
|
+
const result = await apiCall(
|
|
204
|
+
client.post(url, { data: order })
|
|
205
|
+
);
|
|
206
|
+
if (Result.isError(result)) return result;
|
|
207
|
+
if (result.data.errors?.length) {
|
|
208
|
+
const msg = joinMessages(result.data.errors);
|
|
209
|
+
return Result.error(new Error(`Shoptet API errors: ${msg}`));
|
|
210
|
+
}
|
|
211
|
+
const createdOrder = result.data.data?.order;
|
|
212
|
+
if (!createdOrder) {
|
|
213
|
+
return Result.error(new Error("Shoptet API returned no order data"));
|
|
214
|
+
}
|
|
215
|
+
return Result.ok(createdOrder);
|
|
216
|
+
};
|
|
217
|
+
var createIdResolver = (fetch) => {
|
|
218
|
+
let cached = null;
|
|
219
|
+
return async () => {
|
|
220
|
+
if (cached !== null) return Result.ok(cached);
|
|
221
|
+
const result = await fetch();
|
|
222
|
+
if (Result.isOk(result)) cached = result.data;
|
|
223
|
+
return result;
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
var createShoptetClient = (credentials, options = {}) => {
|
|
227
|
+
validateHttpsUrl("ShoptetClient", credentials.apiUrl, "apiUrl");
|
|
228
|
+
requireNonEmpty("ShoptetClient", credentials.oauthAccessToken, "oauthAccessToken");
|
|
229
|
+
requireNonEmpty("ShoptetClient", credentials.webhookSecret, "webhookSecret");
|
|
230
|
+
const maxBatchSize = resolvePositiveInt("ShoptetClient", options.maxBatchSize, SHOPTET_API_BATCH_LIMIT, "maxBatchSize");
|
|
231
|
+
const timeoutMs = resolvePositiveInt("ShoptetClient", options.timeoutMs, SHOPTET_API_TIMEOUT_MS, "timeoutMs");
|
|
232
|
+
const client = axios.create({
|
|
233
|
+
baseURL: credentials.apiUrl,
|
|
234
|
+
timeout: timeoutMs,
|
|
235
|
+
headers: {
|
|
236
|
+
"Content-Type": "application/vnd.shoptet.v1.0+json",
|
|
237
|
+
"Shoptet-Private-API-Token": credentials.oauthAccessToken
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
client.interceptors.response.use(void 0, (error) => {
|
|
241
|
+
if (axios.isAxiosError(error)) {
|
|
242
|
+
scrubSensitiveHeaders(error.config?.headers);
|
|
243
|
+
}
|
|
244
|
+
return Promise.reject(error);
|
|
245
|
+
});
|
|
246
|
+
const getDefaultStockId = createIdResolver(() => fetchStockId(client));
|
|
247
|
+
const getDefaultPricelistId = createIdResolver(() => fetchPricelistId(client));
|
|
248
|
+
return {
|
|
249
|
+
validateWebhookSignature: (payload, signature) => checkWebhookSignature(credentials.webhookSecret, payload, signature),
|
|
250
|
+
getOrder: (orderCode) => fetchOrder(client, orderCode),
|
|
251
|
+
getDefaultStockId,
|
|
252
|
+
batchUpdateStock: (items) => updateStock(client, getDefaultStockId, maxBatchSize, items),
|
|
253
|
+
getDefaultPricelistId,
|
|
254
|
+
batchUpdatePrices: (items) => updatePrices(client, getDefaultPricelistId, maxBatchSize, items),
|
|
255
|
+
createOrder: (order, orderOptions = {}) => postOrder(client, order, orderOptions)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export { createShoptetClient };
|
|
260
|
+
//# sourceMappingURL=index.js.map
|
|
261
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import axios, { AxiosInstance, AxiosResponse } from \"axios\";\nimport crypto from \"crypto\";\nimport { Result } from \"@vencav/result\";\nimport type { CreateOrderOptions, CreateOrderRequest, ShoptetApiResponse, ShoptetOrder } from \"./types\";\n\nexport interface ShoptetCredentials {\n readonly oauthAccessToken: string;\n readonly apiUrl: string;\n readonly webhookSecret: string;\n}\n\nexport interface ShoptetClientOptions {\n /** Maximum items per batch request. Shoptet API hard limit is 300. Default: 300. */\n readonly maxBatchSize?: number;\n /** Request timeout in milliseconds. Default: 30000. */\n readonly timeoutMs?: number;\n}\n\nexport type BatchUpdateResult = {\n readonly success: number;\n readonly failed: number;\n readonly errors: ReadonlyArray<{ readonly code: string; readonly error: string }>;\n};\n\nexport type ShoptetClient = ReturnType<typeof createShoptetClient>;\n\nconst SHOPTET_API_BATCH_LIMIT = 300;\nconst SHOPTET_API_TIMEOUT_MS = 30_000;\n\nconst SUPPRESS_FLAGS = [\n \"suppressDocumentGeneration\",\n \"suppressEmailSending\",\n \"suppressProductChecking\",\n \"suppressStockMovements\",\n \"suppressHistoricalMandatoryFields\",\n \"suppressHistoricalPaymentChecking\",\n \"suppressHistoricalShippingChecking\",\n] as const satisfies ReadonlyArray<keyof CreateOrderOptions>;\n\n// ── Validation helpers ──────────────────────────────────────\n\nconst requireNonEmpty = (ctx: string, value: string, name: string): void => {\n if (!value) throw new Error(`${ctx}: ${name} must not be empty`);\n};\n\nconst resolvePositiveInt = (ctx: string, value: number | undefined, fallback: number, name: string): number => {\n const resolved = value ?? fallback;\n if (!Number.isInteger(resolved) || resolved < 1) {\n throw new Error(`${ctx}: ${name} must be a positive integer`);\n }\n return resolved;\n};\n\nconst validateHttpsUrl = (ctx: string, raw: string, fieldName = \"url\"): void => {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`${ctx}: ${fieldName} is not a valid URL`);\n }\n if (url.protocol !== \"https:\") throw new Error(`${ctx}: ${fieldName} must use HTTPS`);\n if (url.username || url.password) throw new Error(`${ctx}: ${fieldName} must not contain credentials`);\n};\n\n// ── Core API helper ─────────────────────────────────────────\n\nconst apiCall = async <T>(promise: Promise<AxiosResponse<T>>): Promise<Result<T>> => {\n try {\n return Result.ok((await promise).data);\n } catch (error: unknown) {\n if (axios.isAxiosError(error)) {\n const apiErrors = error.response?.data?.errors;\n if (Array.isArray(apiErrors) && apiErrors.length > 0) {\n const msg = joinMessages(apiErrors as Array<{ message: string }>);\n return Result.error(new Error(`Shoptet API error (${error.response?.status}): ${msg}`));\n }\n return Result.error(new Error(error.message || \"Unknown error\"));\n }\n return Result.error(error instanceof Error ? error : new Error(String(error)));\n }\n};\n\n// ── Shared helpers ──────────────────────────────────────────\n\nconst joinMessages = (errors: ReadonlyArray<{ message: string }>): string =>\n errors.map((e) => e.message).join(\", \");\n\nconst parseResponseErrors = (\n raw: Array<{ instance?: string; message?: string }> | null | undefined\n): Array<{ code: string; error: string }> =>\n (raw ?? []).map((e) => ({ code: e.instance ?? \"unknown\", error: e.message ?? \"Unknown error\" }));\n\nconst buildSuppressParams = (options: CreateOrderOptions): URLSearchParams => {\n const params = new URLSearchParams();\n for (const flag of SUPPRESS_FLAGS) {\n if (options[flag]) params.set(flag, \"true\");\n }\n return params;\n};\n\nconst scrubSensitiveHeaders = (headers: unknown): void => {\n if (!headers || typeof headers !== \"object\") return;\n\n const axiosHeaders = headers as { delete?: (name: string) => void };\n if (typeof axiosHeaders.delete === \"function\") {\n axiosHeaders.delete(\"Shoptet-Private-API-Token\");\n return;\n }\n\n const headerEntries = Object.keys(headers as Record<string, unknown>);\n for (const key of headerEntries) {\n if (key.toLowerCase() === \"shoptet-private-api-token\") {\n delete (headers as Record<string, unknown>)[key];\n }\n }\n};\n\n// ── Pure API functions ──────────────────────────────────────\n\nconst checkWebhookSignature = (secret: string, payload: string, signature: string): boolean => {\n const expected = crypto.createHmac(\"sha1\", secret).update(payload).digest(\"hex\");\n try {\n return crypto.timingSafeEqual(\n Uint8Array.from(Buffer.from(signature, \"hex\")),\n Uint8Array.from(Buffer.from(expected, \"hex\"))\n );\n } catch {\n return false;\n }\n};\n\nconst fetchOrder = async (client: AxiosInstance, orderCode: string): Promise<Result<ShoptetOrder>> => {\n const result = await apiCall(\n client.get<ShoptetApiResponse<{ order: ShoptetOrder }>>(`/api/orders/${encodeURIComponent(orderCode)}`)\n );\n if (Result.isError(result)) return result;\n if (result.data.errors?.length) {\n const msg = joinMessages(result.data.errors);\n return Result.error(new Error(`Shoptet API error: ${msg}`));\n }\n const order = result.data.data?.order;\n if (!order) {\n return Result.error(new Error(\"Shoptet API returned no order data\"));\n }\n return Result.ok(order);\n};\n\nconst fetchStockId = async (client: AxiosInstance): Promise<Result<number>> => {\n const result = await apiCall(\n client.get<{ data: { stocks: Array<{ id: number; title: string }> } }>(\"/api/stocks\")\n );\n if (Result.isError(result)) return result;\n const stocks = result.data.data?.stocks;\n if (!stocks || stocks.length === 0) {\n return Result.error(new Error(\"No stocks (warehouses) found in Shoptet\"));\n }\n return Result.ok(stocks[0].id);\n};\n\nconst fetchPricelistId = async (client: AxiosInstance): Promise<Result<number>> => {\n const result = await apiCall(\n client.get<{\n data: {\n priceLists?: Array<{ id: number; name: string }>;\n pricelists?: Array<{ id: number; name: string }>;\n };\n }>(\"/api/pricelists\")\n );\n if (Result.isError(result)) return result;\n const pricelists = result.data.data?.priceLists ?? result.data.data?.pricelists;\n if (!pricelists || pricelists.length === 0) {\n return Result.error(new Error(\"No pricelists found in Shoptet\"));\n }\n return Result.ok(pricelists[0].id);\n};\n\nconst updateStock = async (\n client: AxiosInstance,\n getStockId: () => Promise<Result<number>>,\n maxBatchSize: number,\n items: Array<{ productCode: string; quantity: number }>\n): Promise<Result<BatchUpdateResult>> => {\n if (items.length === 0) return Result.ok({ success: 0, failed: 0, errors: [] });\n if (items.length > maxBatchSize) {\n return Result.error(new Error(`batchUpdateStock: too many items (${items.length} > ${maxBatchSize})`));\n }\n\n const invalidQuantity = items.find((item) => !Number.isFinite(item.quantity));\n if (invalidQuantity) {\n return Result.error(\n new Error(`batchUpdateStock: invalid quantity for \"${invalidQuantity.productCode}\" (${invalidQuantity.quantity})`)\n );\n }\n\n const stockIdResult = await getStockId();\n if (Result.isError(stockIdResult)) {\n return Result.ok({\n success: 0,\n failed: items.length,\n errors: items.map((item) => ({ code: item.productCode, error: stockIdResult.error.message })),\n });\n }\n\n const result = await apiCall(\n client.patch<{ errors?: Array<{ instance?: string; message?: string }> }>(\n `/api/stocks/${stockIdResult.data}/movements`,\n { data: items.map((item) => ({ productCode: item.productCode, quantity: item.quantity })) }\n )\n );\n\n if (Result.isError(result)) {\n return Result.ok({\n success: 0,\n failed: items.length,\n errors: items.map((item) => ({ code: item.productCode, error: result.error.message })),\n });\n }\n\n const errors = parseResponseErrors(result.data.errors);\n return Result.ok({ success: items.length - errors.length, failed: errors.length, errors });\n};\n\nconst updatePrices = async (\n client: AxiosInstance,\n getPricelistId: () => Promise<Result<number>>,\n maxBatchSize: number,\n items: Array<{ code: string; price: number }>\n): Promise<Result<BatchUpdateResult>> => {\n if (items.length === 0) return Result.ok({ success: 0, failed: 0, errors: [] });\n if (items.length > maxBatchSize) {\n return Result.error(new Error(`batchUpdatePrices: too many items (${items.length} > ${maxBatchSize})`));\n }\n\n const invalidPrice = items.find((item) => !Number.isFinite(item.price) || item.price < 0);\n if (invalidPrice) {\n return Result.error(\n new Error(`batchUpdatePrices: invalid price for \"${invalidPrice.code}\" (${invalidPrice.price})`)\n );\n }\n\n const pricelistIdResult = await getPricelistId();\n if (Result.isError(pricelistIdResult)) {\n return Result.ok({\n success: 0,\n failed: items.length,\n errors: items.map((item) => ({ code: item.code, error: pricelistIdResult.error.message })),\n });\n }\n\n const result = await apiCall(\n client.patch<{ errors?: Array<{ instance?: string; message?: string }> }>(\n `/api/pricelists/${pricelistIdResult.data}`,\n {\n data: items.map((item) => ({\n code: item.code,\n includingVat: true,\n price: { price: item.price.toFixed(2) },\n })),\n }\n )\n );\n\n if (Result.isError(result)) {\n return Result.ok({\n success: 0,\n failed: items.length,\n errors: items.map((item) => ({ code: item.code, error: result.error.message })),\n });\n }\n\n const errors = parseResponseErrors(result.data.errors);\n return Result.ok({ success: items.length - errors.length, failed: errors.length, errors });\n};\n\nconst postOrder = async (\n client: AxiosInstance,\n order: CreateOrderRequest,\n orderOptions: CreateOrderOptions\n): Promise<Result<ShoptetOrder>> => {\n const qs = buildSuppressParams(orderOptions).toString();\n const url = `/api/orders${qs ? `?${qs}` : \"\"}`;\n\n const result = await apiCall(\n client.post<ShoptetApiResponse<{ order: ShoptetOrder }>>(url, { data: order })\n );\n if (Result.isError(result)) return result;\n if (result.data.errors?.length) {\n const msg = joinMessages(result.data.errors);\n return Result.error(new Error(`Shoptet API errors: ${msg}`));\n }\n const createdOrder = result.data.data?.order;\n if (!createdOrder) {\n return Result.error(new Error(\"Shoptet API returned no order data\"));\n }\n return Result.ok(createdOrder);\n};\n\n// ── Caching resolvers ───────────────────────────────────────\n\nconst createIdResolver = (fetch: () => Promise<Result<number>>) => {\n let cached: number | null = null;\n return async (): Promise<Result<number>> => {\n if (cached !== null) return Result.ok(cached);\n const result = await fetch();\n if (Result.isOk(result)) cached = result.data;\n return result;\n };\n};\n\n// ── Factory ─────────────────────────────────────────────────\n\nexport const createShoptetClient = (\n credentials: ShoptetCredentials,\n options: ShoptetClientOptions = {}\n) => {\n validateHttpsUrl(\"ShoptetClient\", credentials.apiUrl, \"apiUrl\");\n requireNonEmpty(\"ShoptetClient\", credentials.oauthAccessToken, \"oauthAccessToken\");\n requireNonEmpty(\"ShoptetClient\", credentials.webhookSecret, \"webhookSecret\");\n\n const maxBatchSize = resolvePositiveInt(\"ShoptetClient\", options.maxBatchSize, SHOPTET_API_BATCH_LIMIT, \"maxBatchSize\");\n const timeoutMs = resolvePositiveInt(\"ShoptetClient\", options.timeoutMs, SHOPTET_API_TIMEOUT_MS, \"timeoutMs\");\n\n const client = axios.create({\n baseURL: credentials.apiUrl,\n timeout: timeoutMs,\n headers: {\n \"Content-Type\": \"application/vnd.shoptet.v1.0+json\",\n \"Shoptet-Private-API-Token\": credentials.oauthAccessToken,\n },\n });\n\n client.interceptors.response.use(undefined, (error: unknown) => {\n if (axios.isAxiosError(error)) {\n scrubSensitiveHeaders(error.config?.headers);\n }\n return Promise.reject(error);\n });\n\n const getDefaultStockId = createIdResolver(() => fetchStockId(client));\n const getDefaultPricelistId = createIdResolver(() => fetchPricelistId(client));\n\n return {\n validateWebhookSignature: (payload: string, signature: string) =>\n checkWebhookSignature(credentials.webhookSecret, payload, signature),\n getOrder: (orderCode: string) =>\n fetchOrder(client, orderCode),\n getDefaultStockId,\n batchUpdateStock: (items: Array<{ productCode: string; quantity: number }>) =>\n updateStock(client, getDefaultStockId, maxBatchSize, items),\n getDefaultPricelistId,\n batchUpdatePrices: (items: Array<{ code: string; price: number }>) =>\n updatePrices(client, getDefaultPricelistId, maxBatchSize, items),\n createOrder: (order: CreateOrderRequest, orderOptions: CreateOrderOptions = {}) =>\n postOrder(client, order, orderOptions),\n };\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vencav/shoptet-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript client for the Shoptet Private API. Covers orders, stock, pricelists, and webhook signature validation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"shoptet",
|
|
7
|
+
"ecommerce",
|
|
8
|
+
"api",
|
|
9
|
+
"client",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"author": "Václav Vracovský <vaclavvracovsky@gmail.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://github.com/VencaV/shoptet-client#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/VencaV/shoptet-client.git",
|
|
18
|
+
"directory": "packages/shoptet-client"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/index.d.cts",
|
|
30
|
+
"default": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.cjs",
|
|
35
|
+
"module": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:dist": "pnpm build && node ./smoke-dist.mjs",
|
|
49
|
+
"test:coverage": "vitest run --coverage",
|
|
50
|
+
"lint": "eslint src",
|
|
51
|
+
"lint:fix": "eslint src --fix",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@vencav/result": "^0.1.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"axios": ">=1.0.0 <2.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^22.0.0",
|
|
62
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
63
|
+
"axios": "^1.13.4",
|
|
64
|
+
"tsup": "^8.4.0",
|
|
65
|
+
"typescript": "^5.8.3",
|
|
66
|
+
"vitest": "^3.2.0"
|
|
67
|
+
}
|
|
68
|
+
}
|