@tallyui/connector-woocommerce 3.0.0 → 3.0.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/README.md CHANGED
@@ -13,6 +13,8 @@ It also sends `X-WCPOS-Protocol: 2`, which WCPOS 2.0 requires of POS requests, a
13
13
 
14
14
  ## Product pull
15
15
 
16
+ On WCPOS 1.10.x, the product uuid comes from the `_woocommerce_pos_uuid` meta and the barcode from `global_unique_id`. Non-empty top-level `uuid` / `barcode` fields (WCPOS 2.0) win when present.
17
+
16
18
  The pull works in passes:
17
19
  - a small request finds the store's newest `date_modified_gmt`;
18
20
  - the pass then pages every product modified since the last pass, by id with an offset;
package/dist/index.js CHANGED
@@ -148,109 +148,6 @@ var wooProductSchema = {
148
148
 
149
149
  // src/traits/product.ts
150
150
  import { moneyFromMajor } from "@tallyui/core";
151
- var WOO_STOCK_STATUS = {
152
- instock: "in_stock",
153
- outofstock: "out_of_stock",
154
- onbackorder: "backorder"
155
- };
156
- var wooProductTraits = {
157
- getId: (doc) => String(doc.id),
158
- getName: (doc) => doc.name ?? "",
159
- getSku: (doc) => doc.sku || void 0,
160
- getPrices: (doc, context) => {
161
- const currency = context?.currency ?? "XXX";
162
- const prices = [];
163
- const base = moneyFromMajor(doc.regular_price || doc.price, currency);
164
- if (base) prices.push({ ...base, kind: "base" });
165
- const sale = doc.on_sale === true ? moneyFromMajor(doc.sale_price, currency) : void 0;
166
- if (sale) prices.push({ ...sale, kind: "sale" });
167
- return prices;
168
- },
169
- getStock: (doc) => ({
170
- status: WOO_STOCK_STATUS[doc.stock_status] ?? "unknown",
171
- quantity: doc.manage_stock === false ? void 0 : doc.stock_quantity ?? void 0
172
- }),
173
- getPrice: (doc) => doc.price || void 0,
174
- getRegularPrice: (doc) => doc.regular_price || void 0,
175
- getSalePrice: (doc) => doc.sale_price || void 0,
176
- isOnSale: (doc) => doc.on_sale === true,
177
- getImageUrl: (doc) => doc.images?.[0]?.src || void 0,
178
- getImageUrls: (doc) => (doc.images ?? []).map((img) => img.src).filter(Boolean),
179
- getDescription: (doc) => doc.short_description || doc.description || void 0,
180
- getStockStatus: (doc) => {
181
- const status = doc.stock_status;
182
- if (status === "instock" || status === "outofstock" || status === "onbackorder") {
183
- return status;
184
- }
185
- return "unknown";
186
- },
187
- getStockQuantity: (doc) => doc.stock_quantity ?? null,
188
- hasVariants: (doc) => doc.type === "variable",
189
- isSellable: (doc) => doc.status === void 0 || doc.status === "publish",
190
- getVariantCount: (doc) => doc.type === "variable" ? doc.variations?.length ?? 0 : 1,
191
- getType: (doc) => doc.type ?? "simple",
192
- getBarcode: (doc) => doc.barcode || void 0,
193
- getCategoryNames: (doc) => (doc.categories ?? []).map((cat) => cat.name).filter(Boolean)
194
- };
195
-
196
- // src/sync/products.ts
197
- var wooProductSync = {
198
- async fetchAllIds(context) {
199
- const response = await fetch(`${context.baseUrl}/products`, {
200
- headers: {
201
- ...context.headers,
202
- "Content-Type": "application/json"
203
- },
204
- signal: context.signal
205
- });
206
- if (!response.ok) {
207
- throw new Error(`WooCommerce API error: ${response.status}`);
208
- }
209
- const products = await response.json();
210
- return products.map((p) => ({
211
- id: String(p.id),
212
- dateModified: p.date_modified_gmt
213
- }));
214
- },
215
- async fetchByIds(ids, context) {
216
- const include = ids.join(",");
217
- const response = await fetch(
218
- `${context.baseUrl}/products?include=${include}&per_page=${ids.length}`,
219
- {
220
- headers: {
221
- ...context.headers,
222
- "Content-Type": "application/json"
223
- },
224
- signal: context.signal
225
- }
226
- );
227
- if (!response.ok) {
228
- throw new Error(`WooCommerce API error: ${response.status}`);
229
- }
230
- return response.json();
231
- },
232
- async fetchModifiedAfter(date, context) {
233
- const params = new URLSearchParams({
234
- modified_after: date,
235
- dates_are_gmt: "true",
236
- per_page: "100"
237
- });
238
- const response = await fetch(
239
- `${context.baseUrl}/products?${params}`,
240
- {
241
- headers: {
242
- ...context.headers,
243
- "Content-Type": "application/json"
244
- },
245
- signal: context.signal
246
- }
247
- );
248
- if (!response.ok) {
249
- throw new Error(`WooCommerce API error: ${response.status}`);
250
- }
251
- return response.json();
252
- }
253
- };
254
151
 
255
152
  // src/replication/products.ts
256
153
  import { ConnectorUnauthorizedError } from "@tallyui/core";
@@ -295,9 +192,22 @@ var WooMissingUuidError = class extends Error {
295
192
  this.productId = id;
296
193
  }
297
194
  };
