@sixtyninecommerce/admin 0.11.0 → 0.12.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 (147) hide show
  1. package/README.md +1501 -23
  2. package/dist/auth/index.d.ts +7 -1
  3. package/dist/auth/index.js +8 -1
  4. package/dist/catalog/documents.d.ts +142 -0
  5. package/dist/catalog/documents.js +114 -0
  6. package/dist/catalog/index.d.ts +48 -7
  7. package/dist/catalog/index.js +6 -1
  8. package/dist/catalog/metadata.d.ts +244 -0
  9. package/dist/catalog/metadata.js +220 -0
  10. package/dist/catalog/properties.d.ts +38 -0
  11. package/dist/catalog/properties.js +52 -1
  12. package/dist/catalog/recommendations.d.ts +33 -6
  13. package/dist/catalog/recommendations.js +94 -2
  14. package/dist/catalog/reviews.d.ts +17 -4
  15. package/dist/catalog/reviews.js +7 -0
  16. package/dist/catalog/variants.d.ts +60 -0
  17. package/dist/catalog/variants.js +39 -0
  18. package/dist/catalog/visibility.d.ts +18 -2
  19. package/dist/client.d.ts +29 -3
  20. package/dist/client.js +19 -8
  21. package/dist/content/assets.d.ts +3 -4
  22. package/dist/content/forms.d.ts +332 -1
  23. package/dist/content/forms.js +140 -4
  24. package/dist/content/items.d.ts +31 -8
  25. package/dist/content/items.js +1 -1
  26. package/dist/content/menus.d.ts +254 -0
  27. package/dist/content/menus.js +70 -0
  28. package/dist/content/strings.d.ts +137 -0
  29. package/dist/content/strings.js +82 -0
  30. package/dist/customers/delivery-terms.d.ts +44 -0
  31. package/dist/customers/delivery-terms.js +32 -0
  32. package/dist/customers/index.d.ts +15 -0
  33. package/dist/customers/index.js +4 -0
  34. package/dist/documents/emails.d.ts +150 -0
  35. package/dist/documents/emails.js +220 -0
  36. package/dist/documents/index.d.ts +13 -0
  37. package/dist/documents/index.js +2 -0
  38. package/dist/fulfillments/index.d.ts +119 -1
  39. package/dist/fulfillments/index.js +124 -4
  40. package/dist/index.d.ts +43 -23
  41. package/dist/index.js +11 -5
  42. package/dist/integrations/exact-online-types.d.ts +542 -0
  43. package/dist/integrations/exact-online.d.ts +541 -5
  44. package/dist/integrations/exact-online.js +714 -1
  45. package/dist/integrations/index.d.ts +23 -8
  46. package/dist/integrations/index.js +9 -4
  47. package/dist/integrations/pipeline-discoveries.d.ts +65 -0
  48. package/dist/integrations/pipeline-discoveries.js +65 -0
  49. package/dist/integrations/pipeline-runs.d.ts +119 -0
  50. package/dist/integrations/pipeline-runs.js +87 -0
  51. package/dist/integrations/pipelines.d.ts +73 -2
  52. package/dist/integrations/pipelines.js +36 -1
  53. package/dist/integrations/price-import.d.ts +223 -0
  54. package/dist/integrations/price-import.js +124 -0
  55. package/dist/integrations/types.d.ts +221 -3
  56. package/dist/integrations/types.js +4 -3
  57. package/dist/meta/index.d.ts +10 -0
  58. package/dist/metafields/index.d.ts +8 -1
  59. package/dist/metafields/index.js +10 -0
  60. package/dist/orders/cancel-with-money.d.ts +10 -0
  61. package/dist/orders/cancel-with-money.js +2 -0
  62. package/dist/orders/commit-edit.d.ts +16 -0
  63. package/dist/orders/commit-edit.js +14 -1
  64. package/dist/orders/deposit-policy.d.ts +3 -1
  65. package/dist/orders/index.d.ts +22 -11
  66. package/dist/orders/index.js +5 -2
  67. package/dist/orders/list.d.ts +26 -5
  68. package/dist/orders/list.js +123 -13
  69. package/dist/orders/manual-discounts.d.ts +13 -5
  70. package/dist/orders/money-view.d.ts +149 -0
  71. package/dist/orders/money-view.js +99 -0
  72. package/dist/orders/plan-edit.d.ts +34 -2
  73. package/dist/orders/plan-edit.js +110 -12
  74. package/dist/orders/surcharges.d.ts +12 -0
  75. package/dist/orders/surcharges.js +34 -9
  76. package/dist/payments/account-credit.d.ts +17 -1
  77. package/dist/payments/account-credit.js +25 -2
  78. package/dist/payments/credit-noted-fields.d.ts +8 -1
  79. package/dist/payments/credit-noted-fields.js +34 -2
  80. package/dist/payments/customer-consent.d.ts +30 -0
  81. package/dist/payments/customer-consent.js +49 -0
  82. package/dist/payments/index.d.ts +89 -14
  83. package/dist/payments/index.js +22 -2
  84. package/dist/payments/requests.d.ts +6 -3
  85. package/dist/payments/requests.js +1 -0
  86. package/dist/pickups/index.d.ts +5 -3
  87. package/dist/pickups/index.js +5 -2
  88. package/dist/platform/email-gateways.d.ts +165 -0
  89. package/dist/platform/email-gateways.js +78 -0
  90. package/dist/platform/tax.d.ts +11 -3
  91. package/dist/platform/tax.js +30 -0
  92. package/dist/pos/exchanges.d.ts +6 -1
  93. package/dist/pos/index.d.ts +229 -10
  94. package/dist/pos/index.js +113 -4
  95. package/dist/pos/offline-sales.d.ts +44 -3
  96. package/dist/pos/offline-sales.js +57 -3
  97. package/dist/pos/returns.d.ts +28 -1
  98. package/dist/pos/returns.js +36 -6
  99. package/dist/pricing/price-lists.d.ts +3 -1
  100. package/dist/purchasing/alerts.d.ts +19 -4
  101. package/dist/purchasing/alerts.js +29 -3
  102. package/dist/purchasing/backorders.d.ts +61 -0
  103. package/dist/purchasing/backorders.js +92 -0
  104. package/dist/purchasing/index.d.ts +21 -3
  105. package/dist/purchasing/index.js +8 -0
  106. package/dist/purchasing/links.d.ts +263 -0
  107. package/dist/purchasing/links.js +192 -0
  108. package/dist/purchasing/purchase-orders.d.ts +4 -1
  109. package/dist/purchasing/receipts.d.ts +396 -0
  110. package/dist/purchasing/receipts.js +287 -0
  111. package/dist/purchasing/returns.d.ts +273 -0
  112. package/dist/purchasing/returns.js +229 -0
  113. package/dist/quotes/index.d.ts +20 -3
  114. package/dist/returns/index.d.ts +62 -1
  115. package/dist/returns/index.js +61 -0
  116. package/dist/seo/config.d.ts +128 -0
  117. package/dist/seo/config.js +95 -0
  118. package/dist/seo/redirects.d.ts +125 -0
  119. package/dist/seo/redirects.js +60 -1
  120. package/dist/seo/settings.d.ts +147 -0
  121. package/dist/seo/settings.js +209 -0
  122. package/dist/shipping/index.d.ts +6 -0
  123. package/dist/shipping/index.js +2 -0
  124. package/dist/shipping/methods.d.ts +8 -1
  125. package/dist/shipping/myparcel.d.ts +415 -1
  126. package/dist/shipping/myparcel.js +291 -3
  127. package/dist/shipping/rate-tables.d.ts +149 -4
  128. package/dist/shipping/rate-tables.js +229 -23
  129. package/dist/shipping/ship-to-countries.d.ts +75 -0
  130. package/dist/shipping/ship-to-countries.js +47 -0
  131. package/dist/stock/by-location.d.ts +86 -0
  132. package/dist/stock/by-location.js +94 -0
  133. package/dist/stock/index.d.ts +8 -1
  134. package/dist/stock/index.js +4 -0
  135. package/dist/stock/settings.d.ts +106 -0
  136. package/dist/stock/settings.js +103 -0
  137. package/dist/stock/transfers.d.ts +171 -10
  138. package/dist/stock/transfers.js +82 -4
  139. package/dist/stored-value/index.d.ts +98 -7
  140. package/dist/stored-value/index.js +71 -4
  141. package/dist/tax/display-policy.d.ts +105 -0
  142. package/dist/tax/display-policy.js +84 -0
  143. package/dist/tax/index.d.ts +3 -0
  144. package/dist/tax/index.js +2 -0
  145. package/dist/tenant/email-templates.d.ts +222 -1
  146. package/dist/tenant/email-templates.js +227 -10
  147. package/package.json +2 -2
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The bulk price import (headless spec §9.2 `integrations`: "price import, AI conversion"): the
3
+ * plugins service's `POST /prices/import`, and its AI conversion of any supplier file into a
4
+ * document that import accepts (`POST /prices/import/ai/conversions`, then the
5
+ * `price-import-ai-conversion` progress stream).
6
+ *
7
+ * A thin wrapper over the routes the dashboard has always used (ruling G119): the same documents,
8
+ * the same tax handling, the same dry run and row selection. A dry run and an import are two
9
+ * methods because an assistant asks before an import and never before a dry run (G81).
10
+ */
11
+ import { type PlatformError, type PriceImportAiConversionResult, type PriceImportAiConversionStep, type Transport } from '@sixtyninecommerce/core';
12
+ import type { Call } from '../types.js';
13
+ /** What the plugins service reads a price document as. */
14
+ export type PriceImportFormat = 'csv' | 'json' | 'xml';
15
+ /** Whether the amounts in a document include tax. */
16
+ export type PriceImportTaxMode = 'inclusive' | 'exclusive';
17
+ /** A price document, as its text. */
18
+ export interface PriceImportDocument {
19
+ format: PriceImportFormat;
20
+ /** The whole document, at most 6,000,000 characters and 5,000 data rows. */
21
+ content: string;
22
+ /**
23
+ * Whether the document's amounts include tax. Left out, they are read in the store's own tax
24
+ * mode and nothing is converted. Given and different from the store's, each amount is converted
25
+ * at its variant's tax rate (`taxConverted` on the row).
26
+ */
27
+ fileTaxMode?: PriceImportTaxMode;
28
+ }
29
+ export interface PriceImportRunInput extends PriceImportDocument {
30
+ /**
31
+ * The rows to write, by `rowNumber` as {@link PriceImportModule.preview} answered them. Rows
32
+ * left out answer `skipped`, so `[]` writes nothing. Left out entirely, every row the preview
33
+ * marks `updated` is written (never a `duplicate`). At most 5,000.
34
+ */
35
+ selectedRows?: readonly number[];
36
+ }
37
+ /**
38
+ * One row's outcome.
39
+ * - `updated`: a variant matched and its price differs; written by {@link PriceImportModule.run}.
40
+ * - `unchanged`: the price is already the stored one.
41
+ * - `not_found`: no variant of the store has this SKU.
42
+ * - `duplicate`: an earlier row sets the same SKU and currency (`duplicateOfRow`); a preview only.
43
+ * A selected duplicate that is written answers `updated`.
44
+ * - `invalid`: the row's content failed validation (its `message` says why); never written.
45
+ * - `skipped`: an import only: a row left out of `selectedRows`.
46
+ * - `error`: the row cannot be written (an ambiguous SKU, a tax rate that cannot be resolved, a
47
+ * Price field group an app or API token owns) or its write failed; `message` says why.
48
+ */
49
+ export type PriceImportRowStatus = 'updated' | 'unchanged' | 'not_found' | 'duplicate' | 'invalid' | 'skipped' | 'error';
50
+ export interface PriceImportRow {
51
+ /**
52
+ * Where the row is in the document: its line in a CSV (the header is line 1, so the first data
53
+ * row is 2; blank lines count), its position among the entries of a JSON or XML document (from
54
+ * 1). `selectedRows` names rows by it.
55
+ */
56
+ rowNumber: number;
57
+ /** Empty when the row carried no SKU that could be read (only on `invalid`). */
58
+ sku: string;
59
+ /** The row's currency, or the store's default when the document named none. */
60
+ currency: string;
61
+ status: PriceImportRowStatus;
62
+ /** The amount as the document states it, in major units. */
63
+ amount: number;
64
+ /** The stored price in minor units, in the store's tax mode; null when no variant matched. */
65
+ oldPrice: number | null;
66
+ /** The price in minor units, in the store's tax mode, that is or would be written. */
67
+ newPrice: number | null;
68
+ /** True when the document's tax mode differs from the store's and the amount was converted. */
69
+ taxConverted: boolean;
70
+ /** The earlier row with the same SKU and currency, or null. */
71
+ duplicateOfRow: number | null;
72
+ message?: string;
73
+ }
74
+ export interface PriceImportSummary {
75
+ /** Data rows read from the document. */
76
+ total: number;
77
+ updated: number;
78
+ unchanged: number;
79
+ notFound: number;
80
+ duplicate: number;
81
+ invalid: number;
82
+ skipped: number;
83
+ errored: number;
84
+ }
85
+ /** What a dry run or an import answers: per row, and summed. */
86
+ export interface PriceImportReport {
87
+ dryRun: boolean;
88
+ /** Whether the store's prices include tax: the basis of `oldPrice` and `newPrice`. */
89
+ channelPricesIncludeTax: boolean;
90
+ channelDefaultCurrency: string;
91
+ /** The basis the amounts were read in: the one sent, else the store's own. */
92
+ fileTaxMode: PriceImportTaxMode;
93
+ summary: PriceImportSummary;
94
+ results: PriceImportRow[];
95
+ }
96
+ /** A file handed to the AI conversion. */
97
+ export interface PriceImportConvertInput {
98
+ /**
99
+ * The supplier's file: `.xlsx`, `.xlsm`, `.csv`, `.tsv`, `.txt`, `.json`, `.xml`, `.md`, `.pdf`
100
+ * or `.docx`, at most 20 MB (a workbook 8 MB). The service reads the format from the name.
101
+ */
102
+ file: Blob;
103
+ /** The file's name. Defaults to a `File`'s own; a bare `Blob` needs one. */
104
+ fileName?: string;
105
+ }
106
+ /** The upload's receipt: follow the conversion with its id. */
107
+ export interface PriceImportUpload {
108
+ conversionId: string;
109
+ /** The name the service stored, without any path. */
110
+ fileName: string;
111
+ }
112
+ /** The converted document, with what the converter read. `document` goes to `preview` as `json`. */
113
+ export type PriceImportConversion = PriceImportAiConversionResult;
114
+ export type PriceImportConversionStep = PriceImportAiConversionStep;
115
+ /** One progress line of a conversion. */
116
+ export interface PriceImportConversionProgress {
117
+ step: PriceImportConversionStep;
118
+ message: string;
119
+ }
120
+ export interface PriceImportFollowOptions {
121
+ /** Aborting it closes the stream; `follow` then throws `REQUEST_ABORTED`. */
122
+ signal?: AbortSignal;
123
+ /** Called with each progress line, in order. A callback that throws ends `follow` with its error. */
124
+ onProgress?: (progress: PriceImportConversionProgress) => void;
125
+ /** Give up after this long without a byte, keepalives included. Default 45000 ms. */
126
+ idleTimeoutMs?: number;
127
+ /** Deadline for the stream's response headers. Defaults to the client's own timeout. */
128
+ connectTimeoutMs?: number;
129
+ }
130
+ /**
131
+ * The store's bulk price import, and the AI conversion that turns any supplier file into a
132
+ * document it accepts.
133
+ *
134
+ * - **Every method needs `UpdateCatalog` or `UpdateProduct`** on the store (either is enough,
135
+ * so no method carries a `@permission` tag), and acts on the
136
+ * client's store (`admin.withTenant(...)` for another one). The prices are written through the
137
+ * Admin API with the caller's own credential, so the engine's rules for a price write apply
138
+ * to each row.
139
+ * - **A document the service cannot read at all is refused whole, and nothing is written:** a
140
+ * `BusinessRuleError` (422) coded `PRICE_IMPORT_FORMAT_ERROR` whose `details[]` name each
141
+ * problem (`field` is `header`, `document` or `row N`; at most 100 entries, the last one coded
142
+ * `TRUNCATED` and counting the rest when there are more). More than 5,000 data rows is one of
143
+ * those problems (`TOO_MANY_ROWS`). A row whose CONTENT is wrong is not a refusal: it comes
144
+ * back `invalid`.
145
+ * - **The service records an `idempotencyKey` and acts on nothing**: two calls are two imports.
146
+ */
147
+ export interface PriceImportModule {
148
+ /**
149
+ * Read a document and say, row by row, what an import would do. Nothing is written.
150
+ *
151
+ * Refused: `PRICE_IMPORT_FORMAT_ERROR` (422, see the module), `VALIDATION_FAILED` (400) for a
152
+ * body the service does not accept (an unknown format, an empty document).
153
+ *
154
+ * @effect read
155
+ */
156
+ preview(document: PriceImportDocument, options?: Call): Promise<PriceImportReport>;
157
+ /**
158
+ * Write the document's prices: the rows `selectedRows` names, or every `updated` row when it
159
+ * names none. Rows are written per variant as they come, so a row that fails (`error`) does
160
+ * not undo the others; the report says which were written. A selection naming two rows that
161
+ * set the same SKU and currency is refused whole, `PRICE_IMPORT_SELECTION_CONFLICT` (422, one
162
+ * `details[]` entry per pair), and nothing is written.
163
+ *
164
+ * The prices are what customers pay from the moment each is written, and the import cannot be
165
+ * undone except by importing the old prices again (each row's `oldPrice`). A timeout or a
166
+ * dropped connection does not say whether the prices were written: run {@link preview} with
167
+ * the same document, where rows already at their new price read `unchanged`, before repeating.
168
+ *
169
+ * @effect irreversible
170
+ */
171
+ run(input: PriceImportRunInput, options?: Call): Promise<PriceImportReport>;
172
+ /**
173
+ * Upload a supplier file for the AI conversion and answer the id to {@link follow}. Nothing is
174
+ * converted until it is followed, and nothing is ever written: the converted document goes to
175
+ * {@link preview} and {@link run} like any other. The upload is kept for a few minutes and
176
+ * read once.
177
+ *
178
+ * Refused before sending: a file with no name (`PRICE_IMPORT_FILE_NAME_REQUIRED`, 400). By the
179
+ * service: a file with no extension, a legacy `.xls`, another unsupported type, an empty file
180
+ * or a workbook over 8 MB (400, the sentence says which and what to do); a file over 20 MB
181
+ * (413); too many uploads waiting, on the service (`AI_CONVERSION_CAPACITY_REACHED`) or for
182
+ * this store (`AI_CONVERSION_CHANNEL_BACKLOG`), both 503 and worth repeating later. Never
183
+ * retried by the client: a repeat stores the file twice.
184
+ *
185
+ * @effect write
186
+ */
187
+ convert(input: PriceImportConvertInput, options?: Call): Promise<PriceImportUpload>;
188
+ /**
189
+ * Run the conversion of an upload and follow it to its end: progress lines through
190
+ * `onProgress`, then the converted document. The service sends the file's text to the
191
+ * platform's AI provider while this runs (30 seconds to 3 minutes). Abort it to stop the
192
+ * conversion. It acts on the client's store: an upload of another store answers
193
+ * `AI_IMPORT_NOT_FOUND`.
194
+ *
195
+ * The conversion's own refusal is thrown with its code:
196
+ * - `AI_IMPORT_NOT_FOUND` (`NotFoundError`): the upload expired, was already followed, or is
197
+ * another store's;
198
+ * - `AI_IMPORT_UNSUPPORTED_FILE`, `AI_IMPORT_EXTRACTION_FAILED` (a file with no text: a scan,
199
+ * a protected workbook), `AI_IMPORT_NO_PRICES_FOUND`, `AI_IMPORT_TOO_MANY_ROWS`
200
+ * (`BusinessRuleError`);
201
+ * - `AI_IMPORT_BUSY` (`RateLimitedError`): the service is converting as many files as it can;
202
+ * - `AI_IMPORT_NOT_CONFIGURED` (`UpstreamError`, 503): this deployment has no AI provider;
203
+ * - `AI_IMPORT_LLM_FAILED`, `AI_IMPORT_STREAM_FAILED` and any code this client does not know
204
+ * (`UpstreamError`, 502).
205
+ *
206
+ * The stream itself can fail with `PROGRESS_STREAM_CLOSED`, `PROGRESS_STREAM_STALLED`,
207
+ * `MALFORMED_RESPONSE` or `REQUEST_ABORTED`; an id that is not a conversion id is refused
208
+ * before anything is sent (`PROGRESS_STREAM_ID_INVALID`, 400). Nothing here is retryable: the
209
+ * upload is gone once its conversion ends, so convert the file again.
210
+ *
211
+ * @effect read
212
+ */
213
+ follow(conversionId: string, options?: PriceImportFollowOptions): Promise<PriceImportConversion>;
214
+ }
215
+ /** A conversion's terminal `error` event as the error {@link PriceImportModule.follow} throws. */
216
+ export declare function conversionRefusal(code: string, message: string): PlatformError;
217
+ /**
218
+ * The deadlines `preview` and `run` give the service, longer than the client's default: a
219
+ * 5,000-row import is measured in tens of seconds (the dashboard has always allowed these).
220
+ */
221
+ export declare const PRICE_IMPORT_PREVIEW_TIMEOUT_MS = 30000;
222
+ export declare const PRICE_IMPORT_RUN_TIMEOUT_MS = 120000;
223
+ export declare function createPriceImportModule(transport: Transport): PriceImportModule;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The bulk price import (headless spec §9.2 `integrations`: "price import, AI conversion"): the
3
+ * plugins service's `POST /prices/import`, and its AI conversion of any supplier file into a
4
+ * document that import accepts (`POST /prices/import/ai/conversions`, then the
5
+ * `price-import-ai-conversion` progress stream).
6
+ *
7
+ * A thin wrapper over the routes the dashboard has always used (ruling G119): the same documents,
8
+ * the same tax handling, the same dry run and row selection. A dry run and an import are two
9
+ * methods because an assistant asks before an import and never before a dry run (G81).
10
+ */
11
+ import { errorForStatus, openProgressStream, UpstreamError, ValidationError, } from '@sixtyninecommerce/core';
12
+ /** The HTTP status each conversion refusal stands for, which picks its error class. */
13
+ const CONVERSION_REFUSAL_STATUS = {
14
+ AI_IMPORT_NOT_FOUND: 404,
15
+ AI_IMPORT_UNSUPPORTED_FILE: 422,
16
+ AI_IMPORT_EXTRACTION_FAILED: 422,
17
+ AI_IMPORT_NO_PRICES_FOUND: 422,
18
+ AI_IMPORT_TOO_MANY_ROWS: 422,
19
+ AI_IMPORT_BUSY: 429,
20
+ AI_IMPORT_NOT_CONFIGURED: 503,
21
+ AI_IMPORT_LLM_FAILED: 502,
22
+ AI_IMPORT_STREAM_FAILED: 502,
23
+ };
24
+ /** A conversion's terminal `error` event as the error {@link PriceImportModule.follow} throws. */
25
+ export function conversionRefusal(code, message) {
26
+ const status = Object.hasOwn(CONVERSION_REFUSAL_STATUS, code)
27
+ ? CONVERSION_REFUSAL_STATUS[code]
28
+ : 502;
29
+ // Never retryable: the service deletes the upload when its stream ends, so the same `follow`
30
+ // can only answer AI_IMPORT_NOT_FOUND. The remedy is a fresh `convert`.
31
+ return errorForStatus(status, { code, message, service: 'plugins', retryable: false });
32
+ }
33
+ /**
34
+ * The deadlines `preview` and `run` give the service, longer than the client's default: a
35
+ * 5,000-row import is measured in tens of seconds (the dashboard has always allowed these).
36
+ */
37
+ export const PRICE_IMPORT_PREVIEW_TIMEOUT_MS = 30_000;
38
+ export const PRICE_IMPORT_RUN_TIMEOUT_MS = 120_000;
39
+ function nameOf(file) {
40
+ const name = file.name;
41
+ return typeof name === 'string' ? name : undefined;
42
+ }
43
+ function readUpload(answer) {
44
+ const record = answer !== null && typeof answer === 'object' ? answer : {};
45
+ const { conversionId, fileName } = record;
46
+ if (typeof conversionId !== 'string' || conversionId.trim() === '') {
47
+ // The file is stored; only its ticket is missing. Repeating would store it again.
48
+ throw new UpstreamError({
49
+ code: 'MALFORMED_RESPONSE',
50
+ message: 'The price import upload answered without a conversion id.',
51
+ status: 502,
52
+ service: 'plugins',
53
+ retryable: false,
54
+ });
55
+ }
56
+ return { conversionId, fileName: typeof fileName === 'string' ? fileName : '' };
57
+ }
58
+ export function createPriceImportModule(transport) {
59
+ const importDocument = (body, timeoutMs, options) => {
60
+ const callOptions = { ...options, timeoutMs };
61
+ return transport.rest('plugins', { method: 'POST', path: '/prices/import', body }, callOptions);
62
+ };
63
+ const documentBody = (document) => ({
64
+ format: document.format,
65
+ content: document.content,
66
+ ...(document.fileTaxMode === undefined ? {} : { fileTaxMode: document.fileTaxMode }),
67
+ });
68
+ return {
69
+ preview(document, options) {
70
+ return importDocument({ ...documentBody(document), dryRun: true }, PRICE_IMPORT_PREVIEW_TIMEOUT_MS, options);
71
+ },
72
+ run(input, options) {
73
+ return importDocument({
74
+ ...documentBody(input),
75
+ dryRun: false,
76
+ // `[]` and "none named" differ: the first writes nothing, the second every changed row.
77
+ ...(input.selectedRows === undefined ? {} : { selectedRows: [...input.selectedRows] }),
78
+ }, PRICE_IMPORT_RUN_TIMEOUT_MS, options);
79
+ },
80
+ async convert(input, options) {
81
+ const fileName = input.fileName ?? nameOf(input.file);
82
+ if (fileName === undefined || fileName.trim() === '') {
83
+ throw new ValidationError({
84
+ code: 'PRICE_IMPORT_FILE_NAME_REQUIRED',
85
+ message: 'A file needs a name: pass fileName, or a File that carries one. The service reads the format from its extension. Nothing was sent.',
86
+ status: 400,
87
+ service: 'plugins',
88
+ });
89
+ }
90
+ const answer = await transport.rest('plugins', {
91
+ method: 'POST',
92
+ path: '/prices/import/ai/conversions',
93
+ multipart: { files: [{ field: 'file', file: input.file, fileName }] },
94
+ }, options);
95
+ return readUpload(answer);
96
+ },
97
+ async follow(conversionId, options = {}) {
98
+ const stream = openProgressStream(transport, { kind: 'price-import-ai-conversion', id: conversionId }, {
99
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
100
+ ...(options.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: options.idleTimeoutMs }),
101
+ ...(options.connectTimeoutMs === undefined
102
+ ? {}
103
+ : { connectTimeoutMs: options.connectTimeoutMs }),
104
+ });
105
+ for await (const event of stream) {
106
+ if (event.type === 'progress') {
107
+ options.onProgress?.({ step: event.step, message: event.message });
108
+ continue;
109
+ }
110
+ if (event.type === 'error')
111
+ throw conversionRefusal(event.code, event.message);
112
+ return event.result;
113
+ }
114
+ // The stream ends only after a terminal event, or throws; this is the contract's backstop.
115
+ throw new UpstreamError({
116
+ code: 'PROGRESS_STREAM_CLOSED',
117
+ message: 'The price import conversion ended without an answer; convert the file again.',
118
+ status: 502,
119
+ service: 'plugins',
120
+ retryable: false,
121
+ });
122
+ },
123
+ };
124
+ }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * The shapes of `admin.integrations.pipelines` (headless spec §7 `pipelines.drafts`).
2
+ * The shapes of `admin.integrations.pipelines` (headless spec §7 `pipelines.drafts`, §9.2
3
+ * `integrations`).
3
4
  *
