@sixtyninecommerce/admin 0.8.0 → 0.10.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 (179) hide show
  1. package/README.md +2739 -17
  2. package/dist/api-tokens/index.d.ts +15 -5
  3. package/dist/api-tokens/index.js +1 -0
  4. package/dist/apps/event-subscription.d.ts +59 -0
  5. package/dist/apps/event-subscription.js +32 -0
  6. package/dist/apps/hook-secret.d.ts +22 -0
  7. package/dist/apps/hook-secret.js +36 -0
  8. package/dist/apps/index.d.ts +54 -3
  9. package/dist/apps/index.js +17 -0
  10. package/dist/audit/index.d.ts +7 -0
  11. package/dist/auth/queries.d.ts +5 -1
  12. package/dist/auth/queries.js +6 -0
  13. package/dist/bulk/index.d.ts +323 -0
  14. package/dist/bulk/index.js +162 -0
  15. package/dist/catalog/assets.d.ts +147 -0
  16. package/dist/catalog/assets.js +164 -0
  17. package/dist/catalog/collections.d.ts +109 -0
  18. package/dist/catalog/collections.js +91 -0
  19. package/dist/catalog/facets.d.ts +103 -0
  20. package/dist/catalog/facets.js +100 -0
  21. package/dist/catalog/index.d.ts +115 -0
  22. package/dist/catalog/index.js +21 -0
  23. package/dist/catalog/options.d.ts +89 -0
  24. package/dist/catalog/options.js +88 -0
  25. package/dist/catalog/products-bulk.d.ts +21 -0
  26. package/dist/catalog/products-bulk.js +58 -0
  27. package/dist/catalog/products.d.ts +650 -0
  28. package/dist/catalog/products.js +458 -0
  29. package/dist/catalog/properties.d.ts +234 -0
  30. package/dist/catalog/properties.js +158 -0
  31. package/dist/catalog/types.d.ts +174 -0
  32. package/dist/catalog/types.js +141 -0
  33. package/dist/catalog/variants.d.ts +324 -0
  34. package/dist/catalog/variants.js +288 -0
  35. package/dist/catalog/visibility.d.ts +57 -0
  36. package/dist/catalog/visibility.js +59 -0
  37. package/dist/client.d.ts +192 -3
  38. package/dist/client.js +87 -12
  39. package/dist/compliance/index.d.ts +88 -0
  40. package/dist/compliance/index.js +88 -0
  41. package/dist/customer-data-redaction.d.ts +14 -0
  42. package/dist/customer-data-redaction.js +22 -0
  43. package/dist/customers/index.d.ts +428 -17
  44. package/dist/customers/index.js +322 -15
  45. package/dist/documents/index.d.ts +513 -16
  46. package/dist/documents/index.js +229 -3
  47. package/dist/events/index.d.ts +259 -0
  48. package/dist/events/index.js +147 -0
  49. package/dist/feeds/ai.d.ts +46 -0
  50. package/dist/feeds/ai.js +43 -0
  51. package/dist/feeds/categories.d.ts +50 -0
  52. package/dist/feeds/categories.js +56 -0
  53. package/dist/feeds/configs.d.ts +54 -0
  54. package/dist/feeds/configs.js +85 -0
  55. package/dist/feeds/identifier-fields.d.ts +25 -0
  56. package/dist/feeds/identifier-fields.js +29 -0
  57. package/dist/feeds/index.d.ts +54 -0
  58. package/dist/feeds/index.js +46 -0
  59. package/dist/feeds/preserve.d.ts +38 -0
  60. package/dist/feeds/preserve.js +74 -0
  61. package/dist/feeds/rules.d.ts +42 -0
  62. package/dist/feeds/rules.js +56 -0
  63. package/dist/feeds/types.d.ts +654 -0
  64. package/dist/feeds/types.js +135 -0
  65. package/dist/fulfillments/index.d.ts +437 -0
  66. package/dist/fulfillments/index.js +534 -0
  67. package/dist/heads/index.d.ts +60 -0
  68. package/dist/heads/index.js +45 -0
  69. package/dist/hooks/assignments.d.ts +90 -0
  70. package/dist/hooks/assignments.js +53 -0
  71. package/dist/hooks/index.d.ts +97 -0
  72. package/dist/hooks/index.js +76 -0
  73. package/dist/index.d.ts +95 -9
  74. package/dist/index.js +21 -1
  75. package/dist/integrations/index.d.ts +14 -0
  76. package/dist/integrations/index.js +9 -0
  77. package/dist/integrations/pipelines.d.ts +113 -0
  78. package/dist/integrations/pipelines.js +62 -0
  79. package/dist/integrations/types.d.ts +176 -0
  80. package/dist/integrations/types.js +8 -0
  81. package/dist/jobs/bulk-action-run.d.ts +74 -0
  82. package/dist/jobs/bulk-action-run.js +69 -0
  83. package/dist/locations/index.d.ts +6 -2
  84. package/dist/meta/index.d.ts +32 -0
  85. package/dist/orders/bulk.d.ts +75 -0
  86. package/dist/orders/bulk.js +150 -0
  87. package/dist/orders/commit-edit.d.ts +123 -0
  88. package/dist/orders/commit-edit.js +142 -0
  89. package/dist/orders/deposit-policy.d.ts +103 -0
  90. package/dist/orders/deposit-policy.js +202 -0
  91. package/dist/orders/get.d.ts +722 -0
  92. package/dist/orders/get.js +530 -0
  93. package/dist/orders/index.d.ts +186 -21
  94. package/dist/orders/index.js +57 -0
  95. package/dist/orders/line-price-preview.d.ts +297 -0
  96. package/dist/orders/line-price-preview.js +50 -0
  97. package/dist/orders/manual-discounts.d.ts +141 -0
  98. package/dist/orders/manual-discounts.js +97 -0
  99. package/dist/orders/manual-pricing.d.ts +192 -0
  100. package/dist/orders/manual-pricing.js +72 -0
  101. package/dist/orders/order-entry.d.ts +566 -0
  102. package/dist/orders/order-entry.js +129 -0
  103. package/dist/orders/parcel-charge.d.ts +45 -0
  104. package/dist/orders/parcel-charge.js +55 -0
  105. package/dist/orders/plan-edit.d.ts +489 -0
  106. package/dist/orders/plan-edit.js +93 -0
  107. package/dist/orders/status.d.ts +235 -0
  108. package/dist/orders/status.js +96 -0
  109. package/dist/orders/surcharges.d.ts +75 -2
  110. package/dist/orders/surcharges.js +110 -4
  111. package/dist/payments/account-credit.d.ts +171 -0
  112. package/dist/payments/account-credit.js +107 -0
  113. package/dist/payments/index.d.ts +241 -9
  114. package/dist/payments/index.js +92 -0
  115. package/dist/payments/methods.d.ts +37 -0
  116. package/dist/payments/methods.js +33 -0
  117. package/dist/platform/catalog.d.ts +148 -0
  118. package/dist/platform/catalog.js +74 -0
  119. package/dist/platform/entitlements.d.ts +111 -0
  120. package/dist/platform/entitlements.js +86 -0
  121. package/dist/platform/legal-entities.d.ts +139 -0
  122. package/dist/platform/legal-entities.js +126 -0
  123. package/dist/platform/legal-entity-numbering.d.ts +103 -0
  124. package/dist/platform/legal-entity-numbering.js +101 -0
  125. package/dist/platform/organization-membership.d.ts +80 -0
  126. package/dist/platform/organization-membership.js +71 -0
  127. package/dist/platform/organization-settings.d.ts +33 -0
  128. package/dist/platform/organization-settings.js +42 -0
  129. package/dist/platform/organization-sharing.d.ts +172 -0
  130. package/dist/platform/organization-sharing.js +120 -0
  131. package/dist/platform/organizations.d.ts +205 -0
  132. package/dist/platform/organizations.js +180 -0
  133. package/dist/platform/provision-store.d.ts +88 -8
  134. package/dist/platform/provision-store.js +12 -0
  135. package/dist/platform/reports.d.ts +46 -0
  136. package/dist/platform/reports.js +31 -0
  137. package/dist/platform/server-metrics.d.ts +58 -0
  138. package/dist/platform/server-metrics.js +11 -0
  139. package/dist/pos/index.d.ts +567 -0
  140. package/dist/pos/index.js +205 -0
  141. package/dist/pricing/index.d.ts +408 -0
  142. package/dist/pricing/index.js +353 -0
  143. package/dist/pricing/vouchers.d.ts +103 -0
  144. package/dist/pricing/vouchers.js +69 -0
  145. package/dist/provenance/index.d.ts +93 -0
  146. package/dist/provenance/index.js +53 -0
  147. package/dist/reports/index.d.ts +199 -0
  148. package/dist/reports/index.js +154 -0
  149. package/dist/returns/index.d.ts +225 -0
  150. package/dist/returns/index.js +160 -0
  151. package/dist/search/index.d.ts +99 -0
  152. package/dist/search/index.js +62 -0
  153. package/dist/shipping/parcel-count-suggestion.d.ts +52 -0
  154. package/dist/shipping/parcel-count-suggestion.js +73 -0
  155. package/dist/shipping/parcel-rule.d.ts +48 -0
  156. package/dist/shipping/parcel-rule.js +91 -0
  157. package/dist/stock/index.d.ts +166 -0
  158. package/dist/stock/index.js +115 -0
  159. package/dist/stock/transfers.d.ts +520 -0
  160. package/dist/stock/transfers.js +437 -0
  161. package/dist/stored-value/index.d.ts +154 -0
  162. package/dist/stored-value/index.js +130 -0
  163. package/dist/tenant/capabilities.d.ts +10 -0
  164. package/dist/tenant/entitlements.d.ts +35 -0
  165. package/dist/tenant/entitlements.js +15 -0
  166. package/dist/tenant/field-groups.d.ts +64 -0
  167. package/dist/tenant/field-groups.js +34 -0
  168. package/dist/tenant/legal-entity.d.ts +32 -0
  169. package/dist/tenant/legal-entity.js +41 -0
  170. package/dist/tenant/notifications.d.ts +148 -0
  171. package/dist/tenant/notifications.js +79 -0
  172. package/dist/tenant/retention.d.ts +10 -4
  173. package/dist/tenant/settings-sources.d.ts +103 -0
  174. package/dist/tenant/settings-sources.js +92 -0
  175. package/dist/types.d.ts +86 -11
  176. package/dist/uploads/index.d.ts +11 -7
  177. package/dist/users/index.d.ts +104 -0
  178. package/dist/users/index.js +80 -0
  179. package/package.json +3 -2
