@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,150 @@
1
+ /**
2
+ * `admin.documents.emails` — the store's email delivery log, its counts, a retry of one delivery,
3
+ * and a test send of a template (headless spec §9.2 `documents`, "email sends"; W10-103).
4
+ *
5
+ * Every call needs `UpdateSettings` on the active store. A delivery's recipients, subject, error
6
+ * and metadata are customer data: below `customerData: full` (an app installation) they read
7
+ * `''`/`null` and the row carries `redacted: true`.
8
+ */
9
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
10
+ import type { Call, ListOptions } from '../types.js';
11
+ /**
12
+ * Where a delivery stands. `skipped`: the store's `sender: none` for the event (the platform sent
13
+ * nothing, on purpose). `suppressed_sandbox`: a sandbox store withheld the customer's copy. The
14
+ * engine may add others; a reader keeps a default branch.
15
+ */
16
+ export type EmailDeliveryStatus = 'sent' | 'failed' | 'blocked' | 'pending' | 'skipped' | 'suppressed_sandbox' | (string & {});
17
+ /** One email the store sent, or tried to. */
18
+ export interface EmailDelivery {
19
+ id: string;
20
+ /** The template it was rendered from (`order_confirmation`, …). */
21
+ templateType: string;
22
+ /** The notification event that sent it (`order_placed`, …). */
23
+ notificationEvent: string;
24
+ /** Comma-separated addresses. */
25
+ recipientTo: string;
26
+ recipientCc: string | null;
27
+ recipientBcc: string | null;
28
+ subject: string;
29
+ gatewayId: string | null;
30
+ status: EmailDeliveryStatus;
31
+ /** 1 for the first send; each retry adds one. */
32
+ attempt: number;
33
+ /** The mail provider's message id, when it took the email. */
34
+ messageId: string | null;
35
+ /** Why the last attempt failed, as the provider or the mail server said it. */
36
+ error: string | null;
37
+ durationMs: number | null;
38
+ /** The ids the email was rendered from (`orderId`, `invoiceId`, …) and other send facts. */
39
+ metadata: Record<string, unknown> | null;
40
+ /** ISO-8601 UTC. */
41
+ createdAt: string;
42
+ /**
43
+ * Set when the caller's customer-data level withheld part of the row (an app installation
44
+ * below `customerData: full`): the recipients, subject, error and metadata then read `''` or
45
+ * `null`, meaning "not granted", never "none on record".
46
+ */
47
+ redacted?: true;
48
+ }
49
+ /**
50
+ * Which deliveries a list or a count covers. The two instants need an explicit offset (`Z` or
51
+ * `±HH:mm`) and are refused before the request leaves without one (`INVALID_DATETIME`): a store's
52
+ * day is not the caller's. To cover the store's days, send the first instant of the first day as
53
+ * `createdFrom` and the first instant of the day AFTER the last one as `createdBefore`
54
+ * ({@link EmailDeliveriesModule.list}).
55
+ */
56
+ export interface EmailDeliveryFilter {
57
+ status?: EmailDeliveryStatus;
58
+ templateType?: string;
59
+ /** Created at or after this instant. */
60
+ createdFrom?: string;
61
+ /** Created strictly before this instant. */
62
+ createdBefore?: string;
63
+ }
64
+ /** How many of the store's deliveries are in each state, over a filter. */
65
+ export interface EmailDeliveryStats {
66
+ sent: number;
67
+ failed: number;
68
+ /** Never sent: every recipient is on the mail provider's suppression list. */
69
+ blocked: number;
70
+ pending: number;
71
+ }
72
+ export interface SendTestEmailInput {
73
+ /** A template type the platform renders (`admin.tenant.emailTemplates.catalog()`). */
74
+ templateType: string;
75
+ /** One email address. */
76
+ recipientEmail: string;
77
+ }
78
+ export interface EmailDeliveriesModule {
79
+ /**
80
+ * The store's deliveries, newest first, one page at a time (`limit` 1–100, 50 when omitted).
81
+ *
82
+ * **Store days.** The page that shows "from 24 to 25 October" in Europe/Amsterdam sends
83
+ * `createdFrom: '2026-10-24T00:00:00+02:00'` and `createdBefore: '2026-10-26T00:00:00+01:00'`:
84
+ * 25 October 2026 is 25 hours long there (the clocks go back), and the end is exclusive, so
85
+ * the whole last day is in and nothing of the next.
86
+ *
87
+ * **Coded refusals:** `INVALID_DATETIME` → `ValidationError` (400), an instant without an
88
+ * offset, before the request leaves; `EMAIL_DELIVERY_FILTER_INVALID` → `ValidationError`
89
+ * (400), a `limit` outside 1–100 or a range that ends before it starts.
90
+ *
91
+ * Rows carry `redacted: true` below `customerData: full` (see {@link EmailDelivery}).
92
+ *
93
+ * @effect read
94
+ * @permission UpdateSettings
95
+ */
96
+ list(filter?: EmailDeliveryFilter, options?: ListOptions): Promise<Page<EmailDelivery>>;
97
+ /**
98
+ * How many deliveries are `sent`, `failed`, `blocked` and `pending`, over the same filter as
99
+ * {@link list} without its `status` (a count by status). Without a filter: the store's whole
100
+ * log. On an engine from before this release (no filter argument) a filtered count is made
101
+ * from the list's totals, one read per status.
102
+ *
103
+ * @effect read
104
+ * @permission UpdateSettings
105
+ */
106
+ stats(filter?: Omit<EmailDeliveryFilter, 'status'>, options?: Call): Promise<EmailDeliveryStats>;
107
+ /**
108
+ * Send a `failed` or `blocked` delivery again, now, and answer the row with the new outcome:
109
+ * `status` is `sent`, `failed` again (with its `error`), `blocked` (the address is still on the
110
+ * provider's suppression list) or `suppressed_sandbox`. It resolves either way; read `status`.
111
+ * Synchronous: the email has been handed to the provider (or not) when it resolves.
112
+ *
113
+ * This sends the email to the CUSTOMER (or whoever it was addressed to) once more. Never call
114
+ * it in a loop or on a timer without a person deciding to.
115
+ *
116
+ * **Coded refusals** (nothing is sent): `EMAIL_DELIVERY_NOT_FOUND` → `NotFoundError` (404), no
117
+ * such delivery in this store; `EMAIL_DELIVERY_NOT_RETRYABLE` → `BusinessRuleError` (422), the
118
+ * delivery is not `failed` or `blocked`, it is a gift card's email (its code is not kept), or
119
+ * another retry of it started first.
120
+ *
121
+ * @effect irreversible
122
+ * @permission UpdateSettings
123
+ */
124
+ retry(id: string, options?: Call): Promise<EmailDelivery>;
125
+ }
126
+ export interface DocumentEmailsModule {
127
+ deliveries: EmailDeliveriesModule;
128
+ /**
129
+ * Render `templateType` with sample data and send it, now, through the store's own email
130
+ * gateway, to `recipientEmail`. It is real mail. The send writes no delivery row.
131
+ *
132
+ * **Coded refusals:** `INVALID_TEST_EMAIL_RECIPIENT` and `INVALID_TEST_EMAIL_TEMPLATE` →
133
+ * `ValidationError` (400), before anything is sent; `TEST_EMAIL_SEND_FAILED` →
134
+ * `BusinessRuleError` (422), the gateway did not take it (the message names the SMTP step or
135
+ * the provider's code, never the server's own words).
136
+ *
137
+ * @effect irreversible
138
+ * @permission UpdateSettings
139
+ */
140
+ sendTest(input: SendTestEmailInput, options?: Call): Promise<void>;
141
+ }
142
+ export declare const EMAIL_DELIVERIES_QUERY = "\n query AdminEmailDeliveries($options: EmailDeliveryLogListOptions) {\n emailDeliveryLogs(options: $options) {\n items { \n id\n templateType\n notificationEvent\n recipientTo\n recipientCc\n recipientBcc\n subject\n gatewayId\n status\n attempt\n messageId\n error\n durationMs\n metadata\n createdAt\n }\n totalItems\n }\n }\n";
143
+ export declare const EMAIL_DELIVERY_STATS_QUERY = "\n query AdminEmailDeliveryStats($options: EmailDeliveryStatsOptions) {\n emailDeliveryStats(options: $options) {\n sent\n failed\n blocked\n pending\n }\n }\n";
144
+ /** The store's whole log, without the filter argument an older engine does not know. */
145
+ export declare const EMAIL_DELIVERY_STATS_ALL_QUERY = "\n query AdminEmailDeliveryStatsAll {\n emailDeliveryStats {\n sent\n failed\n blocked\n pending\n }\n }\n";
146
+ /** How many deliveries match: the older engine's way to count over a filter. */
147
+ export declare const EMAIL_DELIVERY_COUNT_QUERY = "\n query AdminEmailDeliveryCount($options: EmailDeliveryLogListOptions) {\n emailDeliveryLogs(options: $options) {\n totalItems\n }\n }\n";
148
+ export declare const RETRY_EMAIL_DELIVERY_MUTATION = "\n mutation AdminRetryEmailDelivery($id: ID!) {\n retryEmailDelivery(id: $id) { \n id\n templateType\n notificationEvent\n recipientTo\n recipientCc\n recipientBcc\n subject\n gatewayId\n status\n attempt\n messageId\n error\n durationMs\n metadata\n createdAt\n }\n }\n";
149
+ export declare const SEND_TEST_EMAIL_MUTATION = "\n mutation AdminSendTestEmail($templateType: String!, $recipientEmail: String!) {\n sendTestEmail(templateType: $templateType, recipientEmail: $recipientEmail)\n }\n";
150
+ export declare function createDocumentEmailsModule(transport: Transport): DocumentEmailsModule;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * `admin.documents.emails` — the store's email delivery log, its counts, a retry of one delivery,
3
+ * and a test send of a template (headless spec §9.2 `documents`, "email sends"; W10-103).
4
+ *
5
+ * Every call needs `UpdateSettings` on the active store. A delivery's recipients, subject, error
6
+ * and metadata are customer data: below `customerData: full` (an app installation) they read
7
+ * `''`/`null` and the row carries `redacted: true`.
8
+ */
9
+ import { pageFromVendureList, readOffsetCursor, ValidationError, } from '@sixtyninecommerce/core';
10
+ import { assertInstant } from '../catalog/types.js';
11
+ import { customerDataField } from '../customers/wire.js';
12
+ const DELIVERY_FIELDS = /* GraphQL */ `
13
+ id
14
+ templateType
15
+ notificationEvent
16
+ recipientTo
17
+ recipientCc
18
+ recipientBcc
19
+ subject
20
+ gatewayId
21
+ status
22
+ attempt
23
+ messageId
24
+ error
25
+ durationMs
26
+ metadata
27
+ createdAt
28
+ `;
29
+ export const EMAIL_DELIVERIES_QUERY = /* GraphQL */ `
30
+ query AdminEmailDeliveries($options: EmailDeliveryLogListOptions) {
31
+ emailDeliveryLogs(options: $options) {
32
+ items { ${DELIVERY_FIELDS} }
33
+ totalItems
34
+ }
35
+ }
36
+ `;
37
+ export const EMAIL_DELIVERY_STATS_QUERY = /* GraphQL */ `
38
+ query AdminEmailDeliveryStats($options: EmailDeliveryStatsOptions) {
39
+ emailDeliveryStats(options: $options) {
40
+ sent
41
+ failed
42
+ blocked
43
+ pending
44
+ }
45
+ }
46
+ `;
47
+ /** The store's whole log, without the filter argument an older engine does not know. */
48
+ export const EMAIL_DELIVERY_STATS_ALL_QUERY = /* GraphQL */ `
49
+ query AdminEmailDeliveryStatsAll {
50
+ emailDeliveryStats {
51
+ sent
52
+ failed
53
+ blocked
54
+ pending
55
+ }
56
+ }
57
+ `;
58
+ /** How many deliveries match: the older engine's way to count over a filter. */
59
+ export const EMAIL_DELIVERY_COUNT_QUERY = /* GraphQL */ `
60
+ query AdminEmailDeliveryCount($options: EmailDeliveryLogListOptions) {
61
+ emailDeliveryLogs(options: $options) {
62
+ totalItems
63
+ }
64
+ }
65
+ `;
66
+ export const RETRY_EMAIL_DELIVERY_MUTATION = /* GraphQL */ `
67
+ mutation AdminRetryEmailDelivery($id: ID!) {
68
+ retryEmailDelivery(id: $id) { ${DELIVERY_FIELDS} }
69
+ }
70
+ `;
71
+ export const SEND_TEST_EMAIL_MUTATION = /* GraphQL */ `
72
+ mutation AdminSendTestEmail($templateType: String!, $recipientEmail: String!) {
73
+ sendTestEmail(templateType: $templateType, recipientEmail: $recipientEmail)
74
+ }
75
+ `;
76
+ /** The engine's default page; the SDK states it so the cursor arithmetic knows the size. */
77
+ const DELIVERIES_PAGE_SIZE = 50;
78
+ function toDelivery(row) {
79
+ return {
80
+ id: String(row.id),
81
+ templateType: row.templateType,
82
+ notificationEvent: row.notificationEvent,
83
+ recipientTo: row.recipientTo,
84
+ recipientCc: row.recipientCc ?? null,
85
+ recipientBcc: row.recipientBcc ?? null,
86
+ subject: row.subject,
87
+ gatewayId: row.gatewayId ?? null,
88
+ status: row.status,
89
+ attempt: row.attempt,
90
+ messageId: row.messageId ?? null,
91
+ error: row.error ?? null,
92
+ durationMs: row.durationMs ?? null,
93
+ metadata: row.metadata ?? null,
94
+ createdAt: row.createdAt,
95
+ };
96
+ }
97
+ /** The engine's filter input: only the keys the caller set, so an older engine still answers. */
98
+ function wireFilter(filter) {
99
+ if (filter === undefined)
100
+ return {};
101
+ if (filter === null || typeof filter !== 'object' || Array.isArray(filter)) {
102
+ throw new ValidationError({
103
+ code: 'EMAIL_DELIVERY_FILTER_INVALID',
104
+ message: 'The delivery filter must be an object.',
105
+ status: 400,
106
+ service: 'vendure',
107
+ });
108
+ }
109
+ assertInstant('createdFrom', filter.createdFrom);
110
+ assertInstant('createdBefore', filter.createdBefore);
111
+ return {
112
+ ...(filter.status ? { status: filter.status } : {}),
113
+ ...(filter.templateType ? { templateType: filter.templateType } : {}),
114
+ ...(filter.createdFrom ? { dateFrom: filter.createdFrom } : {}),
115
+ ...(filter.createdBefore ? { dateBefore: filter.createdBefore } : {}),
116
+ };
117
+ }
118
+ /**
119
+ * The engine refused the document over a field or argument it does not know: an engine from
120
+ * before this release (no `dateBefore`, no `emailDeliveryStats(options:)`). The dashboard can be
121
+ * live before vendure-server (`.claude/rules/backend.md`, additive rule), so the module falls
122
+ * back to the older spelling instead of failing. Matched on the names this module added, so a
123
+ * real refusal is never retried.
124
+ */
125
+ function isOlderEngineRejection(error) {
126
+ if (!(error instanceof ValidationError) || error.service !== 'vendure')
127
+ return false;
128
+ // Apollo answers a document that fails validation (an unknown type or argument: the stats
129
+ // filter) with HTTP 400, which the transport reports as BAD_REQUEST without the engine's
130
+ // words; an unknown input field (dateBefore) is a variable coercion error at HTTP 200 and
131
+ // keeps them. Only ever asked about the documents this module added the names to.
132
+ return (error.code === 'BAD_REQUEST' ||
133
+ error.code === 'GRAPHQL_VALIDATION_FAILED' ||
134
+ /dateBefore|EmailDeliveryStatsOptions|Unknown argument "options"/.test(error.message));
135
+ }
136
+ /** The old inclusive spelling of an exclusive end: one millisecond before it. */
137
+ function legacyFilter(filter) {
138
+ const { dateBefore, ...rest } = filter;
139
+ if (typeof dateBefore !== 'string')
140
+ return rest;
141
+ return { ...rest, dateTo: new Date(Date.parse(dateBefore) - 1).toISOString() };
142
+ }
143
+ const COUNTED_STATUSES = ['sent', 'failed', 'blocked', 'pending'];
144
+ export function createDocumentEmailsModule(transport) {
145
+ /** Latched once the engine proved it predates `dateBefore` (one refused request per client). */
146
+ let olderEngine = false;
147
+ const listPage = async (skip, take, filter, options) => {
148
+ const read = (wire) => customerDataField(transport, EMAIL_DELIVERIES_QUERY, 'emailDeliveryLogs', { options: { ...wire, take, skip } }, options);
149
+ let answer;
150
+ if (olderEngine || !('dateBefore' in filter)) {
151
+ answer = await read(olderEngine ? legacyFilter(filter) : filter);
152
+ }
153
+ else {
154
+ try {
155
+ answer = await read(filter);
156
+ }
157
+ catch (error) {
158
+ if (!isOlderEngineRejection(error))
159
+ throw error;
160
+ olderEngine = true;
161
+ answer = await read(legacyFilter(filter));
162
+ }
163
+ }
164
+ const { value, mark } = answer;
165
+ const items = value.items.map((row) => mark(toDelivery(row)));
166
+ // Each following page re-sends the same filter and per-call options.
167
+ return pageFromVendureList({ items, totalItems: value.totalItems }, skip, take, (cursor) => listPage(readOffsetCursor(cursor), take, filter, options));
168
+ };
169
+ return {
170
+ deliveries: {
171
+ async list(filter, options) {
172
+ const wire = wireFilter(filter);
173
+ const take = options?.limit ?? DELIVERIES_PAGE_SIZE;
174
+ const skip = options?.after === undefined ? 0 : readOffsetCursor(options.after);
175
+ return listPage(skip, take, wire, options);
176
+ },
177
+ async stats(filter, options) {
178
+ const { status: _status, ...counted } = wireFilter(filter);
179
+ if (Object.keys(counted).length === 0) {
180
+ const data = await transport.graphql({ api: 'admin', query: EMAIL_DELIVERY_STATS_ALL_QUERY }, options);
181
+ return data.emailDeliveryStats;
182
+ }
183
+ if (!olderEngine) {
184
+ try {
185
+ const data = await transport.graphql({ api: 'admin', query: EMAIL_DELIVERY_STATS_QUERY, variables: { options: counted } }, options);
186
+ return data.emailDeliveryStats;
187
+ }
188
+ catch (error) {
189
+ if (!isOlderEngineRejection(error))
190
+ throw error;
191
+ olderEngine = true;
192
+ }
193
+ }
194
+ // An older engine counts only the whole log: count each status over the filter through
195
+ // the list's totals instead (one row read per status).
196
+ const totals = await Promise.all(COUNTED_STATUSES.map(async (status) => {
197
+ const data = await transport.graphql({
198
+ api: 'admin',
199
+ query: EMAIL_DELIVERY_COUNT_QUERY,
200
+ variables: { options: { ...legacyFilter(counted), status, take: 1, skip: 0 } },
201
+ }, options);
202
+ return data.emailDeliveryLogs.totalItems;
203
+ }));
204
+ const [sent = 0, failed = 0, blocked = 0, pending = 0] = totals;
205
+ return { sent, failed, blocked, pending };
206
+ },
207
+ async retry(id, options) {
208
+ const { value, mark } = await customerDataField(transport, RETRY_EMAIL_DELIVERY_MUTATION, 'retryEmailDelivery', { id }, options);
209
+ return mark(toDelivery(value));
210
+ },
211
+ },
212
+ async sendTest(input, options) {
213
+ await transport.graphql({
214
+ api: 'admin',
215
+ query: SEND_TEST_EMAIL_MUTATION,
216
+ variables: { templateType: input.templateType, recipientEmail: input.recipientEmail },
217
+ }, options);
218
+ },
219
+ };
220
+ }
@@ -1,6 +1,7 @@
1
1
  import { type Page, type Transport } from '@sixtyninecommerce/core';