4
5
  * Kept verbatim (stored data, never renamed): `triggerFilter`'s keys (reader field names such as
5
- * `toState`), `siteAnalysis` and `publicCredentials` (the connector's own keys), and a schema
6
- * map's `sourceSchema`, `targetSchema` and `subCounts`.
6
+ * `toState`), a pipeline's `config`, `siteAnalysis` and `publicCredentials` (the connector's own
7
+ * keys), and a schema map's `sourceSchema`, `targetSchema` and `subCounts`.
7
8
  */
9
+ import type { ProgressStreamOptions } from '@sixtyninecommerce/core';
10
+ import type { ListOptions } from '../types.js';
8
11
  export type PipelineDirection = 'import' | 'export';
9
12
  /** The wizard's steps before a commit; `import`/`done` come after one, when no draft exists. */
10
13
  export type PipelineDraftStep = 'direction' | 'connector' | 'credentials' | 'discovery' | 'review' | 'entities' | 'trigger' | 'configure' | 'preview';
@@ -320,3 +323,218 @@ export interface DetectPipelineConnectionConfigInput {
320
323
  connectorType: string;
321
324
  credentials: Record<string, string>;
322
325
  }
326
+ /** The connection a pipeline runs over, as a pipeline read embeds it. Never a credential. */
327
+ export interface PipelineConnectionSummary {
328
+ id: string;
329
+ connectorType: string;
330
+ name: string;
331
+ sourceUrl: string;
332
+ /** As of the connection's last probe. */
333
+ status: PipelineConnectionStatus;
334
+ /** `null`: no throttle. */
335
+ requestDelayMs: number | null;
336
+ /** Old hostnames whose image URLs an import rewrites to `sourceUrl`'s host. */
337
+ legacyAssetHosts: string[] | null;
338
+ lastTestedAt: string | null;
339
+ /**
340
+ * Set when the connection was deleted while this pipeline still ran over it (possible only
341
+ * before deleting a used connection was refused): the pipeline keeps running over it.
342
+ */
343
+ removedAt: string | null;
344
+ }
345
+ /**
346
+ * An export pipeline's settings: what it exports, and how a scheduled or "Run now" read is
347
+ * narrowed.
348
+ */
349
+ export interface ExportPipelineConfig {
350
+ /** `orders`: the entity types this deployment can export (`connectors()`'s `supportedExportEntityTypes`). */
351
+ entityTypes: string[];
352
+ /**
353
+ * Keyed by the reader's own field names (`toState`), kept verbatim. The keys and values a
354
+ * filter may name are one shared list (the orders reader's: `toState` and the order states); a
355
+ * save naming anything else is refused (`PIPELINE_EXPORT_FILTER_*`).
356
+ */
357
+ triggerFilter?: Record<string, string>;
358
+ }
359
+ /** An import pipeline's settings. */
360
+ export interface ImportPipelineConfig {
361
+ /** Where a stock import writes its levels: one of the store's stock locations. */
362
+ stockLocationId?: string;
363
+ /** Import the source's Yoast SEO fields (WooCommerce); `true` unless set. */
364
+ importYoastSeo?: boolean;
365
+ /** The language the source writes its names in; the store's default unless set. */
366
+ sourceLanguageCode?: string;
367
+ }
368
+ /**
369
+ * A data pipeline of the store: an import from, or an export to, another system over one of its
370
+ * connections, run by hand or on a schedule.
371
+ */
372
+ export interface Pipeline {
373
+ id: string;
374
+ name: string;
375
+ direction: PipelineDirection;
376
+ connectionId: string;
377
+ /** The discovery whose schema maps an import reads; `null` for an export. */
378
+ discoveryId: string | null;
379
+ /** Whether an import overwrites the store's own edits (`always_source`) or keeps them. */
380
+ syncPolicy: PipelineSyncPolicy;
381
+ /**
382
+ * `one_time`: runs only when started (by hand, or by a commerce plugin for an export). `daily`:
383
+ * every day at 03:00 in `timezone`; `weekly`: Mondays at 03:00 in `timezone`; `custom`: on
384
+ * `cronExpression` in `timezone`. A wall-clock time in that zone, so it follows its daylight
385
+ * saving changes.
386
+ */
387
+ scheduleType: PipelineScheduleType;
388
+ /** A 5-part cron for a `custom` schedule; `null` otherwise. */
389
+ cronExpression: string | null;
390
+ /** The IANA zone the schedule is read in. A pipeline made before zones were asked for holds `UTC`. */
391
+ timezone: string;
392
+ /**
393
+ * `false`: paused. A paused pipeline's schedule is unregistered and a commerce plugin's export
394
+ * through it is refused; starting a run by hand still works.
395
+ */
396
+ isActive: boolean;
397
+ /** Whether the source's prices include tax (an import). */
398
+ sourcePricesIncludeTax: boolean;
399
+ /** As stored, kept verbatim. */
400
+ config: ExportPipelineConfig | ImportPipelineConfig | null;
401
+ createdAt: string;
402
+ updatedAt: string;
403
+ connection: PipelineConnectionSummary;
404
+ }
405
+ export interface ListPipelinesOptions extends ListOptions {
406
+ /** Only imports, or only exports. */
407
+ direction?: PipelineDirection;
408
+ }
409
+ /** Every field optional: one left out stays as stored. */
410
+ export interface UpdatePipelineInput {
411
+ name?: string;
412
+ syncPolicy?: PipelineSyncPolicy;
413
+ scheduleType?: PipelineScheduleType;
414
+ /** A 5-part cron (`*\/30 * * * *`), required when `scheduleType` is `custom`. */
415
+ cronExpression?: string;
416
+ /** An IANA zone (`Europe/Amsterdam`). */
417
+ timezone?: string;
418
+ /** `false` pauses the pipeline, `true` resumes it. */
419
+ isActive?: boolean;
420
+ sourcePricesIncludeTax?: boolean;
421
+ /**
422
+ * An export's filter. MERGED over the stored settings at the top level, so the entity types
423
+ * stay; `triggerFilter: {}` removes the filter (every order not yet sent is exported).
424
+ */
425
+ config?: {
426
+ triggerFilter: Record<string, string>;
427
+ };
428
+ }
429
+ /**
430
+ * `pending`: queued, not started yet; `running`; `paused` (by an operator, resumable); and the
431
+ * ends: `completed`, `failed`, `cancelled`.
432
+ */
433
+ export type PipelineRunStatus = 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
434
+ /** `manual`: started by hand (`runs.start`, a set-up's commit); `scheduled`; `event`: a commerce plugin's single-order export. */
435
+ export type PipelineRunTrigger = 'manual' | 'scheduled' | 'event';
436
+ /**
437
+ * How an import run folds its product→collection assignments into each collection: `merge`
438
+ * (previous ∪ this run, the default and every scheduled run's), `replace_dry_run` (a `merge` run
439
+ * that also logs what `replace` would change, `entityType: 'collection_assignments'`), `replace`
440
+ * (each collection the pipeline maps gets exactly what this run assigned; SuperAdmin only).
441
+ */
442
+ export type PipelineCollectionAssignmentMode = 'merge' | 'replace_dry_run' | 'replace';
443
+ /** One entity type's counts in a run. */
444
+ export interface PipelineRunEntityProgress {
445
+ /** How many the source holds (an estimate until the run has read them all). */
446
+ total: number;
447
+ created: number;
448
+ updated: number;
449
+ skipped: number;
450
+ errored: number;
451
+ /** Sent to the target system (an export). */
452
+ exported: number;
453
+ }
454
+ /** One run of a pipeline: an import from, or an export to, the other system. */
455
+ export interface PipelineRun {
456
+ id: string;
457
+ pipelineId: string;
458
+ trigger: PipelineRunTrigger;
459
+ status: PipelineRunStatus;
460
+ /** Keyed by the entity type as the pipeline names it (`product_variants`, `orders`), verbatim. */
461
+ progress: Record<string, PipelineRunEntityProgress>;
462
+ /**
463
+ * Why it failed, in the operator's words. Also set on a run its pipeline's deletion cancelled,
464
+ * saying so ("The pipeline was deleted, so this run stopped after the batch it was working on.
465
+ * Nothing more was sent."); `null` on a run cancelled by hand.
466
+ */
467
+ errorMessage: string | null;
468
+ /** The sync policy this run imports with: the pipeline's, or the one `start` named. */
469
+ syncPolicy: PipelineSyncPolicy;
470
+ collectionAssignmentMode: PipelineCollectionAssignmentMode;
471
+ /** When it first started (kept across a pause and a resume); `null` while queued. */
472
+ startedAt: string | null;
473
+ completedAt: string | null;
474
+ createdAt: string;
475
+ updatedAt: string;
476
+ }
477
+ export interface StartPipelineRunInput {
478
+ /** This run only: whether an import overwrites the store's own edits. The pipeline's otherwise. */
479
+ syncPolicy?: PipelineSyncPolicy;
480
+ /** An import only; `merge` unless set. `replace` needs SuperAdmin. Such a run cannot be resumed. */
481
+ collectionAssignments?: PipelineCollectionAssignmentMode;
482
+ }
483
+ /** What a run did with one record. */
484
+ export type PipelineRunLogResult = 'created' | 'updated' | 'skipped' | 'errored' | 'exported';
485
+ /** One record a run handled, and what became of it. */
486
+ export interface PipelineRunLog {
487
+ id: string;
488
+ runId: string;
489
+ entityType: string;
490
+ /** The record's id in the system it came from. */
491
+ sourceId: string | null;
492
+ /** Its id (an import) or remote path (an export) in the system it went to. */
493
+ targetId: string | null;
494
+ result: PipelineRunLogResult;
495
+ /** Why it was skipped or errored, in the operator's words. */
496
+ message: string | null;
497
+ createdAt: string;
498
+ }
499
+ export interface ListPipelineRunLogsOptions extends ListOptions {
500
+ /** One entity type's records (`orders`, `products`). */
501
+ entityType?: string;
502
+ /** Only the records with this outcome: the errors, or the skipped ones. */
503
+ result?: PipelineRunLogResult;
504
+ }
505
+ /**
506
+ * How to follow a run or a discovery as it happens (`runs.watch`, `discoveries.watch`).
507
+ *
508
+ * - `signal` closes the connection; the iteration then throws `REQUEST_ABORTED`.
509
+ * - `idleTimeoutMs` (default 45 s, three of the server's keepalives): silence this long counts as
510
+ * a dropped connection.
511
+ * - `connectTimeoutMs`: the deadline for the response headers (the client's own by default).
512
+ * - `maxReconnects` (default 5; 0 never): how many times in a row a connection that closed, broke,
513
+ * stalled or could not open (a network error, 5xx, 429) is reopened before the iteration throws.
514
+ * An event received resets the count. A refusal (401, 403, 404) is never retried.
515
+ * - `reconnectDelayMs` (default 1000): the first reopen's delay, doubling up to 30 s.
516
+ * - `onReconnect`: told before each reopen (attempt, delay, and the error that ended it).
517
+ */
518
+ export type PipelineWatchOptions = ProgressStreamOptions;
519
+ /** A discovery of a connection's source. */
520
+ export interface PipelineDiscovery {
521
+ id: string;
522
+ connectionId: string;
523
+ /** `pending` → `running` → `completed` | `failed`. */
524
+ status: 'pending' | 'running' | 'completed' | 'failed';
525
+ /** `ai`: the AI proposed the maps; `connector_default`: a fixed-schema connector brought them. */
526
+ source: 'ai' | 'connector_default';
527
+ /** Why it failed, in the operator's words. */
528
+ errorMessage: string | null;
529
+ /** What the merchant should know about the source; empty until it completes. */
530
+ warnings: string[];
531
+ /** The source's plugins it recognised; empty until it completes. */
532
+ detectedPlugins: string[];
533
+ startedAt: string | null;
534
+ completedAt: string | null;
535
+ createdAt: string;
536
+ }
537
+ /** A discovery's proposal for one kind of record, as stored. */
538
+ export interface PipelineSchemaMap extends PipelineDraftSchemaMap {
539
+ discoveryId: string;
540
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The shapes of `admin.integrations.pipelines` (headless spec §7 `pipelines.drafts`).
2
+ * The shapes of `admin.integrations.pipelines` (headless spec §7 `pipelines.drafts`, §9.2
3
+ * `integrations`).
3
4
  *
4
5
  * Kept verbatim (stored data, never renamed): `triggerFilter`'s keys (reader field names such as
5
- * `toState`), `siteAnalysis` and `publicCredentials` (the connector's own keys), and a schema
6
- * map's `sourceSchema`, `targetSchema` and `subCounts`.
6
+ * `toState`), a pipeline's `config`, `siteAnalysis` and `publicCredentials` (the connector's own
7
+ * keys), and a schema map's `sourceSchema`, `targetSchema` and `subCounts`.
7
8
  */