195
+ var WCPOS_UUID_META_KEY = "_woocommerce_pos_uuid";
196
+ function wooProductUuid(product) {
197
+ if (typeof product.uuid === "string" && product.uuid.length > 0) return product.uuid;
198
+ return product.meta_data?.find((entry) => entry.key === WCPOS_UUID_META_KEY && typeof entry.value === "string" && entry.value.length > 0)?.value;
199
+ }
200
+ function wooProductBarcode(product) {
201
+ if (typeof product.barcode === "string" && product.barcode.length > 0) return product.barcode;
202
+ if (typeof product.global_unique_id === "string" && product.global_unique_id.length > 0) return product.global_unique_id;
203
+ return void 0;
204
+ }
298
205
  function toProductDocument(product) {
299
- if (typeof product.uuid !== "string" || product.uuid.length === 0) throw new WooMissingUuidError(product.id);
300
- return { ...product, _deleted: product.status !== "publish" };
206
+ const uuid = wooProductUuid(product);
207
+ if (uuid === void 0) throw new WooMissingUuidError(product.id);
208
+ const barcode = wooProductBarcode(product);
209
+ const projected = Object.fromEntries(Object.keys(wooProductSchema.properties).filter((key) => Object.prototype.hasOwnProperty.call(product, key)).map((key) => [key, product[key]]));
210
+ return { ...projected, uuid, ...barcode !== void 0 ? { barcode } : {}, _deleted: product.status !== "publish" };
301
211
  }
