@sixtyninecommerce/admin 0.9.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.
Files changed (258) hide show
  1. package/README.md +4598 -128
  2. package/dist/catalog/assets.d.ts +120 -8
  3. package/dist/catalog/assets.js +98 -12
  4. package/dist/catalog/collections.d.ts +279 -14
  5. package/dist/catalog/collections.js +246 -15
  6. package/dist/catalog/index.d.ts +9 -2
  7. package/dist/catalog/index.js +4 -0
  8. package/dist/catalog/products.d.ts +318 -17
  9. package/dist/catalog/products.js +182 -14
  10. package/dist/catalog/properties.d.ts +102 -14
  11. package/dist/catalog/properties.js +13 -0
  12. package/dist/catalog/recommendations.d.ts +261 -0
  13. package/dist/catalog/recommendations.js +145 -0
  14. package/dist/catalog/reviews.d.ts +368 -0
  15. package/dist/catalog/reviews.js +318 -0
  16. package/dist/catalog/types.d.ts +17 -1
  17. package/dist/catalog/types.js +10 -0
  18. package/dist/catalog/variant-find.d.ts +136 -0
  19. package/dist/catalog/variant-find.js +239 -0
  20. package/dist/catalog/variants.d.ts +282 -25
  21. package/dist/catalog/variants.js +173 -25
  22. package/dist/catalog/visibility.d.ts +20 -5
  23. package/dist/catalog/visibility.js +19 -1
  24. package/dist/client.d.ts +116 -9
  25. package/dist/client.js +41 -7
  26. package/dist/content/assets.d.ts +286 -0
  27. package/dist/content/assets.js +181 -0
  28. package/dist/content/items.d.ts +269 -0
  29. package/dist/content/items.js +170 -0
  30. package/dist/content/types.d.ts +282 -0
  31. package/dist/content/types.js +100 -0
  32. package/dist/customers/age-verification.d.ts +66 -0
  33. package/dist/customers/age-verification.js +79 -0
  34. package/dist/customers/credit.d.ts +263 -0
  35. package/dist/customers/credit.js +244 -0
  36. package/dist/customers/delivery-preferences.d.ts +120 -0
  37. package/dist/customers/delivery-preferences.js +97 -0
  38. package/dist/customers/groups.d.ts +207 -0
  39. package/dist/customers/groups.js +438 -0
  40. package/dist/customers/index.d.ts +447 -37
  41. package/dist/customers/index.js +313 -40
  42. package/dist/customers/invitation-status.d.ts +57 -0
  43. package/dist/customers/invitation-status.js +21 -0
  44. package/dist/customers/notes.d.ts +67 -0
  45. package/dist/customers/notes.js +51 -0
  46. package/dist/customers/notification-recipients.d.ts +97 -0
  47. package/dist/customers/notification-recipients.js +84 -0
  48. package/dist/customers/saved-lists.d.ts +42 -0
  49. package/dist/customers/saved-lists.js +61 -0
  50. package/dist/customers/vat.d.ts +83 -0
  51. package/dist/customers/vat.js +46 -0
  52. package/dist/customers/wire.d.ts +20 -0
  53. package/dist/customers/wire.js +35 -0
  54. package/dist/documents/ar.d.ts +307 -0
  55. package/dist/documents/ar.js +254 -0
  56. package/dist/documents/index.d.ts +731 -21
  57. package/dist/documents/index.js +436 -24
  58. package/dist/documents/pdf.d.ts +2 -0
  59. package/dist/documents/pdf.js +11 -0
  60. package/dist/fulfillments/create.d.ts +167 -0
  61. package/dist/fulfillments/create.js +216 -0
  62. package/dist/fulfillments/index.d.ts +121 -10
  63. package/dist/fulfillments/index.js +120 -2
  64. package/dist/fulfillments/payment-policy.d.ts +64 -0
  65. package/dist/fulfillments/payment-policy.js +140 -0
  66. package/dist/fulfillments/warehouse.d.ts +724 -0
  67. package/dist/fulfillments/warehouse.js +914 -0
  68. package/dist/geo/index.d.ts +119 -0
  69. package/dist/geo/index.js +107 -0
  70. package/dist/index.d.ts +129 -30
  71. package/dist/index.js +47 -9
  72. package/dist/integrations/afas-actions.d.ts +234 -0
  73. package/dist/integrations/afas-actions.js +310 -0
  74. package/dist/integrations/afas-types.d.ts +483 -0
  75. package/dist/integrations/afas-types.js +5 -0
  76. package/dist/integrations/afas.d.ts +234 -0
  77. package/dist/integrations/afas.js +290 -0
  78. package/dist/integrations/connections.d.ts +103 -0
  79. package/dist/integrations/connections.js +55 -0
  80. package/dist/integrations/esp.d.ts +313 -0
  81. package/dist/integrations/esp.js +169 -0
  82. package/dist/integrations/exact-online-types.d.ts +423 -0
  83. package/dist/integrations/exact-online-types.js +6 -0
  84. package/dist/integrations/exact-online.d.ts +248 -0
  85. package/dist/integrations/exact-online.js +376 -0
  86. package/dist/integrations/index.d.ts +40 -0
  87. package/dist/integrations/index.js +24 -0
  88. package/dist/integrations/pipeline-keys.d.ts +13 -0
  89. package/dist/integrations/pipeline-keys.js +22 -0
  90. package/dist/integrations/pipelines.d.ts +114 -0
  91. package/dist/integrations/pipelines.js +46 -0
  92. package/dist/integrations/types.d.ts +322 -0
  93. package/dist/integrations/types.js +8 -0
  94. package/dist/jobs/bulk-action-run.d.ts +11 -2
  95. package/dist/jobs/bulk-action-run.js +4 -5
  96. package/dist/locations/index.d.ts +66 -0
  97. package/dist/locations/index.js +31 -0
  98. package/dist/meta/index.d.ts +162 -0
  99. package/dist/orders/bulk.d.ts +75 -0
  100. package/dist/orders/bulk.js +150 -0
  101. package/dist/orders/cancel-with-money.d.ts +227 -0
  102. package/dist/orders/cancel-with-money.js +153 -0
  103. package/dist/orders/commit-edit.d.ts +7 -1
  104. package/dist/orders/commit-edit.js +5 -1
  105. package/dist/orders/deposit-policy.d.ts +103 -0
  106. package/dist/orders/deposit-policy.js +202 -0
  107. package/dist/orders/drafts.d.ts +800 -0
  108. package/dist/orders/drafts.js +771 -0
  109. package/dist/orders/fields.d.ts +245 -0
  110. package/dist/orders/fields.js +188 -0
  111. package/dist/orders/get.d.ts +29 -19
  112. package/dist/orders/get.js +17 -8
  113. package/dist/orders/index.d.ts +125 -5
  114. package/dist/orders/index.js +35 -2
  115. package/dist/orders/lifecycle.d.ts +128 -0
  116. package/dist/orders/lifecycle.js +158 -0
  117. package/dist/orders/line-price-preview.d.ts +7 -2
  118. package/dist/orders/list.d.ts +222 -0
  119. package/dist/orders/list.js +377 -0
  120. package/dist/orders/manual-discounts.d.ts +11 -2
  121. package/dist/orders/manual-discounts.js +2 -0
  122. package/dist/orders/manual-pricing.d.ts +5 -0
  123. package/dist/orders/order-entry.d.ts +20 -7
  124. package/dist/orders/plan-edit.d.ts +1 -1
  125. package/dist/orders/plan-edit.js +4 -1
  126. package/dist/orders/settings.d.ts +201 -0
  127. package/dist/orders/settings.js +325 -0
  128. package/dist/orders/status.d.ts +159 -13
  129. package/dist/orders/status.js +21 -0
  130. package/dist/orders/surcharges.d.ts +8 -1
  131. package/dist/orders/surcharges.js +3 -2
  132. package/dist/payments/account-credit.d.ts +30 -0
  133. package/dist/payments/account-credit.js +19 -0
  134. package/dist/payments/ar.d.ts +292 -0
  135. package/dist/payments/ar.js +223 -0
  136. package/dist/payments/connected-account.d.ts +47 -0
  137. package/dist/payments/connected-account.js +61 -0
  138. package/dist/payments/credit-noted-fields.d.ts +12 -0
  139. package/dist/payments/credit-noted-fields.js +37 -0
  140. package/dist/payments/index.d.ts +263 -10
  141. package/dist/payments/index.js +133 -1
  142. package/dist/payments/methods.d.ts +332 -0
  143. package/dist/payments/methods.js +363 -0
  144. package/dist/payments/mollie-methods.d.ts +159 -0
  145. package/dist/payments/mollie-methods.js +141 -0
  146. package/dist/payments/payment-links.d.ts +180 -0
  147. package/dist/payments/payment-links.js +97 -0
  148. package/dist/payments/provider-keys.d.ts +150 -0
  149. package/dist/payments/provider-keys.js +118 -0
  150. package/dist/payments/providers.d.ts +86 -11
  151. package/dist/payments/providers.js +29 -0
  152. package/dist/payments/requests.d.ts +144 -0
  153. package/dist/payments/requests.js +92 -0
  154. package/dist/payments/store-credit-fields.d.ts +15 -0
  155. package/dist/payments/store-credit-fields.js +51 -0
  156. package/dist/pickups/index.d.ts +104 -0
  157. package/dist/pickups/index.js +105 -0
  158. package/dist/platform/channels.d.ts +132 -0
  159. package/dist/platform/channels.js +128 -0
  160. package/dist/platform/geo.d.ts +123 -0
  161. package/dist/platform/geo.js +144 -0
  162. package/dist/platform/tax.d.ts +154 -0
  163. package/dist/platform/tax.js +139 -0
  164. package/dist/pos/anomalies.d.ts +158 -0
  165. package/dist/pos/anomalies.js +64 -0
  166. package/dist/pos/assisted-selling.d.ts +79 -0
  167. package/dist/pos/assisted-selling.js +38 -0
  168. package/dist/pos/exchanges.d.ts +103 -0
  169. package/dist/pos/exchanges.js +22 -0
  170. package/dist/pos/gift-cards.d.ts +135 -0
  171. package/dist/pos/gift-cards.js +47 -0
  172. package/dist/pos/index.d.ts +2125 -32
  173. package/dist/pos/index.js +742 -4
  174. package/dist/pos/no-receipt.d.ts +94 -0
  175. package/dist/pos/no-receipt.js +25 -0
  176. package/dist/pos/offline-sales.d.ts +187 -0
  177. package/dist/pos/offline-sales.js +116 -0
  178. package/dist/pos/pay-by-link.d.ts +143 -0
  179. package/dist/pos/pay-by-link.js +57 -0
  180. package/dist/pos/quick-keys.d.ts +103 -0
  181. package/dist/pos/quick-keys.js +45 -0
  182. package/dist/pos/reports.d.ts +360 -0
  183. package/dist/pos/reports.js +62 -0
  184. package/dist/pos/returns.d.ts +217 -0
  185. package/dist/pos/returns.js +53 -0
  186. package/dist/pricing/bulk-coupons.d.ts +139 -0
  187. package/dist/pricing/bulk-coupons.js +112 -0
  188. package/dist/pricing/index.d.ts +177 -4
  189. package/dist/pricing/index.js +176 -1
  190. package/dist/pricing/price-lists.d.ts +604 -0
  191. package/dist/pricing/price-lists.js +600 -0
  192. package/dist/pricing/vouchers.d.ts +103 -0
  193. package/dist/pricing/vouchers.js +69 -0
  194. package/dist/purchasing/alerts.d.ts +89 -0
  195. package/dist/purchasing/alerts.js +62 -0
  196. package/dist/purchasing/cost.d.ts +110 -0
  197. package/dist/purchasing/cost.js +86 -0
  198. package/dist/purchasing/index.d.ts +36 -0
  199. package/dist/purchasing/index.js +16 -0
  200. package/dist/purchasing/purchase-orders.d.ts +548 -0
  201. package/dist/purchasing/purchase-orders.js +491 -0
  202. package/dist/purchasing/replenishment.d.ts +281 -0
  203. package/dist/purchasing/replenishment.js +240 -0
  204. package/dist/purchasing/settings.d.ts +152 -0
  205. package/dist/purchasing/settings.js +181 -0
  206. package/dist/purchasing/suppliers.d.ts +476 -0
  207. package/dist/purchasing/suppliers.js +438 -0
  208. package/dist/quotes/index.d.ts +501 -0
  209. package/dist/quotes/index.js +439 -0
  210. package/dist/reports/index.d.ts +71 -8
  211. package/dist/reports/index.js +74 -11
  212. package/dist/returns/index.d.ts +818 -0
  213. package/dist/returns/index.js +844 -0
  214. package/dist/search/index.d.ts +174 -0
  215. package/dist/search/index.js +125 -0
  216. package/dist/shipping/definitions.d.ts +29 -0
  217. package/dist/shipping/definitions.js +47 -0
  218. package/dist/shipping/fulfillment-profiles.d.ts +174 -0
  219. package/dist/shipping/fulfillment-profiles.js +104 -0
  220. package/dist/shipping/hendriks.d.ts +97 -0
  221. package/dist/shipping/hendriks.js +50 -0
  222. package/dist/shipping/index.d.ts +48 -0
  223. package/dist/shipping/index.js +25 -0
  224. package/dist/shipping/methods.d.ts +198 -0
  225. package/dist/shipping/methods.js +240 -0
  226. package/dist/shipping/myparcel.d.ts +254 -0
  227. package/dist/shipping/myparcel.js +178 -0
  228. package/dist/shipping/parcel-count-suggestion.d.ts +2 -0
  229. package/dist/shipping/parcel-rule.d.ts +6 -5
  230. package/dist/shipping/parcel-rule.js +21 -23
  231. package/dist/shipping/rate-tables.d.ts +179 -0
  232. package/dist/shipping/rate-tables.js +184 -0
  233. package/dist/stock/index.d.ts +10 -3
  234. package/dist/stock/index.js +3 -0
  235. package/dist/stock/locations.d.ts +144 -0
  236. package/dist/stock/locations.js +168 -0
  237. package/dist/stock/transfers.d.ts +216 -43
  238. package/dist/stock/transfers.js +140 -13
  239. package/dist/stored-value/index.d.ts +214 -4
  240. package/dist/stored-value/index.js +152 -0
  241. package/dist/subscriptions/index.d.ts +682 -0
  242. package/dist/subscriptions/index.js +815 -0
  243. package/dist/tax/index.d.ts +76 -1
  244. package/dist/tax/index.js +50 -0
  245. package/dist/tenant/email-style.d.ts +102 -0
  246. package/dist/tenant/email-style.js +72 -0
  247. package/dist/tenant/email-templates.d.ts +66 -0
  248. package/dist/tenant/email-templates.js +54 -0
  249. package/dist/tenant/notifications.d.ts +23 -17
  250. package/dist/tenant/settings-sources.d.ts +1 -1
  251. package/dist/tenant/time-zone.d.ts +17 -0
  252. package/dist/tenant/time-zone.js +11 -0
  253. package/dist/types.d.ts +51 -4
  254. package/dist/users/index.d.ts +3 -0
  255. package/dist/users/index.js +2 -0
  256. package/dist/users/warehouses.d.ts +50 -0
  257. package/dist/users/warehouses.js +51 -0
  258. package/package.json +2 -2
