@endora-commerce/mod-import-export 0.100.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 +21 -0
- package/README.md +53 -0
- package/dist/admin/index.d.ts +30 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +36 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/ImportExportPage.d.ts +3 -0
- package/dist/admin/pages/ImportExportPage.d.ts.map +1 -0
- package/dist/admin/pages/ImportExportPage.js +71 -0
- package/dist/admin/pages/ImportExportPage.js.map +1 -0
- package/dist/backend/index.d.ts +43 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +54 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +9 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +44 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/adapter.d.ts +59 -0
- package/dist/backend/services/adapter.d.ts.map +1 -0
- package/dist/backend/services/adapter.js +2 -0
- package/dist/backend/services/adapter.js.map +1 -0
- package/dist/backend/services/adapters/categories.adapter.d.ts +19 -0
- package/dist/backend/services/adapters/categories.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/categories.adapter.js +93 -0
- package/dist/backend/services/adapters/categories.adapter.js.map +1 -0
- package/dist/backend/services/adapters/customers.adapter.d.ts +16 -0
- package/dist/backend/services/adapters/customers.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/customers.adapter.js +48 -0
- package/dist/backend/services/adapters/customers.adapter.js.map +1 -0
- package/dist/backend/services/adapters/orders.adapter.d.ts +13 -0
- package/dist/backend/services/adapters/orders.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/orders.adapter.js +48 -0
- package/dist/backend/services/adapters/orders.adapter.js.map +1 -0
- package/dist/backend/services/adapters/products.adapter.d.ts +16 -0
- package/dist/backend/services/adapters/products.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/products.adapter.js +88 -0
- package/dist/backend/services/adapters/products.adapter.js.map +1 -0
- package/dist/backend/services/adapters/stock.adapter.d.ts +15 -0
- package/dist/backend/services/adapters/stock.adapter.d.ts.map +1 -0
- package/dist/backend/services/adapters/stock.adapter.js +62 -0
- package/dist/backend/services/adapters/stock.adapter.js.map +1 -0
- package/dist/backend/services/csv-codec.d.ts +17 -0
- package/dist/backend/services/csv-codec.d.ts.map +1 -0
- package/dist/backend/services/csv-codec.js +95 -0
- package/dist/backend/services/csv-codec.js.map +1 -0
- package/dist/backend/services/import-export-service.d.ts +52 -0
- package/dist/backend/services/import-export-service.d.ts.map +1 -0
- package/dist/backend/services/import-export-service.js +100 -0
- package/dist/backend/services/import-export-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +138 -0
- package/dist/manifest.js.map +1 -0
- package/docs/import_export.md +79 -0
- package/i18n/en.json +27 -0
- package/i18n/pl.json +27 -0
- package/package.json +82 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ImportExportAdapter, ImportExportPorts } from '../adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Customer accounts: export only.
|
|
4
|
+
*
|
|
5
|
+
* Importing customer accounts safely requires a password-handling story
|
|
6
|
+
* (random secret + forced reset email) that doesn't belong in a CSV
|
|
7
|
+
* upload UI. Operators that need to provision accounts in bulk should
|
|
8
|
+
* use the API + the password-reset flow.
|
|
9
|
+
*
|
|
10
|
+
* Feature 075 / D-74 — two owners, because the sheet names an account's
|
|
11
|
+
* organisation: the entity is offered only while both modules are effectively
|
|
12
|
+
* present, and `organizations` being off removes this one export rather than
|
|
13
|
+
* breaking the centre.
|
|
14
|
+
*/
|
|
15
|
+
export declare function customersAdapter(ports: ImportExportPorts): ImportExportAdapter;
|
|
16
|
+
//# sourceMappingURL=customers.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customers.adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/adapters/customers.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5E;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAqC9E"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer accounts: export only.
|
|
3
|
+
*
|
|
4
|
+
* Importing customer accounts safely requires a password-handling story
|
|
5
|
+
* (random secret + forced reset email) that doesn't belong in a CSV
|
|
6
|
+
* upload UI. Operators that need to provision accounts in bulk should
|
|
7
|
+
* use the API + the password-reset flow.
|
|
8
|
+
*
|
|
9
|
+
* Feature 075 / D-74 — two owners, because the sheet names an account's
|
|
10
|
+
* organisation: the entity is offered only while both modules are effectively
|
|
11
|
+
* present, and `organizations` being off removes this one export rather than
|
|
12
|
+
* breaking the centre.
|
|
13
|
+
*/
|
|
14
|
+
export function customersAdapter(ports) {
|
|
15
|
+
return {
|
|
16
|
+
name: 'customers',
|
|
17
|
+
owners: ['customer_accounts', 'organizations'],
|
|
18
|
+
exportHeader: [
|
|
19
|
+
'email',
|
|
20
|
+
'organization_name',
|
|
21
|
+
'first_name',
|
|
22
|
+
'last_name',
|
|
23
|
+
'role',
|
|
24
|
+
'email_verified_at',
|
|
25
|
+
'last_login_at',
|
|
26
|
+
],
|
|
27
|
+
async exportRows() {
|
|
28
|
+
const accounts = await ports.customerAccounts.listAll();
|
|
29
|
+
if (accounts.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const orgIds = Array.from(new Set(accounts
|
|
32
|
+
.map((a) => a.organizationId)
|
|
33
|
+
.filter((id) => typeof id === 'string' && id.length > 0)));
|
|
34
|
+
const orgs = orgIds.length > 0 ? await ports.organizations.findByIds(orgIds) : [];
|
|
35
|
+
const orgNameById = new Map(orgs.map((o) => [o.id, o.name]));
|
|
36
|
+
return accounts.map((a) => [
|
|
37
|
+
a.email,
|
|
38
|
+
a.organizationId ? (orgNameById.get(a.organizationId) ?? '') : '',
|
|
39
|
+
a.firstName,
|
|
40
|
+
a.lastName,
|
|
41
|
+
a.role,
|
|
42
|
+
a.emailVerifiedAt?.toISOString() ?? '',
|
|
43
|
+
a.lastLoginAt?.toISOString() ?? '',
|
|
44
|
+
]);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=customers.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customers.adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/adapters/customers.adapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAwB;IACvD,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,CAAC,mBAAmB,EAAE,eAAe,CAAC;QAC9C,YAAY,EAAE;YACZ,OAAO;YACP,mBAAmB;YACnB,YAAY;YACZ,WAAW;YACX,MAAM;YACN,mBAAmB;YACnB,eAAe;SACP;QAEV,KAAK,CAAC,UAAU;YACd,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC;YACxD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CACvB,IAAI,GAAG,CACL,QAAQ;iBACL,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC;iBAC5B,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CACzE,CACF,CAAC;YACF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC7D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACzB,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;gBACjE,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,eAAe,EAAE,WAAW,EAAE,IAAI,EAAE;gBACtC,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE;aACnC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ImportExportAdapter, ImportExportPorts } from '../adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orders: export only.
|
|
4
|
+
*
|
|
5
|
+
* Orders are produced by the checkout flow; importing historical orders
|
|
6
|
+
* into a running system would bypass stock reservation, payment tracking,
|
|
7
|
+
* audit logging, and the credit-limit reservation lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* Feature 075 / D-74 — `OrderReadPort.listAll` was published by Phase P with
|
|
10
|
+
* "for the bulk export adapter" in its own doc comment. This is that adapter.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ordersAdapter(ports: ImportExportPorts): ImportExportAdapter;
|
|
13
|
+
//# sourceMappingURL=orders.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orders.adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/adapters/orders.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5E;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAqC3E"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders: export only.
|
|
3
|
+
*
|
|
4
|
+
* Orders are produced by the checkout flow; importing historical orders
|
|
5
|
+
* into a running system would bypass stock reservation, payment tracking,
|
|
6
|
+
* audit logging, and the credit-limit reservation lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* Feature 075 / D-74 — `OrderReadPort.listAll` was published by Phase P with
|
|
9
|
+
* "for the bulk export adapter" in its own doc comment. This is that adapter.
|
|
10
|
+
*/
|
|
11
|
+
export function ordersAdapter(ports) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'orders',
|
|
14
|
+
owners: ['orders'],
|
|
15
|
+
exportHeader: [
|
|
16
|
+
'id',
|
|
17
|
+
'placed_at',
|
|
18
|
+
'organization_id',
|
|
19
|
+
'placed_by_customer_account_id',
|
|
20
|
+
'status',
|
|
21
|
+
'payment_status',
|
|
22
|
+
'currency',
|
|
23
|
+
'subtotal',
|
|
24
|
+
'tax_total',
|
|
25
|
+
'discount_total',
|
|
26
|
+
'delivery_total',
|
|
27
|
+
'total',
|
|
28
|
+
],
|
|
29
|
+
async exportRows() {
|
|
30
|
+
const orders = await ports.orders.listAll();
|
|
31
|
+
return orders.map((o) => [
|
|
32
|
+
o.id,
|
|
33
|
+
o.placedAt.toISOString(),
|
|
34
|
+
o.organizationId,
|
|
35
|
+
o.placedByCustomerAccountId,
|
|
36
|
+
o.status,
|
|
37
|
+
o.paymentStatus,
|
|
38
|
+
o.currency,
|
|
39
|
+
o.subtotal,
|
|
40
|
+
o.taxTotal,
|
|
41
|
+
o.discountTotal,
|
|
42
|
+
o.deliveryTotal,
|
|
43
|
+
o.total,
|
|
44
|
+
]);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=orders.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orders.adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/adapters/orders.adapter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB;IACpD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,MAAM,EAAE,CAAC,QAAQ,CAAC;QAClB,YAAY,EAAE;YACZ,IAAI;YACJ,WAAW;YACX,iBAAiB;YACjB,+BAA+B;YAC/B,QAAQ;YACR,gBAAgB;YAChB,UAAU;YACV,UAAU;YACV,WAAW;YACX,gBAAgB;YAChB,gBAAgB;YAChB,OAAO;SACC;QAEV,KAAK,CAAC,UAAU;YACd,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACvB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE;gBACxB,CAAC,CAAC,cAAc;gBAChB,CAAC,CAAC,yBAAyB;gBAC3B,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,aAAa;gBACf,CAAC,CAAC,KAAK;aACR,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ImportExportAdapter, ImportExportPorts } from '../adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Products import/export. The CSV is intentionally flat — multilingual
|
|
4
|
+
* `name`/`description` collapse to a single locale column (defaults to
|
|
5
|
+
* `en-US`). Power users who need full multi-locale round-trip should use the
|
|
6
|
+
* JSON API directly.
|
|
7
|
+
*
|
|
8
|
+
* Feature 075 / D-74 — `catalog` owns the write. The two enum columns are
|
|
9
|
+
* parsed against the **published** schemas rather than a private allow-list
|
|
10
|
+
* this module kept: the old one still accepted `archived`, a status feature 032
|
|
11
|
+
* renamed to `inactive` five months ago, and wrote it into a column no other
|
|
12
|
+
* path can produce it in. `productStatusWriteSchema` is the interactive admin
|
|
13
|
+
* API's own answer to a legacy `archived` write, so the two paths now agree.
|
|
14
|
+
*/
|
|
15
|
+
export declare function productsAdapter(ports: ImportExportPorts): ImportExportAdapter;
|
|
16
|
+
//# sourceMappingURL=products.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/adapters/products.adapter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CA+E7E"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { productStatusWriteSchema, productVisibilitySchema, } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Products import/export. The CSV is intentionally flat — multilingual
|
|
4
|
+
* `name`/`description` collapse to a single locale column (defaults to
|
|
5
|
+
* `en-US`). Power users who need full multi-locale round-trip should use the
|
|
6
|
+
* JSON API directly.
|
|
7
|
+
*
|
|
8
|
+
* Feature 075 / D-74 — `catalog` owns the write. The two enum columns are
|
|
9
|
+
* parsed against the **published** schemas rather than a private allow-list
|
|
10
|
+
* this module kept: the old one still accepted `archived`, a status feature 032
|
|
11
|
+
* renamed to `inactive` five months ago, and wrote it into a column no other
|
|
12
|
+
* path can produce it in. `productStatusWriteSchema` is the interactive admin
|
|
13
|
+
* API's own answer to a legacy `archived` write, so the two paths now agree.
|
|
14
|
+
*/
|
|
15
|
+
export function productsAdapter(ports) {
|
|
16
|
+
return {
|
|
17
|
+
name: 'products',
|
|
18
|
+
owners: ['catalog'],
|
|
19
|
+
exportHeader: [
|
|
20
|
+
'id',
|
|
21
|
+
'sku',
|
|
22
|
+
'slug',
|
|
23
|
+
'type',
|
|
24
|
+
'status',
|
|
25
|
+
'visibility',
|
|
26
|
+
'name_en',
|
|
27
|
+
'description_en',
|
|
28
|
+
],
|
|
29
|
+
async exportRows() {
|
|
30
|
+
const rows = await ports.catalogProducts.listAll();
|
|
31
|
+
return rows.map((p) => [
|
|
32
|
+
p.id,
|
|
33
|
+
p.sku,
|
|
34
|
+
p.slug,
|
|
35
|
+
p.type,
|
|
36
|
+
p.status,
|
|
37
|
+
p.visibility,
|
|
38
|
+
p.name['en-US'] ?? '',
|
|
39
|
+
p.description['en-US'] ?? '',
|
|
40
|
+
]);
|
|
41
|
+
},
|
|
42
|
+
importHeader: ['sku', 'status', 'visibility', 'name_en', 'description_en'],
|
|
43
|
+
async importRows(records) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const [index, record] of records.entries()) {
|
|
47
|
+
const sku = record['sku']?.trim();
|
|
48
|
+
if (!sku) {
|
|
49
|
+
errors.push({ index, reason: 'sku is required' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const row = { sku };
|
|
53
|
+
const statusRaw = record['status']?.trim();
|
|
54
|
+
if (statusRaw) {
|
|
55
|
+
const parsed = productStatusWriteSchema.safeParse(statusRaw);
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
errors.push({ index, reason: `invalid status: ${statusRaw}` });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
row.status = parsed.data;
|
|
61
|
+
}
|
|
62
|
+
const visibilityRaw = record['visibility']?.trim();
|
|
63
|
+
if (visibilityRaw) {
|
|
64
|
+
const parsed = productVisibilitySchema.safeParse(visibilityRaw);
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
errors.push({ index, reason: `invalid visibility: ${visibilityRaw}` });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
row.visibility = parsed.data;
|
|
70
|
+
}
|
|
71
|
+
const nameEn = record['name_en'];
|
|
72
|
+
if (nameEn !== undefined && nameEn !== '')
|
|
73
|
+
row.name = { 'en-US': nameEn };
|
|
74
|
+
const descEn = record['description_en'];
|
|
75
|
+
if (descEn !== undefined && descEn !== '')
|
|
76
|
+
row.description = { 'en-US': descEn };
|
|
77
|
+
// The `type` field is intentionally not editable post-create — see the
|
|
78
|
+
// FIELD_IMMUTABLE error code in catalog. Operators changing a product's
|
|
79
|
+
// structural type should issue a delete + recreate via the API.
|
|
80
|
+
rows.push(row);
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0)
|
|
83
|
+
return { imported: 0, errors };
|
|
84
|
+
return ports.catalogBulkImport.importProducts(rows);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=products.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"products.adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/adapters/products.adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ImportExportAdapter, ImportExportPorts } from '../adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stock levels import/export. Reservations are read-only — operators only
|
|
4
|
+
* edit `on_hand`. The variant column is optional; an empty value addresses
|
|
5
|
+
* the simple-product baseline level.
|
|
6
|
+
*
|
|
7
|
+
* Feature 075 / D-74 — this adapter used to write `inventory`'s `stock_levels`
|
|
8
|
+
* while this module declared `catalog` and `auth` as its only dependencies, and
|
|
9
|
+
* it addressed the seeded warehouse by a **copy** of `inventory`'s
|
|
10
|
+
* deterministic UUID pasted into the function body. Both go with the write: the
|
|
11
|
+
* rows belong to `inventory`, so `inventory` applies them, and the row a caller
|
|
12
|
+
* passes names a SKU and a quantity because that is all a spreadsheet knows.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stockAdapter(ports: ImportExportPorts): ImportExportAdapter;
|
|
15
|
+
//# sourceMappingURL=stock.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock.adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/adapters/stock.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE5E;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAoD1E"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stock levels import/export. Reservations are read-only — operators only
|
|
3
|
+
* edit `on_hand`. The variant column is optional; an empty value addresses
|
|
4
|
+
* the simple-product baseline level.
|
|
5
|
+
*
|
|
6
|
+
* Feature 075 / D-74 — this adapter used to write `inventory`'s `stock_levels`
|
|
7
|
+
* while this module declared `catalog` and `auth` as its only dependencies, and
|
|
8
|
+
* it addressed the seeded warehouse by a **copy** of `inventory`'s
|
|
9
|
+
* deterministic UUID pasted into the function body. Both go with the write: the
|
|
10
|
+
* rows belong to `inventory`, so `inventory` applies them, and the row a caller
|
|
11
|
+
* passes names a SKU and a quantity because that is all a spreadsheet knows.
|
|
12
|
+
*/
|
|
13
|
+
export function stockAdapter(ports) {
|
|
14
|
+
return {
|
|
15
|
+
name: 'stock',
|
|
16
|
+
owners: ['inventory', 'catalog'],
|
|
17
|
+
exportHeader: ['product_sku', 'variant_id', 'on_hand', 'reserved'],
|
|
18
|
+
async exportRows() {
|
|
19
|
+
// The SKU is `catalog`'s and the level is `inventory`'s, so the export is
|
|
20
|
+
// two reads and a join here rather than one module knowing the other's
|
|
21
|
+
// table. A level whose product does not resolve is dropped, exactly as it
|
|
22
|
+
// was when the join was a `Map` over an entity query.
|
|
23
|
+
const products = await ports.catalogProducts.listAll();
|
|
24
|
+
if (products.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
const skuByProductId = new Map(products.map((p) => [p.id, p.sku]));
|
|
27
|
+
const levels = await ports.inventoryStock.listStockForProducts([...skuByProductId.keys()]);
|
|
28
|
+
return levels
|
|
29
|
+
.map((l) => {
|
|
30
|
+
const sku = skuByProductId.get(l.productId);
|
|
31
|
+
if (!sku)
|
|
32
|
+
return null;
|
|
33
|
+
return [sku, l.variantId ?? '', String(l.onHand), String(l.reserved)];
|
|
34
|
+
})
|
|
35
|
+
.filter((row) => row !== null);
|
|
36
|
+
},
|
|
37
|
+
importHeader: ['product_sku', 'variant_id', 'on_hand'],
|
|
38
|
+
async importRows(records) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (const [index, record] of records.entries()) {
|
|
42
|
+
const productSku = record['product_sku']?.trim();
|
|
43
|
+
if (!productSku) {
|
|
44
|
+
errors.push({ index, reason: 'product_sku is required' });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const onHandRaw = record['on_hand']?.trim();
|
|
48
|
+
const onHand = onHandRaw ? Number.parseInt(onHandRaw, 10) : NaN;
|
|
49
|
+
if (Number.isNaN(onHand) || onHand < 0) {
|
|
50
|
+
errors.push({ index, reason: `invalid on_hand: ${onHandRaw}` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const variantId = record['variant_id']?.trim() || null;
|
|
54
|
+
rows.push({ productSku, variantId, onHand });
|
|
55
|
+
}
|
|
56
|
+
if (errors.length > 0)
|
|
57
|
+
return { imported: 0, errors };
|
|
58
|
+
return ports.inventoryStockImport.importStockLevels(rows);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=stock.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock.adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/adapters/stock.adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal RFC 4180 CSV codec (Constitution Principle IV — no new dep).
|
|
3
|
+
*
|
|
4
|
+
* Handles quoted fields containing commas, double-quotes, and newlines.
|
|
5
|
+
* Trades feature breadth for zero dependencies; if a real-world CSV
|
|
6
|
+
* variant (e.g. semicolon delimiter, BOM marker, Excel quirks) appears in
|
|
7
|
+
* traffic, that is the trigger to revisit and either extend this codec or
|
|
8
|
+
* adopt a vetted library with a written justification per Principle IV.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCsv(input: string): string[][];
|
|
11
|
+
export declare function serializeCsv(rows: ReadonlyArray<ReadonlyArray<string>>): string;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a parsed CSV (header row + data rows) into an array of
|
|
14
|
+
* string-keyed records. Throws on duplicate header keys.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rowsToRecords(rows: string[][]): Array<Record<string, string>>;
|
|
17
|
+
//# sourceMappingURL=csv-codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv-codec.d.ts","sourceRoot":"","sources":["../../../src/backend/services/csv-codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA2ClD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAE/E;AAcD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiB7E"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal RFC 4180 CSV codec (Constitution Principle IV — no new dep).
|
|
3
|
+
*
|
|
4
|
+
* Handles quoted fields containing commas, double-quotes, and newlines.
|
|
5
|
+
* Trades feature breadth for zero dependencies; if a real-world CSV
|
|
6
|
+
* variant (e.g. semicolon delimiter, BOM marker, Excel quirks) appears in
|
|
7
|
+
* traffic, that is the trigger to revisit and either extend this codec or
|
|
8
|
+
* adopt a vetted library with a written justification per Principle IV.
|
|
9
|
+
*/
|
|
10
|
+
export function parseCsv(input) {
|
|
11
|
+
const rows = [];
|
|
12
|
+
let row = [];
|
|
13
|
+
let field = '';
|
|
14
|
+
let inQuotes = false;
|
|
15
|
+
// Strip a leading UTF-8 BOM if Excel emitted one.
|
|
16
|
+
const text = input.charCodeAt(0) === 0xfeff ? input.slice(1) : input;
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
const c = text[i];
|
|
19
|
+
if (inQuotes) {
|
|
20
|
+
if (c === '"') {
|
|
21
|
+
if (text[i + 1] === '"') {
|
|
22
|
+
field += '"';
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
inQuotes = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
field += c;
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (c === '"' && field.length === 0) {
|
|
35
|
+
inQuotes = true;
|
|
36
|
+
}
|
|
37
|
+
else if (c === ',') {
|
|
38
|
+
row.push(field);
|
|
39
|
+
field = '';
|
|
40
|
+
}
|
|
41
|
+
else if (c === '\r') {
|
|
42
|
+
// Swallow — the trailing \n closes the row.
|
|
43
|
+
}
|
|
44
|
+
else if (c === '\n') {
|
|
45
|
+
row.push(field);
|
|
46
|
+
rows.push(row);
|
|
47
|
+
row = [];
|
|
48
|
+
field = '';
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
field += c;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (field.length > 0 || row.length > 0) {
|
|
55
|
+
row.push(field);
|
|
56
|
+
rows.push(row);
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
export function serializeCsv(rows) {
|
|
61
|
+
return rows.map((r) => r.map(serializeField).join(',')).join('\n') + '\n';
|
|
62
|
+
}
|
|
63
|
+
function serializeField(value) {
|
|
64
|
+
if (value.includes('"') ||
|
|
65
|
+
value.includes(',') ||
|
|
66
|
+
value.includes('\n') ||
|
|
67
|
+
value.includes('\r')) {
|
|
68
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Converts a parsed CSV (header row + data rows) into an array of
|
|
74
|
+
* string-keyed records. Throws on duplicate header keys.
|
|
75
|
+
*/
|
|
76
|
+
export function rowsToRecords(rows) {
|
|
77
|
+
if (rows.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const header = rows[0].map((h) => h.trim());
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const h of header) {
|
|
82
|
+
if (seen.has(h)) {
|
|
83
|
+
throw new Error(`duplicate header column: "${h}"`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(h);
|
|
86
|
+
}
|
|
87
|
+
return rows.slice(1).map((row) => {
|
|
88
|
+
const out = {};
|
|
89
|
+
header.forEach((key, i) => {
|
|
90
|
+
out[key] = row[i] ?? '';
|
|
91
|
+
});
|
|
92
|
+
return out;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=csv-codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv-codec.js","sourceRoot":"","sources":["../../../src/backend/services/csv-codec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ImportExportEntity } from '@endora-commerce/contracts';
|
|
2
|
+
import type { ImportExportPorts } from './adapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* ImportExportService — adapter dispatcher.
|
|
5
|
+
*
|
|
6
|
+
* Each entity has a single source of truth (the adapter), keeping the
|
|
7
|
+
* route layer free of per-entity branches.
|
|
8
|
+
*
|
|
9
|
+
* **What this module owns after D-74**: the CSV codec, the header validation,
|
|
10
|
+
* the row numbering, the 400/404/405 envelope and the entity registry. What it
|
|
11
|
+
* no longer owns is the write — a catalogue import is `catalog`'s transaction
|
|
12
|
+
* and a stock import is `inventory`'s, because every row of either lands in one
|
|
13
|
+
* owner's tables. All-or-nothing did not move with it: the owner applies a run
|
|
14
|
+
* whole or applies none of it, and a column this module rejects stops the run
|
|
15
|
+
* before the owner is asked at all.
|
|
16
|
+
*
|
|
17
|
+
* **Presence is decided per entity** (Principle XVII rule 5). The entity list
|
|
18
|
+
* used to be a literal in the admin SPA, so an operator who switched `inventory`
|
|
19
|
+
* off was still offered a Stock import. It is derived from the effective state
|
|
20
|
+
* here, which is also what makes the module's five `degrades-without`
|
|
21
|
+
* declarations true instead of decorative.
|
|
22
|
+
*/
|
|
23
|
+
export interface ImportReport {
|
|
24
|
+
imported: number;
|
|
25
|
+
errors: Array<{
|
|
26
|
+
rowNumber: number;
|
|
27
|
+
reason: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare class ImportExportService {
|
|
31
|
+
private readonly adapters;
|
|
32
|
+
constructor(ports: ImportExportPorts);
|
|
33
|
+
/**
|
|
34
|
+
* The entities this deployment can offer right now: the ones whose every
|
|
35
|
+
* owner is effectively present. Read per call rather than cached, because an
|
|
36
|
+
* operator flips a module while the process is running.
|
|
37
|
+
*/
|
|
38
|
+
listEntities(): ImportExportEntity[];
|
|
39
|
+
exportToCsv(entity: string): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Apply a CSV file. Either every row lands or none — the owner runs the whole
|
|
42
|
+
* batch in one transaction. Per-row validation errors are collected and
|
|
43
|
+
* returned; the presence of any error means nothing was applied, so the
|
|
44
|
+
* operator can fix the spreadsheet and re-upload deterministically.
|
|
45
|
+
*/
|
|
46
|
+
importFromCsv(entity: string, csv: string): Promise<ImportReport>;
|
|
47
|
+
exportHeaderFor(entity: string): readonly string[];
|
|
48
|
+
importHeaderFor(entity: string): readonly string[] | undefined;
|
|
49
|
+
private available;
|
|
50
|
+
private adapterFor;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=import-export-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-export-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/import-export-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIlF,OAAO,KAAK,EAAuB,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAO3E;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;gBAE9C,KAAK,EAAE,iBAAiB;IAUpC;;;;OAIG;IACH,YAAY,IAAI,kBAAkB,EAAE;IAQ9B,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAMlD;;;;;OAKG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA+CvE,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE;IAIlD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS;IAI9D,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,UAAU;CAiBnB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { parseCsv, rowsToRecords, serializeCsv } from './csv-codec.js';
|
|
5
|
+
import { productsAdapter } from './adapters/products.adapter.js';
|
|
6
|
+
import { categoriesAdapter } from './adapters/categories.adapter.js';
|
|
7
|
+
import { stockAdapter } from './adapters/stock.adapter.js';
|
|
8
|
+
import { customersAdapter } from './adapters/customers.adapter.js';
|
|
9
|
+
import { ordersAdapter } from './adapters/orders.adapter.js';
|
|
10
|
+
export class ImportExportService {
|
|
11
|
+
adapters;
|
|
12
|
+
constructor(ports) {
|
|
13
|
+
this.adapters = [
|
|
14
|
+
productsAdapter(ports),
|
|
15
|
+
categoriesAdapter(ports),
|
|
16
|
+
stockAdapter(ports),
|
|
17
|
+
customersAdapter(ports),
|
|
18
|
+
ordersAdapter(ports),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The entities this deployment can offer right now: the ones whose every
|
|
23
|
+
* owner is effectively present. Read per call rather than cached, because an
|
|
24
|
+
* operator flips a module while the process is running.
|
|
25
|
+
*/
|
|
26
|
+
listEntities() {
|
|
27
|
+
return this.available().map((adapter) => ({
|
|
28
|
+
name: adapter.name,
|
|
29
|
+
exportHeader: [...adapter.exportHeader],
|
|
30
|
+
importHeader: adapter.importHeader ? [...adapter.importHeader] : null,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
async exportToCsv(entity) {
|
|
34
|
+
const adapter = this.adapterFor(entity);
|
|
35
|
+
const rows = await adapter.exportRows();
|
|
36
|
+
return serializeCsv([Array.from(adapter.exportHeader), ...rows]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Apply a CSV file. Either every row lands or none — the owner runs the whole
|
|
40
|
+
* batch in one transaction. Per-row validation errors are collected and
|
|
41
|
+
* returned; the presence of any error means nothing was applied, so the
|
|
42
|
+
* operator can fix the spreadsheet and re-upload deterministically.
|
|
43
|
+
*/
|
|
44
|
+
async importFromCsv(entity, csv) {
|
|
45
|
+
const adapter = this.adapterFor(entity);
|
|
46
|
+
if (!adapter.importHeader || !adapter.importRows) {
|
|
47
|
+
throw new HttpError(405, ERROR_CODES.VALIDATION_FAILED, `Import is not supported for "${entity}".`);
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = parseCsv(csv);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, err instanceof Error ? err.message : 'Failed to parse CSV.');
|
|
55
|
+
}
|
|
56
|
+
if (parsed.length === 0) {
|
|
57
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, 'CSV is empty.');
|
|
58
|
+
}
|
|
59
|
+
const rawHeader = parsed[0].map((h) => h.trim());
|
|
60
|
+
for (const required of adapter.importHeader) {
|
|
61
|
+
if (!rawHeader.includes(required)) {
|
|
62
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Missing required header column: ${required}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const report = await adapter.importRows(rowsToRecords(parsed));
|
|
66
|
+
return {
|
|
67
|
+
imported: report.imported,
|
|
68
|
+
// Row numbers are 1-based and exclude the header line, matching what
|
|
69
|
+
// spreadsheet apps display. The owner counts rows from 0 and knows
|
|
70
|
+
// nothing about a header line, so the translation happens here.
|
|
71
|
+
errors: report.errors.map((error) => ({
|
|
72
|
+
rowNumber: error.index + 2,
|
|
73
|
+
reason: error.reason,
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
exportHeaderFor(entity) {
|
|
78
|
+
return this.adapterFor(entity).exportHeader;
|
|
79
|
+
}
|
|
80
|
+
importHeaderFor(entity) {
|
|
81
|
+
return this.adapterFor(entity).importHeader;
|
|
82
|
+
}
|
|
83
|
+
available() {
|
|
84
|
+
return this.adapters.filter((adapter) => adapter.owners.every((owner) => effectiveState.isPresent(owner)));
|
|
85
|
+
}
|
|
86
|
+
adapterFor(entity) {
|
|
87
|
+
const available = this.available();
|
|
88
|
+
const adapter = available.find((candidate) => candidate.name === entity);
|
|
89
|
+
if (!adapter) {
|
|
90
|
+
// Same 404 an unknown slug has always produced, and deliberately so: an
|
|
91
|
+
// entity whose owner an operator switched off is an entity this
|
|
92
|
+
// deployment does not have, not one that is broken.
|
|
93
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Unknown import/export entity "${entity}". Supported: ${available
|
|
94
|
+
.map((candidate) => candidate.name)
|
|
95
|
+
.join(', ')}.`);
|
|
96
|
+
}
|
|
97
|
+
return adapter;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=import-export-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-export-service.js","sourceRoot":"","sources":["../../../src/backend/services/import-export-service.ts"],"names":[],"mappings":"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"}
|