302
212
  var WooDateFilterError = class extends Error {
303
213
  constructor(productId, bound, received) {
@@ -391,6 +301,111 @@ var wooProductReplication = {
391
301
  }
392
302
  };
393
303
 
304
+ // src/traits/product.ts
305
+ var WOO_STOCK_STATUS = {
306
+ instock: "in_stock",
307
+ outofstock: "out_of_stock",
308
+ onbackorder: "backorder"
309
+ };
310
+ var wooProductTraits = {
311
+ getId: (doc) => String(doc.id),
312
+ getName: (doc) => doc.name ?? "",
313
+ getSku: (doc) => doc.sku || void 0,
314
+ getPrices: (doc, context) => {
315
+ const currency = context?.currency ?? "XXX";
316
+ const prices = [];
317
+ const base = moneyFromMajor(doc.regular_price || doc.price, currency);
318
+ if (base) prices.push({ ...base, kind: "base" });
319
+ const sale = doc.on_sale === true ? moneyFromMajor(doc.sale_price, currency) : void 0;
320
+ if (sale) prices.push({ ...sale, kind: "sale" });
321
+ return prices;
322
+ },
323
+ getStock: (doc) => ({
324
+ status: WOO_STOCK_STATUS[doc.stock_status] ?? "unknown",
325
+ quantity: doc.manage_stock === false ? void 0 : doc.stock_quantity ?? void 0
326
+ }),
327
+ getPrice: (doc) => doc.price || void 0,
328
+ getRegularPrice: (doc) => doc.regular_price || void 0,
329
+ getSalePrice: (doc) => doc.sale_price || void 0,
330
+ isOnSale: (doc) => doc.on_sale === true,
331
+ getImageUrl: (doc) => doc.images?.[0]?.src || void 0,
332
+ getImageUrls: (doc) => (doc.images ?? []).map((img) => img.src).filter(Boolean),
333
+ getDescription: (doc) => doc.short_description || doc.description || void 0,
334
+ getStockStatus: (doc) => {
335
+ const status = doc.stock_status;
336
+ if (status === "instock" || status === "outofstock" || status === "onbackorder") {
337
+ return status;
338
+ }
339
+ return "unknown";
340
+ },
341
+ getStockQuantity: (doc) => doc.stock_quantity ?? null,
342
+ hasVariants: (doc) => doc.type === "variable",
343
+ isSellable: (doc) => doc.status === void 0 || doc.status === "publish",
344
+ getVariantCount: (doc) => doc.type === "variable" ? doc.variations?.length ?? 0 : 1,
345
+ getType: (doc) => doc.type ?? "simple",
346
+ getBarcode: (doc) => wooProductBarcode(doc),
347
+ getCategoryNames: (doc) => (doc.categories ?? []).map((cat) => cat.name).filter(Boolean)
348
+ };
349
+
350
+ // src/sync/products.ts
351
+ var wooProductSync = {
352
+ async fetchAllIds(context) {
353
+ const response = await fetch(`${context.baseUrl}/products`, {
354
+ headers: {
355
+ ...context.headers,
356
+ "Content-Type": "application/json"
357
+ },
358
+ signal: context.signal
359
+ });
360
+ if (!response.ok) {
361
+ throw new Error(`WooCommerce API error: ${response.status}`);
362
+ }
363
+ const products = await response.json();
364
+ return products.map((p) => ({
365
+ id: String(p.id),
366
+ dateModified: p.date_modified_gmt
367
+ }));
368
+ },
369
+ async fetchByIds(ids, context) {
370
+ const include = ids.join(",");
371
+ const response = await fetch(
372
+ `${context.baseUrl}/products?include=${include}&per_page=${ids.length}`,
373
+ {
374
+ headers: {
375
+ ...context.headers,
376
+ "Content-Type": "application/json"
377
+ },
378
+ signal: context.signal
379
+ }
380
+ );
381
+ if (!response.ok) {
382
+ throw new Error(`WooCommerce API error: ${response.status}`);
383
+ }
384
+ return response.json();
385
+ },
386
+ async fetchModifiedAfter(date, context) {
387
+ const params = new URLSearchParams({
388
+ modified_after: date,
389
+ dates_are_gmt: "true",
390
+ per_page: "100"
391
+ });
392
+ const response = await fetch(
393
+ `${context.baseUrl}/products?${params}`,
394
+ {
395
+ headers: {
396
+ ...context.headers,
397
+ "Content-Type": "application/json"
398
+ },
399
+ signal: context.signal
400
+ }
401
+ );
402
+ if (!response.ok) {
403
+ throw new Error(`WooCommerce API error: ${response.status}`);
404
+ }
405
+ return response.json();
406
+ }
407
+ };
408
+
394
409
  // src/reconcile/catalogue.ts
395
410
  import { errorKind } from "@tallyui/core";
396
411
  var PAGE_SIZE = 100;
@@ -461,9 +476,9 @@ function wooCatalogueReconcile(feed) {
461
476
  async confirmGone(locals, context) {
462
477
  const ids = locals.map((doc) => doc.id).filter((id) => Number.isInteger(id));
463
478
  if (!ids.length) return [];
464
- const params = new URLSearchParams({ include: ids.join(","), per_page: String(PAGE_SIZE), status: "any", _fields: "id,uuid,status" });
479
+ const params = new URLSearchParams({ include: ids.join(","), per_page: String(PAGE_SIZE), status: "any", _fields: "id,uuid,status,meta_data" });
465
480
  const rows = await get(`/products?${params}`, context);
466
- const live = new Map(rows.filter((row) => row.status === "publish").map((row) => [row.id, typeof row.uuid === "string" && row.uuid.length > 0 ? row.uuid : void 0]));
481
+ const live = new Map(rows.filter((row) => row.status === "publish").map((row) => [row.id, wooProductUuid(row)]));
467
482
  return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || typeof live.get(doc.id) === "string" && live.get(doc.id) !== doc.uuid)).map((doc) => doc.uuid);
468
483
  },
