@aglyn/plugins-commerce 1.0.0-beta.220 → 1.0.0-beta.222

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 (130) hide show
  1. package/README.md +4 -2
  2. package/package.json +14 -13
  3. package/src/lib/components/console/catalog-organization-card.component.js +9 -0
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/discounts-card.component.js +8 -0
  6. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  7. package/src/lib/components/console/gift-cards-card.component.js +8 -0
  8. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-coupons-card.component.js +8 -0
  10. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-orders-card.component.js +34 -70
  12. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  13. package/src/lib/components/console/product-zones.d.ts +7 -6
  14. package/src/lib/components/console/product-zones.js +5 -4
  15. package/src/lib/components/console/product-zones.js.map +1 -1
  16. package/src/lib/components/console/products-hub-card.component.js +86 -284
  17. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  18. package/src/lib/declarations.console-server.d.ts +38 -0
  19. package/src/lib/declarations.console-server.js +67 -0
  20. package/src/lib/declarations.console-server.js.map +1 -0
  21. package/src/lib/model/commerce-gift-cards.d.ts +26 -0
  22. package/src/lib/model/commerce-gift-cards.js +42 -0
  23. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  24. package/src/lib/model/commerce-orders.d.ts +0 -57
  25. package/src/lib/model/commerce-orders.js +0 -80
  26. package/src/lib/model/commerce-orders.js.map +1 -1
  27. package/src/lib/model/index.d.ts +0 -1
  28. package/src/lib/model/index.js +0 -1
  29. package/src/lib/model/index.js.map +1 -1
  30. package/src/lib/plugin.js +5 -0
  31. package/src/lib/plugin.js.map +1 -1
  32. package/src/lib/server/api-v1/openapi.d.ts +27 -0
  33. package/src/lib/server/api-v1/openapi.js +219 -0
  34. package/src/lib/server/api-v1/openapi.js.map +1 -0
  35. package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
  36. package/src/lib/server/api-v1/orders-and-products.js +419 -0
  37. package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
  38. package/src/lib/server/billing-webhook.js +83 -56
  39. package/src/lib/server/billing-webhook.js.map +1 -1
  40. package/src/lib/server/download.js +4 -1
  41. package/src/lib/server/download.js.map +1 -1
  42. package/src/lib/server/fulfill-order.d.ts +52 -11
  43. package/src/lib/server/fulfill-order.js +6 -9
  44. package/src/lib/server/fulfill-order.js.map +1 -1
  45. package/src/lib/server/gift-card-issue.d.ts +57 -0
  46. package/src/lib/server/gift-card-issue.js +155 -0
  47. package/src/lib/server/gift-card-issue.js.map +1 -0
  48. package/src/lib/server/gift-cards.js +25 -84
  49. package/src/lib/server/gift-cards.js.map +1 -1
  50. package/src/lib/server/membership-register.js +9 -8
  51. package/src/lib/server/membership-register.js.map +1 -1
  52. package/src/lib/server/newsletter.js +7 -5
  53. package/src/lib/server/newsletter.js.map +1 -1
  54. package/src/lib/server/paid-media-delivery.d.ts +21 -0
  55. package/src/lib/server/paid-media-delivery.js +23 -5
  56. package/src/lib/server/paid-media-delivery.js.map +1 -1
  57. package/src/lib/server/paid-media-serve-count.d.ts +28 -0
  58. package/src/lib/server/paid-media-serve-count.js +46 -0
  59. package/src/lib/server/paid-media-serve-count.js.map +1 -0
  60. package/src/lib/server/person-eraser.d.ts +45 -0
  61. package/src/lib/server/person-eraser.js +57 -0
  62. package/src/lib/server/person-eraser.js.map +1 -0
  63. package/src/lib/server/pos-order.js +17 -9
  64. package/src/lib/server/pos-order.js.map +1 -1
  65. package/src/lib/server/refund.js +15 -15
  66. package/src/lib/server/refund.js.map +1 -1
  67. package/src/lib/server/restock-flag.d.ts +2 -2
  68. package/src/lib/server/restock-flag.js +1 -1
  69. package/src/lib/server/restock-flag.js.map +1 -1
  70. package/src/lib/server/stream.js +4 -1
  71. package/src/lib/server/stream.js.map +1 -1
  72. package/src/lib/server.js +5 -21
  73. package/src/lib/server.js.map +1 -1
  74. package/src/lib/testing/stand-in-person-records.d.ts +43 -0
  75. package/src/lib/testing/stand-in-person-records.js +63 -0
  76. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  77. package/src/lib/testing/stand-in-record-system.d.ts +11 -2
  78. package/src/lib/testing/stand-in-record-system.js +5 -3
  79. package/src/lib/testing/stand-in-record-system.js.map +1 -1
  80. package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
  81. package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
  82. package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
  83. package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
  84. package/src/lib/transfer/gift-cards-plan.js +40 -0
  85. package/src/lib/transfer/gift-cards-plan.js.map +1 -0
  86. package/src/lib/transfer/gift-cards.server.d.ts +34 -0
  87. package/src/lib/transfer/gift-cards.server.js +296 -0
  88. package/src/lib/transfer/gift-cards.server.js.map +1 -0
  89. package/src/lib/transfer/list-filter.d.ts +29 -0
  90. package/src/lib/transfer/list-filter.js +112 -0
  91. package/src/lib/transfer/list-filter.js.map +1 -0
  92. package/src/lib/transfer/product-import-results.d.ts +29 -0
  93. package/src/lib/transfer/product-import-results.js +49 -0
  94. package/src/lib/transfer/product-import-results.js.map +1 -0
  95. package/src/lib/transfer/product-import-step.component.d.ts +30 -0
  96. package/src/lib/transfer/product-import-step.component.js +99 -0
  97. package/src/lib/transfer/product-import-step.component.js.map +1 -0
  98. package/src/lib/transfer/product-transfer.d.ts +211 -0
  99. package/src/lib/transfer/product-transfer.js +1594 -0
  100. package/src/lib/transfer/product-transfer.js.map +1 -0
  101. package/src/lib/transfer/products.server.d.ts +44 -0
  102. package/src/lib/transfer/products.server.js +550 -0
  103. package/src/lib/transfer/products.server.js.map +1 -0
  104. package/src/lib/transfer/records-transfer.d.ts +182 -0
  105. package/src/lib/transfer/records-transfer.js +1153 -0
  106. package/src/lib/transfer/records-transfer.js.map +1 -0
  107. package/src/lib/transfer/records.server.d.ts +68 -0
  108. package/src/lib/transfer/records.server.js +455 -0
  109. package/src/lib/transfer/records.server.js.map +1 -0
  110. package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
  111. package/src/lib/transfer/register-transfer-resources.js +171 -0
  112. package/src/lib/transfer/register-transfer-resources.js.map +1 -0
  113. package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
  114. package/src/lib/transfer/register-transfer-ui.js +78 -0
  115. package/src/lib/transfer/register-transfer-ui.js.map +1 -0
  116. package/src/lib/transfer/server-common.d.ts +76 -0
  117. package/src/lib/transfer/server-common.js +165 -0
  118. package/src/lib/transfer/server-common.js.map +1 -0
  119. package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
  120. package/src/lib/transfer/transfer-header-actions.component.js +79 -0
  121. package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
  122. package/src/lib/transfer/transfer-keys.d.ts +31 -0
  123. package/src/lib/transfer/transfer-keys.js +56 -0
  124. package/src/lib/transfer/transfer-keys.js.map +1 -0
  125. package/src/lib/model/commerce-io.d.ts +0 -36
  126. package/src/lib/model/commerce-io.js +0 -245
  127. package/src/lib/model/commerce-io.js.map +0 -1
  128. package/src/lib/server/contact-refund.d.ts +0 -117
  129. package/src/lib/server/contact-refund.js +0 -196
  130. package/src/lib/server/contact-refund.js.map +0 -1