@@ -1,7 +1,19 @@
1
- import { errorFromErrorResult, isErrorResult, pageFromVendureList, readOffsetCursor, } from '@sixtyninecommerce/core';
1
+ import { BusinessRuleError, errorFromErrorResult, isErrorResult, pageFromVendureList, readOffsetCursor, ValidationError, } from '@sixtyninecommerce/core';
2
2
  import { CATALOGUE_OWNERSHIP_FIELDS } from '../catalog/types.js';
3
3
  import { COMPLIANCE_REQUEST_FIELDS } from '../compliance/index.js';
4
4
  import { redactionWatcher } from '../customer-data-redaction.js';
5
+ import { listCustomerOrders, listCustomerRecentOrders, } from '../orders/list.js';
6
+ import { createCustomerDateOfBirthModule, readCustomerAgeVerification, } from './age-verification.js';
7
+ import { createCustomerCreditModule } from './credit.js';
8
+ import { createCustomerDeliveryPreferencesModule, } from './delivery-preferences.js';
9
+ import { createCustomerB2BGroupsModule, createCustomerGroup, deleteCustomerGroup, deleteCustomerGroups, listCustomerGroups, readCustomerGroup, updateCustomerGroup, } from './groups.js';
10
+ import { readCustomerInvitationStatus, } from './invitation-status.js';
11
+ import { createCustomerNotesModule } from './notes.js';
12
+ import { createCustomerNotificationRecipientsModule, } from './notification-recipients.js';
13
+ import { readCustomerSavedLists } from './saved-lists.js';
14
+ import { createCustomerVatModule } from './vat.js';
15
+ import { companyOf } from './wire.js';
16
+ export { CUSTOMER_GROUP_FIELDS, CUSTOMER_GROUP_LIST_INCLUDES, CUSTOMER_GROUP_LIST_SORT_FIELDS, CUSTOMER_GROUP_QUERY, MAX_CUSTOMER_GROUP_LIST_TAKE, } from './groups.js';
5
17
  /**
6
18
  * Customers — the commerce master record (headless spec §6.5).
7
19
  *
@@ -75,14 +87,6 @@ export const CUSTOMER_ADDRESS_FIELDS = /* GraphQL */ `
75
87
  defaultShippingAddress
76
88
  defaultBillingAddress
77
89
  `;
