@sixtyninecommerce/admin 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1509 -28
- package/dist/auth/index.d.ts +7 -1
- package/dist/auth/index.js +8 -1
- package/dist/catalog/documents.d.ts +142 -0
- package/dist/catalog/documents.js +114 -0
- package/dist/catalog/index.d.ts +48 -7
- package/dist/catalog/index.js +6 -1
- package/dist/catalog/metadata.d.ts +244 -0
- package/dist/catalog/metadata.js +220 -0
- package/dist/catalog/properties.d.ts +38 -0
- package/dist/catalog/properties.js +52 -1
- package/dist/catalog/recommendations.d.ts +33 -6
- package/dist/catalog/recommendations.js +94 -2
- package/dist/catalog/reviews.d.ts +17 -4
- package/dist/catalog/reviews.js +7 -0
- package/dist/catalog/variants.d.ts +60 -0
- package/dist/catalog/variants.js +39 -0
- package/dist/catalog/visibility.d.ts +18 -2
- package/dist/client.d.ts +29 -3
- package/dist/client.js +19 -8
- package/dist/content/assets.d.ts +3 -4
- package/dist/content/forms.d.ts +332 -1
- package/dist/content/forms.js +140 -4
- package/dist/content/items.d.ts +31 -8
- package/dist/content/items.js +1 -1
- package/dist/content/menus.d.ts +254 -0
- package/dist/content/menus.js +70 -0
- package/dist/content/strings.d.ts +137 -0
- package/dist/content/strings.js +82 -0
- package/dist/customers/delivery-terms.d.ts +44 -0
- package/dist/customers/delivery-terms.js +32 -0
- package/dist/customers/groups.d.ts +16 -5
- package/dist/customers/groups.js +6 -2
- package/dist/customers/index.d.ts +15 -0
- package/dist/customers/index.js +4 -0
- package/dist/documents/emails.d.ts +154 -0
- package/dist/documents/emails.js +220 -0
- package/dist/documents/index.d.ts +13 -0
- package/dist/documents/index.js +2 -0
- package/dist/fulfillments/create.d.ts +1 -1
- package/dist/fulfillments/create.js +5 -2
- package/dist/fulfillments/index.d.ts +119 -1
- package/dist/fulfillments/index.js +124 -4
- package/dist/fulfillments/warehouse.d.ts +29 -17
- package/dist/fulfillments/warehouse.js +32 -1
- package/dist/index.d.ts +43 -23
- package/dist/index.js +11 -5
- package/dist/integrations/exact-online-types.d.ts +542 -0
- package/dist/integrations/exact-online.d.ts +541 -5
- package/dist/integrations/exact-online.js +714 -1
- package/dist/integrations/index.d.ts +23 -8
- package/dist/integrations/index.js +9 -4
- package/dist/integrations/pipeline-discoveries.d.ts +65 -0
- package/dist/integrations/pipeline-discoveries.js +65 -0
- package/dist/integrations/pipeline-runs.d.ts +119 -0
- package/dist/integrations/pipeline-runs.js +87 -0
- package/dist/integrations/pipelines.d.ts +73 -2
- package/dist/integrations/pipelines.js +36 -1
- package/dist/integrations/price-import.d.ts +223 -0
- package/dist/integrations/price-import.js +124 -0
- package/dist/integrations/types.d.ts +221 -3
- package/dist/integrations/types.js +4 -3
- package/dist/meta/index.d.ts +10 -0
- package/dist/metafields/index.d.ts +8 -1
- package/dist/metafields/index.js +10 -0
- package/dist/orders/cancel-with-money.d.ts +10 -0
- package/dist/orders/cancel-with-money.js +2 -0
- package/dist/orders/commit-edit.d.ts +16 -0
- package/dist/orders/commit-edit.js +14 -1
- package/dist/orders/deposit-policy.d.ts +3 -1
- package/dist/orders/index.d.ts +22 -11
- package/dist/orders/index.js +5 -2
- package/dist/orders/list.d.ts +26 -5
- package/dist/orders/list.js +123 -13
- package/dist/orders/manual-discounts.d.ts +13 -5
- package/dist/orders/money-view.d.ts +149 -0
- package/dist/orders/money-view.js +99 -0
- package/dist/orders/plan-edit.d.ts +34 -2
- package/dist/orders/plan-edit.js +110 -12
- package/dist/orders/surcharges.d.ts +12 -0
- package/dist/orders/surcharges.js +34 -9
- package/dist/payments/account-credit.d.ts +17 -1
- package/dist/payments/account-credit.js +25 -2
- package/dist/payments/credit-noted-fields.d.ts +8 -1
- package/dist/payments/credit-noted-fields.js +34 -2
- package/dist/payments/customer-consent.d.ts +30 -0
- package/dist/payments/customer-consent.js +49 -0
- package/dist/payments/index.d.ts +89 -14
- package/dist/payments/index.js +22 -2
- package/dist/payments/requests.d.ts +6 -3
- package/dist/payments/requests.js +1 -0
- package/dist/pickups/index.d.ts +5 -3
- package/dist/pickups/index.js +5 -2
- package/dist/platform/email-gateways.d.ts +165 -0
- package/dist/platform/email-gateways.js +78 -0
- package/dist/platform/tax.d.ts +11 -3
- package/dist/platform/tax.js +30 -0
- package/dist/pos/exchanges.d.ts +6 -1
- package/dist/pos/index.d.ts +229 -10
- package/dist/pos/index.js +113 -4
- package/dist/pos/offline-sales.d.ts +44 -3
- package/dist/pos/offline-sales.js +57 -3
- package/dist/pos/returns.d.ts +28 -1
- package/dist/pos/returns.js +36 -6
- package/dist/pricing/price-lists.d.ts +3 -1
- package/dist/purchasing/alerts.d.ts +19 -4
- package/dist/purchasing/alerts.js +29 -3
- package/dist/purchasing/backorders.d.ts +61 -0
- package/dist/purchasing/backorders.js +92 -0
- package/dist/purchasing/index.d.ts +21 -3
- package/dist/purchasing/index.js +8 -0
- package/dist/purchasing/links.d.ts +263 -0
- package/dist/purchasing/links.js +192 -0
- package/dist/purchasing/purchase-orders.d.ts +4 -1
- package/dist/purchasing/receipts.d.ts +396 -0
- package/dist/purchasing/receipts.js +287 -0
- package/dist/purchasing/returns.d.ts +273 -0
- package/dist/purchasing/returns.js +229 -0
- package/dist/quotes/index.d.ts +20 -3
- package/dist/returns/index.d.ts +62 -1
- package/dist/returns/index.js +61 -0
- package/dist/seo/config.d.ts +128 -0
- package/dist/seo/config.js +95 -0
- package/dist/seo/redirects.d.ts +125 -0
- package/dist/seo/redirects.js +60 -1
- package/dist/seo/settings.d.ts +147 -0
- package/dist/seo/settings.js +209 -0
- package/dist/shipping/index.d.ts +6 -0
- package/dist/shipping/index.js +2 -0
- package/dist/shipping/methods.d.ts +8 -1
- package/dist/shipping/myparcel.d.ts +415 -1
- package/dist/shipping/myparcel.js +291 -3
- package/dist/shipping/rate-tables.d.ts +149 -4
- package/dist/shipping/rate-tables.js +229 -23
- package/dist/shipping/ship-to-countries.d.ts +75 -0
- package/dist/shipping/ship-to-countries.js +47 -0
- package/dist/stock/by-location.d.ts +86 -0
- package/dist/stock/by-location.js +94 -0
- package/dist/stock/index.d.ts +8 -1
- package/dist/stock/index.js +4 -0
- package/dist/stock/settings.d.ts +106 -0
- package/dist/stock/settings.js +103 -0
- package/dist/stock/transfers.d.ts +171 -10
- package/dist/stock/transfers.js +82 -4
- package/dist/stored-value/index.d.ts +98 -7
- package/dist/stored-value/index.js +71 -4
- package/dist/tax/display-policy.d.ts +105 -0
- package/dist/tax/display-policy.js +84 -0
- package/dist/tax/index.d.ts +3 -0
- package/dist/tax/index.js +2 -0
- package/dist/tenant/email-templates.d.ts +222 -1
- package/dist/tenant/email-templates.js +227 -10
- package/package.json +2 -2
|
@@ -25,6 +25,13 @@ export interface CustomerGroup {
|
|
|
25
25
|
canViewIncomingStock: boolean;
|
|
26
26
|
/** Members may order more than is in stock, within the store's or the variant's backorder policy. */
|
|
27
27
|
canBackorder: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* "View only": signed-in members see the store's retail prices including VAT (the prices an
|
|
30
|
+
* anonymous shopper sees, never a price list's) and can fill a basket on the storefront, but
|
|
31
|
+
* checkout, payments and quote requests are refused (`VIEW_ONLY_ACCOUNT`). Staff can still
|
|
32
|
+
* place orders for them. The Exact customer import never links a customer to such a group.
|
|
33
|
+
*/
|
|
34
|
+
viewOnly: boolean;
|
|
28
35
|
/**
|
|
29
36
|
* The store's own group, or its organization's (shared while the organization shares
|
|
30
37
|
* `customer_groups`); see {@link CatalogueOwnership}. `editableHere` false: rename it, change
|
|
@@ -91,6 +98,7 @@ export interface CustomerGroupListItem {
|
|
|
91
98
|
name: string;
|
|
92
99
|
canViewIncomingStock: boolean;
|
|
93
100
|
canBackorder: boolean;
|
|
101
|
+
viewOnly: boolean;
|
|
94
102
|
/**
|
|
95
103
|
* How many of the store's customers are in the group, present only when the call asked for
|
|
96
104
|
* `include: ['memberCount']`.
|
|
@@ -134,12 +142,15 @@ export interface CreateCustomerGroupInput {
|
|
|
134
142
|
canViewIncomingStock?: boolean;
|
|
135
143
|
/** See {@link CustomerGroup.canBackorder}. Off when omitted. */
|
|
136
144
|
canBackorder?: boolean;
|
|
145
|
+
/** See {@link CustomerGroup.viewOnly}. Off when omitted. */
|
|
146
|
+
viewOnly?: boolean;
|
|
137
147
|
}
|
|
138
148
|
/** A PATCH: an absent field keeps what the group has. */
|
|
139
149
|
export interface UpdateCustomerGroupInput {
|
|
140
150
|
name?: string;
|
|
141
151
|
canViewIncomingStock?: boolean;
|
|
142
152
|
canBackorder?: boolean;
|
|
153
|
+
viewOnly?: boolean;
|
|
143
154
|
}
|
|
144
155
|
/** A group by id and name. */
|
|
145
156
|
export interface CustomerGroupRef {
|
|
@@ -168,9 +179,9 @@ export interface CustomerB2BGroupsModule {
|
|
|
168
179
|
set(groupIds: readonly string[], options?: Call): Promise<CustomerGroupRef[]>;
|
|
169
180
|
}
|
|
170
181
|
/** A customer group as `get`, `create` and `update` read it. */
|
|
171
|
-
export declare const CUSTOMER_GROUP_FIELDS = "\n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n";
|
|
172
|
-
export declare const CUSTOMER_GROUP_QUERY = "\n query CustomerGroup($id: ID!) {\n customerGroup(id: $id) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
173
|
-
export declare const CUSTOMER_GROUP_MEMBERS_QUERY = "\n query CustomerGroupWithMembers($id: ID!, $members: CustomerListOptions) {\n customerGroup(id: $id) {\n \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n\n customers(options: $members) {\n items {\n id\n createdAt\n firstName\n lastName\n emailAddress\n addresses {\n company\n defaultBillingAddress\n defaultShippingAddress\n }\n }\n totalItems\n }\n }\n }\n";
|
|
182
|
+
export declare const CUSTOMER_GROUP_FIELDS = "\n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n viewOnly\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n";
|
|
183
|
+
export declare const CUSTOMER_GROUP_QUERY = "\n query CustomerGroup($id: ID!) {\n customerGroup(id: $id) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n viewOnly\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
184
|
+
export declare const CUSTOMER_GROUP_MEMBERS_QUERY = "\n query CustomerGroupWithMembers($id: ID!, $members: CustomerListOptions) {\n customerGroup(id: $id) {\n \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n viewOnly\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n\n customers(options: $members) {\n items {\n id\n createdAt\n firstName\n lastName\n emailAddress\n addresses {\n company\n defaultBillingAddress\n defaultShippingAddress\n }\n }\n totalItems\n }\n }\n }\n";
|
|
174
185
|
/**
|
|
175
186
|
* The list document for a set of blocks. The member count is the `totalItems` of a one-row page
|
|
176
187
|
* of the group's customers: a `take` of 0 would make that page every member.
|
|
@@ -184,8 +195,8 @@ export declare const CUSTOMER_GROUP_MEMBERS_QUERY = "\n query CustomerGroupWith
|
|
|
184
195
|
* fix is `omit: ['customers']` on those two resolvers, which this package cannot make.
|
|
185
196
|
*/
|
|
186
197
|
export declare function customerGroupListQuery(include: readonly CustomerGroupListInclude[]): string;
|
|
187
|
-
export declare const CREATE_CUSTOMER_GROUP_MUTATION = "\n mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {\n createCustomerGroup(input: $input) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
188
|
-
export declare const UPDATE_CUSTOMER_GROUP_MUTATION = "\n mutation UpdateCustomerGroup($input: UpdateCustomerGroupInput!) {\n updateCustomerGroup(input: $input) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
198
|
+
export declare const CREATE_CUSTOMER_GROUP_MUTATION = "\n mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {\n createCustomerGroup(input: $input) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n viewOnly\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
199
|
+
export declare const UPDATE_CUSTOMER_GROUP_MUTATION = "\n mutation UpdateCustomerGroup($input: UpdateCustomerGroupInput!) {\n updateCustomerGroup(input: $input) { \n id\n createdAt\n updatedAt\n name\n externalKey\n \n customFields {\n canViewIncomingStock\n canBackorder\n viewOnly\n }\n\n \n catalogueOwnership {\n state\n organizationName\n editableHere\n }\n\n }\n }\n";
|
|
189
200
|
export declare const DELETE_CUSTOMER_GROUP_MUTATION = "\n mutation DeleteCustomerGroup($id: ID!) {\n deleteCustomerGroup(id: $id) { \n result\n message\n }\n }\n";
|
|
190
201
|
export declare const DELETE_CUSTOMER_GROUPS_MUTATION = "\n mutation DeleteCustomerGroups($ids: [ID!]!) {\n deleteCustomerGroups(ids: $ids) { \n result\n message\n }\n }\n";
|
|
191
202
|
export declare const B2B_CUSTOMER_GROUPS_QUERY = "\n query CustomerGroupRefs {\n b2bCustomerGroups {\n id\n name\n }\n }\n";
|
package/dist/customers/groups.js
CHANGED
|
@@ -16,10 +16,13 @@ export const MAX_CUSTOMER_GROUP_LIST_TAKE = 1000;
|
|
|
16
16
|
export const CUSTOMER_GROUP_LIST_SORT_FIELDS = ['name', 'createdAt', 'updatedAt'];
|
|
17
17
|
/** The blocks a list row can add: `memberCount`, how many of the store's customers are in it. */
|
|
18
18
|
export const CUSTOMER_GROUP_LIST_INCLUDES = ['memberCount'];
|
|
19
|
+
/** The group's boolean flags, each a `CustomerGroup.customFields` field of the same name. */
|
|
20
|
+
const GROUP_FLAGS = ['canViewIncomingStock', 'canBackorder', 'viewOnly'];
|
|
19
21
|
const GROUP_FLAG_FIELDS = /* GraphQL */ `
|
|
20
22
|
customFields {
|
|
21
23
|
canViewIncomingStock
|
|
22
24
|
canBackorder
|
|
25
|
+
viewOnly
|
|
23
26
|
}
|
|
24
27
|
`;
|
|
25
28
|
/** A customer group as `get`, `create` and `update` read it. */
|
|
@@ -144,11 +147,12 @@ const isPageSize = (take) => typeof take === 'number' &&
|
|
|
144
147
|
take >= 1 &&
|
|
145
148
|
take <= MAX_CUSTOMER_GROUP_LIST_TAKE;
|
|
146
149
|
const isOffset = (skip) => typeof skip === 'number' && Number.isInteger(skip) && skip >= 0;
|
|
147
|
-
/**
|
|
150
|
+
/** Every flag is `false` unless the engine answers `true`: a group has no third state. */
|
|
148
151
|
function flagsOf(wire) {
|
|
149
152
|
return {
|
|
150
153
|
canViewIncomingStock: wire.customFields?.canViewIncomingStock === true,
|
|
151
154
|
canBackorder: wire.customFields?.canBackorder === true,
|
|
155
|
+
viewOnly: wire.customFields?.viewOnly === true,
|
|
152
156
|
};
|
|
153
157
|
}
|
|
154
158
|
function toCustomerGroup(wire) {
|
|
@@ -341,7 +345,7 @@ export async function listCustomerGroups(transport, options) {
|
|
|
341
345
|
/** Only the flags the caller set travel: an omitted flag is never sent as `null`. */
|
|
342
346
|
function flagFields(input) {
|
|
343
347
|
const customFields = {};
|
|
344
|
-
for (const flag of
|
|
348
|
+
for (const flag of GROUP_FLAGS) {
|
|
345
349
|
const value = input[flag];
|
|
346
350
|
if (value === undefined)
|
|
347
351
|
continue;
|
|
@@ -6,6 +6,7 @@ import type { Call, ListOptions } from '../types.js';
|
|
|
6
6
|
import { type CustomerAgeVerification, type CustomerDateOfBirthModule } from './age-verification.js';
|
|
7
7
|
import { type CustomerCreditModule } from './credit.js';
|
|
8
8
|
import { type CustomerDeliveryPreferencesModule } from './delivery-preferences.js';
|
|
9
|
+
import { type CustomerDeliveryTerms } from './delivery-terms.js';
|
|
9
10
|
import { type CreateCustomerGroupInput, type CustomerB2BGroupsModule, type CustomerGroup, type CustomerGroupListItem, type CustomerGroupListOptions, type CustomerGroupMembersOptions, type CustomerGroupRef, type CustomerGroupWithMembers, type UpdateCustomerGroupInput } from './groups.js';
|
|
10
11
|
import { type CustomerInvitationStatus } from './invitation-status.js';
|
|
11
12
|
import { type CustomerNotesModule } from './notes.js';
|
|
@@ -1026,6 +1027,20 @@ export interface CustomersModule {
|
|
|
1026
1027
|
* @effect read
|
|
1027
1028
|
*/
|
|
1028
1029
|
savedLists(customerId: string, options?: Call): Promise<CustomerSavedList[] | null>;
|
|
1030
|
+
/**
|
|
1031
|
+
* The customer's delivery terms as an order placed now would take them: the preferred and
|
|
1032
|
+
* allowed shipping methods, the Incoterm, the dispatch lead time and whether a partial delivery
|
|
1033
|
+
* is accepted, from whichever source governs them (an ERP such as Exact Online). `null` when
|
|
1034
|
+
* nothing governs the customer's delivery. A local read of what the source last synchronized;
|
|
1035
|
+
* it calls no ERP.
|
|
1036
|
+
*
|
|
1037
|
+
* Refused `USER_INPUT_ERROR` (`ValidationError`, 400) for a customer this store does not hold
|
|
1038
|
+
* (another store's included). Needs `ReadCustomer`.
|
|
1039
|
+
*
|
|
1040
|
+
* @effect read
|
|
1041
|
+
* @permission ReadCustomer
|
|
1042
|
+
*/
|
|
1043
|
+
deliveryTerms(customerId: string, options?: Call): Promise<CustomerDeliveryTerms | null>;
|
|
1029
1044
|
groups: CustomerGroupsModule;
|
|
1030
1045
|
addresses: CustomerAddressesModule;
|
|
1031
1046
|
/** A customer's B2B credit: account, limit, terms, status and what may be invoiced. */
|
package/dist/customers/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { listCustomerOrders, listCustomerRecentOrders, } from '../orders/list.js
|
|
|
6
6
|
import { createCustomerDateOfBirthModule, readCustomerAgeVerification, } from './age-verification.js';
|
|
7
7
|
import { createCustomerCreditModule } from './credit.js';
|
|
8
8
|
import { createCustomerDeliveryPreferencesModule, } from './delivery-preferences.js';
|
|
9
|
+
import { readCustomerDeliveryTerms } from './delivery-terms.js';
|
|
9
10
|
import { createCustomerB2BGroupsModule, createCustomerGroup, deleteCustomerGroup, deleteCustomerGroups, listCustomerGroups, readCustomerGroup, updateCustomerGroup, } from './groups.js';
|
|
10
11
|
import { readCustomerInvitationStatus, } from './invitation-status.js';
|
|
11
12
|
import { createCustomerNotesModule } from './notes.js';
|
|
@@ -616,6 +617,9 @@ export function createCustomersModule(transport) {
|
|
|
616
617
|
savedLists(customerId, options) {
|
|
617
618
|
return readCustomerSavedLists(transport, customerId, options);
|
|
618
619
|
},
|
|
620
|
+
deliveryTerms(customerId, options) {
|
|
621
|
+
return readCustomerDeliveryTerms(transport, customerId, options);
|
|
622
|
+
},
|
|
619
623
|
// The answer holds exactly the blocks asked for; `CustomerFor` decides how many of them the
|
|
620
624
|
// caller's type of `include` may rely on. The overloads narrow this one implementation, which
|
|
621
625
|
// TypeScript cannot check for them.
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
/**
|
|
36
|
+
* Why the last attempt failed, as a coded sentence: `CODE: not sent (facts). Plain sentence.`
|
|
37
|
+
* (`SMTP_SEND_FAILED: not sent (code=EAUTH step=AUTH PLAIN reply=535). …`). Never the
|
|
38
|
+
* provider's or the mail server's own words, which can quote the gateway's user name or hosts.
|
|
39
|
+
*/
|
|
40
|
+
error: string | null;
|
|
41
|
+
durationMs: number | null;
|
|
42
|
+
/** The ids the email was rendered from (`orderId`, `invoiceId`, …) and other send facts. */
|
|
43
|
+
metadata: Record<string, unknown> | null;
|
|
44
|
+
/** ISO-8601 UTC. */
|
|
45
|
+
createdAt: string;
|
|
46
|
+
/**
|
|
47
|
+
* Set when the caller's customer-data level withheld part of the row (an app installation
|
|
48
|
+
* below `customerData: full`): the recipients, subject, error and metadata then read `''` or
|
|
49
|
+
* `null`, meaning "not granted", never "none on record".
|
|
50
|
+
*/
|
|
51
|
+
redacted?: true;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which deliveries a list or a count covers. The two instants need an explicit offset (`Z` or
|
|
55
|
+
* `±HH:mm`) and are refused before the request leaves without one (`INVALID_DATETIME`): a store's
|
|
56
|
+
* day is not the caller's. To cover the store's days, send the first instant of the first day as
|
|
57
|
+
* `createdFrom` and the first instant of the day AFTER the last one as `createdBefore`
|
|
58
|
+
* ({@link EmailDeliveriesModule.list}).
|
|
59
|
+
*/
|
|
60
|
+
export interface EmailDeliveryFilter {
|
|
61
|
+
status?: EmailDeliveryStatus;
|
|
62
|
+
templateType?: string;
|
|
63
|
+
/** Created at or after this instant. */
|
|
64
|
+
createdFrom?: string;
|
|
65
|
+
/** Created strictly before this instant. */
|
|
66
|
+
createdBefore?: string;
|
|
67
|
+
}
|
|
68
|
+
/** How many of the store's deliveries are in each state, over a filter. */
|
|
69
|
+
export interface EmailDeliveryStats {
|
|
70
|
+
sent: number;
|
|
71
|
+
failed: number;
|
|
72
|
+
/** Never sent: every recipient is on the mail provider's suppression list. */
|
|
73
|
+
blocked: number;
|
|
74
|
+
pending: number;
|
|
75
|
+
}
|
|
76
|
+
export interface SendTestEmailInput {
|
|
77
|
+
/** A template type the platform renders (`admin.tenant.emailTemplates.catalog()`). */
|
|
78
|
+
templateType: string;
|
|
79
|
+
/** One email address. */
|
|
80
|
+
recipientEmail: string;
|
|
81
|
+
}
|
|
82
|
+
export interface EmailDeliveriesModule {
|
|
83
|
+
/**
|
|
84
|
+
* The store's deliveries, newest first, one page at a time (`limit` 1–100, 50 when omitted).
|
|
85
|
+
*
|
|
86
|
+
* **Store days.** The page that shows "from 24 to 25 October" in Europe/Amsterdam sends
|
|
87
|
+
* `createdFrom: '2026-10-24T00:00:00+02:00'` and `createdBefore: '2026-10-26T00:00:00+01:00'`:
|
|
88
|
+
* 25 October 2026 is 25 hours long there (the clocks go back), and the end is exclusive, so
|
|
89
|
+
* the whole last day is in and nothing of the next.
|
|
90
|
+
*
|
|
91
|
+
* **Coded refusals:** `INVALID_DATETIME` → `ValidationError` (400), an instant without an
|
|
92
|
+
* offset, before the request leaves; `EMAIL_DELIVERY_FILTER_INVALID` → `ValidationError`
|
|
93
|
+
* (400), a `limit` outside 1–100 or a range that ends before it starts.
|
|
94
|
+
*
|
|
95
|
+
* Rows carry `redacted: true` below `customerData: full` (see {@link EmailDelivery}).
|
|
96
|
+
*
|
|
97
|
+
* @effect read
|
|
98
|
+
* @permission UpdateSettings
|
|
99
|
+
*/
|
|
100
|
+
list(filter?: EmailDeliveryFilter, options?: ListOptions): Promise<Page<EmailDelivery>>;
|
|
101
|
+
/**
|
|
102
|
+
* How many deliveries are `sent`, `failed`, `blocked` and `pending`, over the same filter as
|
|
103
|
+
* {@link list} without its `status` (a count by status). Without a filter: the store's whole
|
|
104
|
+
* log. On an engine from before this release (no filter argument) a filtered count is made
|
|
105
|
+
* from the list's totals, one read per status.
|
|
106
|
+
*
|
|
107
|
+
* @effect read
|
|
108
|
+
* @permission UpdateSettings
|
|
109
|
+
*/
|
|
110
|
+
stats(filter?: Omit<EmailDeliveryFilter, 'status'>, options?: Call): Promise<EmailDeliveryStats>;
|
|
111
|
+
/**
|
|
112
|
+
* Send a `failed` or `blocked` delivery again, now, and answer the row with the new outcome:
|
|
113
|
+
* `status` is `sent`, `failed` again (with its `error`), `blocked` (the address is still on the
|
|
114
|
+
* provider's suppression list) or `suppressed_sandbox`. It resolves either way; read `status`.
|
|
115
|
+
* Synchronous: the email has been handed to the provider (or not) when it resolves.
|
|
116
|
+
*
|
|
117
|
+
* This sends the email to the CUSTOMER (or whoever it was addressed to) once more. Never call
|
|
118
|
+
* it in a loop or on a timer without a person deciding to.
|
|
119
|
+
*
|
|
120
|
+
* **Coded refusals** (nothing is sent): `EMAIL_DELIVERY_NOT_FOUND` → `NotFoundError` (404), no
|
|
121
|
+
* such delivery in this store; `EMAIL_DELIVERY_NOT_RETRYABLE` → `BusinessRuleError` (422), the
|
|
122
|
+
* delivery is not `failed` or `blocked`, it is a gift card's email (its code is not kept), or
|
|
123
|
+
* another retry of it started first.
|
|
124
|
+
*
|
|
125
|
+
* @effect irreversible
|
|
126
|
+
* @permission UpdateSettings
|
|
127
|
+
*/
|
|
128
|
+
retry(id: string, options?: Call): Promise<EmailDelivery>;
|
|
129
|
+
}
|
|
130
|
+
export interface DocumentEmailsModule {
|
|
131
|
+
deliveries: EmailDeliveriesModule;
|
|
132
|
+
/**
|
|
133
|
+
* Render `templateType` with sample data and send it, now, through the store's own email
|
|
134
|
+
* gateway, to `recipientEmail`. It is real mail. The send writes no delivery row.
|
|
135
|
+
*
|
|
136
|
+
* **Coded refusals:** `INVALID_TEST_EMAIL_RECIPIENT` and `INVALID_TEST_EMAIL_TEMPLATE` →
|
|
137
|
+
* `ValidationError` (400), before anything is sent; `TEST_EMAIL_SEND_FAILED` →
|
|
138
|
+
* `BusinessRuleError` (422), the gateway did not take it (the message names the SMTP step or
|
|
139
|
+
* the provider's code, never the server's own words).
|
|
140
|
+
*
|
|
141
|
+
* @effect irreversible
|
|
142
|
+
* @permission UpdateSettings
|
|
143
|
+
*/
|
|
144
|
+
sendTest(input: SendTestEmailInput, options?: Call): Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
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";
|
|
147
|
+
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";
|
|
148
|
+
/** The store's whole log, without the filter argument an older engine does not know. */
|
|
149
|
+
export declare const EMAIL_DELIVERY_STATS_ALL_QUERY = "\n query AdminEmailDeliveryStatsAll {\n emailDeliveryStats {\n sent\n failed\n blocked\n pending\n }\n }\n";
|
|
150
|
+
/** How many deliveries match: the older engine's way to count over a filter. */
|
|
151
|
+
export declare const EMAIL_DELIVERY_COUNT_QUERY = "\n query AdminEmailDeliveryCount($options: EmailDeliveryLogListOptions) {\n emailDeliveryLogs(options: $options) {\n totalItems\n }\n }\n";
|
|
152
|
+
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";
|
|
153
|
+
export declare const SEND_TEST_EMAIL_MUTATION = "\n mutation AdminSendTestEmail($templateType: String!, $recipientEmail: String!) {\n sendTestEmail(templateType: $templateType, recipientEmail: $recipientEmail)\n }\n";
|
|
154
|
+
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";
|
package/dist/documents/index.js
CHANGED
|
@@ -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);
|
|
@@ -162,6 +162,6 @@ export interface FulfillmentCreateMethods {
|
|
|
162
162
|
*/
|
|
163
163
|
profileChargePreview(input: FulfillmentProfileChargePreviewInput, options?: Call): Promise<FulfillmentProfileChargePreview>;
|
|
164
164
|
}
|
|
165
|
-
export declare const CREATE_WAREHOUSE_FULFILLMENTS_MUTATION = "\n mutation CreateWarehouseFulfillments(\n $input: CreateWarehouseFulfillmentsInput!\n $idempotencyKey: String\n ) {\n createWarehouseFulfillments(input: $input, idempotencyKey: $idempotencyKey) {\n __typename\n ... on WarehouseCreateResult {\n created\n failed\n results {\n index\n fulfillment {\n \n id\n state\n handlerCode\n method\n mode\n trackingCode\n stockLocationId\n stockLocationName\n createdAt\n stateChangedAt\n overdue\n claimedBy {\n id\n name\n }\n claimedAt\n packedAt\n bookingStatus\n bookingError\n parcels {\n weightGrams\n lengthMm\n widthMm\n heightMm\n }\n orders {\n id\n code\n poNumber\n orderNotes\n requestedDeliveryDate\n deliveryDue\n paymentBadge\n hold {\n reason\n note\n }\n }\n customer {\n id\n name\n phone\n }\n shippingAddress {\n \n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n countryCode\n\n }\n lineCount\n unitCount\n\n }\n error {\n code\n message\n field\n }\n claimError {\n code\n message\n field\n }\n }\n }\n ... on WarehouseError {\n errorCode\n code\n message\n field\n }\n }\n }\n";
|
|
165
|
+
export declare const CREATE_WAREHOUSE_FULFILLMENTS_MUTATION = "\n mutation CreateWarehouseFulfillments(\n $input: CreateWarehouseFulfillmentsInput!\n $idempotencyKey: String\n ) {\n createWarehouseFulfillments(input: $input, idempotencyKey: $idempotencyKey) {\n __typename\n ... on WarehouseCreateResult {\n created\n failed\n results {\n index\n fulfillment {\n \n id\n state\n handlerCode\n method\n mode\n trackingCode\n stockLocationId\n stockLocationName\n dropShipSupplierName\n createdAt\n stateChangedAt\n overdue\n claimedBy {\n id\n name\n }\n claimedAt\n packedAt\n bookingStatus\n bookingError\n parcels {\n weightGrams\n lengthMm\n widthMm\n heightMm\n }\n orders {\n id\n code\n poNumber\n orderNotes\n requestedDeliveryDate\n deliveryDue\n paymentBadge\n hold {\n reason\n note\n }\n }\n customer {\n id\n name\n phone\n }\n shippingAddress {\n \n fullName\n company\n streetLine1\n streetLine2\n city\n province\n postalCode\n countryCode\n\n }\n lineCount\n unitCount\n\n }\n error {\n code\n message\n field\n }\n claimError {\n code\n message\n field\n }\n }\n }\n ... on WarehouseError {\n errorCode\n code\n message\n field\n }\n }\n }\n";
|
|
166
166
|
export declare const FULFILLMENT_PROFILE_CHARGE_PREVIEW_QUERY = "\n query FulfillmentProfileChargePreview($input: FulfillmentProfileChargePreviewInput!) {\n fulfillmentProfileChargePreview(input: $input) {\n profileName\n currencyCode\n minOrder\n selectedNet\n shortfall\n deliveryCharge\n deliveryChargeIncludesTax\n settled\n handedOver\n }\n }\n";
|
|
167
167
|
export declare function createFulfillmentCreateMethods(transport: Transport): FulfillmentCreateMethods;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 400, never a 422). Each group's own refusal is data in `results[].error`.
|
|
10
10
|
*/
|
|
11
11
|
import { idempotencyKey as mintIdempotencyKey, ValidationError, } from '@sixtyninecommerce/core';
|
|
12
|
-
import { assertId, FULFILLMENT_FIELDS, invalid, isWarehouseErrorMember, toFulfillment, toRefusal, warehouseRefusalError, withAttemptKey, } from './warehouse.js';
|
|
12
|
+
import { assertId, dropShipSupplierAware, FULFILLMENT_FIELDS, invalid, isWarehouseErrorMember, toFulfillment, toRefusal, warehouseRefusalError, withAttemptKey, } from './warehouse.js';
|
|
13
13
|
export const CREATE_WAREHOUSE_FULFILLMENTS_MUTATION = /* GraphQL */ `
|
|
14
14
|
mutation CreateWarehouseFulfillments(
|
|
15
15
|
$input: CreateWarehouseFulfillmentsInput!
|
|
@@ -134,12 +134,15 @@ function assertPreviewInput(input) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
export function createFulfillmentCreateMethods(transport) {
|
|
137
|
+
// The create answers the warehouse's fulfilment fields, `dropShipSupplierName` among them: an
|
|
138
|
+
// engine before purchase-orders step 5.5 refuses that field before the mutation runs.
|
|
139
|
+
const engine = dropShipSupplierAware(transport);
|
|
137
140
|
return {
|
|
138
141
|
async create(input, options) {
|
|
139
142
|
assertCreateInput(input);
|
|
140
143
|
const key = options?.idempotencyKey ?? mintIdempotencyKey();
|
|
141
144
|
try {
|
|
142
|
-
const data = await
|
|
145
|
+
const data = await engine.graphql({
|
|
143
146
|
api: 'admin',
|
|
144
147
|
query: CREATE_WAREHOUSE_FULFILLMENTS_MUTATION,
|
|
145
148
|
variables: {
|