@tallyui/connector-woocommerce 1.0.0 → 3.0.0-next.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.
@@ -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
- });