78
- export const LIST_QUERY = /* GraphQL */ `
79
- query Customers($options: CustomerListOptions) {
80
- customers(options: $options) {
81
- items { ${CUSTOMER_FIELDS} }
82
- totalItems
83
- }
84
- }
85
- `;
86
90
  export const GET_QUERY = /* GraphQL */ `
87
91
  query CustomerById($id: ID!) {
88
92
  customer(id: $id) { ${CUSTOMER_FIELDS} }
@@ -96,6 +100,94 @@ export const ADDRESSES_QUERY = /* GraphQL */ `
96
100
  }
97
101
  }
98
102
  `;
103
+ /**
104
+ * A list row: the customer, plus what a list shows beside the name (the address book's company
105
+ * flags, for {@link CustomerListItem.company}). The order count is its own block: the engine
106
+ * counts each row's orders in a query of its own, so a list asks for it only when it shows it.
107
+ */
108
+ export const CUSTOMER_LIST_FIELDS = /* GraphQL */ `
109
+ ${CUSTOMER_FIELDS}
110
+ addresses {
111
+ company
112
+ defaultBillingAddress
113
+ defaultShippingAddress
114
+ }
115
+ `;
116
+ /** The list document, with or without each row's order count. */
117
+ export function customerListQuery(include = []) {
118
+ // `take: 1`, never 0: the engine passes 0 on and TypeORM reads a falsy limit as NO limit, so
119
+ // `take: 0` would load every order of every row. `totalItems` is its own COUNT either way.
120
+ const orderCount = include.includes('orderCount')
121
+ ? 'orders(options: { take: 1 }) { totalItems }'
122
+ : '';
123
+ return /* GraphQL */ `
124
+ query CustomersList($options: CustomerListOptions) {
125
+ customers(options: $options) {
126
+ items {
127
+ ${CUSTOMER_LIST_FIELDS}
128
+ ${orderCount}
129
+ }
130
+ totalItems
131
+ }
132
+ }
133
+ `;
134
+ }
135
+ /** How many history entries `get` reads with `include: ['history']`, newest first. */
136
+ export const CUSTOMER_HISTORY_TAKE = 20;
137
+ /** The plugin custom fields `include: ['customFields']` reads (AFAS, VAT validation, age check). */
138
+ export const CUSTOMER_PLUGIN_FIELDS = /* GraphQL */ `
139
+ afasVrId
140
+ afasOrgId
141
+ afasContactId
142
+ afasPersId
143
+ vatNumber
144
+ vatValidationStatus
145
+ vatValidatedAt
146
+ vatCompanyName
147
+ vatCompanyAddress
148
+ dateOfBirth
149
+ `;
150
+ /**
151
+ * The document `get` sends for a set of blocks. It depends on the SET only, never on the order
152
+ * the caller listed them in. Without blocks it is {@link GET_QUERY}.
153
+ */
154
+ export function customerQuery(include = []) {
155
+ const asked = new Set(include);
156
+ if (asked.size === 0)
157
+ return GET_QUERY;
158
+ const pluginFields = asked.has('customFields');
159
+ const blocks = [];
160
+ if (asked.has('addresses')) {
161
+ // The AFAS address id is a plugin field too, so it rides on `customFields`.
162
+ const addressPluginFields = pluginFields ? 'customFields { afasAddressId }' : '';
163
+ blocks.push(`addresses { ${CUSTOMER_ADDRESS_FIELDS} ${addressPluginFields} }`);
164
+ }
165
+ if (asked.has('user'))
166
+ blocks.push('user { id verified lastLogin }');
167
+ if (asked.has('history')) {
168
+ blocks.push(`history(options: { take: ${String(CUSTOMER_HISTORY_TAKE)}, sort: { createdAt: DESC } }) {
169
+ items {
170
+ id
171
+ type
172
+ isPublic
173
+ data
174
+ createdAt
175
+ administrator { id firstName lastName }
176
+ }
177
+ totalItems
178
+ }`);
179
+ }
180
+ if (pluginFields)
181
+ blocks.push(`customFields { ${CUSTOMER_PLUGIN_FIELDS} }`);
182
+ return /* GraphQL */ `
183
+ query CustomerById($id: ID!) {
184
+ customer(id: $id) {
185
+ ${CUSTOMER_FIELDS}
186
+ ${blocks.join('\n')}
187
+ }
188
+ }
189
+ `;
190
+ }
99
191
  /**
100
192
  * The CHANNEL-SCOPED creation mutation. See the module docblock — core
101
193
  * `createCustomer` is not an option, and this SDK exposes no path to it.
@@ -285,20 +377,6 @@ export const GROUP_BY_EXTERNAL_KEY_QUERY = /* GraphQL */ `
285
377
  customerGroupByExternalKey(key: $key) { ${EXTERNAL_KEYED_GROUP_FIELDS} }