2
2
  import type { Call, ListOptions } from '../types.js';
3
3
  import { type ArDocumentsModule } from './ar.js';
4
+ import { type DocumentEmailsModule } from './emails.js';
4
5
  import { decodePdf } from './pdf.js';
5
6
  export { decodePdf };
6
7
  /**
@@ -402,6 +403,16 @@ export interface InvoiceCorrectionInput {
402
403
  refundPaymentId?: string;
403
404
  /** RETAIL only: move the credited amount to the customer's account instead (`accountCredit`). */
404
405
  creditToAccount?: boolean;
406
+ /**
407
+ * `commitCorrection` with `creditToAccount` in a store whose book is store credit (the plan's
408
+ * `accountCredit.book` `STORE_CREDIT`): whether the customer expressly agreed to store credit
409
+ * instead of a refund to their payment (EU consumer law, CRD art. 13(1)), as staff recorded it.
410
+ * The move records it on its ledger entries (`StoredValueEntry.customerAgreed`); never a
411
+ * condition, nothing is refused for it. The plan ignores it and its `planHash` never sees it, so
412
+ * leave it out of `planCorrection` (an engine before the field, incentives sprint 8 PR 6a,
413
+ * refuses any call that gives it) and add it to the commit's input.
414
+ */
415
+ customerAgreed?: boolean;
405
416
  /**
406
417
  * Details wrong only: the order's payment term in days. The corrected invoice is reissued
407
418
  * under it and the order keeps it (its `paymentTermsDays`; its payment condition is cleared).
@@ -1591,6 +1602,8 @@ export interface DocumentsModule {
1591
1602
  extensions: DocumentExtensionsModule;
1592
1603
  /** The accounts-receivable ledger's documents: AR invoices, statements and credit notes. */