8
9
  export {};
@@ -44,6 +44,11 @@ export interface MetaCatalogLabels {
44
44
  * with purchase orders; absent before.
45
45
  */
46
46
  purchaseOrderStatus?: LabelMap;
47
+ /**
48
+ * A return of goods to a supplier's status (`admin.purchasing.returns`). Served by a platform
49
+ * with supplier returns; absent before.
50
+ */
51
+ supplierReturnStatus?: LabelMap;
47
52
  /**
48
53
  * A vocabulary added after this SDK release arrives camelised, with its map
49
54
  * untouched.
@@ -65,6 +70,11 @@ export interface MetaCatalogTransitions {
65
70
  * absent before.
66
71
  */
67
72
  purchaseOrder?: TransitionMap;
73
+ /**
74
+ * A return of goods to a supplier's lifecycle (`admin.purchasing.returns`): `draft → shipped →
75
+ * resolved`, and `draft → cancelled`. Served by a platform with supplier returns; absent before.
76
+ */
77
+ supplierReturn?: TransitionMap;
68
78
  [extra: string]: unknown;
69
79
  }
70
80
  export interface PermissionGroup {
@@ -8,8 +8,15 @@ import type { Call } from '../types.js';
8
8
  * fields, and commerce refuses a value written against it
9
9
  * (`METAFIELD_OWNER_INVALID`). A manifest declaring one still registers; only
10
10
  * the value surface says no.
11
+ *
12
+ * `purchase_order`, `purchase_order_line` and `supplier` are the purchasing
13
+ * records, which belong to the store's legal entity rather than to the store:
14
+ * one of another legal entity answers `NotFoundError` like an id of nothing,
15
+ * and a store that does not purchase answers `PURCHASING_NOT_ENABLED`. Their
16
+ * values are still the store's own. An engine older than them refuses them with
17
+ * `METAFIELD_OWNER_INVALID`.
11
18
  */
12
- export declare const METAFIELD_OWNER_TYPES: readonly ["product", "variant", "customer", "order", "order_line", "location", "collection"];
19
+ export declare const METAFIELD_OWNER_TYPES: readonly ["product", "variant", "customer", "order", "order_line", "location", "collection", "purchase_order", "purchase_order_line", "supplier"];
13
20
  export type MetafieldOwnerType = (typeof METAFIELD_OWNER_TYPES)[number];
14
21
  /** Scalar value types. `list<T>` of any of them is also a type. */
15
22
  export declare const METAFIELD_SCALAR_TYPES: readonly ["text", "number", "money", "boolean", "date", "json", "reference", "attachment"];
@@ -7,6 +7,13 @@ import { createPage } from '@sixtyninecommerce/core';
7
7
  * fields, and commerce refuses a value written against it
8
8
  * (`METAFIELD_OWNER_INVALID`). A manifest declaring one still registers; only
9
9
  * the value surface says no.
10
+ *
11
+ * `purchase_order`, `purchase_order_line` and `supplier` are the purchasing
12
+ * records, which belong to the store's legal entity rather than to the store:
13
+ * one of another legal entity answers `NotFoundError` like an id of nothing,
14
+ * and a store that does not purchase answers `PURCHASING_NOT_ENABLED`. Their
15
+ * values are still the store's own. An engine older than them refuses them with
16
+ * `METAFIELD_OWNER_INVALID`.
10
17
  */
11
18
  export const METAFIELD_OWNER_TYPES = [
12
19
  'product',
@@ -16,6 +23,9 @@ export const METAFIELD_OWNER_TYPES = [
16
23
  'order_line',
17
24
  'location',
18
25
  'collection',
26
+ 'purchase_order',
27
+ 'purchase_order_line',
28
+ 'supplier',
19
29
  ];
20
30
  /** Scalar value types. `list<T>` of any of them is also a type. */
21
31
  export const METAFIELD_SCALAR_TYPES = [