@tallyui/connector-woocommerce 1.0.0 → 2.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Kilmurray and TallyUI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.ts CHANGED
@@ -32,9 +32,9 @@ type WooProductCheckpoint = {
32
32
  /**
33
33
  * Replication adapter for WooCommerce products.
34
34
  *
35
- * Implements pull (cursor-based pagination by date_modified_gmt) and push
36
- * (individual create/update via WC REST API v3). Designed for use with
37
- * RxDB's replicateRxCollection.
35
+ * Pull-only (cursor-based pagination by date_modified_gmt) for RxDB's
36
+ * replicateRxCollection. Catalogue data is server-owned; the POS never
37
+ * writes products.
38
38
  */
39
39
  declare const wooProductReplication: ReplicationAdapter<any, WooProductCheckpoint>;
40
40
 
package/dist/index.js CHANGED
@@ -144,10 +144,29 @@ var wooProductSchema = {
144
144
  };
145
145
 
146
146
  // src/traits/product.ts
147
+ import { moneyFromMajor } from "@tallyui/core";
148
+ var WOO_STOCK_STATUS = {
149
+ instock: "in_stock",
150
+ outofstock: "out_of_stock",
151
+ onbackorder: "backorder"
152
+ };
147
153
  var wooProductTraits = {
148
154
  getId: (doc) => String(doc.id),
149
155
  getName: (doc) => doc.name ?? "",
150
156
  getSku: (doc) => doc.sku || void 0,
157
+ getPrices: (doc, context) => {
158
+ const currency = context?.currency ?? "XXX";
159
+ const prices = [];
160
+ const base = moneyFromMajor(doc.regular_price || doc.price, currency);
161
+ if (base) prices.push({ ...base, kind: "base" });
162
+ const sale = doc.on_sale === true ? moneyFromMajor(doc.sale_price, currency) : void 0;
163
+ if (sale) prices.push({ ...sale, kind: "sale" });
164
+ return prices;
165
+ },
166
+ getStock: (doc) => ({
167
+ status: WOO_STOCK_STATUS[doc.stock_status] ?? "unknown",
168
+ quantity: doc.manage_stock === false ? void 0 : doc.stock_quantity ?? void 0
169
+ }),
151
170
  getPrice: (doc) => doc.price || void 0,
152
171
  getRegularPrice: (doc) => doc.regular_price || void 0,
153
172
  getSalePrice: (doc) => doc.sale_price || void 0,
@@ -164,6 +183,8 @@ var wooProductTraits = {
164
183
  },
165
184
  getStockQuantity: (doc) => doc.stock_quantity ?? null,
166
185
  hasVariants: (doc) => doc.type === "variable",
186
+ isSellable: (doc) => doc.status === void 0 || doc.status === "publish",
187
+ getVariantCount: (doc) => doc.type === "variable" ? doc.variations?.length ?? 0 : 1,
167
188
  getType: (doc) => doc.type ?? "simple",
168
189
  getBarcode: (doc) => doc.barcode || void 0,
169
190
  getCategoryNames: (doc) => (doc.categories ?? []).map((cat) => cat.name).filter(Boolean)
@@ -253,38 +274,6 @@ var wooProductReplication = {
253
274
  } : lastCheckpoint ?? { id: "", modified: "" };
254
275
  return { documents, checkpoint };
255
276
  }
256
- },
257
- push: {
258
- async handler(changeRows, context) {
259
- const conflicts = [];
260
- for (const row of changeRows) {
261
- const doc = row.newDocumentState;
262
- try {
263
- const isCreate = !row.assumedMasterState;
264
- const method = isCreate ? "POST" : "PUT";
265
- const url = isCreate ? `${context.baseUrl}/products` : `${context.baseUrl}/products/${doc.id}`;
266
- const response = await fetch(url, {
267
- method,
268
- headers: {
269
- ...context.headers,
270
- "Content-Type": "application/json"
271
- },
272
- body: JSON.stringify(doc),
273
- signal: context.signal
274
- });
275
- if (!response.ok) {
276
- if (row.assumedMasterState) {
277
- conflicts.push({ ...row.assumedMasterState, _deleted: false });
278
- }
279
- }
280
- } catch {
281
- if (row.assumedMasterState) {
282
- conflicts.push({ ...row.assumedMasterState, _deleted: false });
283
- }
284
- }
285
- }
286
- return conflicts;
287
- }
288
277
  }