469
484
  // An entry with no numeric id, locally or in the listing, is dropped: the store cannot be asked about it, so
@@ -495,7 +510,7 @@ function createWooReconcileFeed() {
495
510
  }
496
511
 
497
512
  // package.json
498
- var version = "3.0.0";
513
+ var version = "3.0.1";
499
514
 
500
515
  // src/index.ts
501
516
  import { ConnectorUnauthorizedError as ConnectorUnauthorizedError3 } from "@tallyui/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-woocommerce",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "WooCommerce connector for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -26,10 +26,10 @@
26
26
  "directory": "connectors/woocommerce"
27
27
  },
28
28
  "peerDependencies": {
29
- "@tallyui/core": "^3.0.0"
29
+ "@tallyui/core": "^3.0.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@tallyui/database": "3.0.0",
32
+ "@tallyui/database": "3.0.1",
33
33
  "rxdb": "17.5.0"
34
34
  },
35
35
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  import { errorKind, type CatalogueReconcileAdapter, type CatalogueReconcileEntry, type ReconcileFeed, type SyncContext } from '@tallyui/core';
2
- import { checkResponse } from '../replication/products';
2
+ import { checkResponse, wooProductUuid } from '../replication/products';
3
3
 
4
4
  /** The listing's page size, WordPress's per_page maximum. */
5
5
  const PAGE_SIZE = 100;
@@ -107,9 +107,9 @@ export function wooCatalogueReconcile(feed: Pick<ReconcileFeed<any>, 'enqueue'>)
107
107
  const ids = locals.map((doc) => doc.id).filter((id) => Number.isInteger(id));
108
108
  // An empty include= would list the whole store; a local without a numeric id has no proof and is kept.
109
109
  if (!ids.length) return [];
110
- const params = new URLSearchParams({ include: ids.join(','), per_page: String(PAGE_SIZE), status: 'any', _fields: 'id,uuid,status' });
110
+ const params = new URLSearchParams({ include: ids.join(','), per_page: String(PAGE_SIZE), status: 'any', _fields: 'id,uuid,status,meta_data' });
111
111
  const rows: Array<{ id: number; uuid: string; status: string }> = await get(`/products?${params}`, context);
112
- const live = new Map(rows.filter((row) => row.status === 'publish').map((row) => [row.id, typeof row.uuid === 'string' && row.uuid.length > 0 ? row.uuid : undefined]));
112
+ const live = new Map(rows.filter((row) => row.status === 'publish').map((row) => [row.id, wooProductUuid(row)]));
113
113
  // A live id backs only the store's uuid; other local copies are gone (#369).
114
114
  // A row without a uuid can't prove the local copy stale, so a live id keeps it (#375 review).
