@endora-commerce/mod-quick-order 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 +58 -0
- package/dist/admin/api/quick-order-client.d.ts +76 -0
- package/dist/admin/api/quick-order-client.d.ts.map +1 -0
- package/dist/admin/api/quick-order-client.js +18 -0
- package/dist/admin/api/quick-order-client.js.map +1 -0
- package/dist/admin/components/DefaultPreferencesPanel.d.ts +44 -0
- package/dist/admin/components/DefaultPreferencesPanel.d.ts.map +1 -0
- package/dist/admin/components/DefaultPreferencesPanel.js +107 -0
- package/dist/admin/components/DefaultPreferencesPanel.js.map +1 -0
- package/dist/admin/index.d.ts +48 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +120 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts +16 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts.map +1 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.js +85 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.js.map +1 -0
- package/dist/admin/zones/CustomerDefaults.d.ts +27 -0
- package/dist/admin/zones/CustomerDefaults.d.ts.map +1 -0
- package/dist/admin/zones/CustomerDefaults.js +11 -0
- package/dist/admin/zones/CustomerDefaults.js.map +1 -0
- package/dist/admin/zones/OrderEntryQuickTab.d.ts +21 -0
- package/dist/admin/zones/OrderEntryQuickTab.d.ts.map +1 -0
- package/dist/admin/zones/OrderEntryQuickTab.js +26 -0
- package/dist/admin/zones/OrderEntryQuickTab.js.map +1 -0
- package/dist/admin/zones/OrganizationDefaults.d.ts +30 -0
- package/dist/admin/zones/OrganizationDefaults.d.ts.map +1 -0
- package/dist/admin/zones/OrganizationDefaults.js +11 -0
- package/dist/admin/zones/OrganizationDefaults.js.map +1 -0
- package/dist/backend/entities/quick-order-default-preference.entity.d.ts +22 -0
- package/dist/backend/entities/quick-order-default-preference.entity.d.ts.map +1 -0
- package/dist/backend/entities/quick-order-default-preference.entity.js +76 -0
- package/dist/backend/entities/quick-order-default-preference.entity.js.map +1 -0
- package/dist/backend/index.d.ts +88 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +235 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +35 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +64 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.d.ts +35 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +77 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/routes.one-click.d.ts +22 -0
- package/dist/backend/routes.one-click.d.ts.map +1 -0
- package/dist/backend/routes.one-click.js +35 -0
- package/dist/backend/routes.one-click.js.map +1 -0
- package/dist/backend/routes.preferences.admin.d.ts +23 -0
- package/dist/backend/routes.preferences.admin.d.ts.map +1 -0
- package/dist/backend/routes.preferences.admin.js +40 -0
- package/dist/backend/routes.preferences.admin.js.map +1 -0
- package/dist/backend/routes.preferences.d.ts +21 -0
- package/dist/backend/routes.preferences.d.ts.map +1 -0
- package/dist/backend/routes.preferences.js +42 -0
- package/dist/backend/routes.preferences.js.map +1 -0
- package/dist/backend/services/catalog-lookup.d.ts +39 -0
- package/dist/backend/services/catalog-lookup.d.ts.map +1 -0
- package/dist/backend/services/catalog-lookup.js +104 -0
- package/dist/backend/services/catalog-lookup.js.map +1 -0
- package/dist/backend/services/default-preference-authz.d.ts +31 -0
- package/dist/backend/services/default-preference-authz.d.ts.map +1 -0
- package/dist/backend/services/default-preference-authz.js +26 -0
- package/dist/backend/services/default-preference-authz.js.map +1 -0
- package/dist/backend/services/default-preference-resolver.d.ts +28 -0
- package/dist/backend/services/default-preference-resolver.d.ts.map +1 -0
- package/dist/backend/services/default-preference-resolver.js +27 -0
- package/dist/backend/services/default-preference-resolver.js.map +1 -0
- package/dist/backend/services/default-preference-service.d.ts +63 -0
- package/dist/backend/services/default-preference-service.d.ts.map +1 -0
- package/dist/backend/services/default-preference-service.js +176 -0
- package/dist/backend/services/default-preference-service.js.map +1 -0
- package/dist/backend/services/excel-importer.d.ts +11 -0
- package/dist/backend/services/excel-importer.d.ts.map +1 -0
- package/dist/backend/services/excel-importer.js +47 -0
- package/dist/backend/services/excel-importer.js.map +1 -0
- package/dist/backend/services/import-from-request.d.ts +9 -0
- package/dist/backend/services/import-from-request.d.ts.map +1 -0
- package/dist/backend/services/import-from-request.js +17 -0
- package/dist/backend/services/import-from-request.js.map +1 -0
- package/dist/backend/services/import-pipeline.d.ts +89 -0
- package/dist/backend/services/import-pipeline.d.ts.map +1 -0
- package/dist/backend/services/import-pipeline.js +188 -0
- package/dist/backend/services/import-pipeline.js.map +1 -0
- package/dist/backend/services/import-rows.d.ts +43 -0
- package/dist/backend/services/import-rows.d.ts.map +1 -0
- package/dist/backend/services/import-rows.js +102 -0
- package/dist/backend/services/import-rows.js.map +1 -0
- package/dist/backend/services/one-click-service.d.ts +50 -0
- package/dist/backend/services/one-click-service.d.ts.map +1 -0
- package/dist/backend/services/one-click-service.js +83 -0
- package/dist/backend/services/one-click-service.js.map +1 -0
- package/dist/backend/services/quick-order-build-service.d.ts +35 -0
- package/dist/backend/services/quick-order-build-service.d.ts.map +1 -0
- package/dist/backend/services/quick-order-build-service.js +61 -0
- package/dist/backend/services/quick-order-build-service.js.map +1 -0
- package/dist/backend/services/variant-resolver.d.ts +40 -0
- package/dist/backend/services/variant-resolver.d.ts.map +1 -0
- package/dist/backend/services/variant-resolver.js +47 -0
- package/dist/backend/services/variant-resolver.js.map +1 -0
- package/dist/manifest.d.ts +190 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +129 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts +15 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts.map +1 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.js +53 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/quick_order.md +98 -0
- package/i18n/en.json +47 -0
- package/i18n/pl.json +47 -0
- package/package.json +91 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"excel-importer.d.ts","sourceRoot":"","sources":["../../../src/backend/services/excel-importer.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEpE;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsC1E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import exceljs from 'exceljs';
|
|
2
|
+
import { rowsFromTable } from './import-rows.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse an `.xlsx` workbook into the shared `NormalizedRow[]` (feature 039,
|
|
5
|
+
* FR-001). Only the first worksheet is read (research §R1); the header +
|
|
6
|
+
* column semantics are identical to the CSV path via `rowsFromTable`.
|
|
7
|
+
*
|
|
8
|
+
* The dependency on `exceljs` is isolated to this adapter — the rest of the
|
|
9
|
+
* import pipeline only ever sees normalized rows.
|
|
10
|
+
*/
|
|
11
|
+
export async function parseXlsxRows(content) {
|
|
12
|
+
const workbook = new exceljs.Workbook();
|
|
13
|
+
// Hand exceljs a plain ArrayBuffer slice: @types/node now types Buffer as
|
|
14
|
+
// the generic `Buffer<ArrayBufferLike>`, which is not assignable to the
|
|
15
|
+
// `Buffer<ArrayBuffer>` exceljs's bundled typings expect.
|
|
16
|
+
const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
17
|
+
await workbook.xlsx.load(arrayBuffer);
|
|
18
|
+
const worksheet = workbook.worksheets[0];
|
|
19
|
+
if (!worksheet)
|
|
20
|
+
return { headerOk: false, rows: [] };
|
|
21
|
+
// exceljs rows + cells are 1-based; row.values[0] is unused.
|
|
22
|
+
const cellText = (cell) => {
|
|
23
|
+
if (!cell)
|
|
24
|
+
return '';
|
|
25
|
+
const text = cell.text;
|
|
26
|
+
return typeof text === 'string' ? text.trim() : String(cell.value ?? '').trim();
|
|
27
|
+
};
|
|
28
|
+
let header = [];
|
|
29
|
+
let headerColumnCount = 0;
|
|
30
|
+
const dataRows = [];
|
|
31
|
+
worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
|
32
|
+
if (rowNumber === 1) {
|
|
33
|
+
headerColumnCount = row.cellCount;
|
|
34
|
+
header = Array.from({ length: headerColumnCount }, (_, i) => cellText(row.getCell(i + 1)));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const columnCount = Math.max(headerColumnCount, row.cellCount);
|
|
38
|
+
const cells = Array.from({ length: columnCount }, (_, i) => cellText(row.getCell(i + 1)));
|
|
39
|
+
if (cells.every((c) => c.length === 0))
|
|
40
|
+
return;
|
|
41
|
+
dataRows.push({ rowNumber, raw: cells.join(','), cells });
|
|
42
|
+
});
|
|
43
|
+
if (header.length === 0)
|
|
44
|
+
return { headerOk: false, rows: [] };
|
|
45
|
+
return rowsFromTable(header, dataRows);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=excel-importer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"excel-importer.js","sourceRoot":"","sources":["../../../src/backend/services/excel-importer.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAqB,MAAM,kBAAkB,CAAC;AAEpE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe;IACjD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IACxC,0EAA0E;IAC1E,wEAAwE;IACxE,0DAA0D;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CACtC,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CACzB,CAAC;IACjB,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAErD,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,CAAC,IAA8B,EAAU,EAAE;QAC1D,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClF,CAAC,CAAC;IAEF,IAAI,MAAM,GAAa,EAAE,CAAC;IAC1B,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAC1B,MAAM,QAAQ,GAA+D,EAAE,CAAC;IAEhF,SAAS,CAAC,OAAO,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;QAC5D,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,iBAAiB,GAAG,GAAG,CAAC,SAAS,CAAC;YAClC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3F,OAAO;QACT,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1F,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;YAAE,OAAO;QAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAC9D,OAAO,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QuickOrderImportRequest } from '@endora-commerce/contracts';
|
|
2
|
+
import { type ParseOutcome } from './import-rows.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turn an import request body (pasted CSV or an uploaded CSV / .xlsx file)
|
|
5
|
+
* into normalized rows. Shared by the storefront and admin import routes so
|
|
6
|
+
* both branch on format identically (feature 039).
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseImportRequest(body: Pick<QuickOrderImportRequest, 'csv' | 'file'>): Promise<ParseOutcome>;
|
|
9
|
+
//# sourceMappingURL=import-from-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-from-request.d.ts","sourceRoot":"","sources":["../../../src/backend/services/import-from-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGnE;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,IAAI,CAAC,uBAAuB,EAAE,KAAK,GAAG,MAAM,CAAC,GAClD,OAAO,CAAC,YAAY,CAAC,CAQvB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseCsvRows } from './import-rows.js';
|
|
2
|
+
import { parseXlsxRows } from './excel-importer.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turn an import request body (pasted CSV or an uploaded CSV / .xlsx file)
|
|
5
|
+
* into normalized rows. Shared by the storefront and admin import routes so
|
|
6
|
+
* both branch on format identically (feature 039).
|
|
7
|
+
*/
|
|
8
|
+
export async function parseImportRequest(body) {
|
|
9
|
+
if (body.file) {
|
|
10
|
+
const buffer = Buffer.from(body.file.contentBase64, 'base64');
|
|
11
|
+
return body.file.filename.toLowerCase().endsWith('.xlsx')
|
|
12
|
+
? parseXlsxRows(buffer)
|
|
13
|
+
: parseCsvRows(buffer.toString('utf8'));
|
|
14
|
+
}
|
|
15
|
+
return parseCsvRows(body.csv ?? '');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=import-from-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-from-request.js","sourceRoot":"","sources":["../../../src/backend/services/import-from-request.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAqB,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAmD;IAEnD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;YACvD,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC;YACvB,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ProductAudience, QuickOrderImportResponse } from '@endora-commerce/contracts';
|
|
2
|
+
import { type VariantLike } from './variant-resolver.js';
|
|
3
|
+
import type { ParseOutcome } from './import-rows.js';
|
|
4
|
+
/**
|
|
5
|
+
* Catalog lookups the pipeline needs, injected so the row-resolution logic
|
|
6
|
+
* stays unit-testable without a database (mirrors the repo's injected-draw
|
|
7
|
+
* pattern, e.g. BusinessIdGenerator). The DB-backed implementation lives in
|
|
8
|
+
* `catalog-lookup.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ProductLike {
|
|
11
|
+
id: string;
|
|
12
|
+
sku: string;
|
|
13
|
+
status: string;
|
|
14
|
+
deletedAt?: Date | null;
|
|
15
|
+
}
|
|
16
|
+
export interface VariantBySku {
|
|
17
|
+
id: string;
|
|
18
|
+
sku: string;
|
|
19
|
+
parentProductId: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Who an import run answers for (issue #227) — a buyer's audience, or the
|
|
23
|
+
* operator surface `'unrestricted'`.
|
|
24
|
+
*
|
|
25
|
+
* The second arm is not a bypass looking for a home: `POST
|
|
26
|
+
* /api/v1/admin/quick-order/import` is gated by `orders:write` and the operator
|
|
27
|
+
* names the organisation on the *build* call, one step later, so at import time
|
|
28
|
+
* there is no buyer to answer for and the permission is the enforcement. It is
|
|
29
|
+
* spelled as a literal rather than as an optional argument because an optional
|
|
30
|
+
* one is the same bypass with nothing to grep for.
|
|
31
|
+
*
|
|
32
|
+
* **`'unrestricted'` waives the sales-channel narrowing too** (issue #259), and
|
|
33
|
+
* on the same ground rather than as a second exemption: the operator names the
|
|
34
|
+
* organisation one step later, so at import time there is no buyer *and*
|
|
35
|
+
* therefore no storefront this paste is happening on. The channel the admin's
|
|
36
|
+
* own request resolved to is the resolver's step-4 default unless the SPA sent
|
|
37
|
+
* `X-Sales-Channel`, and narrowing an operator's paste against a channel nobody
|
|
38
|
+
* chose would refuse SKUs the operator can see on every admin screen. The waiver
|
|
39
|
+
* lives in one place — `CatalogPortLookup.#publishedHere`, which reads this
|
|
40
|
+
* literal — so it cannot drift apart from the audience half it travels with.
|
|
41
|
+
*/
|
|
42
|
+
export type QuickOrderImportAudience = ProductAudience | 'unrestricted';
|
|
43
|
+
export interface QuickOrderCatalogLookup {
|
|
44
|
+
/**
|
|
45
|
+
* Products whose own SKU matches one of `skus`, **and that this buyer may
|
|
46
|
+
* see** (issue #227).
|
|
47
|
+
*
|
|
48
|
+
* The audience is a parameter rather than a filter the pipeline applies
|
|
49
|
+
* afterwards because that is what keeps the two answers identical: a SKU
|
|
50
|
+
* nobody sells and a SKU restricted to another distributor both come back
|
|
51
|
+
* absent, and the pipeline rejects both as `product_not_found` without
|
|
52
|
+
* knowing there was a difference. A pasted SKU list is otherwise an
|
|
53
|
+
* enumeration oracle over an operator's private assortment — the same defect
|
|
54
|
+
* as issue #174's type-ahead, on the surface next to it.
|
|
55
|
+
*/
|
|
56
|
+
findProductsBySku(skus: string[], audience: QuickOrderImportAudience): Promise<ProductLike[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Variants whose own SKU matches one of `skus` (row referenced a variant
|
|
59
|
+
* directly), restricted by the **parent** product's answer — a variant has no
|
|
60
|
+
* visibility of its own.
|
|
61
|
+
*/
|
|
62
|
+
findVariantsBySku(skus: string[], audience: QuickOrderImportAudience): Promise<VariantBySku[]>;
|
|
63
|
+
/** All variants under the given parent products (for attribute-column resolution). */
|
|
64
|
+
findVariantsByParent(productIds: string[]): Promise<Array<VariantLike & {
|
|
65
|
+
parentProductId: string;
|
|
66
|
+
}>>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* QuickOrderImportPipeline (feature 039, FR-004/FR-009/FR-010). Turns parsed
|
|
70
|
+
* rows into the recognized/rejected partition + summary:
|
|
71
|
+
* - validates sku + integer quantity,
|
|
72
|
+
* - resolves a concrete variant from attribute columns (or a direct variant SKU),
|
|
73
|
+
* - merges duplicate (product, variant) lines summing quantity,
|
|
74
|
+
* - caps the row count and reports overflow as `row_limit_exceeded`.
|
|
75
|
+
*/
|
|
76
|
+
export declare class QuickOrderImportPipeline {
|
|
77
|
+
private readonly lookup;
|
|
78
|
+
constructor(lookup: QuickOrderCatalogLookup);
|
|
79
|
+
run(parse: ParseOutcome, opts: {
|
|
80
|
+
maxRows?: number;
|
|
81
|
+
audience: QuickOrderImportAudience;
|
|
82
|
+
}): Promise<QuickOrderImportResponse>;
|
|
83
|
+
/** Validate sku + integer quantity, rejecting malformed rows with a reason. */
|
|
84
|
+
private toCandidates;
|
|
85
|
+
/** Merge duplicate (productId, variantId) lines, summing quantity (FR-009). */
|
|
86
|
+
private mergeDuplicates;
|
|
87
|
+
private finalize;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=import-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-pipeline.d.ts","sourceRoot":"","sources":["../../../src/backend/services/import-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,wBAAwB,EAGzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,wBAAwB,GAAG,eAAe,GAAG,cAAc,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;;;;OAWG;IACH,iBAAiB,CACf,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,wBAAwB,GACjC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1B;;;;OAIG;IACH,iBAAiB,CACf,IAAI,EAAE,MAAM,EAAE,EACd,QAAQ,EAAE,wBAAwB,GACjC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC3B,sFAAsF;IACtF,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;QAAE,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CACvG;AAyBD;;;;;;;GAOG;AACH,qBAAa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,uBAAuB;IAEtD,GAAG,CACP,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,wBAAwB,CAAA;KAAE,GAC7D,OAAO,CAAC,wBAAwB,CAAC;IA4GpC,+EAA+E;IAC/E,OAAO,CAAC,YAAY;IAuBpB,+EAA+E;IAC/E,OAAO,CAAC,eAAe;IAgCvB,OAAO,CAAC,QAAQ;CAiBjB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { resolveVariant } from './variant-resolver.js';
|
|
2
|
+
function isPurchasable(product) {
|
|
3
|
+
// Feature 032 renamed the withdrawn product status `archived` → `inactive`.
|
|
4
|
+
// The `product_archived` rejection reason code is kept for contract stability.
|
|
5
|
+
return product.status !== 'inactive' && !product.deletedAt;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* QuickOrderImportPipeline (feature 039, FR-004/FR-009/FR-010). Turns parsed
|
|
9
|
+
* rows into the recognized/rejected partition + summary:
|
|
10
|
+
* - validates sku + integer quantity,
|
|
11
|
+
* - resolves a concrete variant from attribute columns (or a direct variant SKU),
|
|
12
|
+
* - merges duplicate (product, variant) lines summing quantity,
|
|
13
|
+
* - caps the row count and reports overflow as `row_limit_exceeded`.
|
|
14
|
+
*/
|
|
15
|
+
export class QuickOrderImportPipeline {
|
|
16
|
+
lookup;
|
|
17
|
+
constructor(lookup) {
|
|
18
|
+
this.lookup = lookup;
|
|
19
|
+
}
|
|
20
|
+
async run(parse, opts) {
|
|
21
|
+
const rejected = [];
|
|
22
|
+
if (!parse.headerOk) {
|
|
23
|
+
for (const row of parse.rows) {
|
|
24
|
+
rejected.push({ line: row.rowNumber, raw: row.raw, reason: 'malformed_row' });
|
|
25
|
+
}
|
|
26
|
+
return this.finalize([], rejected, 0, false);
|
|
27
|
+
}
|
|
28
|
+
// Row cap (FR-010): overflow rows are reported, not silently dropped.
|
|
29
|
+
let rows = parse.rows;
|
|
30
|
+
let truncated = false;
|
|
31
|
+
const maxRows = opts.maxRows;
|
|
32
|
+
if (typeof maxRows === 'number' && Number.isFinite(maxRows) && rows.length > maxRows) {
|
|
33
|
+
for (const row of rows.slice(maxRows)) {
|
|
34
|
+
rejected.push({ line: row.rowNumber, raw: row.raw, reason: 'row_limit_exceeded' });
|
|
35
|
+
}
|
|
36
|
+
rows = rows.slice(0, maxRows);
|
|
37
|
+
truncated = true;
|
|
38
|
+
}
|
|
39
|
+
const candidates = this.toCandidates(rows, rejected);
|
|
40
|
+
if (candidates.length === 0)
|
|
41
|
+
return this.finalize([], rejected, 0, truncated);
|
|
42
|
+
const skus = Array.from(new Set(candidates.map((c) => c.sku)));
|
|
43
|
+
const [products, variantsBySku] = await Promise.all([
|
|
44
|
+
this.lookup.findProductsBySku(skus, opts.audience),
|
|
45
|
+
this.lookup.findVariantsBySku(skus, opts.audience),
|
|
46
|
+
]);
|
|
47
|
+
const productBySku = new Map(products.map((p) => [p.sku, p]));
|
|
48
|
+
const variantBySku = new Map(variantsBySku.map((v) => [v.sku, v]));
|
|
49
|
+
// Variants of every parent product referenced by SKU (for attribute resolution).
|
|
50
|
+
const parentIds = products.map((p) => p.id);
|
|
51
|
+
const variantsByParent = new Map();
|
|
52
|
+
if (parentIds.length > 0) {
|
|
53
|
+
for (const v of await this.lookup.findVariantsByParent(parentIds)) {
|
|
54
|
+
const list = variantsByParent.get(v.parentProductId) ?? [];
|
|
55
|
+
list.push(v);
|
|
56
|
+
variantsByParent.set(v.parentProductId, list);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Parent products of variants referenced directly by their own SKU, for the
|
|
60
|
+
// purchasable check.
|
|
61
|
+
const productById = new Map(products.map((p) => [p.id, p]));
|
|
62
|
+
const resolvedLines = [];
|
|
63
|
+
for (const c of candidates) {
|
|
64
|
+
const product = productBySku.get(c.sku);
|
|
65
|
+
if (product) {
|
|
66
|
+
if (!isPurchasable(product)) {
|
|
67
|
+
rejected.push({ line: c.line, raw: c.raw, reason: 'product_archived' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const variants = variantsByParent.get(product.id) ?? [];
|
|
71
|
+
const resolution = resolveVariant(variants, c.attributes);
|
|
72
|
+
if (resolution.kind === 'no_variants') {
|
|
73
|
+
resolvedLines.push({
|
|
74
|
+
line: c.line,
|
|
75
|
+
sku: product.sku,
|
|
76
|
+
productId: product.id,
|
|
77
|
+
variantId: null,
|
|
78
|
+
resolvedVariantSku: null,
|
|
79
|
+
quantity: c.quantity,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (resolution.kind === 'resolved') {
|
|
83
|
+
resolvedLines.push({
|
|
84
|
+
line: c.line,
|
|
85
|
+
sku: product.sku,
|
|
86
|
+
productId: product.id,
|
|
87
|
+
variantId: resolution.variantId,
|
|
88
|
+
resolvedVariantSku: resolution.variantSku,
|
|
89
|
+
quantity: c.quantity,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (resolution.kind === 'ambiguous') {
|
|
93
|
+
rejected.push({ line: c.line, raw: c.raw, reason: 'variant_ambiguous' });
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
rejected.push({ line: c.line, raw: c.raw, reason: 'variant_not_resolved' });
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const variant = variantBySku.get(c.sku);
|
|
101
|
+
if (variant) {
|
|
102
|
+
const parent = productById.get(variant.parentProductId);
|
|
103
|
+
if (parent && !isPurchasable(parent)) {
|
|
104
|
+
rejected.push({ line: c.line, raw: c.raw, reason: 'product_archived' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
resolvedLines.push({
|
|
108
|
+
line: c.line,
|
|
109
|
+
sku: variant.sku,
|
|
110
|
+
productId: variant.parentProductId,
|
|
111
|
+
variantId: variant.id,
|
|
112
|
+
resolvedVariantSku: variant.sku,
|
|
113
|
+
quantity: c.quantity,
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
rejected.push({ line: c.line, raw: c.raw, reason: 'product_not_found' });
|
|
118
|
+
}
|
|
119
|
+
const { recognized, mergedCount } = this.mergeDuplicates(resolvedLines);
|
|
120
|
+
return this.finalize(recognized, rejected, mergedCount, truncated);
|
|
121
|
+
}
|
|
122
|
+
/** Validate sku + integer quantity, rejecting malformed rows with a reason. */
|
|
123
|
+
toCandidates(rows, rejected) {
|
|
124
|
+
const candidates = [];
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
if (!row.sku) {
|
|
127
|
+
rejected.push({ line: row.rowNumber, raw: row.raw, reason: 'sku_missing' });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const quantity = Number.parseInt(row.quantityRaw, 10);
|
|
131
|
+
if (!Number.isFinite(quantity) || quantity < 1 || String(quantity) !== row.quantityRaw) {
|
|
132
|
+
rejected.push({ line: row.rowNumber, raw: row.raw, reason: 'quantity_invalid' });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
candidates.push({
|
|
136
|
+
line: row.rowNumber,
|
|
137
|
+
raw: row.raw,
|
|
138
|
+
sku: row.sku,
|
|
139
|
+
quantity,
|
|
140
|
+
attributes: row.attributes,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return candidates;
|
|
144
|
+
}
|
|
145
|
+
/** Merge duplicate (productId, variantId) lines, summing quantity (FR-009). */
|
|
146
|
+
mergeDuplicates(lines) {
|
|
147
|
+
const byKey = new Map();
|
|
148
|
+
let mergedCount = 0;
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const key = `${line.productId}::${line.variantId ?? ''}`;
|
|
151
|
+
const existing = byKey.get(key);
|
|
152
|
+
if (existing) {
|
|
153
|
+
existing.quantity += line.quantity;
|
|
154
|
+
existing.mergedFromLines.push(line.line);
|
|
155
|
+
mergedCount += 1;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
byKey.set(key, {
|
|
159
|
+
line: line.line,
|
|
160
|
+
sku: line.sku,
|
|
161
|
+
productId: line.productId,
|
|
162
|
+
variantId: line.variantId,
|
|
163
|
+
resolvedVariantSku: line.resolvedVariantSku,
|
|
164
|
+
quantity: line.quantity,
|
|
165
|
+
mergedFromLines: [],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const recognized = Array.from(byKey.values()).map((item) => {
|
|
170
|
+
const { mergedFromLines, ...rest } = item;
|
|
171
|
+
return mergedFromLines.length > 0 ? { ...rest, mergedFromLines } : rest;
|
|
172
|
+
});
|
|
173
|
+
return { recognized, mergedCount };
|
|
174
|
+
}
|
|
175
|
+
finalize(recognized, rejected, mergedCount, truncated) {
|
|
176
|
+
return {
|
|
177
|
+
recognized,
|
|
178
|
+
rejected,
|
|
179
|
+
summary: {
|
|
180
|
+
recognizedCount: recognized.length,
|
|
181
|
+
rejectedCount: rejected.length,
|
|
182
|
+
mergedCount,
|
|
183
|
+
truncated,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=import-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-pipeline.js","sourceRoot":"","sources":["../../../src/backend/services/import-pipeline.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAoB,MAAM,uBAAuB,CAAC;AA4FzE,SAAS,aAAa,CAAC,OAAoB;IACzC,4EAA4E;IAC5E,+EAA+E;IAC/E,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;AAC7D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,MAA+B;QAA/B,WAAM,GAAN,MAAM,CAAyB;IAAG,CAAC;IAEhE,KAAK,CAAC,GAAG,CACP,KAAmB,EACnB,IAA8D;QAE9D,MAAM,QAAQ,GAA6B,EAAE,CAAC;QAE9C,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpB,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;gBAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;YAChF,CAAC;YACD,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;QAC/C,CAAC;QAED,sEAAsE;QACtE,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACtB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YACrF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC,CAAC;YACrF,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;YAC9B,SAAS,GAAG,IAAI,CAAC;QACnB,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;QAE9E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/D,MAAM,CAAC,QAAQ,EAAE,aAAa,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC;YAClD,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC;SACnD,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAEnE,iFAAiF;QACjF,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA4D,CAAC;QAC7F,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE,CAAC;gBAClE,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;gBAC3D,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACb,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QACD,4EAA4E;QAC5E,qBAAqB;QACrB,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5D,MAAM,aAAa,GAAmB,EAAE,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;oBACxE,SAAS;gBACX,CAAC;gBACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;gBACxD,MAAM,UAAU,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;gBAC1D,IAAI,UAAU,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACtC,aAAa,CAAC,IAAI,CAAC;wBACjB,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,GAAG,EAAE,OAAO,CAAC,GAAG;wBAChB,SAAS,EAAE,OAAO,CAAC,EAAE;wBACrB,SAAS,EAAE,IAAI;wBACf,kBAAkB,EAAE,IAAI;wBACxB,QAAQ,EAAE,CAAC,CAAC,QAAQ;qBACrB,CAAC,CAAC;gBACL,CAAC;qBAAM,IAAI,UAAU,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;oBAC1C,aAAa,CAAC,IAAI,CAAC;wBACjB,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,GAAG,EAAE,OAAO,CAAC,GAAG;wBAChB,SAAS,EAAE,OAAO,CAAC,EAAE;wBACrB,SAAS,EAAE,UAAU,CAAC,SAAS;wBAC/B,kBAAkB,EAAE,UAAU,CAAC,UAAU;wBACzC,QAAQ,EAAE,CAAC,CAAC,QAAQ;qBACrB,CAAC,CAAC;gBACL,CAAC;qBAAM,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;oBAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;gBAC3E,CAAC;qBAAM,CAAC;oBACN,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC,CAAC;gBAC9E,CAAC;gBACD,SAAS;YACX,CAAC;YAED,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;gBACxD,IAAI,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;oBACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;oBACxE,SAAS;gBACX,CAAC;gBACD,aAAa,CAAC,IAAI,CAAC;oBACjB,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,SAAS,EAAE,OAAO,CAAC,eAAe;oBAClC,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,kBAAkB,EAAE,OAAO,CAAC,GAAG;oBAC/B,QAAQ,EAAE,CAAC,CAAC,QAAQ;iBACrB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IACrE,CAAC;IAED,+EAA+E;IACvE,YAAY,CAAC,IAAqB,EAAE,QAAkC;QAC5E,MAAM,UAAU,GAAgB,EAAE,CAAC;QACnC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC5E,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;YACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,WAAW,EAAE,CAAC;gBACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBACjF,SAAS;YACX,CAAC;YACD,UAAU,CAAC,IAAI,CAAC;gBACd,IAAI,EAAE,GAAG,CAAC,SAAS;gBACnB,GAAG,EAAE,GAAG,CAAC,GAAG;gBACZ,GAAG,EAAE,GAAG,CAAC,GAAG;gBACZ,QAAQ;gBACR,UAAU,EAAE,GAAG,CAAC,UAAU;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,+EAA+E;IACvE,eAAe,CAAC,KAAqB;QAI3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoE,CAAC;QAC1F,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC;YACzD,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC;gBACnC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACzC,WAAW,IAAI,CAAC,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;oBACb,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;oBAC3C,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,eAAe,EAAE,EAAE;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACzD,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;YAC1C,OAAO,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;IACrC,CAAC;IAEO,QAAQ,CACd,UAAsC,EACtC,QAAkC,EAClC,WAAmB,EACnB,SAAkB;QAElB,OAAO;YACL,UAAU;YACV,QAAQ;YACR,OAAO,EAAE;gBACP,eAAe,EAAE,UAAU,CAAC,MAAM;gBAClC,aAAa,EAAE,QAAQ,CAAC,MAAM;gBAC9B,WAAW;gBACX,SAAS;aACV;SACF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared import-row model + CSV parsing for quick-order imports (feature 039).
|
|
3
|
+
*
|
|
4
|
+
* Both the CSV path and the Excel path normalize their input into the same
|
|
5
|
+
* `NormalizedRow[]`, which the import pipeline then validates against the
|
|
6
|
+
* catalog. Keeping the parse step pure (no DB) makes it unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
export interface NormalizedRow {
|
|
9
|
+
/** 1-based source row number (header is row 1; first data row is row 2). */
|
|
10
|
+
rowNumber: number;
|
|
11
|
+
/** Best-effort raw text of the row, shown when the row is rejected. */
|
|
12
|
+
raw: string;
|
|
13
|
+
sku: string;
|
|
14
|
+
/** Raw quantity cell, validated downstream so the reason can be precise. */
|
|
15
|
+
quantityRaw: string;
|
|
16
|
+
/** Non-sku / non-quantity columns, keyed by (lower-cased) header. */
|
|
17
|
+
attributes: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface ParseOutcome {
|
|
20
|
+
/** False when no usable `sku` + `quantity` header was found. */
|
|
21
|
+
headerOk: boolean;
|
|
22
|
+
rows: NormalizedRow[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal CSV row parser — handles double-quoted fields with embedded
|
|
26
|
+
* commas and escaped quotes (`""`). Sufficient for the quick-order use
|
|
27
|
+
* case; full RFC 4180 escapes are out of scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseCsvLine(line: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Build normalized rows from a header row + the data cell-arrays. Shared by
|
|
32
|
+
* the CSV and Excel parsers so both honour the same column semantics:
|
|
33
|
+
* `sku` + `quantity` are required (order-independent, case-insensitive);
|
|
34
|
+
* every other column becomes a variant attribute value.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rowsFromTable(header: string[], dataRows: Array<{
|
|
37
|
+
rowNumber: number;
|
|
38
|
+
raw: string;
|
|
39
|
+
cells: string[];
|
|
40
|
+
}>): ParseOutcome;
|
|
41
|
+
/** Parse a CSV blob into normalized rows. */
|
|
42
|
+
export declare function parseCsvRows(csv: string): ParseOutcome;
|
|
43
|
+
//# sourceMappingURL=import-rows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-rows.d.ts","sourceRoot":"","sources":["../../../src/backend/services/import-rows.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,aAAa,EAAE,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BnD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EAAE,EAChB,QAAQ,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GACnE,YAAY,CAmCd;AAED,6CAA6C;AAC7C,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAatD"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared import-row model + CSV parsing for quick-order imports (feature 039).
|
|
3
|
+
*
|
|
4
|
+
* Both the CSV path and the Excel path normalize their input into the same
|
|
5
|
+
* `NormalizedRow[]`, which the import pipeline then validates against the
|
|
6
|
+
* catalog. Keeping the parse step pure (no DB) makes it unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Minimal CSV row parser — handles double-quoted fields with embedded
|
|
10
|
+
* commas and escaped quotes (`""`). Sufficient for the quick-order use
|
|
11
|
+
* case; full RFC 4180 escapes are out of scope.
|
|
12
|
+
*/
|
|
13
|
+
export function parseCsvLine(line) {
|
|
14
|
+
const cells = [];
|
|
15
|
+
let current = '';
|
|
16
|
+
let inQuotes = false;
|
|
17
|
+
for (let i = 0; i < line.length; i++) {
|
|
18
|
+
const ch = line[i];
|
|
19
|
+
if (inQuotes) {
|
|
20
|
+
if (ch === '"' && line[i + 1] === '"') {
|
|
21
|
+
current += '"';
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
else if (ch === '"') {
|
|
25
|
+
inQuotes = false;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
current += ch;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (ch === ',') {
|
|
32
|
+
cells.push(current);
|
|
33
|
+
current = '';
|
|
34
|
+
}
|
|
35
|
+
else if (ch === '"' && current.length === 0) {
|
|
36
|
+
inQuotes = true;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
current += ch;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
cells.push(current);
|
|
43
|
+
return cells;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build normalized rows from a header row + the data cell-arrays. Shared by
|
|
47
|
+
* the CSV and Excel parsers so both honour the same column semantics:
|
|
48
|
+
* `sku` + `quantity` are required (order-independent, case-insensitive);
|
|
49
|
+
* every other column becomes a variant attribute value.
|
|
50
|
+
*/
|
|
51
|
+
export function rowsFromTable(header, dataRows) {
|
|
52
|
+
const normalizedHeader = header.map((h) => h.trim().toLowerCase());
|
|
53
|
+
const skuIdx = normalizedHeader.indexOf('sku');
|
|
54
|
+
const qtyIdx = normalizedHeader.indexOf('quantity');
|
|
55
|
+
if (skuIdx === -1 || qtyIdx === -1) {
|
|
56
|
+
// No usable header — surface every data row so the pipeline can reject
|
|
57
|
+
// each as `malformed_row` (preserves the line numbers for the UI).
|
|
58
|
+
return {
|
|
59
|
+
headerOk: false,
|
|
60
|
+
rows: dataRows.map(({ rowNumber, raw }) => ({
|
|
61
|
+
rowNumber,
|
|
62
|
+
raw,
|
|
63
|
+
sku: '',
|
|
64
|
+
quantityRaw: '',
|
|
65
|
+
attributes: {},
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const attributeColumns = normalizedHeader
|
|
70
|
+
.map((name, idx) => ({ name, idx }))
|
|
71
|
+
.filter(({ name, idx }) => idx !== skuIdx && idx !== qtyIdx && name.length > 0);
|
|
72
|
+
const rows = [];
|
|
73
|
+
for (const { rowNumber, raw, cells } of dataRows) {
|
|
74
|
+
const sku = (cells[skuIdx] ?? '').trim();
|
|
75
|
+
const quantityRaw = (cells[qtyIdx] ?? '').trim();
|
|
76
|
+
const attributes = {};
|
|
77
|
+
for (const { name, idx } of attributeColumns) {
|
|
78
|
+
const value = (cells[idx] ?? '').trim();
|
|
79
|
+
if (value.length > 0)
|
|
80
|
+
attributes[name] = value;
|
|
81
|
+
}
|
|
82
|
+
rows.push({ rowNumber, raw, sku, quantityRaw, attributes });
|
|
83
|
+
}
|
|
84
|
+
return { headerOk: true, rows };
|
|
85
|
+
}
|
|
86
|
+
/** Parse a CSV blob into normalized rows. */
|
|
87
|
+
export function parseCsvRows(csv) {
|
|
88
|
+
const lines = csv.replace(/\r\n?/g, '\n').split('\n');
|
|
89
|
+
if (lines.length === 0 || !lines[0]?.trim()) {
|
|
90
|
+
return { headerOk: false, rows: [] };
|
|
91
|
+
}
|
|
92
|
+
const header = parseCsvLine(lines[0] ?? '');
|
|
93
|
+
const dataRows = [];
|
|
94
|
+
for (let i = 1; i < lines.length; i++) {
|
|
95
|
+
const raw = lines[i] ?? '';
|
|
96
|
+
if (!raw.trim())
|
|
97
|
+
continue;
|
|
98
|
+
dataRows.push({ rowNumber: i + 1, raw, cells: parseCsvLine(raw) });
|
|
99
|
+
}
|
|
100
|
+
return rowsFromTable(header, dataRows);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=import-rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-rows.js","sourceRoot":"","sources":["../../../src/backend/services/import-rows.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type CartWritePort, type OrderPlacementPort, type PlacedOrderRecord, type QuickOrderOneClickEligibility } from '@endora-commerce/contracts';
|
|
2
|
+
import type { DefaultPreferenceService } from './default-preference-service.js';
|
|
3
|
+
export interface OneClickContext {
|
|
4
|
+
customerAccountId: string;
|
|
5
|
+
organizationId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OneClickPlaceInput {
|
|
8
|
+
productId: string;
|
|
9
|
+
variantId?: string | null;
|
|
10
|
+
quantity?: number;
|
|
11
|
+
idempotencyKey?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* OneClickService (feature 039, US5). Computes whether the one-click button
|
|
15
|
+
* should show, and places a one-click order by reusing the platform's
|
|
16
|
+
* established single-active-cart flow: clear the cart, add the chosen product,
|
|
17
|
+
* then run `OrderService.placeOrder` with the buyer's resolved default methods
|
|
18
|
+
* and addresses. All order validation (org active, minimum value, credit limit,
|
|
19
|
+
* stock) and the payment-routing `nextAction` come from `placeOrder` (FR-029 /
|
|
20
|
+
* FR-030); no parallel ordering path (FR-031).
|
|
21
|
+
*
|
|
22
|
+
* Both collaborators are `@endora-commerce/contracts` ports since feature 075's Phase C —
|
|
23
|
+
* `cartWritePort` and `orderPlacementPort`. The lazy `() => OrderService | null`
|
|
24
|
+
* accessor is gone with them: the port's gate answers 503 `MODULE_DISABLED`
|
|
25
|
+
* when `orders` is not present, which is the same refusal the `null` branch
|
|
26
|
+
* spelled by hand, except that a `null` accessor was indistinguishable from a
|
|
27
|
+
* wiring mistake. `placeOrder` answers `PlacedOrderRecord`, which is
|
|
28
|
+
* `OrderRecord` **plus** `nextAction` — the payment routing this reply exists
|
|
29
|
+
* to carry.
|
|
30
|
+
*
|
|
31
|
+
* **The channel is a per-call argument, not a constructor default** (issue #99,
|
|
32
|
+
* D-48). It used to be `salesChannelId: string = 'default'` — a fifth
|
|
33
|
+
* constructor parameter `backend.ts` never passed, so every deployment read the
|
|
34
|
+
* setting against a channel *code*, tripped the settings seam guard, and
|
|
35
|
+
* reported `setting_disabled` indistinguishably from an operator having
|
|
36
|
+
* switched one-click buy off. The routes read the channel the canonical
|
|
37
|
+
* resolver put on the request and hand it down; `null` means there is none, and
|
|
38
|
+
* then the setting is read platform-wide (D-41 case c) and the order records no
|
|
39
|
+
* channel rather than an invented one.
|
|
40
|
+
*/
|
|
41
|
+
export declare class OneClickService {
|
|
42
|
+
private readonly preferenceService;
|
|
43
|
+
private readonly cartWrite;
|
|
44
|
+
private readonly orderPlacement;
|
|
45
|
+
private readonly resolveOneClickEnabled;
|
|
46
|
+
constructor(preferenceService: DefaultPreferenceService, cartWrite: CartWritePort, orderPlacement: OrderPlacementPort, resolveOneClickEnabled: (salesChannelId: string | null) => Promise<boolean>);
|
|
47
|
+
eligibility(customerAccountId: string, salesChannelId: string | null): Promise<QuickOrderOneClickEligibility>;
|
|
48
|
+
place(ctx: OneClickContext, input: OneClickPlaceInput, salesChannelId: string | null): Promise<PlacedOrderRecord>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=one-click-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"one-click-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/one-click-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,6BAA6B,EACnC,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAEhF,MAAM,WAAW,eAAe;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,sBAAsB;gBAHtB,iBAAiB,EAAE,wBAAwB,EAC3C,SAAS,EAAE,aAAa,EACxB,cAAc,EAAE,kBAAkB,EAClC,sBAAsB,EAAE,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC;IAGxF,WAAW,CACf,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,OAAO,CAAC,6BAA6B,CAAC;IAgBnC,KAAK,CACT,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,kBAAkB,EACzB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,OAAO,CAAC,iBAAiB,CAAC;CAiC9B"}
|