1593
1604
  ar: ArDocumentsModule;
1605
+ /** The store's email delivery log (counts, a retry) and a template's test send. */
1606
+ emails: DocumentEmailsModule;
1594
1607
  }
1595
1608
  export declare const GENERATE_INVOICE_MUTATION = "\n mutation GenerateInvoice(\n $orderId: ID!\n $invoicedOn: String!\n $languageCode: String\n $note: String\n $extensions: [DocumentExtensionInput!]\n ) {\n generateInvoice(\n orderId: $orderId\n invoicedOn: $invoicedOn\n languageCode: $languageCode\n note: $note\n extensions: $extensions\n ) { \n id\n invoiceNumber\n invoicedOn\n orderId\n languageCode\n storageKey\n totalWithTax\n currencyCode\n note\n voidedOn\n reversedByCreditNoteId\n supersededByInvoiceId\n reissueOfInvoiceId\n kind\n supplementsInvoiceId\n lines { \n description\n sku\n orderLineId\n quantity\n unitPriceNet\n unitPriceWithTax\n taxRate\n lineTotalNet\n lineTotalWithTax\n unitPriceBasis\n }\n numberingProvider\n amountPaid\n amountDue\n createdAt\n }\n }\n";
1596
1609
  export declare const GENERATE_PACKING_SLIP_MUTATION = "\n mutation GeneratePackingSlip(\n $fulfillmentId: ID!\n $languageCode: String\n $extensions: [DocumentExtensionInput!]\n ) {\n generatePackingSlip(\n fulfillmentId: $fulfillmentId\n languageCode: $languageCode\n extensions: $extensions\n ) { \n id\n packingSlipNumber\n fulfillmentId\n languageCode\n storageKey\n createdAt\n }\n }\n";
