@porulle/import-woocommerce 0.1.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/README.md +51 -0
- package/dist/src/index.d.ts +160 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +258 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +49 -0
- package/src/index.ts +439 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/import-woocommerce",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
15
|
+
"check-types": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint . --max-warnings 1000",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@porulle/core": "workspace:*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@repo/eslint-config": "*",
|
|
24
|
+
"@repo/typescript-config": "*",
|
|
25
|
+
"@types/node": "^24.5.2",
|
|
26
|
+
"eslint": "^9.39.1",
|
|
27
|
+
"typescript": "5.9.2",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"description": "Import a WooCommerce store's catalog into Porulle. Maps WooCommerce's product / variation / attribute / image model onto Porulle's catalog.",
|
|
39
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
46
|
+
"directory": "packages/import/import-woocommerce"
|
|
47
|
+
},
|
|
48
|
+
"author": "Porulle contributors"
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { Err, Ok, type Result } from "@porulle/core";
|
|
2
|
+
|
|
3
|
+
export interface WooImage {
|
|
4
|
+
id: number;
|
|
5
|
+
src: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WooAttribute {
|
|
10
|
+
id?: number;
|
|
11
|
+
name: string;
|
|
12
|
+
variation?: boolean;
|
|
13
|
+
options?: string[];
|
|
14
|
+
option?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WooProductVariation {
|
|
18
|
+
id: number;
|
|
19
|
+
sku?: string;
|
|
20
|
+
price?: string;
|
|
21
|
+
attributes?: WooAttribute[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WooProduct {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
slug?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
short_description?: string;
|
|
30
|
+
type?: string;
|
|
31
|
+
sku?: string;
|
|
32
|
+
categories?: Array<{ id: number; name: string; slug?: string }>;
|
|
33
|
+
attributes?: WooAttribute[];
|
|
34
|
+
images?: WooImage[];
|
|
35
|
+
variationsData?: WooProductVariation[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WooCustomer {
|
|
39
|
+
id: number;
|
|
40
|
+
email?: string;
|
|
41
|
+
first_name?: string;
|
|
42
|
+
last_name?: string;
|
|
43
|
+
billing?: {
|
|
44
|
+
phone?: string;
|
|
45
|
+
address_1?: string;
|
|
46
|
+
address_2?: string;
|
|
47
|
+
city?: string;
|
|
48
|
+
state?: string;
|
|
49
|
+
postcode?: string;
|
|
50
|
+
country?: string;
|
|
51
|
+
};
|
|
52
|
+
shipping?: {
|
|
53
|
+
address_1?: string;
|
|
54
|
+
address_2?: string;
|
|
55
|
+
city?: string;
|
|
56
|
+
state?: string;
|
|
57
|
+
postcode?: string;
|
|
58
|
+
country?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WooImportTarget {
|
|
63
|
+
createEntity(input: {
|
|
64
|
+
type: string;
|
|
65
|
+
slug: string;
|
|
66
|
+
attributes: { title: string; description?: string; subtitle?: string };
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
}): Promise<{ id: string }>;
|
|
69
|
+
createOptionType?(input: {
|
|
70
|
+
entityId: string;
|
|
71
|
+
name: string;
|
|
72
|
+
displayName: string;
|
|
73
|
+
sortOrder?: number;
|
|
74
|
+
}): Promise<{ id: string }>;
|
|
75
|
+
createOptionValue?(input: {
|
|
76
|
+
optionTypeId: string;
|
|
77
|
+
value: string;
|
|
78
|
+
displayValue: string;
|
|
79
|
+
sortOrder?: number;
|
|
80
|
+
}): Promise<{ id: string }>;
|
|
81
|
+
createVariant?(input: {
|
|
82
|
+
entityId: string;
|
|
83
|
+
optionValueIds: string[];
|
|
84
|
+
sku?: string;
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
86
|
+
}): Promise<{ id: string }>;
|
|
87
|
+
uploadMedia?(input: {
|
|
88
|
+
filename: string;
|
|
89
|
+
contentType: string;
|
|
90
|
+
data: ArrayBuffer;
|
|
91
|
+
alt?: string;
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
}): Promise<{ id: string; url: string }>;
|
|
94
|
+
attachMedia?(input: {
|
|
95
|
+
entityId: string;
|
|
96
|
+
mediaAssetId: string;
|
|
97
|
+
role: "primary" | "gallery";
|
|
98
|
+
}): Promise<void>;
|
|
99
|
+
upsertCustomer?(input: {
|
|
100
|
+
userId: string;
|
|
101
|
+
email?: string;
|
|
102
|
+
phone?: string;
|
|
103
|
+
firstName?: string;
|
|
104
|
+
lastName?: string;
|
|
105
|
+
addresses?: Array<{
|
|
106
|
+
type: "shipping" | "billing";
|
|
107
|
+
isDefault: boolean;
|
|
108
|
+
firstName: string;
|
|
109
|
+
lastName: string;
|
|
110
|
+
line1: string;
|
|
111
|
+
line2?: string;
|
|
112
|
+
city: string;
|
|
113
|
+
state?: string;
|
|
114
|
+
postalCode?: string;
|
|
115
|
+
country: string;
|
|
116
|
+
phone?: string;
|
|
117
|
+
}>;
|
|
118
|
+
metadata?: Record<string, unknown>;
|
|
119
|
+
}): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface WooImportOptions {
|
|
123
|
+
target: WooImportTarget;
|
|
124
|
+
storeUrl?: string;
|
|
125
|
+
consumerKey?: string;
|
|
126
|
+
consumerSecret?: string;
|
|
127
|
+
products?: WooProduct[];
|
|
128
|
+
customers?: WooCustomer[];
|
|
129
|
+
fetchImpl?: typeof fetch;
|
|
130
|
+
mediaFetcher?: (url: string) => Promise<{ data: ArrayBuffer; contentType: string; filename?: string }>;
|
|
131
|
+
entityType?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface WooImportSummary {
|
|
135
|
+
entitiesImported: number;
|
|
136
|
+
variantsImported: number;
|
|
137
|
+
mediaImported: number;
|
|
138
|
+
customersImported: number;
|
|
139
|
+
errors: Array<{ scope: "entity" | "variant" | "media" | "customer"; message: string }>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function slugify(value: string): string {
|
|
143
|
+
return value
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
147
|
+
.replace(/^-+|-+$/g, "") || `product-${crypto.randomUUID().slice(0, 8)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildWooUrl(base: string, path: string, key: string, secret: string): string {
|
|
151
|
+
const url = new URL(path, base.replace(/\/$/, "/"));
|
|
152
|
+
url.searchParams.set("consumer_key", key);
|
|
153
|
+
url.searchParams.set("consumer_secret", secret);
|
|
154
|
+
url.searchParams.set("per_page", "100");
|
|
155
|
+
return url.toString();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function fetchJson<T>(fetchImpl: typeof fetch, url: string): Promise<Result<T>> {
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
return Err({
|
|
163
|
+
code: "WOO_API_FAILED",
|
|
164
|
+
message: `WooCommerce request failed (${response.status}) for ${url}.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return Ok((await response.json()) as T);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return Err({
|
|
170
|
+
code: "WOO_API_FAILED",
|
|
171
|
+
message: error instanceof Error ? error.message : "WooCommerce request failed.",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function loadProducts(options: WooImportOptions): Promise<Result<WooProduct[]>> {
|
|
177
|
+
if (options.products) return Ok(options.products);
|
|
178
|
+
if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
|
|
179
|
+
return Err({
|
|
180
|
+
code: "WOO_INPUT_REQUIRED",
|
|
181
|
+
message: "Provide products or storeUrl + consumerKey + consumerSecret.",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
186
|
+
return fetchJson<WooProduct[]>(
|
|
187
|
+
fetchImpl,
|
|
188
|
+
buildWooUrl(options.storeUrl, "/wp-json/wc/v3/products", options.consumerKey, options.consumerSecret),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function loadCustomers(options: WooImportOptions): Promise<Result<WooCustomer[]>> {
|
|
193
|
+
if (options.customers) return Ok(options.customers);
|
|
194
|
+
if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
|
|
195
|
+
return Ok([]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
199
|
+
return fetchJson<WooCustomer[]>(
|
|
200
|
+
fetchImpl,
|
|
201
|
+
buildWooUrl(options.storeUrl, "/wp-json/wc/v3/customers", options.consumerKey, options.consumerSecret),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function filenameFromUrl(url: string): string {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = new URL(url);
|
|
208
|
+
return parsed.pathname.split("/").filter(Boolean).pop() ?? `media-${crypto.randomUUID().slice(0, 8)}.bin`;
|
|
209
|
+
} catch {
|
|
210
|
+
return `media-${crypto.randomUUID().slice(0, 8)}.bin`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function defaultMediaFetcher(
|
|
215
|
+
fetchImpl: typeof fetch,
|
|
216
|
+
url: string,
|
|
217
|
+
): Promise<{ data: ArrayBuffer; contentType: string; filename: string }> {
|
|
218
|
+
const response = await fetchImpl(url);
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw new Error(`Failed to fetch media (${response.status}) from ${url}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
data: await response.arrayBuffer(),
|
|
225
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
226
|
+
filename: filenameFromUrl(url),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function importWooCommerceCatalog(options: WooImportOptions): Promise<Result<WooImportSummary>> {
|
|
231
|
+
const products = await loadProducts(options);
|
|
232
|
+
if (!products.ok) return products;
|
|
233
|
+
|
|
234
|
+
const customers = await loadCustomers(options);
|
|
235
|
+
if (!customers.ok) return customers;
|
|
236
|
+
|
|
237
|
+
const summary: WooImportSummary = {
|
|
238
|
+
entitiesImported: 0,
|
|
239
|
+
variantsImported: 0,
|
|
240
|
+
mediaImported: 0,
|
|
241
|
+
customersImported: 0,
|
|
242
|
+
errors: [],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const mediaFetcher = options.mediaFetcher
|
|
246
|
+
? options.mediaFetcher
|
|
247
|
+
: async (url: string) => defaultMediaFetcher(options.fetchImpl ?? fetch, url);
|
|
248
|
+
|
|
249
|
+
for (const product of products.value) {
|
|
250
|
+
try {
|
|
251
|
+
const createdEntity = await options.target.createEntity({
|
|
252
|
+
type: options.entityType ?? "product",
|
|
253
|
+
slug: product.slug ? slugify(product.slug) : slugify(product.name),
|
|
254
|
+
attributes: {
|
|
255
|
+
title: product.name,
|
|
256
|
+
...(product.description ? { description: product.description } : {}),
|
|
257
|
+
...(product.short_description ? { subtitle: product.short_description } : {}),
|
|
258
|
+
},
|
|
259
|
+
metadata: {
|
|
260
|
+
source: "woocommerce",
|
|
261
|
+
wooProductId: product.id,
|
|
262
|
+
productType: product.type,
|
|
263
|
+
categories: (product.categories ?? []).map((category) => category.slug ?? category.name),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
summary.entitiesImported += 1;
|
|
268
|
+
|
|
269
|
+
const optionTypeIdByName = new Map<string, string>();
|
|
270
|
+
const optionValueIdByKey = new Map<string, string>();
|
|
271
|
+
|
|
272
|
+
if (options.target.createOptionType && options.target.createOptionValue) {
|
|
273
|
+
const attributes = (product.attributes ?? []).filter((attribute) => attribute.variation);
|
|
274
|
+
for (let attrIndex = 0; attrIndex < attributes.length; attrIndex += 1) {
|
|
275
|
+
const attribute = attributes[attrIndex]!;
|
|
276
|
+
const createdType = await options.target.createOptionType({
|
|
277
|
+
entityId: createdEntity.id,
|
|
278
|
+
name: attribute.name,
|
|
279
|
+
displayName: attribute.name,
|
|
280
|
+
sortOrder: attrIndex,
|
|
281
|
+
});
|
|
282
|
+
optionTypeIdByName.set(attribute.name, createdType.id);
|
|
283
|
+
|
|
284
|
+
for (let valueIndex = 0; valueIndex < (attribute.options ?? []).length; valueIndex += 1) {
|
|
285
|
+
const value = attribute.options![valueIndex]!;
|
|
286
|
+
const createdValue = await options.target.createOptionValue({
|
|
287
|
+
optionTypeId: createdType.id,
|
|
288
|
+
value,
|
|
289
|
+
displayValue: value,
|
|
290
|
+
sortOrder: valueIndex,
|
|
291
|
+
});
|
|
292
|
+
optionValueIdByKey.set(`${attribute.name}::${value}`, createdValue.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options.target.createVariant) {
|
|
298
|
+
const variationData = product.variationsData ?? [];
|
|
299
|
+
for (const variant of variationData) {
|
|
300
|
+
try {
|
|
301
|
+
const optionValueIds = (variant.attributes ?? [])
|
|
302
|
+
.map((attribute) => optionValueIdByKey.get(`${attribute.name}::${attribute.option ?? ""}`))
|
|
303
|
+
.filter((value): value is string => typeof value === "string");
|
|
304
|
+
|
|
305
|
+
await options.target.createVariant({
|
|
306
|
+
entityId: createdEntity.id,
|
|
307
|
+
optionValueIds,
|
|
308
|
+
...(variant.sku ? { sku: variant.sku } : {}),
|
|
309
|
+
metadata: {
|
|
310
|
+
source: "woocommerce",
|
|
311
|
+
wooVariationId: variant.id,
|
|
312
|
+
price: variant.price,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
summary.variantsImported += 1;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
summary.errors.push({
|
|
319
|
+
scope: "variant",
|
|
320
|
+
message: error instanceof Error ? error.message : "Variation import failed.",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (options.target.uploadMedia && options.target.attachMedia) {
|
|
327
|
+
for (let imageIndex = 0; imageIndex < (product.images ?? []).length; imageIndex += 1) {
|
|
328
|
+
const image = product.images![imageIndex]!;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const downloaded = await mediaFetcher(image.src);
|
|
332
|
+
const uploaded = await options.target.uploadMedia({
|
|
333
|
+
filename: downloaded.filename ?? filenameFromUrl(image.src),
|
|
334
|
+
contentType: downloaded.contentType,
|
|
335
|
+
data: downloaded.data,
|
|
336
|
+
...(image.alt ? { alt: image.alt } : {}),
|
|
337
|
+
metadata: {
|
|
338
|
+
source: "woocommerce",
|
|
339
|
+
wooImageId: image.id,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
await options.target.attachMedia({
|
|
343
|
+
entityId: createdEntity.id,
|
|
344
|
+
mediaAssetId: uploaded.id,
|
|
345
|
+
role: imageIndex === 0 ? "primary" : "gallery",
|
|
346
|
+
});
|
|
347
|
+
summary.mediaImported += 1;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
summary.errors.push({
|
|
350
|
+
scope: "media",
|
|
351
|
+
message: error instanceof Error ? error.message : "Media import failed.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
summary.errors.push({
|
|
358
|
+
scope: "entity",
|
|
359
|
+
message: error instanceof Error ? error.message : "Entity import failed.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.target.upsertCustomer) {
|
|
365
|
+
for (const customer of customers.value) {
|
|
366
|
+
try {
|
|
367
|
+
const billing = customer.billing;
|
|
368
|
+
const shipping = customer.shipping;
|
|
369
|
+
|
|
370
|
+
const addresses: Array<{
|
|
371
|
+
type: "shipping" | "billing";
|
|
372
|
+
isDefault: boolean;
|
|
373
|
+
firstName: string;
|
|
374
|
+
lastName: string;
|
|
375
|
+
line1: string;
|
|
376
|
+
line2?: string;
|
|
377
|
+
city: string;
|
|
378
|
+
state?: string;
|
|
379
|
+
postalCode?: string;
|
|
380
|
+
country: string;
|
|
381
|
+
phone?: string;
|
|
382
|
+
}> = [];
|
|
383
|
+
|
|
384
|
+
if (billing?.address_1) {
|
|
385
|
+
addresses.push({
|
|
386
|
+
type: "billing",
|
|
387
|
+
isDefault: true,
|
|
388
|
+
firstName: customer.first_name ?? "",
|
|
389
|
+
lastName: customer.last_name ?? "",
|
|
390
|
+
line1: billing.address_1,
|
|
391
|
+
...(billing.address_2 ? { line2: billing.address_2 } : {}),
|
|
392
|
+
city: billing.city ?? "",
|
|
393
|
+
...(billing.state ? { state: billing.state } : {}),
|
|
394
|
+
...(billing.postcode ? { postalCode: billing.postcode } : {}),
|
|
395
|
+
country: billing.country ?? "US",
|
|
396
|
+
...(billing.phone ? { phone: billing.phone } : {}),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (shipping?.address_1) {
|
|
401
|
+
addresses.push({
|
|
402
|
+
type: "shipping",
|
|
403
|
+
isDefault: addresses.length === 0,
|
|
404
|
+
firstName: customer.first_name ?? "",
|
|
405
|
+
lastName: customer.last_name ?? "",
|
|
406
|
+
line1: shipping.address_1,
|
|
407
|
+
...(shipping.address_2 ? { line2: shipping.address_2 } : {}),
|
|
408
|
+
city: shipping.city ?? "",
|
|
409
|
+
...(shipping.state ? { state: shipping.state } : {}),
|
|
410
|
+
...(shipping.postcode ? { postalCode: shipping.postcode } : {}),
|
|
411
|
+
country: shipping.country ?? "US",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await options.target.upsertCustomer({
|
|
416
|
+
userId: `woocommerce:${customer.id}`,
|
|
417
|
+
...(customer.email ? { email: customer.email } : {}),
|
|
418
|
+
...(billing?.phone ? { phone: billing.phone } : {}),
|
|
419
|
+
...(customer.first_name ? { firstName: customer.first_name } : {}),
|
|
420
|
+
...(customer.last_name ? { lastName: customer.last_name } : {}),
|
|
421
|
+
addresses,
|
|
422
|
+
metadata: {
|
|
423
|
+
source: "woocommerce",
|
|
424
|
+
wooCustomerId: customer.id,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
summary.customersImported += 1;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
summary.errors.push({
|
|
431
|
+
scope: "customer",
|
|
432
|
+
message: error instanceof Error ? error.message : "Customer import failed.",
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return Ok(summary);
|
|
439
|
+
}
|