@@ -0,0 +1,353 @@
1
+ import { createPage, errorFromErrorResult, isErrorResult, NotFoundError, ValidationError, } from '@sixtyninecommerce/core';
2
+ import { createPricingVouchersModule } from './vouchers.js';
3
+ /**
4
+ * The targeting conditions (incentives sprint 2), encoded as the engine stores
5
+ * them — every argument a string, lists JSON-encoded. Their argument names and
6
+ * shapes are a stored contract.
7
+ */
8
+ export const promotionConditions = {
9
+ /**
10
+ * `selling_location`: the order is sold from one of these stock locations of
11
+ * the store — a till there, an order `orders.createOrRecover` recorded for it
12
+ * (`location`), or a register sending it as `x-location` on the Admin API. A
13
+ * web order has no location and never matches.
14
+ */
15
+ sellingLocation(locationIds) {
16
+ return {
17
+ code: 'selling_location',
18
+ arguments: [{ name: 'locationIds', value: JSON.stringify([...locationIds]) }],
19
+ };
20
+ },
21
+ /**
22
+ * `time_window`: on one of `days`, from `from` (included) to `until`
23
+ * (excluded), local time — the selling location's profile zone, else the
24
+ * store's. An `until` not after `from` runs into the next day and belongs to
25
+ * the day it opened (`fri 22:00–02:00`); equal times are 24 hours
26
+ * (`00:00`–`00:00`: all day). Judged on the wall clock, so a window in the
27
+ * hour clocks repeat in autumn is open twice, one in the hour they skip in
28
+ * spring never opens. A placed order keeps a window it was placed with.
29
+ */
30
+ timeWindow(window) {
31
+ return {
32
+ code: 'time_window',
33
+ arguments: [
34
+ { name: 'daysOfWeek', value: JSON.stringify([...window.days]) },
35
+ { name: 'startTime', value: window.from },
36
+ { name: 'endTime', value: window.until },
37
+ ],
38
+ };
39
+ },
40
+ /**
41
+ * `first_order`: the customer has no earlier placed order in this store. An
42
+ * order with no customer, or the till's walk-in, never matches.
43
+ */
44
+ firstOrder() {
45
+ return { code: 'first_order', arguments: [] };
46
+ },
47
+ /**
48
+ * `bundle_fixed_price` (incentives sprint 4): each complete set of
49
+ * `components` costs `price` — minor units of the store's DEFAULT currency, in
50
+ * the store's tax basis (as its catalogue prices are entered), converted into
51
+ * an order's currency at the checkout's rate. The discount is spread over the
52
+ * set's lines by value; a set worth `price` or less stays at list price. Pair
53
+ * it with {@link promotionActions}`.bundleFixedPrice()`.
54
+ *
55
+ * `price` is required and must be a whole amount above 0: anything else throws
56
+ * `ValidationError` (`PROMOTION_CONDITION_INVALID`) before a request is sent,
57
+ * as the engine would refuse it. To give sets away use
58
+ * {@link promotionConditions}`.nthSetPercentage` with `discount: 100`.
59
+ */
60
+ bundleFixedPrice(set) {
61
+ assertSetPrice('bundle_fixed_price', set.price);
62
+ return {
63
+ code: 'bundle_fixed_price',
64
+ arguments: [
65
+ { name: 'components', value: componentsValue(set.components) },
66
+ { name: 'price', value: String(set.price) },
67
+ ],
68
+ };
69
+ },
70
+ /**
71
+ * `nth_set_percentage`: of every `nth` complete sets, the cheapest is
72
+ * `discount`% off ("second pair at 50%" is `nth: 2, discount: 50`; `nth: 1`
73
+ * discounts every set). Pair it with {@link promotionActions}`.nthSetPercentage()`.
74
+ */
75
+ nthSetPercentage(set) {
76
+ return {
77
+ code: 'nth_set_percentage',
78
+ arguments: [
79
+ { name: 'components', value: componentsValue(set.components) },
80
+ { name: 'nth', value: String(set.nth) },
81
+ { name: 'discount', value: String(set.discount) },
82
+ ],
83
+ };
84
+ },
85
+ /**
86
+ * `mix_and_match`: any `quantity` units matching the selector (all of
87
+ * `facetValueIds`, any of `collectionIds`) for `price`, per complete group;
88
+ * leftover units stay at list price. `price` as in `bundleFixedPrice`
89
+ * (required, above 0, refused before sending otherwise). Pair it with
90
+ * {@link promotionActions}`.mixAndMatch()`.
91
+ */
92
+ mixAndMatch(group) {
93
+ assertSetPrice('mix_and_match', group.price);
94
+ return {
95
+ code: 'mix_and_match',
96
+ arguments: [
97
+ { name: 'facetValueIds', value: JSON.stringify([...(group.facetValueIds ?? [])]) },
98
+ { name: 'collectionIds', value: JSON.stringify([...(group.collectionIds ?? [])]) },
99
+ { name: 'quantity', value: String(group.quantity) },
100
+ { name: 'price', value: String(group.price) },
101
+ ],
102
+ };
103
+ },
104
+ };
105
+ /**
106
+ * A set or group price as the engine accepts it: a whole number of minor units above 0. A 0
107
+ * would make every set free (the engine refuses it on save and discounts nothing for one it
108
+ * finds stored), so it is refused here, before a request is sent, with the engine's own code.
109
+ */
110
+ function assertSetPrice(condition, price) {
111
+ if (Number.isSafeInteger(price) && price >= 1 && price <= 2_147_483_647)
112
+ return;
113
+ const message = `The "${condition}" condition's price must be a whole amount in minor units above 0; got ${String(price)}. To give sets away, use promotionConditions.nthSetPercentage with discount: 100.`;
114
+ throw new ValidationError({
115
+ code: 'PROMOTION_CONDITION_INVALID',
116
+ message,
117
+ status: 400,
118
+ service: 'vendure',
119
+ details: [{ field: 'price', code: 'PROMOTION_CONDITION_INVALID', message }],
120
+ });
121
+ }
122
+ function componentsValue(components) {
123
+ return JSON.stringify(components.map((component) => ({
124
+ facetValueIds: [...(component.facetValueIds ?? [])],
125
+ collectionIds: [...(component.collectionIds ?? [])],
126
+ quantity: component.quantity,
127
+ })));
128
+ }
129
+ /**
130
+ * The set actions (incentives sprint 4). Each takes no arguments and applies
131
+ * what its same-code condition computed, so it goes on the promotion together
132
+ * with that condition: the engine refuses one without the other.
133
+ */
134
+ export const promotionActions = {
135
+ bundleFixedPrice() {
136
+ return { code: 'bundle_fixed_price', arguments: [] };
137
+ },
138
+ nthSetPercentage() {
139
+ return { code: 'nth_set_percentage', arguments: [] };
140
+ },
141
+ mixAndMatch() {
142
+ return { code: 'mix_and_match', arguments: [] };
143
+ },
144
+ };
145
+ const DISCOUNT_FIELDS = 'promotionId adjustmentSource type description amount amountWithTax';
146
+ /** An order's pricing readout; the manual pricing doors (`orders.setLinePrices`, …) answer it too. */
147
+ export const ORDER_DISCOUNTS_FIELDS = /* GraphQL */ `
148
+ id
149
+ code
150
+ state
151
+ currencyCode
152
+ couponCodes
153
+ subTotal
154
+ subTotalWithTax
155
+ shipping
156
+ shippingWithTax
157
+ total
158
+ totalWithTax
159
+ discounts { ${DISCOUNT_FIELDS} }
160
+ promotions { id name }
161
+ lines {
162
+ id
163
+ quantity
164
+ productVariant { id sku name }
165
+ unitPrice
166
+ unitPriceWithTax
167
+ linePrice
168
+ linePriceWithTax
169
+ discountedLinePrice
170
+ discountedLinePriceWithTax
171
+ proratedLinePrice
172
+ proratedLinePriceWithTax
173
+ discounts { ${DISCOUNT_FIELDS} }
174
+ }
175
+ surcharges { id description sku price priceWithTax }
176
+ packages { promotionId label price priceWithTax lineIds quantity }
177
+ `;
178
+ const OPERATION_FIELDS = 'code args { name value } summary';
179
+ export const ACTIVE_PROMOTIONS_QUERY = /* GraphQL */ `
180
+ query ActivePromotions($locationId: ID, $customerId: ID) {
181
+ activePromotions(locationId: $locationId, customerId: $customerId) {
182
+ id
183
+ name
184
+ description
185
+ startsAt
186
+ endsAt
187
+ requiresCouponCode
188
+ usageLimit
189
+ perCustomerUsageLimit
190
+ conditions { ${OPERATION_FIELDS} }
191
+ actions { ${OPERATION_FIELDS} }
192
+ summary
193
+ }
194
+ }
195
+ `;
196
+ const PROMOTION_FIELDS = /* GraphQL */ `
197
+ id
198
+ name
199
+ description
200
+ enabled
201
+ couponCode
202
+ startsAt
203
+ endsAt
204
+ usageLimit
205
+ perCustomerUsageLimit
206
+ conditions { code args { name value } }
207
+ actions { code args { name value } }
208
+ customFields { combineWithB2b combineWithOtherPromotions priority }
209
+ `;
210
+ export const CREATE_PROMOTION_MUTATION = /* GraphQL */ `
211
+ mutation CreatePromotion($input: CreatePromotionInput!) {
212
+ createPromotion(input: $input) {
213
+ ... on Promotion { ${PROMOTION_FIELDS} }
214
+ ... on ErrorResult { errorCode message }
215
+ }
216
+ }
217
+ `;
218
+ export const UPDATE_PROMOTION_MUTATION = /* GraphQL */ `
219
+ mutation UpdatePromotion($input: UpdatePromotionInput!) {
220
+ updatePromotion(input: $input) {
221
+ ... on Promotion { ${PROMOTION_FIELDS} }
222
+ ... on ErrorResult { errorCode message }
223
+ }
224
+ }
225
+ `;
226
+ /** The condition the dashboard and the admin app put on every promotion they save. */
227
+ export const STACKING_GUARD_CONDITION = 'promotion_stacking_guard';
228
+ function withStackingGuard(conditions) {
229
+ return conditions.some((condition) => condition.code === STACKING_GUARD_CONDITION)
230
+ ? conditions
231
+ : [{ code: STACKING_GUARD_CONDITION, arguments: [] }, ...conditions];
232
+ }
233
+ /** The SDK input as the engine's `Create`/`UpdatePromotionInput`. */
234
+ function promotionInputOf(input) {
235
+ const { combineWithB2b, combineWithOtherPromotions, priority, conditions, ...rest } = input;
236
+ const customFields = {
237
+ ...(combineWithB2b === undefined ? {} : { combineWithB2b }),
238
+ ...(combineWithOtherPromotions === undefined ? {} : { combineWithOtherPromotions }),
239
+ ...(priority === undefined ? {} : { priority }),
240
+ };
241
+ return {
242
+ ...rest,
243
+ ...(conditions === undefined ? {} : { conditions: withStackingGuard(conditions) }),
244
+ ...(Object.keys(customFields).length === 0 ? {} : { customFields }),
245
+ };
246
+ }
247
+ export const APPLY_COUPON_CODE_TO_ORDER_MUTATION = /* GraphQL */ `
248
+ mutation ApplyCouponCodeToOrder($orderId: ID!, $couponCode: String!, $idempotencyKey: String) {
249
+ applyCouponCodeToOrder(
250
+ orderId: $orderId
251
+ couponCode: $couponCode
252
+ idempotencyKey: $idempotencyKey
253
+ ) {
254
+ ${ORDER_DISCOUNTS_FIELDS}
255
+ }
256
+ }
257
+ `;
258
+ export const REMOVE_COUPON_CODE_FROM_ORDER_MUTATION = /* GraphQL */ `
259
+ mutation RemoveCouponCodeFromOrder($orderId: ID!, $couponCode: String!, $idempotencyKey: String) {
260
+ removeCouponCodeFromOrder(
261
+ orderId: $orderId
262
+ couponCode: $couponCode
263
+ idempotencyKey: $idempotencyKey
264
+ ) {
265
+ ${ORDER_DISCOUNTS_FIELDS}
266
+ }
267
+ }
268
+ `;
269
+ export const ORDER_DISCOUNTS_QUERY = /* GraphQL */ `
270
+ query OrderDiscounts($id: ID!) {
271
+ order(id: $id) {
272
+ ${ORDER_DISCOUNTS_FIELDS}
273
+ }
274
+ }
275
+ `;
276
+ /** `undefined` for an absent or blank filter id: an explicit null is a value the engine reads. */
277
+ function idVariable(value) {
278
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined;
279
+ }
280
+ export function createPricingModule(transport) {
281
+ return {
282
+ promotions: {
283
+ async active(filter, options) {
284
+ const data = await transport.graphql({
285
+ api: 'admin',
286
+ query: ACTIVE_PROMOTIONS_QUERY,
287
+ variables: {
288
+ locationId: idVariable(filter?.locationId),
289
+ customerId: idVariable(filter?.customerId),
290
+ },
291
+ }, options);
292
+ const items = data.activePromotions;
293
+ return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
294
+ throw new Error('activePromotions returns a complete list; there is no next page');
295
+ });
296
+ },
297
+ async create(input, options) {
298
+ const data = await transport.graphql({
299
+ api: 'admin',
300
+ query: CREATE_PROMOTION_MUTATION,
301
+ variables: { input: promotionInputOf(input) },
302
+ }, options);
303
+ // A union at HTTP 200 (`Promotion | MissingConditionsError`). With the guard
304
+ // always sent, core does not answer the error member for this input; should
305
+ // it ever, it is thrown, never handed back as a promotion.
306
+ if (isErrorResult(data.createPromotion))
307
+ throw errorFromErrorResult(data.createPromotion);
308
+ return data.createPromotion;
309
+ },
310
+ async update(input, options) {
311
+ const data = await transport.graphql({
312
+ api: 'admin',
313
+ query: UPDATE_PROMOTION_MUTATION,
314
+ variables: { input: promotionInputOf(input) },
315
+ }, options);
316
+ if (isErrorResult(data.updatePromotion))
317
+ throw errorFromErrorResult(data.updatePromotion);
318
+ return data.updatePromotion;
319
+ },
320
+ },
321
+ vouchers: createPricingVouchersModule(transport),
322
+ coupons: {
323
+ async apply(orderId, couponCode, options) {
324
+ const data = await transport.graphql({
325
+ api: 'admin',
326
+ query: APPLY_COUPON_CODE_TO_ORDER_MUTATION,
327
+ variables: { orderId, couponCode, idempotencyKey: options?.idempotencyKey },
328
+ }, options);
329
+ return data.applyCouponCodeToOrder;
330
+ },
331
+ async remove(orderId, couponCode, options) {
332
+ const data = await transport.graphql({
333
+ api: 'admin',
334
+ query: REMOVE_COUPON_CODE_FROM_ORDER_MUTATION,
335
+ variables: { orderId, couponCode, idempotencyKey: options?.idempotencyKey },
336
+ }, options);
337
+ return data.removeCouponCodeFromOrder;
338
+ },
339
+ },
340
+ async discounts(orderId, options) {
341
+ const data = await transport.graphql({ api: 'admin', query: ORDER_DISCOUNTS_QUERY, variables: { id: orderId } }, options);
342
+ if (!data.order) {
343
+ throw new NotFoundError({
344
+ code: 'ORDER_NOT_FOUND',
345
+ message: `This store has no order ${orderId}.`,
346
+ status: 404,
347
+ service: 'vendure',
348
+ });
349
+ }
350
+ return data.order;
351
+ },
352
+ };
353
+ }
@@ -0,0 +1,103 @@
1
+ import type { Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /**
4
+ * Personal vouchers (incentives sprint 9): a voucher TEMPLATE under a promotion, codes issued one
5
+ * by one to a customer of the store, single use, with an optional expiry and the issuing journey
6
+ * recorded as an opaque `source`. Applied like any coupon (`pricing.coupons.apply`, the till's
7
+ * `pos.sale.applyCouponCode`), which refuse `VOUCHER_NOT_FOR_CUSTOMER`, `VOUCHER_EXPIRED` and
8
+ * `VOUCHER_ALREADY_REDEEMED`. Issuing needs `CreatePromotion`.
9
+ */
10
+ /** A customer of this store: exactly one of its id or a master system's key (PR #1176). */
11
+ export type VoucherCustomer = {
12
+ id: string;
13
+ } | {
14
+ externalRef: {
15
+ system: string;
16
+ ref: string;
17
+ };
18
+ };
19
+ export interface IssueVoucherInput {
20
+ templateId: string;
21
+ customer: VoucherCustomer;
22
+ /** Usable through the end of the store's local day this many days after today (1 to 3650). */
23
+ validityDays?: number;
24
+ /** Or an explicit instant, ISO-8601. Neither: the template's default, else the promotion's window. */
25
+ expiresAt?: string;
26
+ /** The issuing journey; with a `ref`, an issue is exactly-once per template, customer and source. */
27
+ source: {
28
+ system: string;
29
+ ref?: string;
30
+ };
31
+ }
32
+ export interface IssueVouchersInput {
33
+ templateId: string;
34
+ sourceSystem: string;
35
+ /** At most 1,000. */
36
+ items: Array<{
37
+ customer: VoucherCustomer;
38
+ validityDays?: number;
39
+ expiresAt?: string;
40
+ sourceRef?: string;
41
+ }>;
42
+ }
43
+ export interface IssuedVoucher {
44
+ id: string;
45
+ code: string;
46
+ customerId: string;
47
+ /** The earlier of its own expiry and its promotion's end; null when neither ends. */
48
+ expiresAt: string | null;
49
+ /** False when this template, customer and source had issued it already: the same code. */
50
+ created: boolean;
51
+ }
52
+ export interface IssueVouchersItem {
53
+ index: number;
54
+ voucher: IssuedVoucher | null;
55
+ /** A `VOUCHER_*` code or `ENTITY_NOT_FOUND` for a refused item; the others were issued. */
56
+ errorCode: string | null;
57
+ message: string | null;
58
+ }
59
+ export type VoucherStatus = 'OPEN' | 'REDEEMED' | 'EXPIRED' | 'REVOKED';
60
+ export interface Voucher {
61
+ id: string;
62
+ code: string;
63
+ templateId: string;
64
+ promotionId: string;
65
+ customerId: string | null;
66
+ status: VoucherStatus;
67
+ issuedAt: string;
68
+ expiresAt: string | null;
69
+ sourceSystem: string | null;
70
+ sourceRef: string | null;
71
+ /** The order that redeemed it. */
72
+ orderId: string | null;
73
+ }
74
+ export interface VoucherTemplate {
75
+ id: string;
76
+ promotionId: string;
77
+ prefix: string;
78
+ codeLength: number;
79
+ defaultValidityDays: number | null;
80
+ }
81
+ export interface PricingVouchersModule {
82
+ /** A voucher template under a promotion of this store (codes of 12 to 32 random characters). */
83
+ createTemplate(input: {
84
+ promotionId: string;
85
+ prefix: string;
86
+ codeLength?: number;
87
+ defaultValidityDays?: number;
88
+ }, options?: Call): Promise<VoucherTemplate>;
89
+ /**
90
+ * Issue one voucher. `options.idempotencyKey` replays the first answer; the same template,
91
+ * customer and `source.ref` answers the voucher already issued (`created: false`) under any key.
92
+ */
93
+ issue(input: IssueVoucherInput, options?: Call): Promise<IssuedVoucher>;
94
+ /** Issue up to 1,000 vouchers of one template; a refused item answers in its place. */
95
+ issueMany(input: IssueVouchersInput, options?: Call): Promise<IssueVouchersItem[]>;
96
+ /** A customer's vouchers in this store, newest first; every status when `status` is omitted. */
97
+ forCustomer(customer: VoucherCustomer, status?: VoucherStatus, options?: Call): Promise<Voucher[]>;
98
+ }
99
+ export declare const CREATE_VOUCHER_TEMPLATE_MUTATION = "\n mutation CreateVoucherTemplate($input: CreateVoucherTemplateInput!) {\n createVoucherTemplate(input: $input) {\n id\n promotionId\n prefix\n codeLength\n defaultValidityDays\n }\n }\n";
100
+ export declare const ISSUE_VOUCHER_MUTATION = "\n mutation IssueVoucher($input: IssueVoucherInput!, $idempotencyKey: String) {\n issueVoucher(input: $input, idempotencyKey: $idempotencyKey) { id code customerId expiresAt created }\n }\n";
101
+ export declare const ISSUE_VOUCHERS_MUTATION = "\n mutation IssueVouchers($input: IssueVouchersInput!, $idempotencyKey: String) {\n issueVouchers(input: $input, idempotencyKey: $idempotencyKey) {\n items { index errorCode message voucher { id code customerId expiresAt created } }\n }\n }\n";
102
+ export declare const VOUCHERS_FOR_CUSTOMER_QUERY = "\n query VouchersForCustomer($customer: VoucherCustomerInput!, $status: VoucherStatus) {\n vouchersForCustomer(customer: $customer, status: $status) {\n id\n code\n templateId\n promotionId\n customerId\n status\n issuedAt\n expiresAt\n sourceSystem\n sourceRef\n orderId\n }\n }\n";
103
+ export declare function createPricingVouchersModule(transport: Transport): PricingVouchersModule;
@@ -0,0 +1,69 @@
1
+ const ISSUED_VOUCHER_FIELDS = `id code customerId expiresAt created`;
2
+ export const CREATE_VOUCHER_TEMPLATE_MUTATION = /* GraphQL */ `
3
+ mutation CreateVoucherTemplate($input: CreateVoucherTemplateInput!) {
4
+ createVoucherTemplate(input: $input) {
5
+ id
6
+ promotionId
7
+ prefix
8
+ codeLength
9
+ defaultValidityDays
10
+ }
11
+ }
12
+ `;
13
+ export const ISSUE_VOUCHER_MUTATION = /* GraphQL */ `
14
+ mutation IssueVoucher($input: IssueVoucherInput!, $idempotencyKey: String) {
15
+ issueVoucher(input: $input, idempotencyKey: $idempotencyKey) { ${ISSUED_VOUCHER_FIELDS} }
16
+ }
17
+ `;
18
+ export const ISSUE_VOUCHERS_MUTATION = /* GraphQL */ `
19
+ mutation IssueVouchers($input: IssueVouchersInput!, $idempotencyKey: String) {
20
+ issueVouchers(input: $input, idempotencyKey: $idempotencyKey) {
21
+ items { index errorCode message voucher { ${ISSUED_VOUCHER_FIELDS} } }
22
+ }
23
+ }
24
+ `;
25
+ export const VOUCHERS_FOR_CUSTOMER_QUERY = /* GraphQL */ `
26
+ query VouchersForCustomer($customer: VoucherCustomerInput!, $status: VoucherStatus) {
27
+ vouchersForCustomer(customer: $customer, status: $status) {
28
+ id
29
+ code
30
+ templateId
31
+ promotionId
32
+ customerId
33
+ status
34
+ issuedAt
35
+ expiresAt
36
+ sourceSystem
37
+ sourceRef
38
+ orderId
39
+ }
40
+ }
41
+ `;
42
+ export function createPricingVouchersModule(transport) {
43
+ return {
44
+ async createTemplate(input, options) {
45
+ const data = await transport.graphql({ api: 'admin', query: CREATE_VOUCHER_TEMPLATE_MUTATION, variables: { input } }, options);
46
+ return data.createVoucherTemplate;
47
+ },
48
+ async issue(input, options) {
49
+ const data = await transport.graphql({
50
+ api: 'admin',
51
+ query: ISSUE_VOUCHER_MUTATION,
52
+ variables: { input, idempotencyKey: options?.idempotencyKey },
53
+ }, options);
54
+ return data.issueVoucher;
55
+ },
56
+ async issueMany(input, options) {
57
+ const data = await transport.graphql({
58
+ api: 'admin',
59
+ query: ISSUE_VOUCHERS_MUTATION,
60
+ variables: { input, idempotencyKey: options?.idempotencyKey },
61
+ }, options);
62
+ return data.issueVouchers.items;
63
+ },
64
+ async forCustomer(customer, status, options) {
65
+ const data = await transport.graphql({ api: 'admin', query: VOUCHERS_FOR_CUSTOMER_QUERY, variables: { customer, status } }, options);
66
+ return data.vouchersForCustomer;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,93 @@
1
+ import { type Page, type Transport } from '@sixtyninecommerce/core';
2
+ import type { Call } from '../types.js';
3
+ /** The records provenance is kept for. An address change is recorded on its customer. */
4
+ export type ProvenanceEntityType = 'PRODUCT' | 'VARIANT' | 'CUSTOMER';
5
+ /**
6
+ * The field groups a row can be about: the four groups an app can be granted (spec §8.10).
7
+ */
8
+ export type ProvenanceFieldGroup = 'price' | 'stock' | 'product_details' | 'customer_details';
9
+ /** Who made the last change of a field group. */
10
+ export interface ProvenanceActor {
11
+ /**
12
+ * `user` (a staff member), `customer` (the shopper, on their own record), `api_token`,
13
+ * `app_installation`, `system` (a platform job), `service`, `anonymous` (a guest), or
14
+ * `unknown` when nothing could name the writer.
15
+ */
16
+ type: string;
17
+ /**
18
+ * A User id for `user` and `customer`, an API token or app installation id, or
19
+ * `system:<job>`. Null for a guest, an unknown writer, or a customer since erased.
20
+ */
21
+ id: string | null;
22
+ /**
23
+ * The app's name, the API token's name, the job (`system:<job>`), or a staff member's name for
24
+ * a caller holding `ReadAdministrator` who can see that staff member. Null otherwise, and never
25
+ * a customer's name.
26
+ */
27
+ label: string | null;
28
+ }
29
+ /** The app or API key whose change a field group last carries: the "Managed by" badge. */
30
+ export interface ProvenanceManagedBy {
31
+ kind: 'app_installation' | 'api_token';
32
+ /** The app installation's or API token's id. */
33
+ id: string;
34
+ /** The app's name or the API token's name. */
35
+ label: string;
36
+ }
37
+ /** The last change of one field group of one record, in the store the client acts on. */
38
+ export interface EntityProvenance {
39
+ entityType: ProvenanceEntityType;
40
+ entityId: string;
41
+ /** A {@link ProvenanceFieldGroup}; a group a newer engine adds reaches you as it is. */
42
+ fieldGroup: string;
43
+ actor: ProvenanceActor;
44
+ /** The platform job that carried a change made for someone else (a bulk import), or null. */
45
+ via: string | null;
46
+ /** When the change committed, ISO-8601 UTC. */
47
+ writtenAt: string;
48
+ /** The trace of the change: the key into `admin.audit.correlation` and the event log. */
49
+ traceId: string | null;
50
+ /**
51
+ * Non-null only when the last change of THIS group was made by an app or an API key of this
52
+ * store: the rule for showing "Managed by <name>". A staff, customer or job change clears it.
53
+ */
54
+ managedBy: ProvenanceManagedBy | null;
55
+ }
56
+ /** The most records one read may name; more is refused with `PROVENANCE_REQUEST_INVALID`. */
57
+ export declare const PROVENANCE_MAX_IDS = 250;
58
+ export declare const ENTITY_PROVENANCE_QUERY = "\n query EntityProvenance($entityType: ProvenanceEntityType!, $ids: [ID!]!) {\n entityProvenance(entityType: $entityType, ids: $ids) {\n entityType\n entityId\n fieldGroup\n actor {\n type\n id\n label\n }\n via\n writtenAt\n traceId\n managedBy {\n kind\n id\n label\n }\n }\n }\n";
59
+ /**
60
+ * Who last changed each field group of a product, variant or customer (spec §8.10): the price, the
61
+ * stock, the product details, the customer's details. Recorded for every change, made by staff,
62
+ * an app, an API key, an import or the shopper, in the change's own transaction.
63
+ *
64
+ * - **Last writer only, per group.** One row per (record, group), replaced by the next write
65
+ * that NAMES a field of that group, an unchanged value re-sent included (the engine sees no
66
+ * prior value, as with `product_variant.price_changed`). What changed and when, in order, is the platform event log filtered by the
67
+ * record (`GET /event-log?aggregate_type=&aggregate_id=` in the plugins service, which needs
68
+ * `ReadPlatformEvents`); it is not here.
69
+ * - **`managedBy` is the badge rule.** Show "Managed by <label>" only when it is non-null.
70
+ * - **Only the store you act on.** A record the store does not hold, and one it holds with
71
+ * nothing recorded since provenance began, answer the same way: no rows.
72
+ * - **Permissions:** products and variants need `ReadCatalog` or `ReadProduct`, customers
73
+ * `ReadCustomer` (`FORBIDDEN` → `PermissionDeniedError`). Staff names need `ReadAdministrator`.
74
+ *
75
+ * Refusals: `PROVENANCE_REQUEST_INVALID` (`ValidationError`) for more than
76
+ * {@link PROVENANCE_MAX_IDS} ids; `ENTITY_NOT_FOUND` (`NotFoundError`) for an id that is not
77
+ * a record id at all, and from `get` for a record with nothing recorded.
78
+ */
79
+ export interface ProvenanceModule {
80
+ /**
81
+ * Every recorded group of each named record, in the order the ids were given, then by group.
82
+ * A `Page` because every list in this SDK is one; it is always complete (one round trip, at
83
+ * most {@link PROVENANCE_MAX_IDS} ids).
84
+ */
85
+ list(entityType: ProvenanceEntityType, ids: readonly string[], options?: Call): Promise<Page<EntityProvenance>>;
86
+ /**
87
+ * The recorded groups of one record. Throws `NotFoundError` (`ENTITY_NOT_FOUND`) when the
88
+ * store holds none: the record is not this store's, or nothing has changed it since
89
+ * provenance began. The two are deliberately indistinguishable.
90
+ */
91
+ get(entityType: ProvenanceEntityType, id: string, options?: Call): Promise<EntityProvenance[]>;
92
+ }
93
+ export declare function createProvenanceModule(transport: Transport): ProvenanceModule;
@@ -0,0 +1,53 @@
1
+ import { createPage, NotFoundError } from '@sixtyninecommerce/core';
2
+ /** The most records one read may name; more is refused with `PROVENANCE_REQUEST_INVALID`. */
3
+ export const PROVENANCE_MAX_IDS = 250;
4
+ export const ENTITY_PROVENANCE_QUERY = /* GraphQL */ `
5
+ query EntityProvenance($entityType: ProvenanceEntityType!, $ids: [ID!]!) {
6
+ entityProvenance(entityType: $entityType, ids: $ids) {
7
+ entityType
8
+ entityId
9
+ fieldGroup
10
+ actor {
11
+ type
12
+ id
13
+ label
14
+ }
15
+ via
16
+ writtenAt
17
+ traceId
18
+ managedBy {
19
+ kind
20
+ id
21
+ label
22
+ }
23
+ }
24
+ }
25
+ `;
26
+ export function createProvenanceModule(transport) {
27
+ async function read(entityType, ids, options) {
28
+ const data = await transport.graphql({ api: 'admin', query: ENTITY_PROVENANCE_QUERY, variables: { entityType, ids: [...ids] } }, options);
29
+ return data.entityProvenance;
30
+ }
31
+ return {
32
+ async list(entityType, ids, options) {
33
+ const items = await read(entityType, ids, options);
34
+ return createPage(items, { nextCursor: null, hasNext: false, total: items.length }, () => {
35
+ // Unreachable while `hasNext` is false — `Page.next()` short-circuits — and a rejection
36
+ // rather than a silent re-fetch if that changes.
37
+ throw new Error('entityProvenance returns a complete list; there is no next page');
38
+ });
39
+ },
40
+ async get(entityType, id, options) {
41
+ const rows = await read(entityType, [id], options);
42
+ if (rows.length === 0) {
43
+ throw new NotFoundError({
44
+ code: 'ENTITY_NOT_FOUND',
45
+ message: `This store has no recorded change of ${entityType.toLowerCase()} ${id}.`,
46
+ status: 404,
47
+ service: 'vendure',
48
+ });
49
+ }
50
+ return rows;
51
+ },
52
+ };
53
+ }