@@ -1,6 +1,7 @@
1
1
  import { createPage, idempotencyKey as mintIdempotencyKey, pageFromVendureList, readOffsetCursor, ValidationError, } from '@sixtyninecommerce/core';
2
2
  import { storeCreditAware } from '../payments/store-credit-fields.js';
3
3
  import { createArDocumentsModule } from './ar.js';
4
+ import { createDocumentEmailsModule } from './emails.js';
4
5
  import { decodePdf } from './pdf.js';
5
6
  // Also what `documents.ar.pdf` decodes with (headless W10-30).
6
7
  export { decodePdf };
@@ -903,6 +904,7 @@ export function createDocumentsModule(transport) {
903
904
  },
904
905
  },
905
906
  ar: createArDocumentsModule(transport),
907
+ emails: createDocumentEmailsModule(transport),
906
908
  extensions: {
907
909
  async list(options) {
908
910
  const data = await transport.graphql({ api: 'admin', query: DOCUMENT_EXTENSIONS_QUERY }, options);
@@ -1,4 +1,4 @@
1
- import { type Transport } from '@sixtyninecommerce/core';
1
+ import { type PlatformError, type Transport } from '@sixtyninecommerce/core';
2
2
  import type { OrderOverpaymentChoice, OverpaymentOption } from '../orders/surcharges.js';
3
3
  import type { Call } from '../types.js';
4
4
  import { type FulfillmentCreateMethods } from './create.js';
@@ -182,6 +182,15 @@ export interface FulfillmentCancelPreview {
182
182
  export interface FulfillmentCancelChoice {
183
183
  orderId: string;
184
184
  choice: OrderOverpaymentChoice;
185
+ /**
186
+ * With `ACCOUNT_CREDIT` in a store whose book is store credit (`cancelPreview`'s
187
+ * `accountCredit.book` `STORE_CREDIT`): whether THIS order's customer expressly agreed to store
188
+ * credit instead of a refund to their payment (EU consumer law, CRD art. 13(1)), as staff
189
+ * recorded it. The settlement's move records it on its ledger entries
190
+ * (`StoredValueEntry.customerAgreed`); never a condition, nothing is refused for it. Sent only
191
+ * when given; an engine before the field (incentives sprint 8 PR 6a) refuses a call that gives it.
192
+ */
193
+ customerAgreed?: boolean;
185
194
  }
186
195
  /** What `cancel` sends besides the fulfillment. */
187
196
  export interface CancelFulfillmentInput {
@@ -265,6 +274,35 @@ export interface OrderFulfillmentCoverage {
265
274
  }
266
275
  /** The longest override reason the engine stores. */
267
276
  export declare const FULFILLMENT_COVERAGE_REASON_MAX_LENGTH = 500;
277
+ /** A fulfillment as `transition` leaves it. */
278
+ export interface FulfillmentStateChange {
279
+ id: string;
280
+ /** The state it is in now (`Shipped`, `Delivered`, …). */
281
+ state: string;
282
+ /** The states it may move to next, as the store's fulfillment process allows. */
283
+ nextStates: string[];
284
+ }
285
+ /**
286
+ * The facts a refused `transition` carries on `PlatformError.extensions`, beside its code:
287
+ * the move that was asked for and the refusing step's own words.
288
+ */
289
+ export interface FulfillmentTransitionRefusalFacts {
290
+ /** Core's code for every refused move: `FULFILLMENT_STATE_TRANSITION_ERROR`. */
291
+ errorCode: string;
292
+ fromState: string;
293
+ toState: string;
294
+ /** The refusing step's text, `[CODE]` prefix included when it had one. */
295
+ transitionError: string;
296
+ }
297
+ /**
298
+ * What re-sending a fulfillment's label to the store's 3PL came to (`resendLabelHandoff`):
299
+ * - `QUEUED`: the email with the label is queued for the 3PL recipient;
300
+ * - `NOT_READY`: the fulfillment has no ready MyParcel label yet, so nothing was sent;
301
+ * - `DISABLED`: the store has the label hand-off notification switched off;
302
+ * - `NO_RECIPIENT`: it is on, but names no recipient;
303
+ * - `UNAVAILABLE`: the platform's email delivery is not installed.
304
+ */
305
+ export type LabelHandoffOutcome = 'QUEUED' | 'DISABLED' | 'NO_RECIPIENT' | 'NOT_READY' | 'UNAVAILABLE';
268
306
  /**
269
307
  * Fulfillment parcels (multi-parcel shipments). Each method is its own request
270
308
  * on purpose: a caller deployed ahead of an engine without these fields sees
@@ -412,6 +450,66 @@ export interface FulfillmentsModule extends FulfillmentCreateMethods {
412
450
  allowUncovered(orderId: string, reason: string, options?: Call & {
413
451
  idempotencyKey?: string;
414
452
  }): Promise<FulfillmentCoverageOverride>;
453
+ /**
454
+ * Move a fulfillment to one of its `nextStates` (the order page's "Mark as Shipped" and "Mark as
455
+ * Delivered"). Needs `UpdateOrder`. The move runs the store's own fulfillment process, so the
456
+ * order's state follows and whatever the store does on a shipped or delivered fulfillment (its
457
+ * events, its notifications: the customer gets the shipped e-mail) happens; the process has no
458
+ * way back to an earlier state. Shipping an order's last open lines moves the ORDER to
459
+ * `Shipped`, which on a store that pushes orders to its ERP when they ship (Exact Online's
460
+ * "fulfilled" auto-push) sends the order there, where it cannot be taken back.
461
+ *
462
+ * A move into `Cancelled` is a cancel: it ends the fulfillment for good and has its carrier let
463
+ * go of the shipment, and the delivery charge it added comes off what each of its orders bills.
464
+ * It is refused, before the carrier is told, while the customer has PAID such a charge
465
+ * (`FULFILLMENT_CANCEL_OVERPAYMENT_CHOICE_REQUIRED`): {@link FulfillmentsModule.cancel} takes the
466
+ * choice of how that money goes back. An unpaid charge simply leaves the bill.
467
+ *
468
+ * Tagged `money`, its strongest effect: a cancel changes what a placed order bills.
469
+ *
470
+ * Refused, with nothing moved:
471
+ * - a step of the store's process that refuses the move answers ITS OWN code, which the engine
472
+ * prefixes to its text as `[CODE]`, with the text after it as the message, classed as the
473
+ * warehouse API classes the same code: `ORDER_NOT_COVERED` (the store requires full payment
474
+ * and an order is not paid in full: `coverage` reads what it owes, `allowUncovered` records
475
+ * an override), `ORDER_ON_HOLD` (release the order's fulfilment hold first),
476
+ * `BOOKING_REQUIRED`, `PARCELS_REQUIRED`, `CONSOLIDATION_ADDRESS_DIVERGED`, …;
477
+ * - any other refusal of the process, an edge it does not have included, is
478
+ * `FULFILLMENT_STATE_TRANSITION_ERROR` (`BusinessRuleError`, 422) with the refusing step's
479
+ * text as the message;
480
+ * - both carry {@link FulfillmentTransitionRefusalFacts} on `extensions`;
481
+ * - another store's or a missing fulfillment is `ENTITY_NOT_FOUND` (`NotFoundError`); a
482
+ * credential restricted to stock locations (an API token or app installation with
483
+ * `locationIds`) moves only a fulfillment that ships from one of them
484
+ * (`LOCATION_OUT_OF_SCOPE`, 422);
485
+ * - a blank id or state is refused before any request (`FULFILLMENT_TRANSITION_INVALID`,
486
+ * `ValidationError`).
487
+ *
488
+ * Takes no idempotency key: repeating a move that went through asks for an edge the process no
489
+ * longer has, and is refused as above, never moved twice.
490
+ *
491
+ * @effect money
492
+ * @permission UpdateOrder
493
+ */
494
+ transition(fulfillmentId: string, state: string, options?: Call): Promise<FulfillmentStateChange>;
495
+ /**
496
+ * E-mail the fulfillment's ready MyParcel label again to the store's 3PL recipient (the
497
+ * `fulfillment_label_handoff` notification), whatever the store's automatic hand-off setting.
498
+ * Answers what came of it ({@link LabelHandoffOutcome}): only `QUEUED` sent anything; the
499
+ * others say why nothing was sent. Every outcome leaves an audit row. Needs `UpdateOrder`.
500
+ *
501
+ * Takes no idempotency key: a repeat sends the e-mail again.
502
+ *
503
+ * Refused: another store's or a missing fulfillment is `ENTITY_NOT_FOUND` (`NotFoundError`); a
504
+ * store without the MyParcel plugin `PLUGIN_ACCESS_DENIED` (`PermissionDeniedError`, 403) for
505
+ * an ordinary caller; a credential restricted to other stock locations `LOCATION_OUT_OF_SCOPE`
506
+ * (422). A blank id is refused before any request (`MYPARCEL_LABEL_INPUT_INVALID`,
507
+ * `ValidationError`).
508
+ *
509
+ * @effect irreversible
510
+ * @permission UpdateOrder
511
+ */
512
+ resendLabelHandoff(fulfillmentId: string, options?: Call): Promise<LabelHandoffOutcome>;
415
513
  /**
416
514
  * The store's fulfilment payment policy (`fulfillmentPaymentPolicy`): whether a fulfilment of
417
515
  * an order not paid in full is refused `ORDER_NOT_COVERED`, and which payment methods are
@@ -431,6 +529,12 @@ export interface FulfillmentsModule extends FulfillmentCreateMethods {
431
529
  export declare const FULFILLMENT_CANCEL_PREVIEW_QUERY = "\n query FulfillmentCancelPreview($fulfillmentId: ID!) {\n fulfillmentCancelPreview(fulfillmentId: $fulfillmentId) {\n fulfillmentId\n overpayments {\n orderId\n orderCode\n amount\n currencyCode\n refund { \n available\n code\n message\n book\n }\n accountCredit { \n available\n code\n message\n book\n }\n }\n }\n }\n";
432
530
  /** `$overpayment` and `$idempotencyKey` are optional and left out of the variables when not given. */
433
531
  export declare const CANCEL_FULFILLMENT_MUTATION = "\n mutation CancelFulfillment(\n $fulfillmentId: ID!\n $overpayment: [FulfillmentCancelOverpaymentInput!]\n $idempotencyKey: String\n ) {\n cancelFulfillment(\n fulfillmentId: $fulfillmentId\n overpayment: $overpayment\n idempotencyKey: $idempotencyKey\n ) {\n fulfillment {\n id\n state\n }\n settlements {\n orderId\n orderCode\n amount\n currencyCode\n choice\n book\n }\n }\n }\n";
532
+ /**
533
+ * The refusal member is selected for its facts: `message` is core's per-code template ("Cannot
534
+ * transition Fulfillment from X to Y"), and the refusing step's reason is `transitionError` alone.
535
+ */
536
+ export declare const TRANSITION_FULFILLMENT_MUTATION = "\n mutation TransitionFulfillment($id: ID!, $state: String!) {\n transitionFulfillmentToState(id: $id, state: $state) {\n __typename\n ... on Fulfillment {\n id\n state\n nextStates\n }\n ... on FulfillmentStateTransitionError {\n errorCode\n message\n fromState\n toState\n transitionError\n }\n }\n }\n";
537
+ export declare const RESEND_FULFILLMENT_LABEL_HANDOFF_MUTATION = "\n mutation ResendFulfillmentLabelHandoff($fulfillmentId: ID!) {\n resendFulfillmentLabelHandoff(fulfillmentId: $fulfillmentId)\n }\n";
434
538
  /** Its own document: on an engine without the query this call alone fails validation. */
435
539
  export declare const ORDER_FULFILLMENT_COVERAGE_QUERY = "\n query OrderFulfillmentCoverage($orderId: ID!) {\n orderFulfillmentCoverage(orderId: $orderId) {\n orderId\n orderCode\n currencyCode\n policyActive\n covered\n outstanding\n exempt\n exemptReason\n allowed\n override { \n id\n orderId\n reason\n actorKind\n actorId\n outstandingAtGrant\n currencyCode\n createdAt\n active\n }\n }\n }\n";
436
540
  /** `$idempotencyKey` is optional and left out of the variables when not given. */
@@ -468,4 +572,18 @@ export declare const MYPARCEL_LABEL_TIMEOUT_MS = 55000;
468
572
  export declare function toUnretractedShipments(raw: readonly MyparcelUnretractedShipment[] | null | undefined): MyparcelUnretractedShipment[];
469
573
  /** The bytes of a base64 label document (`atob`: no Node `Buffer`, it ships to browsers). */
470
574
  export declare function decodeBase64(base64: string): Uint8Array;
575
+ interface RawTransitionRefusal {
576
+ errorCode: string;
577
+ message: string;
578
+ fromState?: string | null;
579
+ toState?: string | null;
580
+ transitionError?: string | null;
581
+ }
582
+ /**
583
+ * A refused move as the coded error it is. Core carries a step's refusal only as text, so the
584
+ * step's own code is read off its `[CODE]` prefix and classed through the GraphQL dialect, as the
585
+ * warehouse API's refusals of the same codes are (`warehouseRefusalError`): one code, one class,
586
+ * whichever door answered it.
587
+ */
588
+ export declare function fulfillmentTransitionRefusal(result: RawTransitionRefusal): PlatformError;
471
589
  export declare function createFulfillmentsModule(transport: Transport): FulfillmentsModule;