@sixtyninecommerce/admin 0.10.0 → 0.11.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 +4347 -171
- package/dist/catalog/assets.d.ts +120 -8
- package/dist/catalog/assets.js +98 -12
- package/dist/catalog/collections.d.ts +279 -14
- package/dist/catalog/collections.js +246 -15
- package/dist/catalog/index.d.ts +9 -2
- package/dist/catalog/index.js +4 -0
- package/dist/catalog/products.d.ts +159 -19
- package/dist/catalog/products.js +88 -13
- package/dist/catalog/properties.d.ts +102 -14
- package/dist/catalog/properties.js +13 -0
- package/dist/catalog/recommendations.d.ts +261 -0
- package/dist/catalog/recommendations.js +145 -0
- package/dist/catalog/reviews.d.ts +368 -0
- package/dist/catalog/reviews.js +318 -0
- package/dist/catalog/types.d.ts +17 -1
- package/dist/catalog/types.js +10 -0
- package/dist/catalog/variant-find.d.ts +136 -0
- package/dist/catalog/variant-find.js +239 -0
- package/dist/catalog/variants.d.ts +233 -26
- package/dist/catalog/variants.js +125 -7
- package/dist/catalog/visibility.d.ts +20 -5
- package/dist/catalog/visibility.js +19 -1
- package/dist/client.d.ts +95 -9
- package/dist/client.js +35 -7
- package/dist/content/assets.d.ts +286 -0
- package/dist/content/assets.js +181 -0
- package/dist/content/items.d.ts +269 -0
- package/dist/content/items.js +170 -0
- package/dist/content/types.d.ts +282 -0
- package/dist/content/types.js +100 -0
- package/dist/customers/age-verification.d.ts +66 -0
- package/dist/customers/age-verification.js +79 -0
- package/dist/customers/credit.d.ts +263 -0
- package/dist/customers/credit.js +244 -0
- package/dist/customers/delivery-preferences.d.ts +120 -0
- package/dist/customers/delivery-preferences.js +97 -0
- package/dist/customers/groups.d.ts +207 -0
- package/dist/customers/groups.js +438 -0
- package/dist/customers/index.d.ts +447 -37
- package/dist/customers/index.js +313 -40
- package/dist/customers/invitation-status.d.ts +57 -0
- package/dist/customers/invitation-status.js +21 -0
- package/dist/customers/notes.d.ts +67 -0
- package/dist/customers/notes.js +51 -0
- package/dist/customers/notification-recipients.d.ts +97 -0
- package/dist/customers/notification-recipients.js +84 -0
- package/dist/customers/saved-lists.d.ts +42 -0
- package/dist/customers/saved-lists.js +61 -0
- package/dist/customers/vat.d.ts +83 -0
- package/dist/customers/vat.js +46 -0
- package/dist/customers/wire.d.ts +20 -0
- package/dist/customers/wire.js +35 -0
- package/dist/documents/ar.d.ts +307 -0
- package/dist/documents/ar.js +254 -0
- package/dist/documents/index.d.ts +731 -21
- package/dist/documents/index.js +436 -24
- package/dist/documents/pdf.d.ts +2 -0
- package/dist/documents/pdf.js +11 -0
- package/dist/fulfillments/create.d.ts +167 -0
- package/dist/fulfillments/create.js +216 -0
- package/dist/fulfillments/index.d.ts +44 -10
- package/dist/fulfillments/index.js +11 -2
- package/dist/fulfillments/payment-policy.d.ts +64 -0
- package/dist/fulfillments/payment-policy.js +140 -0
- package/dist/fulfillments/warehouse.d.ts +724 -0
- package/dist/fulfillments/warehouse.js +914 -0
- package/dist/geo/index.d.ts +119 -0
- package/dist/geo/index.js +107 -0
- package/dist/index.d.ts +123 -35
- package/dist/index.js +45 -12
- package/dist/integrations/afas-actions.d.ts +234 -0
- package/dist/integrations/afas-actions.js +310 -0
- package/dist/integrations/afas-types.d.ts +483 -0
- package/dist/integrations/afas-types.js +5 -0
- package/dist/integrations/afas.d.ts +234 -0
- package/dist/integrations/afas.js +290 -0
- package/dist/integrations/connections.d.ts +103 -0
- package/dist/integrations/connections.js +55 -0
- package/dist/integrations/esp.d.ts +313 -0
- package/dist/integrations/esp.js +169 -0
- package/dist/integrations/exact-online-types.d.ts +423 -0
- package/dist/integrations/exact-online-types.js +6 -0
- package/dist/integrations/exact-online.d.ts +248 -0
- package/dist/integrations/exact-online.js +376 -0
- package/dist/integrations/index.d.ts +29 -3
- package/dist/integrations/index.js +18 -3
- package/dist/integrations/pipeline-keys.d.ts +13 -0
- package/dist/integrations/pipeline-keys.js +22 -0
- package/dist/integrations/pipelines.d.ts +15 -14
- package/dist/integrations/pipelines.js +6 -22
- package/dist/integrations/types.d.ts +146 -0
- package/dist/locations/index.d.ts +66 -0
- package/dist/locations/index.js +31 -0
- package/dist/meta/index.d.ts +162 -0
- package/dist/orders/cancel-with-money.d.ts +227 -0
- package/dist/orders/cancel-with-money.js +153 -0
- package/dist/orders/commit-edit.d.ts +7 -1
- package/dist/orders/commit-edit.js +5 -1
- package/dist/orders/drafts.d.ts +800 -0
- package/dist/orders/drafts.js +771 -0
- package/dist/orders/fields.d.ts +245 -0
- package/dist/orders/fields.js +188 -0
- package/dist/orders/get.d.ts +29 -19
- package/dist/orders/get.js +17 -8
- package/dist/orders/index.d.ts +49 -9
- package/dist/orders/index.js +22 -2
- package/dist/orders/lifecycle.d.ts +128 -0
- package/dist/orders/lifecycle.js +158 -0
- package/dist/orders/line-price-preview.d.ts +7 -2
- package/dist/orders/list.d.ts +222 -0
- package/dist/orders/list.js +377 -0
- package/dist/orders/manual-pricing.d.ts +5 -0
- package/dist/orders/order-entry.d.ts +20 -7
- package/dist/orders/plan-edit.d.ts +1 -1
- package/dist/orders/plan-edit.js +4 -1
- package/dist/orders/settings.d.ts +201 -0
- package/dist/orders/settings.js +325 -0
- package/dist/orders/status.d.ts +159 -13
- package/dist/orders/status.js +21 -0
- package/dist/orders/surcharges.d.ts +8 -1
- package/dist/orders/surcharges.js +3 -2
- package/dist/payments/account-credit.d.ts +30 -0
- package/dist/payments/account-credit.js +19 -0
- package/dist/payments/ar.d.ts +292 -0
- package/dist/payments/ar.js +223 -0
- package/dist/payments/connected-account.d.ts +47 -0
- package/dist/payments/connected-account.js +61 -0
- package/dist/payments/credit-noted-fields.d.ts +12 -0
- package/dist/payments/credit-noted-fields.js +37 -0
- package/dist/payments/index.d.ts +257 -11
- package/dist/payments/index.js +131 -1
- package/dist/payments/methods.d.ts +303 -8
- package/dist/payments/methods.js +337 -7
- package/dist/payments/mollie-methods.d.ts +159 -0
- package/dist/payments/mollie-methods.js +141 -0
- package/dist/payments/payment-links.d.ts +180 -0
- package/dist/payments/payment-links.js +97 -0
- package/dist/payments/provider-keys.d.ts +150 -0
- package/dist/payments/provider-keys.js +118 -0
- package/dist/payments/providers.d.ts +86 -11
- package/dist/payments/providers.js +29 -0
- package/dist/payments/requests.d.ts +144 -0
- package/dist/payments/requests.js +92 -0
- package/dist/payments/store-credit-fields.d.ts +15 -0
- package/dist/payments/store-credit-fields.js +51 -0
- package/dist/pickups/index.d.ts +104 -0
- package/dist/pickups/index.js +105 -0
- package/dist/platform/channels.d.ts +132 -0
- package/dist/platform/channels.js +128 -0
- package/dist/platform/geo.d.ts +123 -0
- package/dist/platform/geo.js +144 -0
- package/dist/platform/tax.d.ts +154 -0
- package/dist/platform/tax.js +139 -0
- package/dist/pos/anomalies.d.ts +158 -0
- package/dist/pos/anomalies.js +64 -0
- package/dist/pos/assisted-selling.d.ts +79 -0
- package/dist/pos/assisted-selling.js +38 -0
- package/dist/pos/exchanges.d.ts +103 -0
- package/dist/pos/exchanges.js +22 -0
- package/dist/pos/gift-cards.d.ts +135 -0
- package/dist/pos/gift-cards.js +47 -0
- package/dist/pos/index.d.ts +2125 -32
- package/dist/pos/index.js +742 -4
- package/dist/pos/no-receipt.d.ts +94 -0
- package/dist/pos/no-receipt.js +25 -0
- package/dist/pos/offline-sales.d.ts +187 -0
- package/dist/pos/offline-sales.js +116 -0
- package/dist/pos/pay-by-link.d.ts +143 -0
- package/dist/pos/pay-by-link.js +57 -0
- package/dist/pos/quick-keys.d.ts +103 -0
- package/dist/pos/quick-keys.js +45 -0
- package/dist/pos/reports.d.ts +360 -0
- package/dist/pos/reports.js +62 -0
- package/dist/pos/returns.d.ts +217 -0
- package/dist/pos/returns.js +53 -0
- package/dist/pricing/bulk-coupons.d.ts +139 -0
- package/dist/pricing/bulk-coupons.js +112 -0
- package/dist/pricing/index.d.ts +174 -4
- package/dist/pricing/index.js +174 -1
- package/dist/pricing/price-lists.d.ts +604 -0
- package/dist/pricing/price-lists.js +600 -0
- package/dist/purchasing/alerts.d.ts +89 -0
- package/dist/purchasing/alerts.js +62 -0
- package/dist/purchasing/cost.d.ts +110 -0
- package/dist/purchasing/cost.js +86 -0
- package/dist/purchasing/index.d.ts +36 -0
- package/dist/purchasing/index.js +16 -0
- package/dist/purchasing/purchase-orders.d.ts +548 -0
- package/dist/purchasing/purchase-orders.js +491 -0
- package/dist/purchasing/replenishment.d.ts +281 -0
- package/dist/purchasing/replenishment.js +240 -0
- package/dist/purchasing/settings.d.ts +152 -0
- package/dist/purchasing/settings.js +181 -0
- package/dist/purchasing/suppliers.d.ts +476 -0
- package/dist/purchasing/suppliers.js +438 -0
- package/dist/quotes/index.d.ts +501 -0
- package/dist/quotes/index.js +439 -0
- package/dist/reports/index.d.ts +71 -8
- package/dist/reports/index.js +74 -11
- package/dist/returns/index.d.ts +614 -21
- package/dist/returns/index.js +695 -11
- package/dist/search/index.d.ts +76 -1
- package/dist/search/index.js +63 -0
- package/dist/shipping/definitions.d.ts +29 -0
- package/dist/shipping/definitions.js +47 -0
- package/dist/shipping/fulfillment-profiles.d.ts +174 -0
- package/dist/shipping/fulfillment-profiles.js +104 -0
- package/dist/shipping/hendriks.d.ts +97 -0
- package/dist/shipping/hendriks.js +50 -0
- package/dist/shipping/index.d.ts +48 -0
- package/dist/shipping/index.js +25 -0
- package/dist/shipping/methods.d.ts +198 -0
- package/dist/shipping/methods.js +240 -0
- package/dist/shipping/myparcel.d.ts +254 -0
- package/dist/shipping/myparcel.js +178 -0
- package/dist/shipping/parcel-count-suggestion.d.ts +2 -0
- package/dist/shipping/parcel-rule.d.ts +6 -5
- package/dist/shipping/parcel-rule.js +21 -23
- package/dist/shipping/rate-tables.d.ts +179 -0
- package/dist/shipping/rate-tables.js +184 -0
- package/dist/stock/index.d.ts +10 -3
- package/dist/stock/index.js +3 -0
- package/dist/stock/locations.d.ts +144 -0
- package/dist/stock/locations.js +168 -0
- package/dist/stock/transfers.d.ts +216 -43
- package/dist/stock/transfers.js +140 -13
- package/dist/stored-value/index.d.ts +214 -4
- package/dist/stored-value/index.js +152 -0
- package/dist/subscriptions/index.d.ts +682 -0
- package/dist/subscriptions/index.js +815 -0
- package/dist/tax/index.d.ts +76 -1
- package/dist/tax/index.js +50 -0
- package/dist/tenant/email-style.d.ts +102 -0
- package/dist/tenant/email-style.js +72 -0
- package/dist/tenant/email-templates.d.ts +66 -0
- package/dist/tenant/email-templates.js +54 -0
- package/dist/tenant/notifications.d.ts +23 -17
- package/dist/tenant/settings-sources.d.ts +1 -1
- package/dist/tenant/time-zone.d.ts +17 -0
- package/dist/tenant/time-zone.js +11 -0
- package/dist/types.d.ts +50 -3
- package/dist/users/index.d.ts +3 -0
- package/dist/users/index.js +2 -0
- package/dist/users/warehouses.d.ts +50 -0
- package/dist/users/warehouses.js +51 -0
- package/package.json +2 -2
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer groups: the store's list, one group with a page of its members, create, change and
|
|
3
|
+
* delete, and which groups the store treats as trade (headless spec §9.2 `customers` "groups";
|
|
4
|
+
* W10-34). The membership doors (`add`, `remove`) and the customer master's external keys are
|
|
5
|
+
* wired beside these in `index.ts`.
|
|
6
|
+
*
|
|
7
|
+
* A group is the store's own row, or its organization's (`catalogueOwnership`): one row for every
|
|
8
|
+
* store that holds it. Its members are always the store's own customers.
|
|
9
|
+
*/
|
|
10
|
+
import { ConflictError, pageFromVendureList, readOffsetCursor, ValidationError, } from '@sixtyninecommerce/core';
|
|
11
|
+
import { assertDeleted, CATALOGUE_OWNERSHIP_FIELDS, DELETION_FIELDS, fieldOf, } from '../catalog/types.js';
|
|
12
|
+
import { redactionWatcher } from '../customer-data-redaction.js';
|
|
13
|
+
import { companyOf } from './wire.js';
|
|
14
|
+
/** The engine's `adminListQueryLimit`: a larger page is refused. */
|
|
15
|
+
export const MAX_CUSTOMER_GROUP_LIST_TAKE = 1000;
|
|
16
|
+
export const CUSTOMER_GROUP_LIST_SORT_FIELDS = ['name', 'createdAt', 'updatedAt'];
|
|
17
|
+
/** The blocks a list row can add: `memberCount`, how many of the store's customers are in it. */
|
|
18
|
+
export const CUSTOMER_GROUP_LIST_INCLUDES = ['memberCount'];
|
|
19
|
+
const GROUP_FLAG_FIELDS = /* GraphQL */ `
|
|
20
|
+
customFields {
|
|
21
|
+
canViewIncomingStock
|
|
22
|
+
canBackorder
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
/** A customer group as `get`, `create` and `update` read it. */
|
|
26
|
+
export const CUSTOMER_GROUP_FIELDS = /* GraphQL */ `
|
|
27
|
+
id
|
|
28
|
+
createdAt
|
|
29
|
+
updatedAt
|
|
30
|
+
name
|
|
31
|
+
externalKey
|
|
32
|
+
${GROUP_FLAG_FIELDS}
|
|
33
|
+
${CATALOGUE_OWNERSHIP_FIELDS}
|
|
34
|
+
`;
|
|
35
|
+
export const CUSTOMER_GROUP_QUERY = /* GraphQL */ `
|
|
36
|
+
query CustomerGroup($id: ID!) {
|
|
37
|
+
customerGroup(id: $id) { ${CUSTOMER_GROUP_FIELDS} }
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
export const CUSTOMER_GROUP_MEMBERS_QUERY = /* GraphQL */ `
|
|
41
|
+
query CustomerGroupWithMembers($id: ID!, $members: CustomerListOptions) {
|
|
42
|
+
customerGroup(id: $id) {
|
|
43
|
+
${CUSTOMER_GROUP_FIELDS}
|
|
44
|
+
customers(options: $members) {
|
|
45
|
+
items {
|
|
46
|
+
id
|
|
47
|
+
createdAt
|
|
48
|
+
firstName
|
|
49
|
+
lastName
|
|
50
|
+
emailAddress
|
|
51
|
+
addresses {
|
|
52
|
+
company
|
|
53
|
+
defaultBillingAddress
|
|
54
|
+
defaultShippingAddress
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
totalItems
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const GROUP_LIST_FIELDS = /* GraphQL */ `
|
|
63
|
+
id
|
|
64
|
+
createdAt
|
|
65
|
+
updatedAt
|
|
66
|
+
name
|
|
67
|
+
${GROUP_FLAG_FIELDS}
|
|
68
|
+
`;
|
|
69
|
+
/**
|
|
70
|
+
* The list document for a set of blocks. The member count is the `totalItems` of a one-row page
|
|
71
|
+
* of the group's customers: a `take` of 0 would make that page every member.
|
|
72
|
+
*
|
|
73
|
+
* The count is still not a one-row read on the engine. channel-customer's `customerGroups` and
|
|
74
|
+
* `customerGroup` resolvers carry core's bare `@Relations(CustomerGroup)`, so for a document that
|
|
75
|
+
* selects `customers` under a group they also hand the service the `customers` relation, and it
|
|
76
|
+
* loads every member of each answered group before the field resolver counts or pages them (read
|
|
77
|
+
* in the plugin and core 3.6.3, not measured; the dashboard's documents before this module had
|
|
78
|
+
* the same shape). So `memberCount` is an `include`, asked for only where a count is shown. The
|
|
79
|
+
* fix is `omit: ['customers']` on those two resolvers, which this package cannot make.
|
|
80
|
+
*/
|
|
81
|
+
export function customerGroupListQuery(include) {
|
|
82
|
+
const memberCount = include.includes('memberCount')
|
|
83
|
+
? 'customers(options: { take: 1 }) { totalItems }'
|
|
84
|
+
: '';
|
|
85
|
+
return /* GraphQL */ `
|
|
86
|
+
query CustomerGroups($options: CustomerGroupListOptions) {
|
|
87
|
+
customerGroups(options: $options) {
|
|
88
|
+
items {
|
|
89
|
+
${GROUP_LIST_FIELDS}
|
|
90
|
+
${memberCount}
|
|
91
|
+
}
|
|
92
|
+
totalItems
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
export const CREATE_CUSTOMER_GROUP_MUTATION = /* GraphQL */ `
|
|
98
|
+
mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
|
|
99
|
+
createCustomerGroup(input: $input) { ${CUSTOMER_GROUP_FIELDS} }
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
export const UPDATE_CUSTOMER_GROUP_MUTATION = /* GraphQL */ `
|
|
103
|
+
mutation UpdateCustomerGroup($input: UpdateCustomerGroupInput!) {
|
|
104
|
+
updateCustomerGroup(input: $input) { ${CUSTOMER_GROUP_FIELDS} }
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
export const DELETE_CUSTOMER_GROUP_MUTATION = /* GraphQL */ `
|
|
108
|
+
mutation DeleteCustomerGroup($id: ID!) {
|
|
109
|
+
deleteCustomerGroup(id: $id) { ${DELETION_FIELDS} }
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
export const DELETE_CUSTOMER_GROUPS_MUTATION = /* GraphQL */ `
|
|
113
|
+
mutation DeleteCustomerGroups($ids: [ID!]!) {
|
|
114
|
+
deleteCustomerGroups(ids: $ids) { ${DELETION_FIELDS} }
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
export const B2B_CUSTOMER_GROUPS_QUERY = /* GraphQL */ `
|
|
118
|
+
query CustomerGroupRefs {
|
|
119
|
+
b2bCustomerGroups {
|
|
120
|
+
id
|
|
121
|
+
name
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
export const SET_B2B_CUSTOMER_GROUPS_MUTATION = /* GraphQL */ `
|
|
126
|
+
mutation SetCustomerGroupRefs($groupIds: [ID!]!) {
|
|
127
|
+
setB2BCustomerGroups(groupIds: $groupIds) {
|
|
128
|
+
id
|
|
129
|
+
name
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
134
|
+
function invalid(code, message) {
|
|
135
|
+
return new ValidationError({ code, message, status: 400, service: 'vendure' });
|
|
136
|
+
}
|
|
137
|
+
const invalidListOptions = (message) => invalid('INVALID_CUSTOMER_GROUP_LIST_OPTIONS', message);
|
|
138
|
+
const invalidMembersOptions = (message) => invalid('INVALID_CUSTOMER_GROUP_MEMBERS_OPTIONS', message);
|
|
139
|
+
const invalidInput = (message) => invalid('INVALID_CUSTOMER_GROUP_INPUT', message);
|
|
140
|
+
const isIdList = (value) => Array.isArray(value) && value.every((id) => typeof id === 'string');
|
|
141
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
142
|
+
const isPageSize = (take) => typeof take === 'number' &&
|
|
143
|
+
Number.isInteger(take) &&
|
|
144
|
+
take >= 1 &&
|
|
145
|
+
take <= MAX_CUSTOMER_GROUP_LIST_TAKE;
|
|
146
|
+
const isOffset = (skip) => typeof skip === 'number' && Number.isInteger(skip) && skip >= 0;
|
|
147
|
+
/** Both flags are `false` unless the engine answers `true`: a group has no third state. */
|
|
148
|
+
function flagsOf(wire) {
|
|
149
|
+
return {
|
|
150
|
+
canViewIncomingStock: wire.customFields?.canViewIncomingStock === true,
|
|
151
|
+
canBackorder: wire.customFields?.canBackorder === true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function toCustomerGroup(wire) {
|
|
155
|
+
return {
|
|
156
|
+
id: wire.id,
|
|
157
|
+
createdAt: wire.createdAt,
|
|
158
|
+
updatedAt: wire.updatedAt,
|
|
159
|
+
name: wire.name,
|
|
160
|
+
externalKey: wire.externalKey,
|
|
161
|
+
...flagsOf(wire),
|
|
162
|
+
catalogueOwnership: wire.catalogueOwnership,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function toMember(wire) {
|
|
166
|
+
const addresses = wire.addresses ?? [];
|
|
167
|
+
return {
|
|
168
|
+
id: wire.id,
|
|
169
|
+
createdAt: wire.createdAt,
|
|
170
|
+
firstName: wire.firstName,
|
|
171
|
+
lastName: wire.lastName,
|
|
172
|
+
emailAddress: wire.emailAddress,
|
|
173
|
+
addresses,
|
|
174
|
+
company: companyOf(addresses),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function toListItem(wire) {
|
|
178
|
+
return {
|
|
179
|
+
id: wire.id,
|
|
180
|
+
createdAt: wire.createdAt,
|
|
181
|
+
updatedAt: wire.updatedAt,
|
|
182
|
+
name: wire.name,
|
|
183
|
+
...flagsOf(wire),
|
|
184
|
+
...(wire.customers === undefined ? {} : { memberCount: wire.customers.totalItems }),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Members are read in one fixed order, by id. The engine orders a list with no sort however the
|
|
189
|
+
* database returns it, so two pages of one walk could repeat or skip a member; an id is unique,
|
|
190
|
+
* and is not customer data (an app below `customerData: full` is refused a sort on a field its
|
|
191
|
+
* level withholds).
|
|
192
|
+
*/
|
|
193
|
+
const MEMBERS_SORT = { id: 'ASC' };
|
|
194
|
+
/** The Admin API's `CustomerListOptions` for a page of members; anything else is refused. */
|
|
195
|
+
function membersVariables(members) {
|
|
196
|
+
if (!isObject(members))
|
|
197
|
+
throw invalidMembersOptions('members must be an object.');
|
|
198
|
+
for (const key of Object.keys(members)) {
|
|
199
|
+
if (key !== 'skip' && key !== 'take' && key !== 'filter') {
|
|
200
|
+
throw invalidMembersOptions(`members.${key} is not an option of a members page.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const take = members.take ?? DEFAULT_PAGE_SIZE;
|
|
204
|
+
const skip = members.skip ?? 0;
|
|
205
|
+
if (!isPageSize(take)) {
|
|
206
|
+
throw invalidMembersOptions(`members.take must be a whole number from 1 to ${String(MAX_CUSTOMER_GROUP_LIST_TAKE)}.`);
|
|
207
|
+
}
|
|
208
|
+
if (!isOffset(skip)) {
|
|
209
|
+
throw invalidMembersOptions('members.skip must be a whole number of 0 or more.');
|
|
210
|
+
}
|
|
211
|
+
const page = { skip, take, sort: MEMBERS_SORT };
|
|
212
|
+
const filter = members.filter;
|
|
213
|
+
if (filter === undefined)
|
|
214
|
+
return page;
|
|
215
|
+
if (!isObject(filter))
|
|
216
|
+
throw invalidMembersOptions('members.filter must be an object.');
|
|
217
|
+
for (const key of Object.keys(filter)) {
|
|
218
|
+
if (key !== 'ids')
|
|
219
|
+
throw invalidMembersOptions(`members.filter.${key} is not a members filter.`);
|
|
220
|
+
}
|
|
221
|
+
if (filter.ids === undefined)
|
|
222
|
+
return page;
|
|
223
|
+
if (!isIdList(filter.ids)) {
|
|
224
|
+
throw invalidMembersOptions('members.filter.ids must be a list of strings.');
|
|
225
|
+
}
|
|
226
|
+
return { ...page, filter: { id: { in: [...filter.ids] } } };
|
|
227
|
+
}
|
|
228
|
+
/** `customers.groups.get`. */
|
|
229
|
+
export async function readCustomerGroup(transport, id, options) {
|
|
230
|
+
const members = options?.members === undefined ? null : membersVariables(options.members);
|
|
231
|
+
const redaction = redactionWatcher();
|
|
232
|
+
const data = await transport.graphql({
|
|
233
|
+
api: 'admin',
|
|
234
|
+
query: members === null ? CUSTOMER_GROUP_QUERY : CUSTOMER_GROUP_MEMBERS_QUERY,
|
|
235
|
+
variables: members === null ? { id } : { id, members },
|
|
236
|
+
onExtensions: redaction.onExtensions,
|
|
237
|
+
}, options);
|
|
238
|
+
const wire = data.customerGroup;
|
|
239
|
+
if (wire === null)
|
|
240
|
+
return null;
|
|
241
|
+
const group = toCustomerGroup(wire);
|
|
242
|
+
if (members === null)
|
|
243
|
+
return group;
|
|
244
|
+
const page = wire.customers ?? { items: [], totalItems: 0 };
|
|
245
|
+
return {
|
|
246
|
+
...group,
|
|
247
|
+
members: {
|
|
248
|
+
items: page.items.map((member) => redaction.mark(toMember(member))),
|
|
249
|
+
total: page.totalItems,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function listFilter(filter) {
|
|
254
|
+
if (filter === undefined)
|
|
255
|
+
return {};
|
|
256
|
+
if (!isObject(filter))
|
|
257
|
+
throw invalidListOptions('filter must be an object.');
|
|
258
|
+
const out = {};
|
|
259
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
260
|
+
if (value === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
switch (key) {
|
|
263
|
+
case 'ids':
|
|
264
|
+
if (!isIdList(value))
|
|
265
|
+
throw invalidListOptions('filter.ids must be a list of strings.');
|
|
266
|
+
out.id = { in: [...value] };
|
|
267
|
+
break;
|
|
268
|
+
case 'name':
|
|
269
|
+
if (typeof value !== 'string')
|
|
270
|
+
throw invalidListOptions('filter.name must be a string.');
|
|
271
|
+
out.name = { contains: value };
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
throw invalidListOptions(`filter.${key} is not a customer group list filter.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
function listRequest(options) {
|
|
280
|
+
if (options?.after !== undefined && options.skip !== undefined) {
|
|
281
|
+
throw invalidListOptions('Page with after or with skip, not both.');
|
|
282
|
+
}
|
|
283
|
+
const take = options?.take ?? options?.limit ?? DEFAULT_PAGE_SIZE;
|
|
284
|
+
const skip = options?.skip ?? (options?.after === undefined ? 0 : readOffsetCursor(options.after));
|
|
285
|
+
if (!isPageSize(take)) {
|
|
286
|
+
throw invalidListOptions(`take must be a whole number from 1 to ${String(MAX_CUSTOMER_GROUP_LIST_TAKE)}.`);
|
|
287
|
+
}
|
|
288
|
+
if (!isOffset(skip))
|
|
289
|
+
throw invalidListOptions('skip must be a whole number of 0 or more.');
|
|
290
|
+
const sort = options?.sort;
|
|
291
|
+
if (sort !== undefined &&
|
|
292
|
+
(!isObject(sort) ||
|
|
293
|
+
!CUSTOMER_GROUP_LIST_SORT_FIELDS.includes(sort.field) ||
|
|
294
|
+
(sort.direction !== 'ASC' && sort.direction !== 'DESC'))) {
|
|
295
|
+
throw invalidListOptions(`sort must be { field: ${CUSTOMER_GROUP_LIST_SORT_FIELDS.join(' | ')}, direction: ASC | DESC }.`);
|
|
296
|
+
}
|
|
297
|
+
const include = options?.include ?? [];
|
|
298
|
+
if (!Array.isArray(include) ||
|
|
299
|
+
!include.every((name) => CUSTOMER_GROUP_LIST_INCLUDES.includes(name))) {
|
|
300
|
+
throw invalidListOptions(`include takes a list of ${CUSTOMER_GROUP_LIST_INCLUDES.join(', ')}.`);
|
|
301
|
+
}
|
|
302
|
+
const filter = listFilter(options?.filter);
|
|
303
|
+
// The engine orders a list with no sort however the database returns it, so two pages of one
|
|
304
|
+
// walk could repeat or skip a group: every list is sorted, by name unless the caller says.
|
|
305
|
+
// ONE key only: the engine applies several in its schema's field order, never the order sent.
|
|
306
|
+
const order = sort;
|
|
307
|
+
return {
|
|
308
|
+
skip,
|
|
309
|
+
take,
|
|
310
|
+
request: {
|
|
311
|
+
query: customerGroupListQuery(include),
|
|
312
|
+
...(Object.keys(filter).length > 0 ? { filter } : {}),
|
|
313
|
+
sort: order ? { [order.field]: order.direction } : { name: 'ASC' },
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function listPage(transport, skip, take, request, options) {
|
|
318
|
+
const data = await transport.graphql({
|
|
319
|
+
api: 'admin',
|
|
320
|
+
query: request.query,
|
|
321
|
+
variables: {
|
|
322
|
+
options: {
|
|
323
|
+
skip,
|
|
324
|
+
take,
|
|
325
|
+
sort: request.sort,
|
|
326
|
+
...(request.filter ? { filter: request.filter, filterOperator: 'AND' } : {}),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
}, options);
|
|
330
|
+
// Each following page re-sends the same filter, sort, blocks and per-call options.
|
|
331
|
+
return pageFromVendureList({
|
|
332
|
+
items: data.customerGroups.items.map(toListItem),
|
|
333
|
+
totalItems: data.customerGroups.totalItems,
|
|
334
|
+
}, skip, take, (cursor) => listPage(transport, readOffsetCursor(cursor), take, request, options));
|
|
335
|
+
}
|
|
336
|
+
/** `customers.groups.list`. A refused option rejects the promise, like an engine refusal. */
|
|
337
|
+
export async function listCustomerGroups(transport, options) {
|
|
338
|
+
const { skip, take, request } = listRequest(options);
|
|
339
|
+
return listPage(transport, skip, take, request, options);
|
|
340
|
+
}
|
|
341
|
+
/** Only the flags the caller set travel: an omitted flag is never sent as `null`. */
|
|
342
|
+
function flagFields(input) {
|
|
343
|
+
const customFields = {};
|
|
344
|
+
for (const flag of ['canViewIncomingStock', 'canBackorder']) {
|
|
345
|
+
const value = input[flag];
|
|
346
|
+
if (value === undefined)
|
|
347
|
+
continue;
|
|
348
|
+
if (typeof value !== 'boolean')
|
|
349
|
+
throw invalidInput(`${flag} must be true or false.`);
|
|
350
|
+
customFields[flag] = value;
|
|
351
|
+
}
|
|
352
|
+
return Object.keys(customFields).length > 0 ? { customFields } : {};
|
|
353
|
+
}
|
|
354
|
+
/** `customers.groups.create`. */
|
|
355
|
+
export async function createCustomerGroup(transport, input, options) {
|
|
356
|
+
if (!isObject(input) || typeof input.name !== 'string') {
|
|
357
|
+
throw invalidInput('A customer group needs a name.');
|
|
358
|
+
}
|
|
359
|
+
if (input.customerIds !== undefined && !isIdList(input.customerIds)) {
|
|
360
|
+
throw invalidInput('customerIds must be a list of strings.');
|
|
361
|
+
}
|
|
362
|
+
const wire = await fieldOf(transport, CREATE_CUSTOMER_GROUP_MUTATION, 'createCustomerGroup', {
|
|
363
|
+
input: {
|
|
364
|
+
name: input.name,
|
|
365
|
+
...(input.customerIds === undefined ? {} : { customerIds: [...input.customerIds] }),
|
|
366
|
+
...flagFields(input),
|
|
367
|
+
},
|
|
368
|
+
}, options);
|
|
369
|
+
return toCustomerGroup(wire);
|
|
370
|
+
}
|
|
371
|
+
/** `customers.groups.update`. */
|
|
372
|
+
export async function updateCustomerGroup(transport, id, input, options) {
|
|
373
|
+
if (!isObject(input))
|
|
374
|
+
throw invalidInput('The change to a customer group must be an object.');
|
|
375
|
+
if (input.name !== undefined && typeof input.name !== 'string') {
|
|
376
|
+
throw invalidInput('name must be a string.');
|
|
377
|
+
}
|
|
378
|
+
const wire = await fieldOf(transport, UPDATE_CUSTOMER_GROUP_MUTATION, 'updateCustomerGroup', {
|
|
379
|
+
input: {
|
|
380
|
+
id,
|
|
381
|
+
...(input.name === undefined ? {} : { name: input.name }),
|
|
382
|
+
...flagFields(input),
|
|
383
|
+
},
|
|
384
|
+
}, options);
|
|
385
|
+
return toCustomerGroup(wire);
|
|
386
|
+
}
|
|
387
|
+
/** `customers.groups.delete`. */
|
|
388
|
+
export async function deleteCustomerGroup(transport, id, options) {
|
|
389
|
+
assertDeleted('CUSTOMER_GROUP_NOT_DELETED', [
|
|
390
|
+
await fieldOf(transport, DELETE_CUSTOMER_GROUP_MUTATION, 'deleteCustomerGroup', { id }, options),
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
/** `customers.groups.deleteMany`. */
|
|
394
|
+
export async function deleteCustomerGroups(transport, ids, options) {
|
|
395
|
+
if (!isIdList(ids))
|
|
396
|
+
throw invalidInput('ids must be a list of strings.');
|
|
397
|
+
if (ids.length === 0)
|
|
398
|
+
return;
|
|
399
|
+
const responses = await fieldOf(transport, DELETE_CUSTOMER_GROUPS_MUTATION, 'deleteCustomerGroups', { ids: [...ids] }, options);
|
|
400
|
+
// One answer per id, in the order sent. A missing answer is not a deletion.
|
|
401
|
+
const kept = ids.flatMap((id, index) => {
|
|
402
|
+
const response = responses[index];
|
|
403
|
+
return response?.result === 'DELETED'
|
|
404
|
+
? []
|
|
405
|
+
: [{ id, index, reason: response?.message ?? 'The server gave no reason.' }];
|
|
406
|
+
});
|
|
407
|
+
if (kept.length === 0)
|
|
408
|
+
return;
|
|
409
|
+
// Answered at HTTP 200, so the transport never throws for it. The groups not named here WERE
|
|
410
|
+
// deleted: the engine refuses the whole call only for a group its sharing rule protects.
|
|
411
|
+
throw new ConflictError({
|
|
412
|
+
code: 'CUSTOMER_GROUP_NOT_DELETED',
|
|
413
|
+
message: kept.length === ids.length
|
|
414
|
+
? `No customer group was deleted: ${kept[0]?.reason ?? ''}`
|
|
415
|
+
: `${String(kept.length)} of ${String(ids.length)} customer groups were not deleted (the others were): ${kept[0]?.reason ?? ''}`,
|
|
416
|
+
status: 409,
|
|
417
|
+
service: 'vendure',
|
|
418
|
+
details: kept.map(({ id, index, reason }) => ({
|
|
419
|
+
field: `ids.${String(index)}`,
|
|
420
|
+
code: 'CUSTOMER_GROUP_NOT_DELETED',
|
|
421
|
+
message: `${id}: ${reason}`,
|
|
422
|
+
})),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
export function createCustomerB2BGroupsModule(transport) {
|
|
426
|
+
return {
|
|
427
|
+
list(options) {
|
|
428
|
+
return fieldOf(transport, B2B_CUSTOMER_GROUPS_QUERY, 'b2bCustomerGroups', {}, options);
|
|
429
|
+
},
|
|
430
|
+
async set(groupIds, options) {
|
|
431
|
+
if (!isIdList(groupIds))
|
|
432
|
+
throw invalidInput('groupIds must be a list of strings.');
|
|
433
|
+
return fieldOf(transport, SET_B2B_CUSTOMER_GROUPS_MUTATION, 'setB2BCustomerGroups',
|
|
434
|
+
// Each group once: the engine keeps one row per group and store, and a repeat fails its insert.
|
|
435
|
+
{ groupIds: [...new Set(groupIds)] }, options);
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|