289
278
  };
290
279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/connector-woocommerce",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "WooCommerce connector for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -15,11 +15,17 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "src"
18
+ "src",
19
+ "!src/**/*.test.ts"
19
20
  ],
20
21
  "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/TallyUI/tallyui.git",
25
+ "directory": "connectors/woocommerce"
26
+ },
21
27
  "peerDependencies": {
22
- "@tallyui/core": "0.2.0"
28
+ "@tallyui/core": "2.0.0"
23
29
  },
24
30
  "devDependencies": {
25
31
  "rxdb": "16.21.1"
@@ -1,4 +1,4 @@
1
- import type { ReplicationAdapter, SyncContext } from '@tallyui/core';
1
+ import type { ReplicationAdapter } from '@tallyui/core';
2
2
 
3
3
  export type WooProductCheckpoint = {
4
4
  id: string;
@@ -8,9 +8,9 @@ export type WooProductCheckpoint = {
8
8
  /**
9
9
  * Replication adapter for WooCommerce products.
10
10
  *
11
- * Implements pull (cursor-based pagination by date_modified_gmt) and push
12
- * (individual create/update via WC REST API v3). Designed for use with
13
- * RxDB's replicateRxCollection.
11
+ * Pull-only (cursor-based pagination by date_modified_gmt) for RxDB's
12
+ * replicateRxCollection. Catalogue data is server-owned; the POS never
13
+ * writes products.
14
14
  */
15
15
  export const wooProductReplication: ReplicationAdapter<any, WooProductCheckpoint> = {
16
16
  pull: {
@@ -50,44 +50,4 @@ export const wooProductReplication: ReplicationAdapter<any, WooProductCheckpoint
50
50
  return { documents, checkpoint };
51
51
  },
52
52
  },
53
-
54
- push: {
55
- async handler(changeRows, context) {
56
- const conflicts: any[] = [];
57
-
58
- for (const row of changeRows) {
59
- const doc = row.newDocumentState as any;
60
-
61
- try {
62
- const isCreate = !row.assumedMasterState;
63
- const method = isCreate ? 'POST' : 'PUT';
64
- const url = isCreate
65
- ? `${context.baseUrl}/products`
66
- : `${context.baseUrl}/products/${doc.id}`;
67
-
68
- const response = await fetch(url, {
69
- method,
70
- headers: {
71
- ...context.headers,
72
- 'Content-Type': 'application/json',
73
- },
74
- body: JSON.stringify(doc),
75
- signal: context.signal,
76
- });
77
-
78
- if (!response.ok) {
79
- if (row.assumedMasterState) {
80
- conflicts.push({ ...row.assumedMasterState, _deleted: false });
81
- }
82
- }
83
- } catch {
84
- if (row.assumedMasterState) {
85
- conflicts.push({ ...row.assumedMasterState, _deleted: false });
86
- }
87
- }
88
- }
89
-
90
- return conflicts;
91
- },
92
- },
93
53
  };
@@ -1,4 +1,12 @@
1
- import type { ProductTraits } from '@tallyui/core';
1
+ import { moneyFromMajor } from '@tallyui/core';
2
+ import type { ProductPrice, ProductTraits, StockStatus } from '@tallyui/core';
3
+
4
+ /** WooCommerce's stock_status strings, mapped to the neutral enum. */
5
+ const WOO_STOCK_STATUS: Record<string, StockStatus> = {
6
+ instock: 'in_stock',
7
+ outofstock: 'out_of_stock',
8
+ onbackorder: 'backorder',
9
+ };
2
10
 
3
11
  /**
4
12
  * WooCommerce product trait implementations.
@@ -13,6 +21,22 @@ export const wooProductTraits: ProductTraits = {
13
21
 
14
22
  getSku: (doc) => doc.sku || undefined,
15
23
 
24
+ getPrices: (doc, context) => {
25
+ // Woo prices are decimal strings with no currency; the store supplies it.
26
+ const currency = context?.currency ?? 'XXX';
27
+ const prices: ProductPrice[] = [];
28
+ const base = moneyFromMajor(doc.regular_price || doc.price, currency);
29
+ if (base) prices.push({ ...base, kind: 'base' });
30
+ const sale = doc.on_sale === true ? moneyFromMajor(doc.sale_price, currency) : undefined;
31
+ if (sale) prices.push({ ...sale, kind: 'sale' });
32
+ return prices;
33
+ },
34
+
35
+ getStock: (doc) => ({
36
+ status: WOO_STOCK_STATUS[doc.stock_status] ?? 'unknown',
37
+ quantity: doc.manage_stock === false ? undefined : doc.stock_quantity ?? undefined,
38
+ }),
39
+
16
40
  getPrice: (doc) => doc.price || undefined,
17
41
 
18
42
  getRegularPrice: (doc) => doc.regular_price || undefined,
@@ -39,6 +63,10 @@ export const wooProductTraits: ProductTraits = {
39
63
 
40
64
  hasVariants: (doc) => doc.type === 'variable',
41
65
 
66
+ isSellable: (doc) => doc.status === undefined || doc.status === 'publish',
67
+
68
+ getVariantCount: (doc) => doc.type === 'variable' ? doc.variations?.length ?? 0 : 1,
69
+
42
70
  getType: (doc) => doc.type ?? 'simple',
43
71
 
44
72
  getBarcode: (doc) => doc.barcode || undefined,
@@ -1,191 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { wooProductTraits } from '../traits/product';
3
-
4
- /**
5
- * Realistic WooCommerce product document, shaped like the REST API v3 response.
6
- */
7
- const fullProduct = {
8
- id: 42,
9
- name: 'Espresso Machine Pro',
10
- slug: 'espresso-machine-pro',
11
- type: 'simple',
12
- status: 'publish',
13
- sku: 'ESP-001',
14
- price: '599.99',
15
- regular_price: '599.99',
16
- sale_price: '',
17
- on_sale: false,
18
- stock_status: 'instock',
19
- stock_quantity: 15,
20
- barcode: '1234567890123',
21
- short_description: 'High-end espresso machine.',
22
- description: 'Full description here.',
23
- images: [
24
- { id: 1, src: 'https://store.example/espresso.jpg', alt: 'Espresso Machine' },
25
- { id: 2, src: 'https://store.example/espresso-side.jpg', alt: '' },
26
- ],
27
- categories: [
28
- { id: 1, name: 'Equipment', slug: 'equipment' },
29
- { id: 2, name: 'Coffee', slug: 'coffee' },
30
- ],
31
- };
32
-
33
- const saleProduct = {
34
- ...fullProduct,
35
- id: 43,
36
- price: '499.99',
37
- sale_price: '499.99',
38
- on_sale: true,
39
- };
40
-
41
- const variableProduct = {
42
- ...fullProduct,
43
- id: 44,
44
- type: 'variable',
45
- variations: [101, 102],
46
- };
47
-
48
- const minimalProduct = {
49
- id: 99,
50
- };
51
-
52
- describe('WooCommerce product traits', () => {
53
- describe('getId', () => {
54
- it('returns the product id as a string', () => {
55
- expect(wooProductTraits.getId(fullProduct)).toBe('42');
56
- });
57
- });
58
-
59
- describe('getName', () => {
60
- it('returns the product name', () => {
61
- expect(wooProductTraits.getName(fullProduct)).toBe('Espresso Machine Pro');
62
- });
63
-
64
- it('returns empty string for missing name', () => {
65
- expect(wooProductTraits.getName(minimalProduct)).toBe('');
66
- });
67
- });
68
-
69
- describe('getSku', () => {
70
- it('returns the SKU', () => {
71
- expect(wooProductTraits.getSku(fullProduct)).toBe('ESP-001');
72
- });
73
-
74
- it('returns undefined when no SKU', () => {
75
- expect(wooProductTraits.getSku(minimalProduct)).toBeUndefined();
76
- });
77
- });
78
-
79
- describe('getPrice', () => {
80
- it('returns the price string', () => {
81
- expect(wooProductTraits.getPrice(fullProduct)).toBe('599.99');
82
- });
83
-
84
- it('returns undefined for missing price', () => {
85
- expect(wooProductTraits.getPrice(minimalProduct)).toBeUndefined();
86
- });
87
- });
88
-
89
- describe('getRegularPrice / getSalePrice / isOnSale', () => {
90
- it('returns regular price', () => {
91
- expect(wooProductTraits.getRegularPrice(fullProduct)).toBe('599.99');
92
- });
93
-
94
- it('detects non-sale product', () => {
95
- expect(wooProductTraits.isOnSale(fullProduct)).toBe(false);
96
- expect(wooProductTraits.getSalePrice(fullProduct)).toBeUndefined();
97
- });
98
-
99
- it('detects sale product', () => {
100
- expect(wooProductTraits.isOnSale(saleProduct)).toBe(true);
101
- expect(wooProductTraits.getSalePrice(saleProduct)).toBe('499.99');
102
- });
103
- });
104
-
105
- describe('getImageUrl / getImageUrls', () => {
106
- it('returns the first image src', () => {
107
- expect(wooProductTraits.getImageUrl(fullProduct)).toBe('https://store.example/espresso.jpg');
108
- });
109
-
110
- it('returns all image URLs', () => {
111
- expect(wooProductTraits.getImageUrls(fullProduct)).toEqual([
112
- 'https://store.example/espresso.jpg',
113
- 'https://store.example/espresso-side.jpg',
114
- ]);
115
- });
116
-
117
- it('returns undefined / empty for missing images', () => {
118
- expect(wooProductTraits.getImageUrl(minimalProduct)).toBeUndefined();
119
- expect(wooProductTraits.getImageUrls(minimalProduct)).toEqual([]);
120
- });
121
- });
122
-
123
- describe('getDescription', () => {
124
- it('prefers short_description', () => {
125
- expect(wooProductTraits.getDescription(fullProduct)).toBe('High-end espresso machine.');
126
- });
127
-
128
- it('falls back to description', () => {
129
- const doc = { ...fullProduct, short_description: '' };
130
- expect(wooProductTraits.getDescription(doc)).toBe('Full description here.');
131
- });
132
-
133
- it('returns undefined when neither exists', () => {
134
- expect(wooProductTraits.getDescription(minimalProduct)).toBeUndefined();
135
- });
136
- });
137
-
138
- describe('getStockStatus / getStockQuantity', () => {
139
- it('returns instock status', () => {
140
- expect(wooProductTraits.getStockStatus(fullProduct)).toBe('instock');
141
- });
142
-
143
- it('returns stock quantity', () => {
144
- expect(wooProductTraits.getStockQuantity(fullProduct)).toBe(15);
145
- });
146
-
147
- it('returns unknown for unrecognised status', () => {
148
- expect(wooProductTraits.getStockStatus({ ...fullProduct, stock_status: 'weird' })).toBe('unknown');
149
- });
150
-
151
- it('returns null quantity when missing', () => {
152
- expect(wooProductTraits.getStockQuantity(minimalProduct)).toBeNull();
153
- });
154
- });
155
-
156
- describe('hasVariants / getType', () => {
157
- it('simple product has no variants', () => {
158
- expect(wooProductTraits.hasVariants(fullProduct)).toBe(false);
159
- expect(wooProductTraits.getType(fullProduct)).toBe('simple');
160
- });
161
-
162
- it('variable product has variants', () => {
163
- expect(wooProductTraits.hasVariants(variableProduct)).toBe(true);
164
- expect(wooProductTraits.getType(variableProduct)).toBe('variable');
165
- });
166
-
167
- it('defaults to simple for missing type', () => {
168
- expect(wooProductTraits.getType(minimalProduct)).toBe('simple');
169
- });
170
- });
171
-
172
- describe('getBarcode', () => {
173
- it('returns barcode', () => {
174
- expect(wooProductTraits.getBarcode(fullProduct)).toBe('1234567890123');
175
- });
176
-
177
- it('returns undefined when missing', () => {
178
- expect(wooProductTraits.getBarcode(minimalProduct)).toBeUndefined();
179
- });
180
- });
181
-
182
- describe('getCategoryNames', () => {
183
- it('returns category name strings', () => {
184
- expect(wooProductTraits.getCategoryNames(fullProduct)).toEqual(['Equipment', 'Coffee']);
185
- });
186
-
187
- it('returns empty array when no categories', () => {
188
- expect(wooProductTraits.getCategoryNames(minimalProduct)).toEqual([]);
189
- });
190
- });
191
- });
@@ -1,189 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { SyncContext } from '@tallyui/core';
3
-
4
- import { wooProductReplication } from './products';
5
-
6
- const context: SyncContext = {
7
- connectorId: 'woocommerce',
8
- baseUrl: 'https://example.com/wp-json/wc/v3',
9
- headers: { Authorization: 'Basic dGVzdDp0ZXN0' },
10
- };
11
-
12
- describe('wooProductReplication.pull.handler', () => {
13
- beforeEach(() => {
14
- vi.restoreAllMocks();
15
- });
16
-
17
- it('fetches products from initial checkpoint (undefined)', async () => {
18
- const mockProducts = [
19
- { id: 1, uuid: 'abc', name: 'Widget', date_modified_gmt: '2026-01-01T00:00:00' },
20
- { id: 2, uuid: 'def', name: 'Gadget', date_modified_gmt: '2026-01-02T00:00:00' },
21
- ];
22
-
23
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
24
- new Response(JSON.stringify(mockProducts), { status: 200 }),
25
- );
26
-
27
- const result = await wooProductReplication.pull.handler(undefined, 100, context);
28
-
29
- expect(result.documents).toHaveLength(2);
30
- expect(result.documents[0]._deleted).toBe(false);
31
- expect(result.checkpoint).toEqual({
32
- id: 'def',
33
- modified: '2026-01-02T00:00:00',
34
- });
35
- });
36
-
37
- it('fetches products after a checkpoint', async () => {
38
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
39
- new Response(JSON.stringify([]), { status: 200 }),
40
- );
41
-
42
- const checkpoint = { id: 'abc', modified: '2026-01-01T00:00:00' };
43
- const result = await wooProductReplication.pull.handler(checkpoint, 100, context);
44
-
45
- expect(result.documents).toHaveLength(0);
46
-
47
- const calledUrl = (globalThis.fetch as any).mock.calls[0][0];
48
- expect(calledUrl).toContain('modified_after=2026-01-01T00%3A00%3A00');
49
- expect(calledUrl).toContain('orderby=modified');
50
- expect(calledUrl).toContain('order=asc');
51
- });
52
-
53
- it('uses batchSize as per_page', async () => {
54
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
55
- new Response(JSON.stringify([]), { status: 200 }),
56
- );
57
-
58
- await wooProductReplication.pull.handler(undefined, 25, context);
59
-
60
- const calledUrl = (globalThis.fetch as any).mock.calls[0][0];
61
- expect(calledUrl).toContain('per_page=25');
62
- });
63
-
64
- it('throws on non-OK response', async () => {
65
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
66
- new Response('Internal Server Error', { status: 500 }),
67
- );
68
-
69
- await expect(
70
- wooProductReplication.pull.handler(undefined, 100, context),
71
- ).rejects.toThrow('WooCommerce API error: 500');
72
- });
73
-
74
- it('returns lastCheckpoint when no products are returned', async () => {
75
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
76
- new Response(JSON.stringify([]), { status: 200 }),
77
- );
78
-
79
- const checkpoint = { id: 'abc', modified: '2026-01-01T00:00:00' };
80
- const result = await wooProductReplication.pull.handler(checkpoint, 100, context);
81
-
82
- expect(result.checkpoint).toEqual(checkpoint);
83
- });
84
-
85
- it('falls back to id when uuid is missing', async () => {
86
- const mockProducts = [
87
- { id: 42, name: 'No UUID', date_modified_gmt: '2026-03-01T00:00:00' },
88
- ];
89
-
90
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
91
- new Response(JSON.stringify(mockProducts), { status: 200 }),
92
- );
93
-
94
- const result = await wooProductReplication.pull.handler(undefined, 100, context);
95
-
96
- expect(result.checkpoint.id).toBe('42');
97
- });
98
- });
99
-
100
- describe('wooProductReplication.push.handler', () => {
101
- beforeEach(() => {
102
- vi.restoreAllMocks();
103
- });
104
-
105
- it('sends PUT for updates', async () => {
106
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
107
- new Response(JSON.stringify({}), { status: 200 }),
108
- );
109
-
110
- const conflicts = await wooProductReplication.push!.handler(
111
- [{
112
- newDocumentState: { id: '1', name: 'Updated', _deleted: false } as any,
113
- assumedMasterState: { id: '1', name: 'Old', _deleted: false } as any,
114
- }],
115
- context,
116
- );
117
-
118
- expect(conflicts).toHaveLength(0);
119
- const [url, opts] = (globalThis.fetch as any).mock.calls[0];
120
- expect(url).toContain('/products/1');
121
- expect(opts.method).toBe('PUT');
122
- });
123
-
124
- it('sends POST for creates', async () => {
125
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
126
- new Response(JSON.stringify({}), { status: 200 }),
127
- );
128
-
129
- const conflicts = await wooProductReplication.push!.handler(
130
- [{
131
- newDocumentState: { id: '1', name: 'New Product', _deleted: false } as any,
132
- // No assumedMasterState = create
133
- }],
134
- context,
135
- );
136
-
137
- expect(conflicts).toHaveLength(0);
138
- const [, opts] = (globalThis.fetch as any).mock.calls[0];
139
- expect(opts.method).toBe('POST');
140
- });
141
-
142
- it('returns conflicts on server error', async () => {
143
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
144
- new Response('conflict', { status: 409 }),
145
- );
146
-
147
- const conflicts = await wooProductReplication.push!.handler(
148
- [{
149
- newDocumentState: { id: '1', name: 'Updated', _deleted: false } as any,
150
- assumedMasterState: { id: '1', name: 'Server Version', _deleted: false } as any,
151
- }],
152
- context,
153
- );
154
-
155
- expect(conflicts).toHaveLength(1);
156
- expect(conflicts[0].name).toBe('Server Version');
157
- });
158
-
159
- it('returns conflicts on network failure', async () => {
160
- vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Network error'));
161
-
162
- const conflicts = await wooProductReplication.push!.handler(
163
- [{
164
- newDocumentState: { id: '1', name: 'Updated', _deleted: false } as any,
165
- assumedMasterState: { id: '1', name: 'Server Version', _deleted: false } as any,
166
- }],
167
- context,
168
- );
169
-
170
- expect(conflicts).toHaveLength(1);
171
- });
172
-
173
- it('does not return conflict for creates that fail', async () => {
174
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
175
- new Response('error', { status: 500 }),
176
- );
177
-
178
- const conflicts = await wooProductReplication.push!.handler(
179
- [{
180
- newDocumentState: { id: '1', name: 'New Product', _deleted: false } as any,
181
- // No assumedMasterState = create, so no conflict to report
182
- }],
183
- context,
184
- );
185
-
186
- // Creates without assumedMasterState don't produce conflicts
187
- expect(conflicts).toHaveLength(0);
188
- });
189
- });