286
378
  }
287
379
  `;
288
- /** A customer group as {@link CustomerGroupsModule.get} reads it. */
289
- export const CUSTOMER_GROUP_FIELDS = /* GraphQL */ `
290
- id
291
- createdAt
292
- updatedAt
293
- name
294
- externalKey
295
- ${CATALOGUE_OWNERSHIP_FIELDS}
296
- `;
297
- export const CUSTOMER_GROUP_QUERY = /* GraphQL */ `
298
- query CustomerGroup($id: ID!) {
299
- customerGroup(id: $id) { ${CUSTOMER_GROUP_FIELDS} }
300
- }
301
- `;
302
380
  export const GROUP_EXTERNAL_KEY_QUERY = /* GraphQL */ `
303
381
  query CustomerGroupExternalKey($id: ID!) {
304
382
  customerGroup(id: $id) {
@@ -357,32 +435,191 @@ export const DELETE_ADDRESS_MUTATION = /* GraphQL */ `
357
435
  deleteCustomerAddress(id: $id) { success }
358
436
  }
359
437
  `;
438
+ export const CUSTOMER_LIST_SORT_FIELDS = [
439
+ 'createdAt',
440
+ 'updatedAt',
441
+ 'firstName',
442
+ 'lastName',
443
+ 'emailAddress',
444
+ 'phoneNumber',
445
+ ];
446
+ /** The blocks a list row can add: `orderCount`, each row's order count. */
447
+ export const CUSTOMER_LIST_INCLUDES = ['orderCount'];
448
+ /** The engine's `adminListQueryLimit`: a larger page is refused. */
449
+ export const MAX_CUSTOMER_LIST_TAKE = 1000;
450
+ /**
451
+ * The blocks `get` can add to a customer, each asked for by name in `include`:
452
+ *
453
+ * - `addresses`: the address book.
454
+ * - `user`: the sign-in account (`null` for a guest customer).
455
+ * - `history`: the {@link CUSTOMER_HISTORY_TAKE} most recent history entries, newest first.
456
+ * - `customFields`: the fields the AFAS, VAT-validation and age-check plugins keep on the customer
457
+ * (and, with `addresses`, each address's AFAS id).
458
+ */
459
+ export const CUSTOMER_INCLUDES = ['addresses', 'user', 'history', 'customFields'];
360
460
  const DEFAULT_PAGE_SIZE = 25;
461
+ function invalidListOptions(message) {
462
+ return new ValidationError({
463
+ code: 'INVALID_CUSTOMER_LIST_OPTIONS',
464
+ message,
465
+ status: 400,
466
+ service: 'vendure',
467
+ });
468
+ }
469
+ function listText(value, path) {
470
+ if (typeof value !== 'string')
471
+ throw invalidListOptions(`${path} must be a string.`);
472
+ return value;
473
+ }
474
+ function escapeRegex(value) {
475
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
476
+ }
477
+ /** The Admin API's `CustomerFilterParameter` for a typed filter; unknown keys are refused. */
478
+ export function toCustomerFilterParameter(filter) {
479
+ if (filter === undefined)
480
+ return {};
481
+ if (typeof filter !== 'object' || filter === null || Array.isArray(filter)) {
482
+ throw invalidListOptions('filter must be an object.');
483
+ }
484
+ const out = {};
485
+ for (const [key, value] of Object.entries(filter)) {
486
+ if (value === undefined)
487
+ continue;
488
+ switch (key) {
489
+ case 'ids':
490
+ if (!Array.isArray(value) || value.some((id) => typeof id !== 'string')) {
491
+ throw invalidListOptions('filter.ids must be a list of strings.');
492
+ }
493
+ out.id = { in: [...value] };
494
+ break;
495
+ case 'emailStartsWith':
496
+ // The engine matches `regex` case-insensitively (`~*`), and an anchored, escaped prefix
497
+ // is a starts-with: `contains` would match anywhere and read `%`/`_` as wildcards.
498
+ out.emailAddress = { regex: `^${escapeRegex(listText(value, 'filter.emailStartsWith'))}` };
499
+ break;
500
+ case 'lastName':
501
+ out.lastName = { contains: listText(value, 'filter.lastName') };
502
+ break;
503
+ default:
504
+ throw invalidListOptions(`filter.${key} is not a customer list filter.`);
505
+ }
506
+ }
507
+ return out;
508
+ }
509
+ function customerListRequest(options) {
510
+ if (options?.after !== undefined && options.skip !== undefined) {
511
+ throw invalidListOptions('Page with after or with skip, not both.');
512
+ }
513
+ const take = options?.take ?? options?.limit ?? DEFAULT_PAGE_SIZE;
514
+ const skip = options?.skip ?? (options?.after === undefined ? 0 : readOffsetCursor(options.after));
515
+ if (!Number.isInteger(take) || take < 1 || take > MAX_CUSTOMER_LIST_TAKE) {
516
+ throw invalidListOptions(`take must be a whole number from 1 to ${String(MAX_CUSTOMER_LIST_TAKE)}.`);
517
+ }
518
+ if (!Number.isInteger(skip) || skip < 0) {
519
+ throw invalidListOptions('skip must be a whole number of 0 or more.');
520
+ }
521
+ const sort = options?.sort;
522
+ if (sort !== undefined &&
523
+ (typeof sort !== 'object' ||
524
+ sort === null ||
525
+ !CUSTOMER_LIST_SORT_FIELDS.includes(sort.field) ||
526
+ (sort.direction !== 'ASC' && sort.direction !== 'DESC'))) {
527
+ throw invalidListOptions(`sort must be { field: ${CUSTOMER_LIST_SORT_FIELDS.join(' | ')}, direction: ASC | DESC }.`);
528
+ }
529
+ const include = options?.include ?? [];
530
+ if (!Array.isArray(include) ||
531
+ !include.every((name) => CUSTOMER_LIST_INCLUDES.includes(name))) {
532
+ throw invalidListOptions(`include takes a list of ${CUSTOMER_LIST_INCLUDES.join(', ')}.`);
533
+ }
534
+ const filter = toCustomerFilterParameter(options?.filter);
535
+ return {
536
+ skip,
537
+ take,
538
+ request: {
539
+ query: customerListQuery(include),
540
+ ...(Object.keys(filter).length > 0 ? { filter } : {}),
541
+ ...(sort ? { sort: { [sort.field]: sort.direction } } : {}),
542
+ },
543
+ };
544
+ }
545
+ function toCustomerListItem(row) {
546
+ const { orders, addresses, ...customer } = row;
547
+ const book = addresses ?? [];
548
+ return {
549
+ ...customer,
550
+ addresses: book,
551
+ company: companyOf(book),
552
+ ...(orders === undefined ? {} : { orderCount: orders.totalItems }),
553
+ };
554
+ }
361
555
  export function createCustomersModule(transport) {
362
- const listPage = async (skip, take, options) => {
556
+ const listPage = async (skip, take, request, options) => {
363
557
  const redaction = redactionWatcher();
364
558
  const data = await transport.graphql({
365
559
  api: 'admin',
366
- query: LIST_QUERY,
367
- variables: { options: { skip, take } },
560
+ query: request.query,
561
+ variables: {
562
+ options: {
563
+ skip,
564
+ take,
565
+ ...(request.filter ? { filter: request.filter, filterOperator: 'AND' } : {}),
566
+ ...(request.sort ? { sort: request.sort } : {}),
567
+ },
568
+ },
368
569
  onExtensions: redaction.onExtensions,
369
570
  }, options);
370
- data.customers.items.forEach(redaction.mark);
371
- return pageFromVendureList(data.customers, skip, take, (cursor) => listPage(readOffsetCursor(cursor), take, options));
571
+ const items = data.customers.items.map((row) => redaction.mark(toCustomerListItem(row)));
572
+ // Each following page re-sends the same filter, sort and per-call options.
573
+ return pageFromVendureList({ items, totalItems: data.customers.totalItems }, skip, take, (cursor) => listPage(readOffsetCursor(cursor), take, request, options));
372
574
  };
575
+ async function get(id, options) {
576
+ // Typed callers cannot get here wrong. A JavaScript caller can, and an unknown name would
577
+ // otherwise be dropped in silence: the read would answer without the block it asked for.
578
+ const include = options?.include ?? [];
579
+ const known = (name) => CUSTOMER_INCLUDES.includes(name);
580
+ if (!Array.isArray(include) || !include.every(known)) {
581
+ const given = Array.isArray(include)
582
+ ? include.filter((name) => !known(name)).map((name) => `"${String(name)}"`)
583
+ : [String(include)];
584
+ throw new ValidationError({
585
+ code: 'INVALID_CUSTOMER_INCLUDE',
586
+ message: `\`include\` takes a list of a customer's blocks, and ${given.join(', ')} is not one. The blocks are: ${CUSTOMER_INCLUDES.join(', ')}.`,
587
+ status: 400,
588
+ service: 'vendure',
589
+ });
590
+ }
591
+ const redaction = redactionWatcher();
592
+ const data = await transport.graphql({
593
+ api: 'admin',
594
+ query: customerQuery(include),
595
+ variables: { id },
596
+ onExtensions: redaction.onExtensions,
597
+ }, options);
598
+ const customer = redaction.mark(data.customer);
599
+ customer?.addresses?.forEach(redaction.mark);
600
+ return customer;
601
+ }
373
602
  return {
374
- list(options) {
603
+ async list(options) {
604
+ // A refused option rejects the promise, like an engine refusal, rather than throwing here.
375
605
  // The Admin API is offset-paged; the offset hides inside the cursor so a
376
606
  // consumer only ever sees `Page<T>` (spec §9.3).
377
- const take = options?.limit ?? DEFAULT_PAGE_SIZE;
378
- const skip = options?.after === undefined ? 0 : readOffsetCursor(options.after);
379
- return listPage(skip, take, options);
607
+ const { skip, take, request } = customerListRequest(options);
608
+ return listPage(skip, take, request, options);
380
609
  },
381
- async get(id, options) {
382
- const redaction = redactionWatcher();
383
- const data = await transport.graphql({ api: 'admin', query: GET_QUERY, variables: { id }, onExtensions: redaction.onExtensions }, options);
384
- return redaction.mark(data.customer);
610
+ orders(customerId, options) {
611
+ return listCustomerOrders(transport, customerId, options);
612
+ },
613
+ recentOrders(customerId, options) {
614
+ return listCustomerRecentOrders(transport, customerId, options);
615
+ },
616
+ savedLists(customerId, options) {
617
+ return readCustomerSavedLists(transport, customerId, options);
385
618
  },
619
+ // The answer holds exactly the blocks asked for; `CustomerFor` decides how many of them the
620
+ // caller's type of `include` may rely on. The overloads narrow this one implementation, which
621
+ // TypeScript cannot check for them.
622
+ get: get,
386
623
  async create(input, options) {
387
624
  const redaction = redactionWatcher();
388
625
  const data = await transport.graphql({
@@ -421,6 +658,12 @@ export function createCustomersModule(transport) {
421
658
  const data = await transport.graphql({ api: 'admin', query, variables: { customerId: id, mode: options?.mode ?? null } }, options);
422
659
  return (data.inviteCustomer ?? data.resendCustomerInvitation);
423
660
  },
661
+ invitationStatus(id, options) {
662
+ return readCustomerInvitationStatus(transport, id, options);
663
+ },
664
+ ageVerification(id, options) {
665
+ return readCustomerAgeVerification(transport, id, options);
666
+ },
424
667
  async inviteB2B(options) {
425
668
  const data = await transport.graphql({
426
669
  api: 'admin',
@@ -498,10 +741,24 @@ export function createCustomersModule(transport) {
498
741
  return result;
499
742
  },
500
743
  groups: {
501
- async get(id, options) {
502
- const data = await transport.graphql({ api: 'admin', query: CUSTOMER_GROUP_QUERY, variables: { id } }, options);
503
- return data.customerGroup;
744
+ list(options) {
745
+ return listCustomerGroups(transport, options);
746
+ },
747
+ // The overloads narrow this one implementation: with `members` the answer carries them.
748
+ get: ((id, options) => readCustomerGroup(transport, id, options)),
749
+ create(input, options) {
750
+ return createCustomerGroup(transport, input, options);
751
+ },
752
+ update(id, input, options) {
753
+ return updateCustomerGroup(transport, id, input, options);
754
+ },
755
+ delete(id, options) {
756
+ return deleteCustomerGroup(transport, id, options);
757
+ },
758
+ deleteMany(ids, options) {
759
+ return deleteCustomerGroups(transport, ids, options);
504
760
  },
761
+ b2b: createCustomerB2BGroupsModule(transport),
505
762
  async byExternalKey(key, options) {
506
763
  const data = await transport.graphql({ api: 'admin', query: GROUP_BY_EXTERNAL_KEY_QUERY, variables: { key } }, options);
507
764
  return data.customerGroupByExternalKey;
@@ -547,6 +804,12 @@ export function createCustomersModule(transport) {
547
804
  return data.removeCustomersFromGroup;
548
805
  },
549
806
  },
807
+ credit: createCustomerCreditModule(transport),
808
+ notes: createCustomerNotesModule(transport),
809
+ deliveryPreferences: createCustomerDeliveryPreferencesModule(transport),
810
+ notificationRecipients: createCustomerNotificationRecipientsModule(transport),
811
+ dateOfBirth: createCustomerDateOfBirthModule(transport),
812
+ vat: createCustomerVatModule(transport),
550
813
  addresses: {
551
814
  async list(customerId, options) {
552
815
  const redaction = redactionWatcher();
@@ -579,7 +842,17 @@ export function createCustomersModule(transport) {
579
842
  return redaction.mark(data.updateCustomerAddress);
580
843
  },
581
844
  async delete(id, options) {
582
- await transport.graphql({ api: 'admin', query: DELETE_ADDRESS_MUTATION, variables: { id } }, options);
845
+ const data = await transport.graphql({ api: 'admin', query: DELETE_ADDRESS_MUTATION, variables: { id } }, options);
846
+ // Answered at HTTP 200, so the transport never throws for it: returned unchecked, an
847
+ // address the engine kept would read as deleted.
848
+ if (data.deleteCustomerAddress.success !== true) {
849
+ throw new BusinessRuleError({
850
+ code: 'ADDRESS_NOT_DELETED',
851
+ message: 'The address was not deleted. Reload the customer and try again.',
852
+ status: 422,
853
+ service: 'vendure',
854
+ });
855
+ }
583
856
  },
584
857
  },
585
858
  };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Whether a customer can be sent an invitation, and what the last one was (headless spec §9.2
3
+ * `customers` "invitations"; W10-33). The engine decides: read it before offering
4
+ * `customers.invite`.
5
+ */
6
+ import type { Transport } from '@sixtyninecommerce/core';
7
+ import type { Call } from '../types.js';
8
+ /**
9
+ * Why no invitation can be sent, the first that applies in this order:
10
+ *
11
+ * - `DEFAULT_CHANNEL`: the call names the platform root channel or an organization's workspace,
12
+ * not a store. Nothing of the customer is read there.
13
+ * - `ERASED`: the store erased the customer. Add a new customer to serve the person again.
14
+ * - `NO_EMAIL`: the customer has no email address.
15
+ * - `PLACEHOLDER_EMAIL`: the address is a stand-in under the reserved `.invalid` domain (an ERP
16
+ * sync writes one for an account it has no address for). Set the real address first.
17
+ * - `NO_LOGIN`: a guest checkout record with no sign-in account.
18
+ * - `NO_STOREFRONT_URL`: the store has no storefront URL, so the link would lead nowhere.
19
+ */
20
+ export type CustomerInvitationBlockedReason = 'DEFAULT_CHANNEL' | 'ERASED' | 'NO_EMAIL' | 'PLACEHOLDER_EMAIL' | 'NO_LOGIN' | 'NO_STOREFRONT_URL';
21
+ /** What {@link CustomersModule.invitationStatus} answers. */
22
+ export interface CustomerInvitationStatus {
23
+ /**
24
+ * Null when nothing known here stops an invitation. `customers.invite` still judges the send
25
+ * itself and can refuse what this read cannot see: the store's invitation email switched off
26
+ * or without a recipient, or a sign-in the address shares with another store
27
+ * (`CUSTOMER_LOGIN_ON_OTHER_STORES`).
28
+ */
29
+ blockedReason: CustomerInvitationBlockedReason | (string & {}) | null;
30
+ /**
31
+ * The newest invitation sent to the address the customer has NOW, or null. An invitation to
32
+ * an earlier address does not count: it never reached the current one.
33
+ */
34
+ lastInvitation: {
35
+ customerId: string;
36
+ emailAddress: string;
37
+ mode: 'PASSWORD_SETUP' | 'MAGIC_LINK' | (string & {});
38
+ /** ISO-8601 UTC. */
39
+ sentAt: string;
40
+ } | null;
41
+ /**
42
+ * ISO-8601 UTC while the cooldown of the newest invitation (to any address) still runs:
43
+ * `customers.invite` refuses another one until then. Null when none is running.
44
+ */
45
+ cooldownUntil: string | null;
46
+ /** How long a customer waits between two invitations, in seconds. */
47
+ cooldownSeconds: number;
48
+ /** `RESEND` once {@link lastInvitation} is set: pass `resend: true` to `customers.invite`. */
49
+ nextMode: 'SEND' | 'RESEND' | (string & {});
50
+ /**
51
+ * Set when the caller's customer-data level blanked the last invitation's address (an app
52
+ * installation below `identity`): "" then means "not granted".
53
+ */
54
+ redacted?: true;
55
+ }
56
+ export declare const INVITATION_STATUS_QUERY = "\n query CustomerInvitationStatus($customerId: ID!) {\n customerInvitationStatus(customerId: $customerId) {\n blockedReason\n lastInvitation {\n customerId\n emailAddress\n mode\n sentAt\n }\n cooldownUntil\n cooldownSeconds\n nextMode\n }\n }\n";
57
+ export declare function readCustomerInvitationStatus(transport: Transport, customerId: string, options?: Call): Promise<CustomerInvitationStatus>;
@@ -0,0 +1,21 @@
1
+ import { customerDataField } from './wire.js';
2
+ export const INVITATION_STATUS_QUERY = /* GraphQL */ `
3
+ query CustomerInvitationStatus($customerId: ID!) {
4
+ customerInvitationStatus(customerId: $customerId) {
5
+ blockedReason
6
+ lastInvitation {
7
+ customerId
8
+ emailAddress
9
+ mode
10
+ sentAt
11
+ }
12
+ cooldownUntil
13
+ cooldownSeconds
14
+ nextMode
15
+ }
16
+ }
17
+ `;
18
+ export async function readCustomerInvitationStatus(transport, customerId, options) {
19
+ const { value, mark } = await customerDataField(transport, INVITATION_STATUS_QUERY, 'customerInvitationStatus', { customerId }, options);
20
+ return mark(value);
21
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * A customer's staff notes (headless spec §9.2 `customers`; W10-33). A note is an entry of the
3
+ * customer's history: `customers.get(id, { include: ['history'] })` lists it, type
4
+ * `CUSTOMER_NOTE`, with its id.
5
+ */
6
+ import type { Transport } from '@sixtyninecommerce/core';
7
+ import type { Call } from '../types.js';
8
+ /** A staff note on a customer. */
9
+ export interface CustomerNote {
10
+ id: string;
11
+ note: string;
12
+ /** Whether the customer sees it. */
13
+ isPublic: boolean;
14
+ /** ISO-8601 with offset. */
15
+ createdAt: string;
16
+ /** ISO-8601 with offset. */
17
+ updatedAt: string;
18
+ /**
19
+ * Set when the caller's customer-data level withheld the text (an app installation below
20
+ * `full`): `note` is then "", meaning "not granted".
21
+ */
22
+ redacted?: true;
23
+ }
24
+ export interface CustomerNoteInput {
25
+ note: string;
26
+ /** Whether the customer sees it. Default false: a note is for staff unless you say otherwise. */
27
+ isPublic?: boolean;
28
+ }
29
+ export interface CustomerNotesModule {
30
+ /**
31
+ * Add a note to the customer's history. Needs `UpdateCustomer`.
32
+ *
33
+ * It resolves with nothing: the engine answers the customer, not the note. The note's id (for
34
+ * `update` and `delete`) is on `customers.get(id, { include: ['history'] })`, newest first. A
35
+ * customer this store does not hold is a `NotFoundError` (`ENTITY_NOT_FOUND`).
36
+ *
37
+ * @effect write
38
+ */
39
+ add(customerId: string, input: CustomerNoteInput, options?: Call): Promise<void>;
40
+ /**
41
+ * Change a note's text. Needs `UpdateCustomer`. An empty text leaves the note as it is. An id
42
+ * that is not a note of this store is a `NotFoundError` (`ENTITY_NOT_FOUND`), the same as a
43
+ * missing one.
44
+ *
45
+ * The engine records an invitation as a note reading `Invitation sent (<MODE>) to <address>`,
46
+ * and {@link CustomersModule.invitationStatus} reads the last invitation from it: rewording or
47
+ * deleting that note makes the customer read as never invited.
48
+ *
49
+ * @effect write
50
+ */
51
+ update(noteId: string, input: {
52
+ note: string;
53
+ }, options?: Call): Promise<CustomerNote>;
54
+ /**
55
+ * Delete a note for good. Needs `UpdateCustomer`.
56
+ *
57
+ * Refused `CUSTOMER_NOTE_NOT_DELETED` (`ConflictError`, 409, as every `*_NOT_DELETED` of this
58
+ * SDK) with the engine's own reason when this store has no such note.
59
+ *
60
+ * @effect destructive
61
+ */
62
+ delete(noteId: string, options?: Call): Promise<void>;
63
+ }
64
+ export declare const ADD_CUSTOMER_NOTE_MUTATION = "\n mutation AddNoteToCustomer($input: AddNoteToCustomerInput!) {\n addNoteToCustomer(input: $input) {\n id\n }\n }\n";
65
+ export declare const UPDATE_CUSTOMER_NOTE_MUTATION = "\n mutation UpdateCustomerNote($input: UpdateCustomerNoteInput!) {\n updateCustomerNote(input: $input) {\n id\n isPublic\n data\n createdAt\n updatedAt\n }\n }\n";
66
+ export declare const DELETE_CUSTOMER_NOTE_MUTATION = "\n mutation DeleteCustomerNote($id: ID!) {\n deleteCustomerNote(id: $id) {\n result\n message\n }\n }\n";
67
+ export declare function createCustomerNotesModule(transport: Transport): CustomerNotesModule;
@@ -0,0 +1,51 @@
1
+ import { assertDeleted, fieldOf } from '../catalog/types.js';
2
+ import { customerDataField } from './wire.js';
3
+ export const ADD_CUSTOMER_NOTE_MUTATION = /* GraphQL */ `
4
+ mutation AddNoteToCustomer($input: AddNoteToCustomerInput!) {
5
+ addNoteToCustomer(input: $input) {
6
+ id
7
+ }
8
+ }
9
+ `;
10
+ export const UPDATE_CUSTOMER_NOTE_MUTATION = /* GraphQL */ `
11
+ mutation UpdateCustomerNote($input: UpdateCustomerNoteInput!) {
12
+ updateCustomerNote(input: $input) {
13
+ id
14
+ isPublic
15
+ data
16
+ createdAt
17
+ updatedAt
18
+ }
19
+ }
20
+ `;
21
+ export const DELETE_CUSTOMER_NOTE_MUTATION = /* GraphQL */ `
22
+ mutation DeleteCustomerNote($id: ID!) {
23
+ deleteCustomerNote(id: $id) {
24
+ result
25
+ message
26
+ }
27
+ }
28
+ `;
29
+ export function createCustomerNotesModule(transport) {
30
+ return {
31
+ async add(customerId, input, options) {
32
+ await fieldOf(transport, ADD_CUSTOMER_NOTE_MUTATION, 'addNoteToCustomer', { input: { id: customerId, note: input.note, isPublic: input.isPublic ?? false } }, options);
33
+ },
34
+ async update(noteId, input, options) {
35
+ const { value: entry, mark } = await customerDataField(transport, UPDATE_CUSTOMER_NOTE_MUTATION, 'updateCustomerNote', { input: { noteId, note: input.note } }, options);
36
+ const text = entry.data?.note;
37
+ return mark({
38
+ id: entry.id,
39
+ note: typeof text === 'string' ? text : '',
40
+ isPublic: entry.isPublic,
41
+ createdAt: entry.createdAt,
42
+ updatedAt: entry.updatedAt,
43
+ });
44
+ },
45
+ async delete(noteId, options) {
46
+ assertDeleted('CUSTOMER_NOTE_NOT_DELETED', [
47
+ await fieldOf(transport, DELETE_CUSTOMER_NOTE_MUTATION, 'deleteCustomerNote', { id: noteId }, options),
48
+ ]);
49
+ },
50
+ };
51
+ }