@porulle/adapter-woocommerce 0.10.8 → 0.11.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/dist/index.js +149 -13
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/index.ts +181 -18
package/dist/index.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { defineChannelConnector, Err, Ok } from "@porulle/core";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
const zeroDecimalCurrencies = new Set([
|
|
4
|
+
"BIF",
|
|
5
|
+
"CLP",
|
|
6
|
+
"DJF",
|
|
7
|
+
"GNF",
|
|
8
|
+
"ISK",
|
|
9
|
+
"JPY",
|
|
10
|
+
"KMF",
|
|
11
|
+
"KRW",
|
|
12
|
+
"PYG",
|
|
13
|
+
"RWF",
|
|
14
|
+
"UGX",
|
|
15
|
+
"VND",
|
|
16
|
+
"VUV",
|
|
17
|
+
"XAF",
|
|
18
|
+
"XOF",
|
|
19
|
+
"XPF",
|
|
20
|
+
]);
|
|
3
21
|
function buildWooUrl(base, path, key, secret, page, cursor) {
|
|
4
22
|
const url = new URL(path, base.replace(/\/$/, "/"));
|
|
5
23
|
url.searchParams.set("consumer_key", key);
|
|
@@ -10,11 +28,85 @@ function buildWooUrl(base, path, key, secret, page, cursor) {
|
|
|
10
28
|
url.searchParams.set("modified_after", cursor);
|
|
11
29
|
return url.toString();
|
|
12
30
|
}
|
|
13
|
-
function
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
function normalizeCurrency(value) {
|
|
32
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
33
|
+
return undefined;
|
|
34
|
+
return value.trim().toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
function parseMoney(value, currency) {
|
|
37
|
+
if (value == null || value.trim() === "")
|
|
38
|
+
return undefined;
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (!Number.isFinite(parsed))
|
|
41
|
+
return undefined;
|
|
42
|
+
const exponent = zeroDecimalCurrencies.has(currency) ? 0 : 2;
|
|
43
|
+
return Math.round(parsed * (10 ** exponent));
|
|
44
|
+
}
|
|
45
|
+
function pricesForVariation(variation, currency) {
|
|
46
|
+
if (!currency)
|
|
47
|
+
return undefined;
|
|
48
|
+
const amount = parseMoney(variation.price, currency);
|
|
49
|
+
return amount === undefined ? undefined : [{ currency, amount }];
|
|
50
|
+
}
|
|
51
|
+
function catalogStatus(value) {
|
|
52
|
+
if (value === "publish")
|
|
53
|
+
return "active";
|
|
54
|
+
if (value === "draft" || value === "private")
|
|
55
|
+
return "draft";
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function asRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
function settingCurrency(data) {
|
|
62
|
+
if (Array.isArray(data)) {
|
|
63
|
+
for (const entry of data) {
|
|
64
|
+
const setting = asRecord(entry);
|
|
65
|
+
if (setting?.id === "woocommerce_currency")
|
|
66
|
+
return normalizeCurrency(setting.value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const object = asRecord(data);
|
|
70
|
+
if (!object)
|
|
71
|
+
return undefined;
|
|
72
|
+
const direct = normalizeCurrency(object.woocommerce_currency);
|
|
73
|
+
if (direct)
|
|
74
|
+
return direct;
|
|
75
|
+
const systemStatus = asRecord(object.system_status);
|
|
76
|
+
return normalizeCurrency(systemStatus?.woocommerce_currency);
|
|
77
|
+
}
|
|
78
|
+
async function fetchWooCurrency(fetchImpl, url) {
|
|
79
|
+
const result = await request(fetchImpl, url);
|
|
80
|
+
return result.ok ? settingCurrency(result.value.data) : undefined;
|
|
81
|
+
}
|
|
82
|
+
async function fetchProductVariations(fetchImpl, storeDomain, auth, productId, modifiedAfter) {
|
|
83
|
+
const variations = [];
|
|
84
|
+
let page = 1;
|
|
85
|
+
while (true) {
|
|
86
|
+
const result = await request(fetchImpl, buildWooUrl(storeDomain, `/wp-json/wc/v3/products/${encodeURIComponent(productId)}/variations`, auth.key, auth.secret, page, modifiedAfter));
|
|
87
|
+
if (!result.ok)
|
|
88
|
+
return result;
|
|
89
|
+
variations.push(...result.value.data);
|
|
90
|
+
const totalPages = Number.parseInt(result.value.response.headers.get("x-wp-totalpages") ?? "1", 10);
|
|
91
|
+
if (!Number.isFinite(totalPages) || page >= totalPages)
|
|
92
|
+
break;
|
|
93
|
+
page += 1;
|
|
94
|
+
}
|
|
95
|
+
return Ok(variations);
|
|
96
|
+
}
|
|
97
|
+
function variationFromReference(reference) {
|
|
98
|
+
return typeof reference === "object" && reference !== null ? reference : { id: reference };
|
|
99
|
+
}
|
|
100
|
+
function mergeProductVariations(references, details) {
|
|
101
|
+
const detailById = new Map(details.map((variation) => [String(variation.id), variation]));
|
|
102
|
+
const referencedIds = new Set();
|
|
103
|
+
const merged = references.map((reference) => {
|
|
104
|
+
const fallback = variationFromReference(reference);
|
|
105
|
+
const id = String(fallback.id);
|
|
106
|
+
referencedIds.add(id);
|
|
107
|
+
return detailById.get(id) ?? fallback;
|
|
108
|
+
});
|
|
109
|
+
return [...merged, ...details.filter((variation) => !referencedIds.has(String(variation.id)))];
|
|
18
110
|
}
|
|
19
111
|
async function request(fetchImpl, url, init) {
|
|
20
112
|
try {
|
|
@@ -64,6 +156,7 @@ function storeUrl(domain) {
|
|
|
64
156
|
}
|
|
65
157
|
export function wooConnector(options = {}) {
|
|
66
158
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
159
|
+
const currencyCache = new Map();
|
|
67
160
|
return defineChannelConnector({
|
|
68
161
|
providerId: "woocommerce",
|
|
69
162
|
capabilities: { importCatalog: true, importInventory: true, pushOrder: true, receiveWebhooks: true },
|
|
@@ -120,22 +213,65 @@ export function wooConnector(options = {}) {
|
|
|
120
213
|
const parsedPage = isPage && pagePart ? Number.parseInt(pagePart, 10) : 1;
|
|
121
214
|
const page = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1;
|
|
122
215
|
const modifiedAfter = afterParts.length > 0 ? afterParts.join("|") : (!isPage ? cursor : undefined);
|
|
216
|
+
const currencyKey = store.storeDomain.replace(/\/$/, "");
|
|
217
|
+
let currencyPromise = currencyCache.get(currencyKey);
|
|
218
|
+
if (!currencyPromise) {
|
|
219
|
+
currencyPromise = fetchWooCurrency(fetchImpl, buildWooUrl(store.storeDomain, "/wp-json/wc/v3/settings/general", auth.key, auth.secret, 1));
|
|
220
|
+
currencyCache.set(currencyKey, currencyPromise);
|
|
221
|
+
}
|
|
222
|
+
const currency = await currencyPromise;
|
|
123
223
|
const result = await request(fetchImpl, buildWooUrl(store.storeDomain, "/wp-json/wc/v3/products", auth.key, auth.secret, page, modifiedAfter));
|
|
124
224
|
if (!result.ok)
|
|
125
225
|
return result;
|
|
126
226
|
const totalPages = Number.parseInt(result.value.response.headers.get("x-wp-totalpages") ?? "1", 10);
|
|
127
227
|
const nextCursor = page < totalPages ? (modifiedAfter ? `${page + 1}|${modifiedAfter}` : String(page + 1)) : null;
|
|
128
|
-
|
|
228
|
+
const items = [];
|
|
229
|
+
for (const product of result.value.data) {
|
|
230
|
+
const references = product.variations ?? [];
|
|
231
|
+
const details = references.length > 0
|
|
232
|
+
? await fetchProductVariations(fetchImpl, store.storeDomain, auth, String(product.id), modifiedAfter)
|
|
233
|
+
: Ok([]);
|
|
234
|
+
if (!details.ok)
|
|
235
|
+
return details;
|
|
236
|
+
const variants = mergeProductVariations(references, details.value).map((variant) => {
|
|
237
|
+
const optionValues = Object.fromEntries((variant.attributes ?? []).flatMap((attribute) => (attribute.option != null && attribute.option !== "" ? [[attribute.name, attribute.option]] : [])));
|
|
238
|
+
const prices = pricesForVariation(variant, currency);
|
|
239
|
+
return {
|
|
240
|
+
externalId: String(variant.id),
|
|
241
|
+
...(variant.sku ? { sku: variant.sku } : {}),
|
|
242
|
+
...(Object.keys(optionValues).length > 0 ? { optionValues } : {}),
|
|
243
|
+
...(prices ? { prices } : {}),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
const options = product.attributes?.filter((attribute) => attribute.variation === true).map((attribute, index) => ({
|
|
247
|
+
name: attribute.name,
|
|
248
|
+
displayName: attribute.name,
|
|
249
|
+
...(attribute.position != null ? { sortOrder: attribute.position } : { sortOrder: index }),
|
|
250
|
+
values: (attribute.options ?? []).map((value, valueIndex) => ({ value, displayValue: value, sortOrder: valueIndex })),
|
|
251
|
+
}));
|
|
252
|
+
const status = catalogStatus(product.status);
|
|
253
|
+
items.push({
|
|
129
254
|
externalId: String(product.id),
|
|
130
255
|
slug: product.slug ?? String(product.id),
|
|
131
256
|
title: product.name,
|
|
132
|
-
...(product.description ? { description: product.description } : {}),
|
|
133
|
-
variants
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
257
|
+
attributes: [{ locale: "en", title: product.name, ...(product.description != null ? { description: product.description } : {}) }],
|
|
258
|
+
variants,
|
|
259
|
+
...(product.images ? {
|
|
260
|
+
images: product.images.map((image, index) => ({
|
|
261
|
+
externalId: String(image.id),
|
|
262
|
+
url: image.src,
|
|
263
|
+
...(image.alt != null ? { alt: image.alt } : {}),
|
|
264
|
+
role: index === 0 ? "primary" : "gallery",
|
|
265
|
+
...(image.position != null ? { sortOrder: image.position } : {}),
|
|
266
|
+
})),
|
|
267
|
+
} : {}),
|
|
268
|
+
...(options ? { options } : {}),
|
|
269
|
+
...(product.tags ? { tags: product.tags.flatMap((tag) => tag.slug ? [tag.slug] : []) } : {}),
|
|
270
|
+
...(product.categories ? { categories: product.categories.flatMap((category) => category.slug ? [category.slug] : []) } : {}),
|
|
271
|
+
...(status ? { status } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return Ok({ items, nextCursor });
|
|
139
275
|
},
|
|
140
276
|
async fetchInventory(store, ids) {
|
|
141
277
|
const auth = credentials(store);
|