@porulle/adapter-shopify 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 CHANGED
@@ -7,11 +7,60 @@ export const REQUIRED_SCOPES = [
7
7
  "write_orders",
8
8
  "read_fulfillments",
9
9
  ];
10
- function parseMoney(value) {
11
- if (!value)
12
- return 0;
13
- const parsed = Number.parseFloat(value);
14
- return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
10
+ const zeroDecimalCurrencies = new Set([
11
+ "BIF",
12
+ "CLP",
13
+ "DJF",
14
+ "GNF",
15
+ "ISK",
16
+ "JPY",
17
+ "KMF",
18
+ "KRW",
19
+ "PYG",
20
+ "RWF",
21
+ "UGX",
22
+ "VND",
23
+ "VUV",
24
+ "XAF",
25
+ "XOF",
26
+ "XPF",
27
+ ]);
28
+ function normalizeCurrency(value) {
29
+ if (typeof value !== "string" || value.trim() === "")
30
+ return undefined;
31
+ return value.trim().toUpperCase();
32
+ }
33
+ function parseMoney(value, currency) {
34
+ if (value == null || value.trim() === "")
35
+ return undefined;
36
+ const parsed = Number(value);
37
+ if (!Number.isFinite(parsed))
38
+ return undefined;
39
+ const exponent = zeroDecimalCurrencies.has(currency) ? 0 : 2;
40
+ return Math.round(parsed * (10 ** exponent));
41
+ }
42
+ function pricesForVariant(variant, currency) {
43
+ if (!currency)
44
+ return undefined;
45
+ const amount = parseMoney(variant.price, currency);
46
+ if (amount === undefined)
47
+ return undefined;
48
+ const compareAtAmount = parseMoney(variant.compare_at_price, currency);
49
+ return [{
50
+ currency,
51
+ amount,
52
+ ...(compareAtAmount !== undefined && compareAtAmount !== amount ? { compareAtAmount } : {}),
53
+ }];
54
+ }
55
+ function catalogStatus(value) {
56
+ return value === "draft" || value === "active" || value === "archived" ? value : undefined;
57
+ }
58
+ function slugify(value) {
59
+ return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
60
+ }
61
+ async function fetchShopCurrency(fetchImpl, url, accessToken) {
62
+ const result = await request(fetchImpl, url, accessToken);
63
+ return result.ok ? normalizeCurrency(result.value.data.shop?.currency) : undefined;
15
64
  }
16
65
  function apiBase(store, version) {
17
66
  return `https://${store.storeDomain.replace(/^https?:\/\//, "").replace(/\/$/, "")}/admin/api/${version}`;
@@ -76,6 +125,7 @@ function oauthError(code, message) {
76
125
  export function shopifyConnector(options = {}) {
77
126
  const fetchImpl = options.fetchImpl ?? fetch;
78
127
  const version = options.apiVersion ?? "2024-10";
128
+ const currencyCache = new Map();
79
129
  return defineChannelConnector({
80
130
  providerId: "shopify",
81
131
  capabilities: { importCatalog: true, importInventory: true, pushOrder: true, receiveWebhooks: true },
@@ -135,6 +185,14 @@ export function shopifyConnector(options = {}) {
135
185
  const token = credentials(store);
136
186
  if (!token)
137
187
  return Err({ code: "SHOPIFY_CREDENTIALS_REQUIRED", message: "Shopify accessToken is required." });
188
+ const currencyUrl = `${apiBase(store, version)}/shop.json`;
189
+ const currencyKey = apiBase(store, version);
190
+ let currencyPromise = currencyCache.get(currencyKey);
191
+ if (!currencyPromise) {
192
+ currencyPromise = fetchShopCurrency(fetchImpl, currencyUrl, token);
193
+ currencyCache.set(currencyKey, currencyPromise);
194
+ }
195
+ const currency = await currencyPromise;
138
196
  const url = cursor ?? `${apiBase(store, version)}/products.json?limit=250`;
139
197
  const result = await request(fetchImpl, url, token);
140
198
  if (!result.ok)
@@ -142,18 +200,53 @@ export function shopifyConnector(options = {}) {
142
200
  const link = result.value.response.headers.get("link") ?? "";
143
201
  const next = link.match(/<([^>]+)>;\s*rel="next"/)?.[1] ?? null;
144
202
  return Ok({
145
- items: result.value.data.products.map((product) => ({
146
- externalId: String(product.id),
147
- slug: product.handle ?? String(product.id),
148
- title: product.title,
149
- ...(product.body_html ? { description: product.body_html } : {}),
150
- variants: (product.variants ?? []).map((variant) => ({
151
- externalId: String(variant.id),
152
- ...(variant.sku ? { sku: variant.sku } : {}),
153
- ...(variant.barcode ? { barcode: variant.barcode } : {}),
154
- metadata: { price: parseMoney(variant.price) },
155
- })),
156
- })),
203
+ items: result.value.data.products.map((product) => {
204
+ const options = product.options?.map((option, index) => ({
205
+ name: option.name,
206
+ displayName: option.name,
207
+ ...(option.position != null ? { sortOrder: option.position } : { sortOrder: index }),
208
+ values: (option.values ?? []).map((value, valueIndex) => ({ value, displayValue: value, sortOrder: valueIndex })),
209
+ }));
210
+ const variants = (product.variants ?? []).map((variant) => {
211
+ const selectors = [variant.option1, variant.option2, variant.option3];
212
+ const optionValues = Object.fromEntries((product.options ?? []).slice(0, 3).flatMap((option, index) => {
213
+ const value = selectors[index];
214
+ return value != null && value !== "" ? [[option.name, value]] : [];
215
+ }));
216
+ const prices = pricesForVariant(variant, currency);
217
+ return {
218
+ externalId: String(variant.id),
219
+ ...(variant.sku ? { sku: variant.sku } : {}),
220
+ ...(variant.barcode ? { barcode: variant.barcode } : {}),
221
+ ...(Object.keys(optionValues).length > 0 ? { optionValues } : {}),
222
+ ...(prices ? { prices } : {}),
223
+ };
224
+ });
225
+ const category = product.product_type ? slugify(product.product_type) : "";
226
+ const status = catalogStatus(product.status);
227
+ return {
228
+ externalId: String(product.id),
229
+ slug: product.handle ?? String(product.id),
230
+ title: product.title,
231
+ attributes: [{ locale: "en", title: product.title, ...(product.body_html != null ? { description: product.body_html } : {}) }],
232
+ variants,
233
+ ...(product.images ? {
234
+ images: product.images.map((image, index) => ({
235
+ externalId: String(image.id),
236
+ url: image.src,
237
+ ...(image.alt != null ? { alt: image.alt } : {}),
238
+ role: index === 0 ? "primary" : "gallery",
239
+ ...(image.position != null ? { sortOrder: image.position } : {}),
240
+ ...(image.variant_ids != null ? { variantExternalIds: image.variant_ids.map(String) } : {}),
241
+ })),
242
+ } : {}),
243
+ ...(options ? { options } : {}),
244
+ ...(product.tags != null ? { tags: product.tags.split(",").map((tag) => tag.trim()).filter(Boolean) } : {}),
245
+ ...(product.vendor ? { brand: product.vendor } : {}),
246
+ ...(category ? { categories: [category] } : {}),
247
+ ...(status ? { status } : {}),
248
+ };
249
+ }),
157
250
  nextCursor: next,
158
251
  });
159
252
  },