@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/admin/api/quick-order-client.d.ts +76 -0
  4. package/dist/admin/api/quick-order-client.d.ts.map +1 -0
  5. package/dist/admin/api/quick-order-client.js +18 -0
  6. package/dist/admin/api/quick-order-client.js.map +1 -0
  7. package/dist/admin/components/DefaultPreferencesPanel.d.ts +44 -0
  8. package/dist/admin/components/DefaultPreferencesPanel.d.ts.map +1 -0
  9. package/dist/admin/components/DefaultPreferencesPanel.js +107 -0
  10. package/dist/admin/components/DefaultPreferencesPanel.js.map +1 -0
  11. package/dist/admin/index.d.ts +48 -0
  12. package/dist/admin/index.d.ts.map +1 -0
  13. package/dist/admin/index.js +120 -0
  14. package/dist/admin/index.js.map +1 -0
  15. package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts +16 -0
  16. package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts.map +1 -0
  17. package/dist/admin/pages/QuickOrderOnBehalfPage.js +85 -0
  18. package/dist/admin/pages/QuickOrderOnBehalfPage.js.map +1 -0
  19. package/dist/admin/zones/CustomerDefaults.d.ts +27 -0
  20. package/dist/admin/zones/CustomerDefaults.d.ts.map +1 -0
  21. package/dist/admin/zones/CustomerDefaults.js +11 -0
  22. package/dist/admin/zones/CustomerDefaults.js.map +1 -0
  23. package/dist/admin/zones/OrderEntryQuickTab.d.ts +21 -0
  24. package/dist/admin/zones/OrderEntryQuickTab.d.ts.map +1 -0
  25. package/dist/admin/zones/OrderEntryQuickTab.js +26 -0
  26. package/dist/admin/zones/OrderEntryQuickTab.js.map +1 -0
  27. package/dist/admin/zones/OrganizationDefaults.d.ts +30 -0
  28. package/dist/admin/zones/OrganizationDefaults.d.ts.map +1 -0
  29. package/dist/admin/zones/OrganizationDefaults.js +11 -0
  30. package/dist/admin/zones/OrganizationDefaults.js.map +1 -0
  31. package/dist/backend/entities/quick-order-default-preference.entity.d.ts +22 -0
  32. package/dist/backend/entities/quick-order-default-preference.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/quick-order-default-preference.entity.js +76 -0
  34. package/dist/backend/entities/quick-order-default-preference.entity.js.map +1 -0
  35. package/dist/backend/index.d.ts +88 -0
  36. package/dist/backend/index.d.ts.map +1 -0
  37. package/dist/backend/index.js +235 -0
  38. package/dist/backend/index.js.map +1 -0
  39. package/dist/backend/routes.admin.d.ts +35 -0
  40. package/dist/backend/routes.admin.d.ts.map +1 -0
  41. package/dist/backend/routes.admin.js +64 -0
  42. package/dist/backend/routes.admin.js.map +1 -0
  43. package/dist/backend/routes.d.ts +35 -0
  44. package/dist/backend/routes.d.ts.map +1 -0
  45. package/dist/backend/routes.js +77 -0
  46. package/dist/backend/routes.js.map +1 -0
  47. package/dist/backend/routes.one-click.d.ts +22 -0
  48. package/dist/backend/routes.one-click.d.ts.map +1 -0
  49. package/dist/backend/routes.one-click.js +35 -0
  50. package/dist/backend/routes.one-click.js.map +1 -0
  51. package/dist/backend/routes.preferences.admin.d.ts +23 -0
  52. package/dist/backend/routes.preferences.admin.d.ts.map +1 -0
  53. package/dist/backend/routes.preferences.admin.js +40 -0
  54. package/dist/backend/routes.preferences.admin.js.map +1 -0
  55. package/dist/backend/routes.preferences.d.ts +21 -0
  56. package/dist/backend/routes.preferences.d.ts.map +1 -0
  57. package/dist/backend/routes.preferences.js +42 -0
  58. package/dist/backend/routes.preferences.js.map +1 -0
  59. package/dist/backend/services/catalog-lookup.d.ts +39 -0
  60. package/dist/backend/services/catalog-lookup.d.ts.map +1 -0
  61. package/dist/backend/services/catalog-lookup.js +104 -0
  62. package/dist/backend/services/catalog-lookup.js.map +1 -0
  63. package/dist/backend/services/default-preference-authz.d.ts +31 -0
  64. package/dist/backend/services/default-preference-authz.d.ts.map +1 -0
  65. package/dist/backend/services/default-preference-authz.js +26 -0
  66. package/dist/backend/services/default-preference-authz.js.map +1 -0
  67. package/dist/backend/services/default-preference-resolver.d.ts +28 -0
  68. package/dist/backend/services/default-preference-resolver.d.ts.map +1 -0
  69. package/dist/backend/services/default-preference-resolver.js +27 -0
  70. package/dist/backend/services/default-preference-resolver.js.map +1 -0
  71. package/dist/backend/services/default-preference-service.d.ts +63 -0
  72. package/dist/backend/services/default-preference-service.d.ts.map +1 -0
  73. package/dist/backend/services/default-preference-service.js +176 -0
  74. package/dist/backend/services/default-preference-service.js.map +1 -0
  75. package/dist/backend/services/excel-importer.d.ts +11 -0
  76. package/dist/backend/services/excel-importer.d.ts.map +1 -0
  77. package/dist/backend/services/excel-importer.js +47 -0
  78. package/dist/backend/services/excel-importer.js.map +1 -0
  79. package/dist/backend/services/import-from-request.d.ts +9 -0
  80. package/dist/backend/services/import-from-request.d.ts.map +1 -0
  81. package/dist/backend/services/import-from-request.js +17 -0
  82. package/dist/backend/services/import-from-request.js.map +1 -0
  83. package/dist/backend/services/import-pipeline.d.ts +89 -0
  84. package/dist/backend/services/import-pipeline.d.ts.map +1 -0
  85. package/dist/backend/services/import-pipeline.js +188 -0
  86. package/dist/backend/services/import-pipeline.js.map +1 -0
  87. package/dist/backend/services/import-rows.d.ts +43 -0
  88. package/dist/backend/services/import-rows.d.ts.map +1 -0
  89. package/dist/backend/services/import-rows.js +102 -0
  90. package/dist/backend/services/import-rows.js.map +1 -0
  91. package/dist/backend/services/one-click-service.d.ts +50 -0
  92. package/dist/backend/services/one-click-service.d.ts.map +1 -0
  93. package/dist/backend/services/one-click-service.js +83 -0
  94. package/dist/backend/services/one-click-service.js.map +1 -0
  95. package/dist/backend/services/quick-order-build-service.d.ts +35 -0
  96. package/dist/backend/services/quick-order-build-service.d.ts.map +1 -0
  97. package/dist/backend/services/quick-order-build-service.js +61 -0
  98. package/dist/backend/services/quick-order-build-service.js.map +1 -0
  99. package/dist/backend/services/variant-resolver.d.ts +40 -0
  100. package/dist/backend/services/variant-resolver.d.ts.map +1 -0
  101. package/dist/backend/services/variant-resolver.js +47 -0
  102. package/dist/backend/services/variant-resolver.js.map +1 -0
  103. package/dist/manifest.d.ts +190 -0
  104. package/dist/manifest.d.ts.map +1 -0
  105. package/dist/manifest.js +129 -0
  106. package/dist/manifest.js.map +1 -0
  107. package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts +15 -0
  108. package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts.map +1 -0
  109. package/dist/migrations/20260611T140359_quick_order_default_preferences.js +53 -0
  110. package/dist/migrations/20260611T140359_quick_order_default_preferences.js.map +1 -0
  111. package/dist/migrations/index.d.ts +27 -0
  112. package/dist/migrations/index.d.ts.map +1 -0
  113. package/dist/migrations/index.js +29 -0
  114. package/dist/migrations/index.js.map +1 -0
  115. package/docs/quick_order.md +98 -0
  116. package/i18n/en.json +47 -0
  117. package/i18n/pl.json +47 -0
  118. package/package.json +91 -0
  119. 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":"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"}
@@ -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"}