@@ -0,0 +1,1153 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { buildTransferPlan, isBlankTransferValue, TRANSFER_FILE_SAMPLE_ROW, TRANSFER_SYSTEM_GROUP } from "@aglyn/aglyn/data-transfer";
18
+ import { commerceSlug } from "../model/commerce.js";
19
+ import { GIFT_CARD_CONFIRM_STEP_ID } from "./transfer-keys.js";
20
+ import { GIFT_CARD_CURRENCY, giftCardAmountProblem, giftCardCodeOf, giftCardCodeProblem } from "../model/commerce-gift-cards.js";
21
+ import { ORDER_CHANNEL_LABELS, ORDER_STATUS_LABELS, liftLegacyOrder } from "../model/commerce-orders.js";
22
+ /*
23
+ * ORDERS, DISCOUNTS, COUPONS, GIFT CARDS AND CATEGORIES AS FILES (AGL-3531):
24
+ * each resource's fields, and its stored document as the record a file
25
+ * names. Pure — the server half is `records.server.ts`.
26
+ *
27
+ * ## What imports, and why the rest does not
28
+ *
29
+ * - Categories, discounts and coupons import: each is a document a person
30
+ * writes from the console with nothing derived behind it, so a file
31
+ * writes the same fields the console's card does. A discount or coupon
32
+ * created from a file starts SWITCHED OFF unless the file says Active —
33
+ * a promotion going live from a spreadsheet is money leaving the store.
34
+ * How many times one was used is history, exported and never written.
35
+ * - Orders are exported only: an order is the record of a sale, written by
36
+ * checkout, the register or a paid draft.
37
+ * - Gift cards import by ISSUING each card (AGL-3551, below), never by
38
+ * writing a balance: a balance is money a shopper can spend. A gift
39
+ * card's ID is its code, so the file is worth what the cards are.
40
+ */ const iso = (ms)=>{
41
+ var _ref;
42
+ var _ms_toDate;
43
+ const time = typeof ms === 'number' ? ms : ms && typeof ms.toMillis === 'function' ? ms.toMillis() : ms && typeof ms.toDate === 'function' ? (_ref = (_ms_toDate = ms.toDate()) == null ? void 0 : _ms_toDate.getTime()) != null ? _ref : NaN : NaN;
44
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
45
+ };
46
+ const dollars = (cents)=>{
47
+ const value = Number(cents);
48
+ return cents === null || cents === undefined || !Number.isFinite(value) ? null : Math.round(value) / 100;
49
+ };
50
+ const text = (value)=>value === null || value === undefined ? '' : String(value).trim();
51
+ /** A planned row's changes, by field id. */ export function rowChanges(row) {
52
+ return Object.fromEntries(row.diff.map((change)=>[
53
+ change.fieldId,
54
+ change.after
55
+ ]));
56
+ }
57
+ /**
58
+ * The core's plan with the fields a NEW record cannot be made without
59
+ * marked required — never in the catalog, where a required field must be
60
+ * mapped, and a file that only switches discounts off would be refused.
61
+ */ export function planWithCreateRequired(input, required) {
62
+ return buildTransferPlan(_extends({}, input, {
63
+ fields: input.fields.map((field)=>required.includes(field.id) ? _extends({}, field, {
64
+ required: true
65
+ }) : field)
66
+ }));
67
+ }
68
+ /*==========================================
69
+ * ORDERS
70
+ *=========================================*/ export const ORDER_TRANSFER_FIELDS = [
71
+ {
72
+ id: 'number',
73
+ label: 'Order number',
74
+ group: 'order',
75
+ type: 'integer',
76
+ readOnly: true
77
+ },
78
+ {
79
+ id: 'date',
80
+ label: 'Date',
81
+ group: 'order',
82
+ type: 'datetime',
83
+ readOnly: true
84
+ },
85
+ {
86
+ id: 'status',
87
+ label: 'Status',
88
+ group: 'order',
89
+ type: 'text',
90
+ readOnly: true
91
+ },
92
+ {
93
+ id: 'channel',
94
+ label: 'Channel',
95
+ group: 'order',
96
+ type: 'text',
97
+ readOnly: true
98
+ },
99
+ {
100
+ id: 'customerEmail',
101
+ label: 'Customer email',
102
+ group: 'customer',
103
+ type: 'email',
104
+ readOnly: true
105
+ },
106
+ {
107
+ id: 'customerName',
108
+ label: 'Customer name',
109
+ group: 'customer',
110
+ type: 'text',
111
+ readOnly: true
112
+ },
113
+ {
114
+ id: 'product',
115
+ label: 'Product',
116
+ group: 'items',
117
+ type: 'text',
118
+ readOnly: true,
119
+ description: 'The first line’s product, and how many more the order holds.'
120
+ },
121
+ {
122
+ id: 'items',
123
+ label: 'Items',
124
+ group: 'items',
125
+ type: 'longText',
126
+ readOnly: true,
127
+ description: 'Every line: quantity × name (variant), SKU.'
128
+ },
129
+ {
130
+ id: 'itemCount',
131
+ label: 'Units',
132
+ group: 'items',
133
+ type: 'integer',
134
+ readOnly: true
135
+ },
136
+ {
137
+ id: 'subtotal',
138
+ label: 'Items subtotal',
139
+ group: 'money',
140
+ type: 'number',
141
+ readOnly: true
142
+ },
143
+ {
144
+ id: 'shipping',
145
+ label: 'Shipping',
146
+ group: 'money',
147
+ type: 'number',
148
+ readOnly: true
149
+ },
150
+ {
151
+ id: 'tax',
152
+ label: 'Tax',
153
+ group: 'money',
154
+ type: 'number',
155
+ readOnly: true
156
+ },
157
+ {
158
+ id: 'discount',
159
+ label: 'Discount',
160
+ group: 'money',
161
+ type: 'number',
162
+ readOnly: true
163
+ },
164
+ {
165
+ id: 'total',
166
+ label: 'Total',
167
+ group: 'money',
168
+ type: 'number',
169
+ readOnly: true,
170
+ description: 'What was charged, in dollars.'
171
+ },
172
+ {
173
+ id: 'fee',
174
+ label: 'Fee',
175
+ group: 'money',
176
+ type: 'number',
177
+ readOnly: true
178
+ },
179
+ {
180
+ id: 'refunded',
181
+ label: 'Refunded',
182
+ group: 'money',
183
+ type: 'number',
184
+ readOnly: true
185
+ },
186
+ {
187
+ id: 'net',
188
+ label: 'Net',
189
+ group: 'money',
190
+ type: 'number',
191
+ readOnly: true,
192
+ description: 'The total less what was refunded.'
193
+ },
194
+ {
195
+ id: 'coupon',
196
+ label: 'Coupon',
197
+ group: 'money',
198
+ type: 'text',
199
+ readOnly: true
200
+ },
201
+ {
202
+ id: 'shippingAddress',
203
+ label: 'Shipping address',
204
+ group: 'customer',
205
+ type: 'text',
206
+ readOnly: true
207
+ },
208
+ {
209
+ id: 'billingAddress',
210
+ label: 'Billing address',
211
+ group: 'customer',
212
+ type: 'text',
213
+ readOnly: true
214
+ },
215
+ {
216
+ id: 'note',
217
+ label: 'Note',
218
+ group: 'order',
219
+ type: 'longText',
220
+ readOnly: true
221
+ },
222
+ {
223
+ id: 'dispute',
224
+ label: 'Dispute',
225
+ group: 'order',
226
+ type: 'text',
227
+ readOnly: true
228
+ },
229
+ {
230
+ id: 'paymentIntentId',
231
+ label: 'Payment ID',
232
+ group: TRANSFER_SYSTEM_GROUP.id,
233
+ type: 'text',
234
+ system: true
235
+ }
236
+ ];
237
+ export const ORDER_TRANSFER_GROUPS = [
238
+ {
239
+ id: 'order',
240
+ label: 'Order'
241
+ },
242
+ {
243
+ id: 'customer',
244
+ label: 'Customer'
245
+ },
246
+ {
247
+ id: 'items',
248
+ label: 'Items'
249
+ },
250
+ {
251
+ id: 'money',
252
+ label: 'Money'
253
+ }
254
+ ];
255
+ const addressLine = (address)=>{
256
+ if (!address) return null;
257
+ const parts = [
258
+ address.name,
259
+ address.line1,
260
+ address.line2,
261
+ address.city,
262
+ address.state,
263
+ address.postalCode,
264
+ address.country
265
+ ].map(text).filter(Boolean);
266
+ return parts.length ? parts.join(', ') : null;
267
+ };
268
+ /**
269
+ * One order as a file names it. The money is what was ACTUALLY charged:
270
+ * the order's `totals`, and the legacy flat fields only where a buy-now order
271
+ * of old carries nothing else (AGL-1747) — a POS or draft order has no flat
272
+ * field, and reading those first exported every one of them at $0.00. A
273
+ * legacy row is named from `productNames`, never as "Product".
274
+ */ export function orderRecord(id, stored, productNames = {}) {
275
+ var _stored_lineItems, _ref, _ref1, _ref2, _stored_productId, _ref3, _ref4, _stored_refundedCents, _iso, _ORDER_STATUS_LABELS_lifted_status, _ref5, _ORDER_CHANNEL_LABELS_, _lifted_channel, _stored_customerEmail, _stored_customerName, _ref6, _ref7, _stored_couponCode, _stored_note, _stored_paymentIntentId;
276
+ var _lineItems_;
277
+ const lifted = liftLegacyOrder(stored);
278
+ const lineItems = (_stored_lineItems = stored.lineItems) != null ? _stored_lineItems : [];
279
+ const first = (_ref = (_ref1 = (_ref2 = (_lineItems_ = lineItems[0]) == null ? void 0 : _lineItems_.name) != null ? _ref2 : productNames[(_stored_productId = stored.productId) != null ? _stored_productId : '']) != null ? _ref1 : stored.productId) != null ? _ref : '';
280
+ const totals = lifted.totals;
281
+ const totalCents = Number((_ref3 = (_ref4 = totals == null ? void 0 : totals.totalCents) != null ? _ref4 : stored.amountCents) != null ? _ref3 : 0);
282
+ const refundedCents = Number((_stored_refundedCents = stored.refundedCents) != null ? _stored_refundedCents : 0);
283
+ return {
284
+ id,
285
+ number: typeof stored.number === 'number' ? stored.number : null,
286
+ date: (_iso = iso(stored.createdAt)) != null ? _iso : iso(stored.createdAtMs),
287
+ status: (_ORDER_STATUS_LABELS_lifted_status = ORDER_STATUS_LABELS[lifted.status]) != null ? _ORDER_STATUS_LABELS_lifted_status : lifted.status,
288
+ channel: (_ref5 = (_ORDER_CHANNEL_LABELS_ = ORDER_CHANNEL_LABELS[(_lifted_channel = lifted.channel) != null ? _lifted_channel : 'online']) != null ? _ORDER_CHANNEL_LABELS_ : lifted.channel) != null ? _ref5 : 'online',
289
+ customerEmail: (_stored_customerEmail = stored.customerEmail) != null ? _stored_customerEmail : null,
290
+ customerName: (_stored_customerName = stored.customerName) != null ? _stored_customerName : null,
291
+ product: lineItems.length > 1 ? `${first} +${lineItems.length - 1} more` : first || null,
292
+ items: lineItems.length ? lineItems.map((line)=>`${line.quantity} × ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''}${line.sku ? `, ${line.sku}` : ''}`).join('; ') : null,
293
+ itemCount: lineItems.reduce((sum, line)=>sum + (Number(line.quantity) || 0), 0),
294
+ subtotal: dollars(totals == null ? void 0 : totals.itemsCents),
295
+ shipping: dollars(totals == null ? void 0 : totals.shippingCents),
296
+ tax: dollars(totals == null ? void 0 : totals.taxCents),
297
+ discount: dollars(totals == null ? void 0 : totals.discountCents),
298
+ total: dollars(totalCents),
299
+ fee: dollars((_ref6 = (_ref7 = totals == null ? void 0 : totals.feeCents) != null ? _ref7 : stored.feeCents) != null ? _ref6 : 0),
300
+ refunded: dollars(refundedCents),
301
+ net: dollars(totalCents - refundedCents),
302
+ coupon: (_stored_couponCode = stored.couponCode) != null ? _stored_couponCode : null,
303
+ shippingAddress: addressLine(stored.shippingAddress),
304
+ billingAddress: addressLine(stored.billingAddress),
305
+ note: (_stored_note = stored.note) != null ? _stored_note : null,
306
+ dispute: stored.dispute ? text(stored.dispute.status) || 'open' : null,
307
+ paymentIntentId: (_stored_paymentIntentId = stored.paymentIntentId) != null ? _stored_paymentIntentId : null
308
+ };
309
+ }
310
+ /*==========================================
311
+ * DISCOUNTS
312
+ *=========================================*/ export const DISCOUNT_KIND_PICKLIST = 'commerce.discountKind';
313
+ const DISCOUNT_KIND_LABELS = {
314
+ percent: 'Percent off',
315
+ fixed: 'Amount off',
316
+ free_shipping: 'Free shipping'
317
+ };
318
+ export const DISCOUNT_KIND_SPEC = {
319
+ restricted: true,
320
+ standardValues: Object.entries(DISCOUNT_KIND_LABELS).map(([id, label])=>({
321
+ id,
322
+ label
323
+ })),
324
+ defaultValueId: 'percent'
325
+ };
326
+ /** A discount kind's id from a label or id in any case, or `null`. */ export function discountKindOf(value) {
327
+ const key = text(value).toLowerCase();
328
+ const found = Object.entries(DISCOUNT_KIND_LABELS).find(([id, label])=>id === key || label.toLowerCase() === key);
329
+ return found ? found[0] : null;
330
+ }
331
+ export const DISCOUNT_TRANSFER_FIELDS = [
332
+ {
333
+ id: 'code',
334
+ label: 'Code',
335
+ group: 'discount',
336
+ type: 'text',
337
+ aliases: [
338
+ 'discount code',
339
+ 'promo code',
340
+ 'coupon code'
341
+ ],
342
+ description: 'What a shopper types; blank for a discount that applies on its own.'
343
+ },
344
+ {
345
+ id: 'name',
346
+ label: 'Name',
347
+ group: 'discount',
348
+ type: 'text',
349
+ aliases: [
350
+ 'title',
351
+ 'discount name'
352
+ ]
353
+ },
354
+ {
355
+ id: 'kind',
356
+ label: 'Kind',
357
+ group: 'discount',
358
+ type: 'picklist',
359
+ picklistId: DISCOUNT_KIND_PICKLIST,
360
+ aliases: [
361
+ 'type',
362
+ 'discount type',
363
+ 'value type'
364
+ ],
365
+ description: 'Percent off, Amount off or Free shipping. Needed for a new discount.'
366
+ },
367
+ {
368
+ id: 'percentOff',
369
+ label: 'Percent off',
370
+ group: 'discount',
371
+ type: 'integer',
372
+ aliases: [
373
+ 'percent',
374
+ 'percentage'
375
+ ]
376
+ },
377
+ {
378
+ id: 'amountOff',
379
+ label: 'Amount off',
380
+ group: 'discount',
381
+ type: 'currency',
382
+ aliases: [
383
+ 'amount',
384
+ 'value'
385
+ ]
386
+ },
387
+ {
388
+ id: 'minimumSubtotal',
389
+ label: 'Minimum subtotal',
390
+ group: 'conditions',
391
+ type: 'currency',
392
+ aliases: [
393
+ 'minimum',
394
+ 'minimum purchase',
395
+ 'minimum subtotal'
396
+ ]
397
+ },
398
+ {
399
+ id: 'productIds',
400
+ label: 'Product IDs',
401
+ group: 'conditions',
402
+ type: 'text',
403
+ description: 'The products it covers, by their IDs separated by commas; blank covers the whole store.'
404
+ },
405
+ {
406
+ id: 'maxRedemptions',
407
+ label: 'Use limit',
408
+ group: 'conditions',
409
+ type: 'integer',
410
+ aliases: [
411
+ 'usage limit',
412
+ 'max uses'
413
+ ]
414
+ },
415
+ {
416
+ id: 'startsAt',
417
+ label: 'Starts',
418
+ group: 'conditions',
419
+ type: 'datetime',
420
+ aliases: [
421
+ 'start date',
422
+ 'starts at'
423
+ ]
424
+ },
425
+ {
426
+ id: 'endsAt',
427
+ label: 'Ends',
428
+ group: 'conditions',
429
+ type: 'datetime',
430
+ aliases: [
431
+ 'end date',
432
+ 'ends at',
433
+ 'expires'
434
+ ]
435
+ },
436
+ {
437
+ id: 'active',
438
+ label: 'Active',
439
+ group: 'discount',
440
+ type: 'boolean',
441
+ aliases: [
442
+ 'enabled',
443
+ 'status'
444
+ ],
445
+ description: 'A new discount starts switched off unless the file says it is active.'
446
+ },
447
+ {
448
+ id: 'redemptions',
449
+ label: 'Times used',
450
+ group: 'conditions',
451
+ type: 'integer',
452
+ readOnly: true
453
+ }
454
+ ];
455
+ export const DISCOUNT_MATCH_KEYS = [
456
+ {
457
+ fieldId: 'code',
458
+ normalizer: 'caseless'
459
+ },
460
+ {
461
+ fieldId: 'name',
462
+ normalizer: 'name'
463
+ },
464
+ {
465
+ fieldId: 'id',
466
+ normalizer: 'aglynId'
467
+ }
468
+ ];
469
+ /** A code as the discounts card stores it: upper case, letters, digits, `_` and `-`. */ export function promotionCode(value, max = 40) {
470
+ return text(value).toUpperCase().replace(/[^A-Z0-9_-]/g, '').slice(0, max);
471
+ }
472
+ const cents = (value)=>{
473
+ if (value === null || value === undefined || value === '') return null;
474
+ if (typeof value === 'object' && 'amountMinor' in value) return Math.round(Number(value.amountMinor));
475
+ const number = Number(value);
476
+ return Number.isFinite(number) ? Math.round(number * 100) : null;
477
+ };
478
+ const msOf = (value)=>{
479
+ if (value === null || value === undefined || value === '') return null;
480
+ const time = typeof value === 'number' ? value : Date.parse(String(value));
481
+ return Number.isFinite(time) ? time : null;
482
+ };
483
+ export function discountRecord(id, stored) {
484
+ var _stored_name, _ref, _DISCOUNT_KIND_LABELS_stored_kind, _stored_valuePct, _stored_maxRedemptions, _stored_redemptions;
485
+ var _stored_productIds;
486
+ return {
487
+ id,
488
+ code: stored.code || null,
489
+ name: (_stored_name = stored.name) != null ? _stored_name : null,
490
+ kind: (_ref = (_DISCOUNT_KIND_LABELS_stored_kind = DISCOUNT_KIND_LABELS[stored.kind]) != null ? _DISCOUNT_KIND_LABELS_stored_kind : stored.kind) != null ? _ref : null,
491
+ percentOff: stored.kind === 'percent' ? (_stored_valuePct = stored.valuePct) != null ? _stored_valuePct : null : null,
492
+ amountOff: stored.kind === 'fixed' ? dollars(stored.valueCents) : null,
493
+ minimumSubtotal: dollars(stored.minSubtotalCents),
494
+ productIds: ((_stored_productIds = stored.productIds) == null ? void 0 : _stored_productIds.length) ? stored.productIds.join(', ') : null,
495
+ maxRedemptions: (_stored_maxRedemptions = stored.maxRedemptions) != null ? _stored_maxRedemptions : null,
496
+ startsAt: iso(stored.startAtMs),
497
+ endsAt: iso(stored.endAtMs),
498
+ active: stored.enabled !== false,
499
+ redemptions: (_stored_redemptions = stored.redemptions) != null ? _stored_redemptions : 0
500
+ };
501
+ }
502
+ /**
503
+ * The document a discount's changes make, over what it holds: the stored
504
+ * shape the discounts card writes, or the sentence refusing it.
505
+ */ export function discountWrite(current, changes) {
506
+ const doc = current ? _extends({}, current) : {
507
+ enabled: false,
508
+ redemptions: 0
509
+ };
510
+ const has = (fieldId)=>fieldId in changes;
511
+ if (has('code')) doc['code'] = promotionCode(changes['code']) || null;
512
+ if (has('name')) doc['name'] = text(changes['name']).slice(0, 80) || null;
513
+ if (has('kind')) doc['kind'] = discountKindOf(changes['kind']);
514
+ if (has('percentOff')) doc['valuePct'] = changes['percentOff'] === null ? null : Math.round(Number(changes['percentOff']));
515
+ if (has('amountOff')) doc['valueCents'] = cents(changes['amountOff']);
516
+ if (has('minimumSubtotal')) doc['minSubtotalCents'] = cents(changes['minimumSubtotal']);
517
+ if (has('productIds')) {
518
+ const ids = text(changes['productIds']).split(/[,;|\s]+/).map(text).filter(Boolean);
519
+ doc['productIds'] = ids.length ? [
520
+ ...new Set(ids)
521
+ ] : null;
522
+ }
523
+ if (has('maxRedemptions')) {
524
+ const limit = Number(changes['maxRedemptions']);
525
+ doc['maxRedemptions'] = changes['maxRedemptions'] === null || !Number.isFinite(limit) ? null : Math.max(1, Math.round(limit));
526
+ }
527
+ if (has('startsAt')) doc['startAtMs'] = msOf(changes['startsAt']);
528
+ if (has('endsAt')) doc['endAtMs'] = msOf(changes['endsAt']);
529
+ if (has('active')) doc['enabled'] = changes['active'] === true;
530
+ if (!('code' in doc)) doc['code'] = null;
531
+ const kind = doc['kind'];
532
+ if (!kind) return {
533
+ problem: 'Say what kind of discount it is: Percent off, Amount off or Free shipping.'
534
+ };
535
+ if (kind === 'percent') {
536
+ const pct = Number(doc['valuePct']);
537
+ if (!(pct >= 1 && pct <= 100)) return {
538
+ problem: 'A percent-off discount takes 1 to 100 percent.'
539
+ };
540
+ }
541
+ if (kind === 'fixed' && !(Number(doc['valueCents']) > 0)) return {
542
+ problem: 'An amount-off discount needs an amount.'
543
+ };
544
+ if (!doc['code'] && !text(doc['name'])) return {
545
+ problem: 'A discount with no code needs a name shoppers see.'
546
+ };
547
+ const start = doc['startAtMs'];
548
+ const end = doc['endAtMs'];
549
+ if (start != null && end != null && end <= start) return {
550
+ problem: 'A discount ends after it starts.'
551
+ };
552
+ return {
553
+ doc
554
+ };
555
+ }
556
+ /*==========================================
557
+ * COUPONS
558
+ *=========================================*/ export const COUPON_TRANSFER_FIELDS = [
559
+ {
560
+ id: 'code',
561
+ label: 'Code',
562
+ group: 'coupon',
563
+ type: 'text',
564
+ matchKey: true,
565
+ aliases: [
566
+ 'coupon code',
567
+ 'promo code',
568
+ 'discount code'
569
+ ],
570
+ description: 'What a shopper types; a coupon’s code is its ID, so it is never changed.'
571
+ },
572
+ {
573
+ id: 'percentOff',
574
+ label: 'Percent off',
575
+ group: 'coupon',
576
+ type: 'integer',
577
+ aliases: [
578
+ 'percent',
579
+ 'percentage',
580
+ 'value'
581
+ ]
582
+ },
583
+ {
584
+ id: 'maxRedemptions',
585
+ label: 'Use limit',
586
+ group: 'coupon',
587
+ type: 'integer',
588
+ aliases: [
589
+ 'usage limit',
590
+ 'max uses'
591
+ ]
592
+ },
593
+ {
594
+ id: 'expiresAt',
595
+ label: 'Expires',
596
+ group: 'coupon',
597
+ type: 'datetime',
598
+ aliases: [
599
+ 'expiry',
600
+ 'expires at',
601
+ 'end date'
602
+ ]
603
+ },
604
+ {
605
+ id: 'active',
606
+ label: 'Active',
607
+ group: 'coupon',
608
+ type: 'boolean',
609
+ aliases: [
610
+ 'enabled',
611
+ 'status'
612
+ ],
613
+ description: 'A new coupon starts switched off unless the file says it is active.'
614
+ },
615
+ {
616
+ id: 'redemptions',
617
+ label: 'Times used',
618
+ group: 'coupon',
619
+ type: 'integer',
620
+ readOnly: true
621
+ },
622
+ {
623
+ id: 'createdAt',
624
+ label: 'Created',
625
+ group: TRANSFER_SYSTEM_GROUP.id,
626
+ type: 'datetime',
627
+ system: true
628
+ }
629
+ ];
630
+ export const COUPON_MATCH_KEYS = [
631
+ {
632
+ fieldId: 'code',
633
+ normalizer: 'caseless'
634
+ },
635
+ {
636
+ fieldId: 'id',
637
+ normalizer: 'aglynId'
638
+ }
639
+ ];
640
+ export function couponRecord(id, stored) {
641
+ var _stored_percentOff, _stored_maxRedemptions, _stored_redemptions;
642
+ return {
643
+ id,
644
+ code: id,
645
+ percentOff: (_stored_percentOff = stored.percentOff) != null ? _stored_percentOff : null,
646
+ maxRedemptions: (_stored_maxRedemptions = stored.maxRedemptions) != null ? _stored_maxRedemptions : null,
647
+ expiresAt: iso(stored.expiresAtMs),
648
+ active: stored.enabled !== false,
649
+ redemptions: (_stored_redemptions = stored.redemptions) != null ? _stored_redemptions : 0,
650
+ createdAt: iso(stored.createdAt)
651
+ };
652
+ }
653
+ /** A coupon's document from its changes, as the coupons card writes one, or the refusal. */ export function couponWrite(current, changes) {
654
+ const doc = current ? _extends({}, current) : {
655
+ enabled: false,
656
+ redemptions: 0
657
+ };
658
+ if ('percentOff' in changes) doc['percentOff'] = Math.round(Number(changes['percentOff']));
659
+ if ('maxRedemptions' in changes) {
660
+ const limit = Number(changes['maxRedemptions']);
661
+ doc['maxRedemptions'] = changes['maxRedemptions'] === null || !Number.isFinite(limit) ? null : Math.max(1, Math.round(limit));
662
+ }
663
+ if ('expiresAt' in changes) doc['expiresAtMs'] = msOf(changes['expiresAt']);
664
+ if ('active' in changes) doc['enabled'] = changes['active'] === true;
665
+ const pct = Number(doc['percentOff']);
666
+ if (!(pct > 0 && pct <= 100)) return {
667
+ problem: 'A coupon takes 1 to 100 percent off.'
668
+ };
669
+ return {
670
+ doc
671
+ };
672
+ }
673
+ /*==========================================
674
+ * GIFT CARDS
675
+ *=========================================*/ /*
676
+ * A GIFT CARD IMPORT ISSUES CARDS (AGL-3551). A row is never written as a
677
+ * balance: each one the dry run passes is issued through the same path as
678
+ * the Gift cards card's Issue (`issueGiftCard`), so its amount is bounded,
679
+ * its code is never overwritten and its activity line is written.
680
+ *
681
+ * - The plan mints the code a row will carry — the file's own, a new one for
682
+ * a blank cell, or a new one for a row whose code another card holds and
683
+ * whose Conflicts choice is "create a new record" — so the dry run shows
684
+ * every card exactly as it will be issued.
685
+ * - A row whose code another card holds and whose choice is "update" is
686
+ * refused: a file never changes a card. "Skip" leaves the card as it is.
687
+ * - Every card the dry run passes adds to a total the person types back on
688
+ * the wizard's Confirm step (`GIFT_CARD_CONFIRM_STEP_ID`). The server reads
689
+ * the typed total from `extras` and issues nothing unless it is exactly
690
+ * the total of the cards about to be issued.
691
+ */ export const GIFT_CARD_TRANSFER_FIELDS = [
692
+ {
693
+ id: 'code',
694
+ label: 'Code',
695
+ group: 'card',
696
+ type: 'text',
697
+ matchKey: true,
698
+ aliases: [
699
+ 'gift card code',
700
+ 'card code',
701
+ 'gift card number',
702
+ 'card number'
703
+ ],
704
+ description: 'The card’s code, which spends its balance: keep the file as safe as the cards. On import, a blank code gets a new one.'
705
+ },
706
+ {
707
+ id: 'balance',
708
+ label: 'Balance',
709
+ group: 'card',
710
+ type: 'currency',
711
+ aliases: [
712
+ 'amount',
713
+ 'value',
714
+ 'current balance',
715
+ 'remaining balance',
716
+ 'balance remaining'
717
+ ],
718
+ description: 'On import, what the card is issued for, in US dollars — the balance it carries over.'
719
+ },
720
+ {
721
+ id: 'currency',
722
+ label: 'Currency',
723
+ group: 'card',
724
+ type: 'text',
725
+ aliases: [
726
+ 'currency code'
727
+ ],
728
+ description: 'Optional. Cards are held in US dollars; a row in another currency is refused, never converted.'
729
+ },
730
+ {
731
+ id: 'initial',
732
+ label: 'Issued for',
733
+ group: 'card',
734
+ type: 'number',
735
+ readOnly: true
736
+ },
737
+ {
738
+ id: 'status',
739
+ label: 'Status',
740
+ group: 'card',
741
+ type: 'text',
742
+ readOnly: true,
743
+ description: 'Active, Used up, Frozen or Voided.'
744
+ },
745
+ {
746
+ id: 'recipientEmail',
747
+ label: 'Recipient email',
748
+ group: 'card',
749
+ type: 'email',
750
+ aliases: [
751
+ 'email',
752
+ 'customer email',
753
+ 'recipient'
754
+ ]
755
+ },
756
+ {
757
+ id: 'orderId',
758
+ label: 'Order ID',
759
+ group: 'card',
760
+ type: 'text',
761
+ readOnly: true,
762
+ description: 'Blank for a card issued by hand.'
763
+ },
764
+ {
765
+ id: 'note',
766
+ label: 'Note',
767
+ group: 'card',
768
+ type: 'text',
769
+ aliases: [
770
+ 'notes',
771
+ 'message'
772
+ ]
773
+ },
774
+ {
775
+ id: 'issuedAt',
776
+ label: 'Issued',
777
+ group: 'card',
778
+ type: 'datetime',
779
+ readOnly: true
780
+ },
781
+ {
782
+ id: 'lastUsedAt',
783
+ label: 'Last used',
784
+ group: 'card',
785
+ type: 'datetime',
786
+ readOnly: true
787
+ }
788
+ ];
789
+ /** A card is found by its code, which is its ID. */ export const GIFT_CARD_MATCH_KEYS = [
790
+ {
791
+ fieldId: 'code',
792
+ normalizer: 'caseless'
793
+ },
794
+ {
795
+ fieldId: 'id',
796
+ normalizer: 'aglynId'
797
+ }
798
+ ];
799
+ /** The fields an issued card's undo entry holds: any of them moving means the card was spent. */ export const GIFT_CARD_UNDO_FIELDS = [
800
+ 'balance',
801
+ 'status',
802
+ 'lastUsedAt'
803
+ ];
804
+ export function giftCardRecord(id, stored) {
805
+ var _stored_balanceCents, _stored_recipientEmail, _stored_orderId, _stored_note;
806
+ const balance = Number((_stored_balanceCents = stored.balanceCents) != null ? _stored_balanceCents : 0);
807
+ return {
808
+ id,
809
+ code: id,
810
+ balance: dollars(balance),
811
+ currency: GIFT_CARD_CURRENCY,
812
+ initial: dollars(stored.initialCents),
813
+ status: stored.voidedAtMs ? 'Voided' : stored.frozenAtMs ? 'Frozen' : balance > 0 ? 'Active' : 'Used up',
814
+ recipientEmail: (_stored_recipientEmail = stored.recipientEmail) != null ? _stored_recipientEmail : null,
815
+ orderId: (_stored_orderId = stored.orderId) != null ? _stored_orderId : null,
816
+ note: (_stored_note = stored.note) != null ? _stored_note : null,
817
+ issuedAt: iso(stored.createdAtMs),
818
+ lastUsedAt: iso(stored.lastUsedAtMs)
819
+ };
820
+ }
821
+ /** Dollars and cents as a person reads them: `$1,250.00`. */ export function giftCardMoney(amountCents) {
822
+ return `$${(amountCents / 100).toLocaleString('en-US', {
823
+ minimumFractionDigits: 2,
824
+ maximumFractionDigits: 2
825
+ })}`;
826
+ }
827
+ /** A total as a person types it (`1250`, `$1,250.00`), in cents; `null` for anything else. */ export function typedDollarsToCents(typed) {
828
+ const body = String(typed != null ? typed : '').trim().replace(/^\$|^usd\s*/i, '').replace(/\s*usd$/i, '').replace(/,/g, '').trim();
829
+ if (!/^\d+(\.\d{1,2})?$/.test(body)) return null;
830
+ return Math.round(Number(body) * 100);
831
+ }
832
+ /** The Confirm step's answer from `extras`, or `null` for none: a total is a whole number of cents. */ export function readGiftCardConfirmation(extras) {
833
+ const value = extras == null ? void 0 : extras[GIFT_CARD_CONFIRM_STEP_ID];
834
+ if (!value || typeof value !== 'object') return null;
835
+ const totalCents = value['totalCents'];
836
+ if (typeof totalCents !== 'number' || !Number.isInteger(totalCents) || totalCents <= 0) return null;
837
+ return {
838
+ totalCents,
839
+ email: value['email'] === true
840
+ };
841
+ }
842
+ /** A money cell in cents and its currency: the derived amount, or a number as dollars. */ function giftCardAmount(value) {
843
+ if (value === null || value === undefined || value === '') return null;
844
+ if (typeof value === 'object' && 'amountMinor' in value) {
845
+ const amount = value;
846
+ return {
847
+ cents: Math.round(Number(amount.amountMinor)),
848
+ currency: text(amount.currency).toUpperCase() || GIFT_CARD_CURRENCY
849
+ };
850
+ }
851
+ const number = Number(value);
852
+ return Number.isFinite(number) ? {
853
+ cents: Math.round(number * 100),
854
+ currency: GIFT_CARD_CURRENCY
855
+ } : null;
856
+ }
857
+ /** The card a row's changes issue, or the sentence refusing it. */ export function giftCardIssueOf(changes, code) {
858
+ const recipientEmail = text(changes['recipientEmail']).toLowerCase().slice(0, 200) || null;
859
+ const note = text(changes['note']).slice(0, 200) || null;
860
+ const base = {
861
+ code: code.value,
862
+ amountCents: 0,
863
+ recipientEmail,
864
+ note
865
+ };
866
+ const codeProblem = code.fromFile ? giftCardCodeProblem(code.value) : null;
867
+ if (codeProblem) return _extends({}, base, {
868
+ problem: codeProblem
869
+ });
870
+ const amount = giftCardAmount(changes['balance']);
871
+ if (!amount) return _extends({}, base, {
872
+ problem: 'Say what the card is issued for in the Balance column.'
873
+ });
874
+ const named = text(changes['currency']).toUpperCase();
875
+ for (const currency of [
876
+ amount.currency,
877
+ named
878
+ ].filter(Boolean)){
879
+ if (currency !== GIFT_CARD_CURRENCY) {
880
+ return _extends({}, base, {
881
+ problem: `Gift cards are held in US dollars; ${currency} is not converted.`
882
+ });
883
+ }
884
+ }
885
+ const amountProblem = giftCardAmountProblem(amount.cents);
886
+ if (amountProblem) return _extends({}, base, {
887
+ problem: amountProblem
888
+ });
889
+ return _extends({}, base, {
890
+ amountCents: amount.cents
891
+ });
892
+ }
893
+ /**
894
+ * The core's plan for a gift card file, made into the cards it issues: each
895
+ * create carrying the card it issues ({@link GiftCardRowPlan}), each row
896
+ * that would change an existing card refused, the total of every card
897
+ * counted, and a `screening` warning stating that total — and, when the
898
+ * Confirm step's answer is missing or names another total, that nothing is
899
+ * issued until it is confirmed.
900
+ */ export function planGiftCardRows(plan, input) {
901
+ var _input_confirmation;
902
+ const issuing = new Map();
903
+ const rows = plan.rows.map((row)=>{
904
+ var _input_taken;
905
+ if (row.verdict === 'update' || row.verdict === 'unchanged') {
906
+ var _row_recordId;
907
+ const code = (_row_recordId = row.recordId) != null ? _row_recordId : '';
908
+ return _extends({}, row, {
909
+ verdict: 'update',
910
+ diff: [],
911
+ giftCard: {
912
+ code,
913
+ newCode: false,
914
+ madeCode: false,
915
+ amountCents: 0,
916
+ recipientEmail: null,
917
+ note: null,
918
+ problem: `A gift card with the code ${code} already exists, and a file never changes a card. ` + 'On the Conflicts step, skip this row, or create a new record to issue it under a new code.',
919
+ totalCents: 0,
920
+ confirmed: false
921
+ }
922
+ });
923
+ }
924
+ if (row.verdict !== 'create') return row;
925
+ const changes = rowChanges(row);
926
+ const fileCode = giftCardCodeOf(changes['code']);
927
+ // A create for a row that matched a card is the person's "create a new
928
+ // record": the card it names keeps its code, and this one gets another.
929
+ const newCode = Boolean(fileCode) && row.match.kind !== 'new';
930
+ const madeCode = !fileCode;
931
+ const code = newCode || madeCode ? input.mintCode() : fileCode;
932
+ const fromFile = !newCode && !madeCode;
933
+ let issue = giftCardIssueOf(changes, {
934
+ value: code,
935
+ fromFile
936
+ });
937
+ const earlier = issuing.get(code);
938
+ if (!issue.problem && fromFile && ((_input_taken = input.taken) == null ? void 0 : _input_taken.has(code))) {
939
+ issue = _extends({}, issue, {
940
+ problem: `A gift card with the code ${code} already exists, and a file never changes a card.`
941
+ });
942
+ } else if (!issue.problem && earlier !== undefined) {
943
+ issue = _extends({}, issue, {
944
+ problem: `Row ${earlier + 1} issues the code ${code} already.`
945
+ });
946
+ }
947
+ if (!issue.problem) issuing.set(code, row.index);
948
+ const diff = [
949
+ ...row.diff.filter((change)=>change.fieldId !== 'code'),
950
+ {
951
+ fieldId: 'code',
952
+ before: null,
953
+ after: code,
954
+ mode: 'overwrite',
955
+ source: 'default',
956
+ rule: 'written'
957
+ }
958
+ ];
959
+ return _extends({}, row, {
960
+ diff,
961
+ giftCard: _extends({}, issue, {
962
+ newCode,
963
+ madeCode,
964
+ totalCents: 0,
965
+ confirmed: false
966
+ })
967
+ });
968
+ });
969
+ const issued = rows.filter((row)=>{
970
+ var _row_giftCard;
971
+ return row.verdict === 'create' && Boolean(row.giftCard) && !((_row_giftCard = row.giftCard) == null ? void 0 : _row_giftCard.problem);
972
+ });
973
+ const totalCents = issued.reduce((sum, row)=>sum + row.giftCard.amountCents, 0);
974
+ const confirmed = ((_input_confirmation = input.confirmation) == null ? void 0 : _input_confirmation.totalCents) === totalCents;
975
+ for (const row of rows){
976
+ if (row.giftCard) row.giftCard = _extends({}, row.giftCard, {
977
+ totalCents,
978
+ confirmed
979
+ });
980
+ }
981
+ const warnings = plan.warnings.filter((warning)=>warning.class !== 'screening');
982
+ if (issued.length) {
983
+ const samples = [
984
+ {
985
+ row: TRANSFER_FILE_SAMPLE_ROW,
986
+ fieldId: 'balance',
987
+ value: giftCardMoney(totalCents),
988
+ detail: `Issues ${issued.length.toLocaleString('en-US')} gift card${issued.length === 1 ? '' : 's'} worth ${giftCardMoney(totalCents)} in total.`
989
+ }
990
+ ];
991
+ if (!confirmed) {
992
+ samples.push({
993
+ row: TRANSFER_FILE_SAMPLE_ROW,
994
+ detail: input.confirmation ? `The total confirmed was ${giftCardMoney(input.confirmation.totalCents)}, not ${giftCardMoney(totalCents)}: no card is issued until the total is confirmed again on the Confirm step.` : 'The total has not been confirmed: no card is issued until it is typed on the Confirm step.'
995
+ });
996
+ }
997
+ warnings.push({
998
+ class: 'screening',
999
+ count: issued.length,
1000
+ rows: issued.length,
1001
+ fieldIds: [
1002
+ 'balance'
1003
+ ],
1004
+ samples,
1005
+ requiresAcknowledgement: true
1006
+ });
1007
+ }
1008
+ const summary = {
1009
+ create: 0,
1010
+ update: 0,
1011
+ unchanged: 0,
1012
+ skip: 0,
1013
+ fail: 0,
1014
+ total: rows.length
1015
+ };
1016
+ for (const row of rows)summary[row.verdict] += 1;
1017
+ return {
1018
+ rows,
1019
+ summary,
1020
+ warnings,
1021
+ acknowledgementsRequired: warnings.filter((warning)=>warning.requiresAcknowledgement).map((warning)=>warning.class)
1022
+ };
1023
+ }
1024
+ /*==========================================
1025
+ * CATEGORIES
1026
+ *=========================================*/ export const CATEGORY_TRANSFER_FIELDS = [
1027
+ {
1028
+ id: 'name',
1029
+ label: 'Name',
1030
+ group: 'category',
1031
+ type: 'text',
1032
+ aliases: [
1033
+ 'category',
1034
+ 'category name',
1035
+ 'title'
1036
+ ]
1037
+ },
1038
+ {
1039
+ id: 'slug',
1040
+ label: 'Slug',
1041
+ group: 'category',
1042
+ type: 'text',
1043
+ readOnly: true,
1044
+ matchKey: true,
1045
+ aliases: [
1046
+ 'handle'
1047
+ ],
1048
+ description: 'Made from the name, as the console makes it; a file finds a category by it.'
1049
+ },
1050
+ {
1051
+ id: 'parent',
1052
+ label: 'Parent',
1053
+ group: 'category',
1054
+ type: 'text',
1055
+ aliases: [
1056
+ 'parent category',
1057
+ 'parent slug'
1058
+ ],
1059
+ description: 'The parent category’s slug or name; blank for a top-level category.'
1060
+ },
1061
+ {
1062
+ id: 'order',
1063
+ label: 'Position',
1064
+ group: 'category',
1065
+ type: 'integer',
1066
+ aliases: [
1067
+ 'sort order',
1068
+ 'order'
1069
+ ]
1070
+ }
1071
+ ];
1072
+ export const CATEGORY_MATCH_KEYS = [
1073
+ {
1074
+ fieldId: 'slug',
1075
+ normalizer: 'slug'
1076
+ },
1077
+ {
1078
+ fieldId: 'name',
1079
+ normalizer: 'name'
1080
+ },
1081
+ {
1082
+ fieldId: 'id',
1083
+ normalizer: 'aglynId'
1084
+ }
1085
+ ];
1086
+ export function categoryRecord(id, stored, slugs = new Map()) {
1087
+ var _stored_name, _stored_slug, _slugs_get;
1088
+ return {
1089
+ id,
1090
+ name: (_stored_name = stored.name) != null ? _stored_name : null,
1091
+ slug: (_stored_slug = stored.slug) != null ? _stored_slug : null,
1092
+ parent: stored.parentId ? (_slugs_get = slugs.get(stored.parentId)) != null ? _slugs_get : stored.parentId : null,
1093
+ order: typeof stored.order === 'number' ? stored.order : null
1094
+ };
1095
+ }
1096
+ /** The category a parent cell names — by id, slug, then name — or `null`. */ export function resolveCategoryParent(index, value) {
1097
+ const wanted = text(value);
1098
+ if (!wanted) return null;
1099
+ if (index.has(wanted)) return wanted;
1100
+ const slug = commerceSlug(wanted);
1101
+ for (const [id, category] of index)if (category.slug === slug) return id;
1102
+ const name = wanted.toLowerCase();
1103
+ for (const [id, category] of index)if (text(category.name).toLowerCase() === name) return id;
1104
+ return null;
1105
+ }
1106
+ /**
1107
+ * A category's document from its changes: the name and the slug made from
1108
+ * it, the parent resolved — never itself or one of its own descendants —
1109
+ * and the position. A parent the site does not have is refused, not
1110
+ * guessed.
1111
+ */ export function categoryWrite(id, current, changes, index) {
1112
+ const doc = current ? _extends({}, current) : {
1113
+ parentId: null
1114
+ };
1115
+ if ('name' in changes) {
1116
+ const name = text(changes['name']).slice(0, 80);
1117
+ if (!name) return {
1118
+ problem: 'A category needs a name.'
1119
+ };
1120
+ doc['name'] = name;
1121
+ doc['slug'] = commerceSlug(name) || id;
1122
+ }
1123
+ if (!text(doc['name'])) return {
1124
+ problem: 'A category needs a name.'
1125
+ };
1126
+ if ('parent' in changes) {
1127
+ if (isBlankTransferValue(changes['parent'])) doc['parentId'] = null;
1128
+ else {
1129
+ const parentId = resolveCategoryParent(index, changes['parent']);
1130
+ if (!parentId) return {
1131
+ problem: `No category "${text(changes['parent'])}" to put it under.`
1132
+ };
1133
+ for(let at = parentId, steps = 0; at && steps < 100; steps += 1){
1134
+ var _ref;
1135
+ var _index_get;
1136
+ if (at === id) return {
1137
+ problem: 'A category cannot sit under itself or one of its own.'
1138
+ };
1139
+ at = (_ref = (_index_get = index.get(at)) == null ? void 0 : _index_get.parentId) != null ? _ref : null;
1140
+ }
1141
+ doc['parentId'] = parentId;
1142
+ }
1143
+ }
1144
+ if ('order' in changes) {
1145
+ const order = Number(changes['order']);
1146
+ doc['order'] = changes['order'] === null || !Number.isFinite(order) ? null : Math.round(order);
1147
+ }
1148
+ return {
1149
+ doc
1150
+ };
1151
+ }
1152
+
1153
+ //# sourceMappingURL=records-transfer.js.map