@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/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @porulle/import-woocommerce
|
|
2
|
+
|
|
3
|
+
Import a WooCommerce store's catalog into Porulle. Maps WooCommerce's product / variation / attribute / image model onto Porulle's catalog.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Export your WooCommerce catalog as JSON (via the WooCommerce REST API or the WP-CLI):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
wp wc product list --user=admin --format=json > woo-catalog.json
|
|
11
|
+
porulle import ./woo-catalog.json --format woocommerce
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or programmatically:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { importWooProducts } from "@porulle/import-woocommerce";
|
|
18
|
+
import { commerce } from "./server";
|
|
19
|
+
|
|
20
|
+
const products = JSON.parse(await fs.readFile("./woo-catalog.json", "utf-8"));
|
|
21
|
+
const result = await importWooProducts(commerce.api, products);
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
console.error("import failed:", result.error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it maps
|
|
29
|
+
|
|
30
|
+
| WooCommerce | Porulle |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `product` (simple, variable) | `sellable_entities` (type: `product`) |
|
|
33
|
+
| `variation` | `variants` |
|
|
34
|
+
| `attribute` (pa_size, pa_color, …) | `option_types` + `option_values` |
|
|
35
|
+
| `images[]` | `media_assets` + `entity_media` |
|
|
36
|
+
| `categories[]` | `entity_categories` (creates `categories` rows on first sight) |
|
|
37
|
+
| `tags[]` | `metadata.tags` |
|
|
38
|
+
| `meta_data[]` | `metadata.<key>` |
|
|
39
|
+
|
|
40
|
+
## What it doesn't map
|
|
41
|
+
|
|
42
|
+
- Customers, orders, coupons — same reasoning as the Shopify importer
|
|
43
|
+
- Tax rates — Porulle's tax adapter handles this; configure separately
|
|
44
|
+
- Shipping zones — set up via `config.shipping`
|
|
45
|
+
- WP users / themes / plugins — out of scope
|
|
46
|
+
|
|
47
|
+
## See also
|
|
48
|
+
|
|
49
|
+
- `@porulle/import-flat` — neutral JSON format
|
|
50
|
+
- `@porulle/import-shopify` — for Shopify origins
|
|
51
|
+
- [WooCommerce REST API — Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type Result } from "@porulle/core";
|
|
2
|
+
export interface WooImage {
|
|
3
|
+
id: number;
|
|
4
|
+
src: string;
|
|
5
|
+
alt?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WooAttribute {
|
|
8
|
+
id?: number;
|
|
9
|
+
name: string;
|
|
10
|
+
variation?: boolean;
|
|
11
|
+
options?: string[];
|
|
12
|
+
option?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface WooProductVariation {
|
|
15
|
+
id: number;
|
|
16
|
+
sku?: string;
|
|
17
|
+
price?: string;
|
|
18
|
+
attributes?: WooAttribute[];
|
|
19
|
+
}
|
|
20
|
+
export interface WooProduct {
|
|
21
|
+
id: number;
|
|
22
|
+
name: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
short_description?: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
sku?: string;
|
|
28
|
+
categories?: Array<{
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
slug?: string;
|
|
32
|
+
}>;
|
|
33
|
+
attributes?: WooAttribute[];
|
|
34
|
+
images?: WooImage[];
|
|
35
|
+
variationsData?: WooProductVariation[];
|
|
36
|
+
}
|
|
37
|
+
export interface WooCustomer {
|
|
38
|
+
id: number;
|
|
39
|
+
email?: string;
|
|
40
|
+
first_name?: string;
|
|
41
|
+
last_name?: string;
|
|
42
|
+
billing?: {
|
|
43
|
+
phone?: string;
|
|
44
|
+
address_1?: string;
|
|
45
|
+
address_2?: string;
|
|
46
|
+
city?: string;
|
|
47
|
+
state?: string;
|
|
48
|
+
postcode?: string;
|
|
49
|
+
country?: string;
|
|
50
|
+
};
|
|
51
|
+
shipping?: {
|
|
52
|
+
address_1?: string;
|
|
53
|
+
address_2?: string;
|
|
54
|
+
city?: string;
|
|
55
|
+
state?: string;
|
|
56
|
+
postcode?: string;
|
|
57
|
+
country?: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export interface WooImportTarget {
|
|
61
|
+
createEntity(input: {
|
|
62
|
+
type: string;
|
|
63
|
+
slug: string;
|
|
64
|
+
attributes: {
|
|
65
|
+
title: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
subtitle?: string;
|
|
68
|
+
};
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
}>;
|
|
73
|
+
createOptionType?(input: {
|
|
74
|
+
entityId: string;
|
|
75
|
+
name: string;
|
|
76
|
+
displayName: string;
|
|
77
|
+
sortOrder?: number;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
id: string;
|
|
80
|
+
}>;
|
|
81
|
+
createOptionValue?(input: {
|
|
82
|
+
optionTypeId: string;
|
|
83
|
+
value: string;
|
|
84
|
+
displayValue: string;
|
|
85
|
+
sortOrder?: number;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
id: string;
|
|
88
|
+
}>;
|
|
89
|
+
createVariant?(input: {
|
|
90
|
+
entityId: string;
|
|
91
|
+
optionValueIds: string[];
|
|
92
|
+
sku?: string;
|
|
93
|
+
metadata?: Record<string, unknown>;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
id: string;
|
|
96
|
+
}>;
|
|
97
|
+
uploadMedia?(input: {
|
|
98
|
+
filename: string;
|
|
99
|
+
contentType: string;
|
|
100
|
+
data: ArrayBuffer;
|
|
101
|
+
alt?: string;
|
|
102
|
+
metadata?: Record<string, unknown>;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
id: string;
|
|
105
|
+
url: string;
|
|
106
|
+
}>;
|
|
107
|
+
attachMedia?(input: {
|
|
108
|
+
entityId: string;
|
|
109
|
+
mediaAssetId: string;
|
|
110
|
+
role: "primary" | "gallery";
|
|
111
|
+
}): Promise<void>;
|
|
112
|
+
upsertCustomer?(input: {
|
|
113
|
+
userId: string;
|
|
114
|
+
email?: string;
|
|
115
|
+
phone?: string;
|
|
116
|
+
firstName?: string;
|
|
117
|
+
lastName?: string;
|
|
118
|
+
addresses?: Array<{
|
|
119
|
+
type: "shipping" | "billing";
|
|
120
|
+
isDefault: boolean;
|
|
121
|
+
firstName: string;
|
|
122
|
+
lastName: string;
|
|
123
|
+
line1: string;
|
|
124
|
+
line2?: string;
|
|
125
|
+
city: string;
|
|
126
|
+
state?: string;
|
|
127
|
+
postalCode?: string;
|
|
128
|
+
country: string;
|
|
129
|
+
phone?: string;
|
|
130
|
+
}>;
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
132
|
+
}): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
export interface WooImportOptions {
|
|
135
|
+
target: WooImportTarget;
|
|
136
|
+
storeUrl?: string;
|
|
137
|
+
consumerKey?: string;
|
|
138
|
+
consumerSecret?: string;
|
|
139
|
+
products?: WooProduct[];
|
|
140
|
+
customers?: WooCustomer[];
|
|
141
|
+
fetchImpl?: typeof fetch;
|
|
142
|
+
mediaFetcher?: (url: string) => Promise<{
|
|
143
|
+
data: ArrayBuffer;
|
|
144
|
+
contentType: string;
|
|
145
|
+
filename?: string;
|
|
146
|
+
}>;
|
|
147
|
+
entityType?: string;
|
|
148
|
+
}
|
|
149
|
+
export interface WooImportSummary {
|
|
150
|
+
entitiesImported: number;
|
|
151
|
+
variantsImported: number;
|
|
152
|
+
mediaImported: number;
|
|
153
|
+
customersImported: number;
|
|
154
|
+
errors: Array<{
|
|
155
|
+
scope: "entity" | "variant" | "media" | "customer";
|
|
156
|
+
message: string;
|
|
157
|
+
}>;
|
|
158
|
+
}
|
|
159
|
+
export declare function importWooCommerceCatalog(options: WooImportOptions): Promise<Result<WooImportSummary>>;
|
|
160
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Err, Ok } from "@porulle/core";
|
|
2
|
+
function slugify(value) {
|
|
3
|
+
return value
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "") || `product-${crypto.randomUUID().slice(0, 8)}`;
|
|
8
|
+
}
|
|
9
|
+
function buildWooUrl(base, path, key, secret) {
|
|
10
|
+
const url = new URL(path, base.replace(/\/$/, "/"));
|
|
11
|
+
url.searchParams.set("consumer_key", key);
|
|
12
|
+
url.searchParams.set("consumer_secret", secret);
|
|
13
|
+
url.searchParams.set("per_page", "100");
|
|
14
|
+
return url.toString();
|
|
15
|
+
}
|
|
16
|
+
async function fetchJson(fetchImpl, url) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
return Err({
|
|
21
|
+
code: "WOO_API_FAILED",
|
|
22
|
+
message: `WooCommerce request failed (${response.status}) for ${url}.`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return Ok((await response.json()));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return Err({
|
|
29
|
+
code: "WOO_API_FAILED",
|
|
30
|
+
message: error instanceof Error ? error.message : "WooCommerce request failed.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function loadProducts(options) {
|
|
35
|
+
if (options.products)
|
|
36
|
+
return Ok(options.products);
|
|
37
|
+
if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
|
|
38
|
+
return Err({
|
|
39
|
+
code: "WOO_INPUT_REQUIRED",
|
|
40
|
+
message: "Provide products or storeUrl + consumerKey + consumerSecret.",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
44
|
+
return fetchJson(fetchImpl, buildWooUrl(options.storeUrl, "/wp-json/wc/v3/products", options.consumerKey, options.consumerSecret));
|
|
45
|
+
}
|
|
46
|
+
async function loadCustomers(options) {
|
|
47
|
+
if (options.customers)
|
|
48
|
+
return Ok(options.customers);
|
|
49
|
+
if (!options.storeUrl || !options.consumerKey || !options.consumerSecret) {
|
|
50
|
+
return Ok([]);
|
|
51
|
+
}
|
|
52
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
53
|
+
return fetchJson(fetchImpl, buildWooUrl(options.storeUrl, "/wp-json/wc/v3/customers", options.consumerKey, options.consumerSecret));
|
|
54
|
+
}
|
|
55
|
+
function filenameFromUrl(url) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
return parsed.pathname.split("/").filter(Boolean).pop() ?? `media-${crypto.randomUUID().slice(0, 8)}.bin`;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return `media-${crypto.randomUUID().slice(0, 8)}.bin`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function defaultMediaFetcher(fetchImpl, url) {
|
|
65
|
+
const response = await fetchImpl(url);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Failed to fetch media (${response.status}) from ${url}`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
data: await response.arrayBuffer(),
|
|
71
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
72
|
+
filename: filenameFromUrl(url),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function importWooCommerceCatalog(options) {
|
|
76
|
+
const products = await loadProducts(options);
|
|
77
|
+
if (!products.ok)
|
|
78
|
+
return products;
|
|
79
|
+
const customers = await loadCustomers(options);
|
|
80
|
+
if (!customers.ok)
|
|
81
|
+
return customers;
|
|
82
|
+
const summary = {
|
|
83
|
+
entitiesImported: 0,
|
|
84
|
+
variantsImported: 0,
|
|
85
|
+
mediaImported: 0,
|
|
86
|
+
customersImported: 0,
|
|
87
|
+
errors: [],
|
|
88
|
+
};
|
|
89
|
+
const mediaFetcher = options.mediaFetcher
|
|
90
|
+
? options.mediaFetcher
|
|
91
|
+
: async (url) => defaultMediaFetcher(options.fetchImpl ?? fetch, url);
|
|
92
|
+
for (const product of products.value) {
|
|
93
|
+
try {
|
|
94
|
+
const createdEntity = await options.target.createEntity({
|
|
95
|
+
type: options.entityType ?? "product",
|
|
96
|
+
slug: product.slug ? slugify(product.slug) : slugify(product.name),
|
|
97
|
+
attributes: {
|
|
98
|
+
title: product.name,
|
|
99
|
+
...(product.description ? { description: product.description } : {}),
|
|
100
|
+
...(product.short_description ? { subtitle: product.short_description } : {}),
|
|
101
|
+
},
|
|
102
|
+
metadata: {
|
|
103
|
+
source: "woocommerce",
|
|
104
|
+
wooProductId: product.id,
|
|
105
|
+
productType: product.type,
|
|
106
|
+
categories: (product.categories ?? []).map((category) => category.slug ?? category.name),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
summary.entitiesImported += 1;
|
|
110
|
+
const optionTypeIdByName = new Map();
|
|
111
|
+
const optionValueIdByKey = new Map();
|
|
112
|
+
if (options.target.createOptionType && options.target.createOptionValue) {
|
|
113
|
+
const attributes = (product.attributes ?? []).filter((attribute) => attribute.variation);
|
|
114
|
+
for (let attrIndex = 0; attrIndex < attributes.length; attrIndex += 1) {
|
|
115
|
+
const attribute = attributes[attrIndex];
|
|
116
|
+
const createdType = await options.target.createOptionType({
|
|
117
|
+
entityId: createdEntity.id,
|
|
118
|
+
name: attribute.name,
|
|
119
|
+
displayName: attribute.name,
|
|
120
|
+
sortOrder: attrIndex,
|
|
121
|
+
});
|
|
122
|
+
optionTypeIdByName.set(attribute.name, createdType.id);
|
|
123
|
+
for (let valueIndex = 0; valueIndex < (attribute.options ?? []).length; valueIndex += 1) {
|
|
124
|
+
const value = attribute.options[valueIndex];
|
|
125
|
+
const createdValue = await options.target.createOptionValue({
|
|
126
|
+
optionTypeId: createdType.id,
|
|
127
|
+
value,
|
|
128
|
+
displayValue: value,
|
|
129
|
+
sortOrder: valueIndex,
|
|
130
|
+
});
|
|
131
|
+
optionValueIdByKey.set(`${attribute.name}::${value}`, createdValue.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (options.target.createVariant) {
|
|
136
|
+
const variationData = product.variationsData ?? [];
|
|
137
|
+
for (const variant of variationData) {
|
|
138
|
+
try {
|
|
139
|
+
const optionValueIds = (variant.attributes ?? [])
|
|
140
|
+
.map((attribute) => optionValueIdByKey.get(`${attribute.name}::${attribute.option ?? ""}`))
|
|
141
|
+
.filter((value) => typeof value === "string");
|
|
142
|
+
await options.target.createVariant({
|
|
143
|
+
entityId: createdEntity.id,
|
|
144
|
+
optionValueIds,
|
|
145
|
+
...(variant.sku ? { sku: variant.sku } : {}),
|
|
146
|
+
metadata: {
|
|
147
|
+
source: "woocommerce",
|
|
148
|
+
wooVariationId: variant.id,
|
|
149
|
+
price: variant.price,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
summary.variantsImported += 1;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
summary.errors.push({
|
|
156
|
+
scope: "variant",
|
|
157
|
+
message: error instanceof Error ? error.message : "Variation import failed.",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.target.uploadMedia && options.target.attachMedia) {
|
|
163
|
+
for (let imageIndex = 0; imageIndex < (product.images ?? []).length; imageIndex += 1) {
|
|
164
|
+
const image = product.images[imageIndex];
|
|
165
|
+
try {
|
|
166
|
+
const downloaded = await mediaFetcher(image.src);
|
|
167
|
+
const uploaded = await options.target.uploadMedia({
|
|
168
|
+
filename: downloaded.filename ?? filenameFromUrl(image.src),
|
|
169
|
+
contentType: downloaded.contentType,
|
|
170
|
+
data: downloaded.data,
|
|
171
|
+
...(image.alt ? { alt: image.alt } : {}),
|
|
172
|
+
metadata: {
|
|
173
|
+
source: "woocommerce",
|
|
174
|
+
wooImageId: image.id,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await options.target.attachMedia({
|
|
178
|
+
entityId: createdEntity.id,
|
|
179
|
+
mediaAssetId: uploaded.id,
|
|
180
|
+
role: imageIndex === 0 ? "primary" : "gallery",
|
|
181
|
+
});
|
|
182
|
+
summary.mediaImported += 1;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
summary.errors.push({
|
|
186
|
+
scope: "media",
|
|
187
|
+
message: error instanceof Error ? error.message : "Media import failed.",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
summary.errors.push({
|
|
195
|
+
scope: "entity",
|
|
196
|
+
message: error instanceof Error ? error.message : "Entity import failed.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (options.target.upsertCustomer) {
|
|
201
|
+
for (const customer of customers.value) {
|
|
202
|
+
try {
|
|
203
|
+
const billing = customer.billing;
|
|
204
|
+
const shipping = customer.shipping;
|
|
205
|
+
const addresses = [];
|
|
206
|
+
if (billing?.address_1) {
|
|
207
|
+
addresses.push({
|
|
208
|
+
type: "billing",
|
|
209
|
+
isDefault: true,
|
|
210
|
+
firstName: customer.first_name ?? "",
|
|
211
|
+
lastName: customer.last_name ?? "",
|
|
212
|
+
line1: billing.address_1,
|
|
213
|
+
...(billing.address_2 ? { line2: billing.address_2 } : {}),
|
|
214
|
+
city: billing.city ?? "",
|
|
215
|
+
...(billing.state ? { state: billing.state } : {}),
|
|
216
|
+
...(billing.postcode ? { postalCode: billing.postcode } : {}),
|
|
217
|
+
country: billing.country ?? "US",
|
|
218
|
+
...(billing.phone ? { phone: billing.phone } : {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (shipping?.address_1) {
|
|
222
|
+
addresses.push({
|
|
223
|
+
type: "shipping",
|
|
224
|
+
isDefault: addresses.length === 0,
|
|
225
|
+
firstName: customer.first_name ?? "",
|
|
226
|
+
lastName: customer.last_name ?? "",
|
|
227
|
+
line1: shipping.address_1,
|
|
228
|
+
...(shipping.address_2 ? { line2: shipping.address_2 } : {}),
|
|
229
|
+
city: shipping.city ?? "",
|
|
230
|
+
...(shipping.state ? { state: shipping.state } : {}),
|
|
231
|
+
...(shipping.postcode ? { postalCode: shipping.postcode } : {}),
|
|
232
|
+
country: shipping.country ?? "US",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
await options.target.upsertCustomer({
|
|
236
|
+
userId: `woocommerce:${customer.id}`,
|
|
237
|
+
...(customer.email ? { email: customer.email } : {}),
|
|
238
|
+
...(billing?.phone ? { phone: billing.phone } : {}),
|
|
239
|
+
...(customer.first_name ? { firstName: customer.first_name } : {}),
|
|
240
|
+
...(customer.last_name ? { lastName: customer.last_name } : {}),
|
|
241
|
+
addresses,
|
|
242
|
+
metadata: {
|
|
243
|
+
source: "woocommerce",
|
|
244
|
+
wooCustomerId: customer.id,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
summary.customersImported += 1;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
summary.errors.push({
|
|
251
|
+
scope: "customer",
|
|
252
|
+
message: error instanceof Error ? error.message : "Customer import failed.",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return Ok(summary);
|
|
258
|
+
}
|