115
115
  return locals.filter((doc) => Number.isInteger(doc.id) && (!live.has(doc.id) || (typeof live.get(doc.id) === 'string' && live.get(doc.id) !== doc.uuid))).map((doc) => doc.uuid);
@@ -1,4 +1,5 @@
1
1
  import { ConnectorUnauthorizedError, type ReplicationAdapter } from '@tallyui/core';
2
+ import { wooProductSchema } from '../schemas/products';
2
3
 
3
4
  export type WooProductCheckpoint = {
4
5
  /** Inclusive lower bound (date_modified_gmt) of the current pass; '' = the whole catalogue. */
@@ -65,13 +66,35 @@ export class WooMissingUuidError extends Error {
65
66
  }
66
67
  }
67
68
 
69
+ /** WCPOS's own uuid meta key; 1.10.x sends the product uuid only here (Uuid_Handler.php:29). */
70
+ export const WCPOS_UUID_META_KEY = '_woocommerce_pos_uuid';
71
+
72
+ /** The product's uuid: top-level `uuid` (the `next` shape) wins, else the `_woocommerce_pos_uuid` meta (1.10.x). */
73
+ export function wooProductUuid(product: any): string | undefined {
74
+ if (typeof product.uuid === 'string' && product.uuid.length > 0) return product.uuid;
75
+ return product.meta_data?.find((entry: any) =>
76
+ entry.key === WCPOS_UUID_META_KEY && typeof entry.value === 'string' && entry.value.length > 0)?.value;
77
+ }
78
+
79
+ /** The product's barcode: top-level `barcode` wins, else `global_unique_id` (WCPOS's default barcode field), else undefined. */
80
+ export function wooProductBarcode(product: any): string | undefined {
81
+ if (typeof product.barcode === 'string' && product.barcode.length > 0) return product.barcode;
82
+ if (typeof product.global_unique_id === 'string' && product.global_unique_id.length > 0) return product.global_unique_id;
83
+ return undefined;
84
+ }
85
+
68
86
  /**
69
87
  * One product row as the till stores it, the rule the pull and the reconcile feed share: a uuid is
70
88
  * required (WooMissingUuidError), and a product that is not published arrives deleted (#229).
71
89
  */
72
90
  export function toProductDocument(product: any): any {
73
- if (typeof product.uuid !== 'string' || product.uuid.length === 0) throw new WooMissingUuidError(product.id);
74
- return { ...product, _deleted: product.status !== 'publish' };
91
+ const uuid = wooProductUuid(product);
92
+ if (uuid === undefined) throw new WooMissingUuidError(product.id);
93
+ const barcode = wooProductBarcode(product);
94
+ const projected = Object.fromEntries(Object.keys(wooProductSchema.properties)
95
+ .filter((key) => Object.prototype.hasOwnProperty.call(product, key))
96
+ .map((key) => [key, product[key]]));
97
+ return { ...projected, uuid, ...(barcode !== undefined ? { barcode } : {}), _deleted: product.status !== 'publish' };
75
98
  }
76
99
 
77
100
  /** The store returned a product outside a modified_after window, so it does not apply the filter. */
@@ -1,5 +1,6 @@
1
1
  import { moneyFromMajor } from '@tallyui/core';
2
2
  import type { ProductPrice, ProductTraits, StockStatus } from '@tallyui/core';
3
+ import { wooProductBarcode } from '../replication/products';
3
4
 
4
5
  /** WooCommerce's stock_status strings, mapped to the neutral enum. */
5
6
  const WOO_STOCK_STATUS: Record<string, StockStatus> = {
@@ -69,7 +70,7 @@ export const wooProductTraits: ProductTraits = {
69
70
 
70
71
  getType: (doc) => doc.type ?? 'simple',
71
72
 
72
- getBarcode: (doc) => doc.barcode || undefined,
73
+ getBarcode: (doc) => wooProductBarcode(doc),
73
74
 
74
75
  getCategoryNames: (doc) =>
75
76
  (doc.categories ?? []).map((cat: any) => cat.name).filter(Boolean),