@aglyn/plugins-commerce 1.0.0-beta.291 → 1.0.0-beta.292

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 (92) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  3. package/src/lib/components/cart-fulfillment.js.map +1 -1
  4. package/src/lib/components/cart.js +14 -4
  5. package/src/lib/components/cart.js.map +1 -1
  6. package/src/lib/components/console/invoice-send.component.js +1 -1
  7. package/src/lib/components/console/invoice-send.component.js.map +1 -1
  8. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  9. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  10. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  11. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  12. package/src/lib/components/console/pos-page.component.js +1 -1
  13. package/src/lib/components/console/pos-page.component.js.map +1 -1
  14. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  15. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  16. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  17. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  18. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  19. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  20. package/src/lib/components/console/product-register-fields.component.js +8 -197
  21. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  22. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  23. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  24. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  25. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  26. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  27. package/src/lib/components/console/quote-send.component.js +1 -4
  28. package/src/lib/components/console/quote-send.component.js.map +1 -1
  29. package/src/lib/components/product-add-ons.d.ts +28 -0
  30. package/src/lib/components/product-add-ons.js +136 -0
  31. package/src/lib/components/product-add-ons.js.map +1 -0
  32. package/src/lib/components/product-detail.d.ts +16 -0
  33. package/src/lib/components/product-detail.js +112 -19
  34. package/src/lib/components/product-detail.js.map +1 -1
  35. package/src/lib/components/product-grid.d.ts +8 -2
  36. package/src/lib/components/product-grid.js +6 -1
  37. package/src/lib/components/product-grid.js.map +1 -1
  38. package/src/lib/model/commerce-cart.d.ts +15 -3
  39. package/src/lib/model/commerce-cart.js +10 -3
  40. package/src/lib/model/commerce-cart.js.map +1 -1
  41. package/src/lib/model/commerce-invoices.d.ts +7 -0
  42. package/src/lib/model/commerce-invoices.js +11 -0
  43. package/src/lib/model/commerce-invoices.js.map +1 -1
  44. package/src/lib/model/commerce-orders.d.ts +6 -3
  45. package/src/lib/model/commerce-orders.js +3 -0
  46. package/src/lib/model/commerce-orders.js.map +1 -1
  47. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  48. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  49. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  50. package/src/lib/model/commerce-quote-email.d.ts +14 -0
  51. package/src/lib/model/commerce-quote-email.js +25 -9
  52. package/src/lib/model/commerce-quote-email.js.map +1 -1
  53. package/src/lib/model/commerce-quotes.js +1 -1
  54. package/src/lib/model/commerce-quotes.js.map +1 -1
  55. package/src/lib/model/commerce.d.ts +18 -0
  56. package/src/lib/model/commerce.js +26 -0
  57. package/src/lib/model/commerce.js.map +1 -1
  58. package/src/lib/model/product-modifiers.d.ts +74 -4
  59. package/src/lib/model/product-modifiers.js +131 -10
  60. package/src/lib/model/product-modifiers.js.map +1 -1
  61. package/src/lib/server/billing-webhook.js +64 -43
  62. package/src/lib/server/billing-webhook.js.map +1 -1
  63. package/src/lib/server/cart-checkout.js +14 -2
  64. package/src/lib/server/cart-checkout.js.map +1 -1
  65. package/src/lib/server/cart.d.ts +5 -0
  66. package/src/lib/server/cart.js +41 -4
  67. package/src/lib/server/cart.js.map +1 -1
  68. package/src/lib/server/catalog.d.ts +6 -0
  69. package/src/lib/server/catalog.js +2 -0
  70. package/src/lib/server/catalog.js.map +1 -1
  71. package/src/lib/server/checkout-extras.js +3 -1
  72. package/src/lib/server/checkout-extras.js.map +1 -1
  73. package/src/lib/server/checkout.js +28 -3
  74. package/src/lib/server/checkout.js.map +1 -1
  75. package/src/lib/server/local-fulfillment.js +5 -1
  76. package/src/lib/server/local-fulfillment.js.map +1 -1
  77. package/src/lib/server/pos-offline-sync.js +1 -1
  78. package/src/lib/server/pos-offline-sync.js.map +1 -1
  79. package/src/lib/server/pos-order.js +1 -1
  80. package/src/lib/server/pos-order.js.map +1 -1
  81. package/src/lib/server/product.d.ts +26 -0
  82. package/src/lib/server/product.js +18 -0
  83. package/src/lib/server/product.js.map +1 -1
  84. package/src/lib/server/products-write.js +5 -2
  85. package/src/lib/server/products-write.js.map +1 -1
  86. package/src/lib/server/quote-email.js.map +1 -1
  87. package/src/lib/utils/add-to-cart.d.ts +8 -0
  88. package/src/lib/utils/add-to-cart.js +10 -2
  89. package/src/lib/utils/add-to-cart.js.map +1 -1
  90. package/src/lib/utils/quote-email-content.d.ts +1 -0
  91. package/src/lib/utils/quote-email-content.js +7 -4
  92. package/src/lib/utils/quote-email-content.js.map +1 -1
@@ -26,9 +26,19 @@
26
26
  * structured in `modifiers`: every receipt, email, order view and export
27
27
  * already prints the label, so the choice reaches all of them without each
28
28
  * learning a new field.
29
+ *
30
+ * ADD-ONS ON THE WEBSITE (AGL-4100). A group switched to "Show on the
31
+ * website" (`online`) is offered on the product's page too, and a cart line,
32
+ * Buy now and the order carry its picks the same way: ids from the shopper,
33
+ * prices from the product. A choice may cost more on one variant than
34
+ * another (`priceCentsByVariant`), which is how one page sells a canopy whose
35
+ * walls cost more at a larger size without a variant for every combination.
29
36
  *=========================================*/ /** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */ export const POS_MAX_MODIFIER_GROUPS = 10;
30
37
  export const POS_MAX_MODIFIER_OPTIONS = 20;
31
38
  /** $1,000 on one modifier: anything larger is a product, not an add-on. */ export const POS_MAX_MODIFIER_PRICE_CENTS = 100000;
39
+ function modifierPriceIsValid(price) {
40
+ return typeof price === 'number' && Number.isInteger(price) && price >= 0 && price <= POS_MAX_MODIFIER_PRICE_CENTS;
41
+ }
32
42
  /** Why a product's modifier groups cannot be saved, or null when they can. */ export function modifierGroupsProblem(groups) {
33
43
  if (groups == null) return null;
34
44
  if (!Array.isArray(groups)) return 'Modifiers must be a list';
@@ -53,9 +63,21 @@ export const POS_MAX_MODIFIER_OPTIONS = 20;
53
63
  optionIds.add(option.id);
54
64
  if (!String((_option_name = option.name) != null ? _option_name : '').trim()) return `Name every choice in “${group.name}”`;
55
65
  const price = option.priceCents;
56
- if (!Number.isInteger(price) || price < 0 || price > POS_MAX_MODIFIER_PRICE_CENTS) {
66
+ if (!modifierPriceIsValid(price)) {
57
67
  return `“${option.name}” needs a price of $0 to $1,000`;
58
68
  }
69
+ const byVariant = option.priceCentsByVariant;
70
+ if (byVariant != null) {
71
+ if (typeof byVariant !== 'object' || Array.isArray(byVariant)) {
72
+ return `“${option.name}” has prices that are not a list by option`;
73
+ }
74
+ if (Object.values(byVariant).some((cents)=>!modifierPriceIsValid(cents))) {
75
+ return `“${option.name}” needs a price of $0 to $1,000 for each option it is priced by`;
76
+ }
77
+ }
78
+ }
79
+ if (group.online != null && typeof group.online !== 'boolean') {
80
+ return `“${group.name}” is shown on the website or not`;
59
81
  }
60
82
  const { min, max } = group;
61
83
  if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`;
@@ -74,7 +96,42 @@ export const POS_MAX_MODIFIER_OPTIONS = 20;
74
96
  /** Whether a group must be answered before the item can be added. */ export function modifierGroupRequired(group) {
75
97
  return group.min > 0;
76
98
  }
77
- export function resolveLineModifiers(product, raw) {
99
+ /**
100
+ * The groups the product's page on the website offers (AGL-4100): those
101
+ * switched to "Show on the website". A register-only group is never sent to
102
+ * a shopper, never asked of one and never priced on an online order.
103
+ */ export function websiteModifierGroups(product) {
104
+ return productModifierGroups(product).filter((group)=>group.online === true);
105
+ }
106
+ /** Whether the website must ask a shopper a choice before the item goes in a cart. */ export function websiteModifiersRequired(product) {
107
+ return websiteModifierGroups(product).some(modifierGroupRequired);
108
+ }
109
+ /**
110
+ * What one choice adds to one unit of the picked variant (AGL-4100): its
111
+ * price on that variant where it has one, else its own price.
112
+ */ export function modifierOptionPriceCents(option, variantId) {
113
+ var _option_priceCentsByVariant;
114
+ const byVariant = variantId ? (_option_priceCentsByVariant = option.priceCentsByVariant) == null ? void 0 : _option_priceCentsByVariant[variantId] : undefined;
115
+ return typeof byVariant === 'number' && Number.isFinite(byVariant) ? byVariant : option.priceCents;
116
+ }
117
+ /** The picks a request names, as ids only, bounded; nothing for a value that is not a list. */ export function modifierSelectionFrom(raw) {
118
+ return (Array.isArray(raw) ? raw : []).slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map((entry)=>{
119
+ var _ref, _ref1;
120
+ return {
121
+ groupId: String((_ref = entry == null ? void 0 : entry.groupId) != null ? _ref : ''),
122
+ optionId: String((_ref1 = entry == null ? void 0 : entry.optionId) != null ? _ref1 : '')
123
+ };
124
+ });
125
+ }
126
+ /**
127
+ * Prices a line's chosen modifiers from the PRODUCT, in group order, and
128
+ * checks every group's minimum and maximum. Unknown ids, a repeated choice
129
+ * and a group answered past its maximum are refused with what to fix.
130
+ *
131
+ * `variantId` is the variant the line sells (AGL-4100): a choice priced by
132
+ * variant adds its price on that one. Without it every choice adds its own
133
+ * `priceCents`, as it always did.
134
+ */ export function resolveLineModifiers(product, raw, variantId) {
78
135
  const refuse = (error)=>({
79
136
  ok: false,
80
137
  modifiers: [],
@@ -82,13 +139,7 @@ export function resolveLineModifiers(product, raw) {
82
139
  error
83
140
  });
84
141
  const groups = productModifierGroups(product);
85
- const picks = (Array.isArray(raw) ? raw : []).slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map((entry)=>{
86
- var _ref, _ref1;
87
- return {
88
- groupId: String((_ref = entry == null ? void 0 : entry.groupId) != null ? _ref : ''),
89
- optionId: String((_ref1 = entry == null ? void 0 : entry.optionId) != null ? _ref1 : '')
90
- };
91
- });
142
+ const picks = modifierSelectionFrom(raw);
92
143
  const seen = new Set();
93
144
  for (const pick of picks){
94
145
  const key = `${pick.groupId}:${pick.optionId}`;
@@ -114,7 +165,7 @@ export function resolveLineModifiers(product, raw) {
114
165
  optionId: option.id,
115
166
  group: group.name,
116
167
  name: option.name,
117
- priceCents: option.priceCents
168
+ priceCents: modifierOptionPriceCents(option, variantId)
118
169
  });
119
170
  }
120
171
  }
@@ -124,6 +175,76 @@ export function resolveLineModifiers(product, raw) {
124
175
  extraCents: modifiers.reduce((sum, modifier)=>sum + modifier.priceCents, 0)
125
176
  };
126
177
  }
178
+ /**
179
+ * {@link resolveLineModifiers} for a sale made online (AGL-4100): only the
180
+ * groups shown on the website are offered, asked or priced. A register-only
181
+ * group, required or not, is not the shopper's to answer.
182
+ */ export function resolveWebsiteLineModifiers(product, raw, variantId) {
183
+ return resolveLineModifiers({
184
+ name: product.name,
185
+ modifierGroups: websiteModifierGroups(product)
186
+ }, raw, variantId);
187
+ }
188
+ /**
189
+ * The modifiers a PAID line names, for its order record (AGL-4100): every
190
+ * pick the product still offers, in group order, priced as it is now, with no
191
+ * minimum or maximum asked. The money was taken when the sale was priced;
192
+ * a choice the merchant removed since is left off the record, never a reason
193
+ * to lose the order.
194
+ */ export function soldLineModifiers(product, raw, variantId) {
195
+ const picks = modifierSelectionFrom(raw);
196
+ const modifiers = [];
197
+ for (const group of productModifierGroups(product)){
198
+ for (const option of group.options){
199
+ if (picks.some((pick)=>pick.groupId === group.id && pick.optionId === option.id)) {
200
+ modifiers.push({
201
+ groupId: group.id,
202
+ optionId: option.id,
203
+ group: group.name,
204
+ name: option.name,
205
+ priceCents: modifierOptionPriceCents(option, variantId)
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return modifiers;
211
+ }
212
+ /**
213
+ * What a shopper is told about a cart line whose add-ons no longer answer
214
+ * the product (AGL-4100): the resolver's reason, and how to put it right.
215
+ */ export function cartLineModifierProblem(error) {
216
+ const reason = String(error != null ? error : '').trim() || 'The choices on this item have changed.';
217
+ return /Remove it and add it again/.test(reason) ? reason : `${reason} Remove it and add it again from its page.`;
218
+ }
219
+ /** The picks a priced line keeps: ids only, in the product's group order. */ export function modifierSelectionOf(modifiers) {
220
+ return modifiers.map((modifier)=>({
221
+ groupId: modifier.groupId,
222
+ optionId: modifier.optionId
223
+ }));
224
+ }
225
+ /**
226
+ * The picks as one metadata value (AGL-4100), for a payment processor that
227
+ * holds strings only: `[["group","option"],…]`. Empty for no picks.
228
+ */ export function encodeModifierSelection(selection) {
229
+ if (!(selection == null ? void 0 : selection.length)) return '';
230
+ return JSON.stringify(selection.map((pick)=>[
231
+ pick.groupId,
232
+ pick.optionId
233
+ ]));
234
+ }
235
+ /** {@link encodeModifierSelection} read back; nothing for a value it did not write. */ export function decodeModifierSelection(value) {
236
+ if (typeof value !== 'string' || !value) return [];
237
+ try {
238
+ const parsed = JSON.parse(value);
239
+ if (!Array.isArray(parsed)) return [];
240
+ return parsed.filter((pair)=>Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'string').slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map(([groupId, optionId])=>({
241
+ groupId,
242
+ optionId
243
+ }));
244
+ } catch (unused) {
245
+ return [];
246
+ }
247
+ }
127
248
  /** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */ export function lineLabelWithModifiers(variantLabel, modifiers) {
128
249
  const added = modifiers.map((modifier)=>modifier.name).join(', ');
129
250
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/product-modifiers.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * REGISTER MODIFIERS AND QUICK KEYS (AGL-3607).\n *\n * A modifier is a choice added to an item as it is rung up — \"Oat milk\",\n * \"Extra shot +$0.75\", \"No onions\" — that is not a stocked variant. Groups\n * live on the product (`modifierGroups`); the register sends the chosen\n * option ids with each line and the SERVER prices them from the product\n * document, so a register never names what a modifier costs.\n *\n * A chosen modifier is folded into the line's `variantLabel` as well as kept\n * structured in `modifiers`: every receipt, email, order view and export\n * already prints the label, so the choice reaches all of them without each\n * learning a new field.\n *=========================================*/\n\n/** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */\nexport interface ProductModifierOption {\n id: string\n name: string\n priceCents: number\n}\n\n/**\n * A set of choices offered together (\"Milk\", \"Add-ons\"). `min` of 1 or more\n * makes it required; `max` caps how many can be picked (1 = pick one).\n */\nexport interface ProductModifierGroup {\n id: string\n name: string\n min: number\n max: number\n options: ProductModifierOption[]\n}\n\n/** A modifier as the order line keeps it: names and price as sold. */\nexport interface OrderLineModifier {\n groupId: string\n optionId: string\n group: string\n name: string\n priceCents: number\n}\n\n/** What the register sends for a line: the option ids it picked, nothing priced. */\nexport interface ModifierSelection {\n groupId: string\n optionId: string\n}\n\nexport const POS_MAX_MODIFIER_GROUPS = 10\nexport const POS_MAX_MODIFIER_OPTIONS = 20\n/** $1,000 on one modifier: anything larger is a product, not an add-on. */\nexport const POS_MAX_MODIFIER_PRICE_CENTS = 100_000\n\n/** Why a product's modifier groups cannot be saved, or null when they can. */\nexport function modifierGroupsProblem(groups: unknown): string | null {\n if (groups == null) return null\n if (!Array.isArray(groups)) return 'Modifiers must be a list'\n if (groups.length > POS_MAX_MODIFIER_GROUPS) {\n return `At most ${POS_MAX_MODIFIER_GROUPS} modifier groups per product`\n }\n const groupIds = new Set<string>()\n for (const group of groups as ProductModifierGroup[]) {\n if (!group?.id || groupIds.has(group.id)) return 'Modifier groups need unique ids'\n groupIds.add(group.id)\n if (!String(group.name ?? '').trim()) return 'Name every modifier group'\n const options = Array.isArray(group.options) ? group.options : []\n if (options.length === 0) return `Add a choice to “${group.name}”`\n if (options.length > POS_MAX_MODIFIER_OPTIONS) {\n return `“${group.name}” has more than ${POS_MAX_MODIFIER_OPTIONS} choices`\n }\n const optionIds = new Set<string>()\n for (const option of options) {\n if (!option?.id || optionIds.has(option.id)) return 'Modifier choices need unique ids'\n optionIds.add(option.id)\n if (!String(option.name ?? '').trim()) return `Name every choice in “${group.name}”`\n const price = option.priceCents\n if (!Number.isInteger(price) || price < 0 || price > POS_MAX_MODIFIER_PRICE_CENTS) {\n return `“${option.name}” needs a price of $0 to $1,000`\n }\n }\n const { min, max } = group\n if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`\n if (!Number.isInteger(max) || max < 1 || max > options.length) {\n return `“${group.name}” allows 1 to ${options.length} choices`\n }\n if (min > max) return `“${group.name}” requires more choices than it allows`\n }\n return null\n}\n\n/** The product's groups, or none when they are missing or malformed. */\nexport function productModifierGroups(product: {\n modifierGroups?: unknown\n}): ProductModifierGroup[] {\n const groups = product?.modifierGroups\n if (!Array.isArray(groups) || modifierGroupsProblem(groups)) return []\n return groups as ProductModifierGroup[]\n}\n\n/** Whether a group must be answered before the item can be added. */\nexport function modifierGroupRequired(group: ProductModifierGroup): boolean {\n return group.min > 0\n}\n\n/**\n * Prices a line's chosen modifiers from the PRODUCT, in group order, and\n * checks every group's minimum and maximum. Unknown ids, a repeated choice\n * and a group answered past its maximum are refused with what to fix.\n */\nexport interface ResolvedLineModifiers {\n ok: boolean\n /** The chosen modifiers, in group order; empty when not `ok`. */\n modifiers: OrderLineModifier[]\n /** What they add to one unit, in cents. */\n extraCents: number\n /** What to fix, when not `ok`. */\n error?: string\n}\n\nexport function resolveLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n): ResolvedLineModifiers {\n const refuse = (error: string): ResolvedLineModifiers => ({\n ok: false,\n modifiers: [],\n extraCents: 0,\n error,\n })\n const groups = productModifierGroups(product)\n const picks: ModifierSelection[] = (Array.isArray(raw) ? raw : [])\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map((entry: any) => ({\n groupId: String(entry?.groupId ?? ''),\n optionId: String(entry?.optionId ?? ''),\n }))\n const seen = new Set<string>()\n for (const pick of picks) {\n const key = `${pick.groupId}:${pick.optionId}`\n if (seen.has(key)) return refuse(`A choice on ${product.name} was picked twice.`)\n seen.add(key)\n const group = groups.find((entry) => entry.id === pick.groupId)\n if (!group || !group.options.some((option) => option.id === pick.optionId)) {\n return refuse(`A choice on ${product.name} is no longer offered. Remove it and add it again.`)\n }\n }\n const modifiers: OrderLineModifier[] = []\n for (const group of groups) {\n const chosen = group.options.filter((option) =>\n picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id),\n )\n if (chosen.length < group.min) {\n return refuse(`Choose ${group.name.toLowerCase()} for ${product.name}.`)\n }\n if (chosen.length > group.max) {\n return refuse(`Choose at most ${group.max} for ${group.name.toLowerCase()} on ${product.name}.`)\n }\n for (const option of chosen) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: option.priceCents,\n })\n }\n }\n return {\n ok: true,\n modifiers,\n extraCents: modifiers.reduce((sum, modifier) => sum + modifier.priceCents, 0),\n }\n}\n\n/** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */\nexport function lineLabelWithModifiers(\n variantLabel: string | undefined,\n modifiers: readonly Pick<OrderLineModifier, 'name'>[],\n): string {\n const added = modifiers.map((modifier) => modifier.name).join(', ')\n return [variantLabel, added].filter(Boolean).join(' / ')\n}\n\n/** Two lines merge only when they are the same item with the same choices. */\nexport function modifierSelectionKey(selection: readonly ModifierSelection[] | undefined): string {\n return [...(selection ?? [])]\n .map((pick) => `${pick.groupId}:${pick.optionId}`)\n .sort()\n .join('|')\n}\n"],"names":["POS_MAX_MODIFIER_GROUPS","POS_MAX_MODIFIER_OPTIONS","POS_MAX_MODIFIER_PRICE_CENTS","modifierGroupsProblem","groups","Array","isArray","length","groupIds","Set","group","id","has","add","String","name","trim","options","optionIds","option","price","priceCents","Number","isInteger","min","max","productModifierGroups","product","modifierGroups","modifierGroupRequired","resolveLineModifiers","raw","refuse","error","ok","modifiers","extraCents","picks","slice","map","entry","groupId","optionId","seen","pick","key","find","some","chosen","filter","toLowerCase","push","reduce","sum","modifier","lineLabelWithModifiers","variantLabel","added","join","Boolean","modifierSelectionKey","selection","sort"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/product-modifiers.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * REGISTER MODIFIERS AND QUICK KEYS (AGL-3607).\n *\n * A modifier is a choice added to an item as it is rung up — \"Oat milk\",\n * \"Extra shot +$0.75\", \"No onions\" — that is not a stocked variant. Groups\n * live on the product (`modifierGroups`); the register sends the chosen\n * option ids with each line and the SERVER prices them from the product\n * document, so a register never names what a modifier costs.\n *\n * A chosen modifier is folded into the line's `variantLabel` as well as kept\n * structured in `modifiers`: every receipt, email, order view and export\n * already prints the label, so the choice reaches all of them without each\n * learning a new field.\n *\n * ADD-ONS ON THE WEBSITE (AGL-4100). A group switched to \"Show on the\n * website\" (`online`) is offered on the product's page too, and a cart line,\n * Buy now and the order carry its picks the same way: ids from the shopper,\n * prices from the product. A choice may cost more on one variant than\n * another (`priceCentsByVariant`), which is how one page sells a canopy whose\n * walls cost more at a larger size without a variant for every combination.\n *=========================================*/\n\n/** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */\nexport interface ProductModifierOption {\n id: string\n name: string\n priceCents: number\n /**\n * What the choice adds on one variant, keyed by variant id (AGL-4100):\n * walls cost more on a larger canopy. A variant not listed pays\n * `priceCents`. Read through {@link modifierOptionPriceCents}.\n */\n priceCentsByVariant?: Record<string, number>\n}\n\n/**\n * A set of choices offered together (\"Milk\", \"Add-ons\"). `min` of 1 or more\n * makes it required; `max` caps how many can be picked (1 = pick one).\n */\nexport interface ProductModifierGroup {\n id: string\n name: string\n min: number\n max: number\n options: ProductModifierOption[]\n /**\n * Offered on the product's page on the website as well as at the register\n * (AGL-4100), \"Show on the website\". Absent reads as register only, so a\n * group made before the switch changes nothing on a live store; the\n * console makes new groups with it on.\n */\n online?: boolean\n}\n\n/** A modifier as the order line keeps it: names and price as sold. */\nexport interface OrderLineModifier {\n groupId: string\n optionId: string\n group: string\n name: string\n priceCents: number\n}\n\n/** What the register sends for a line: the option ids it picked, nothing priced. */\nexport interface ModifierSelection {\n groupId: string\n optionId: string\n}\n\nexport const POS_MAX_MODIFIER_GROUPS = 10\nexport const POS_MAX_MODIFIER_OPTIONS = 20\n/** $1,000 on one modifier: anything larger is a product, not an add-on. */\nexport const POS_MAX_MODIFIER_PRICE_CENTS = 100_000\n\nfunction modifierPriceIsValid(price: unknown): boolean {\n return (\n typeof price === 'number' &&\n Number.isInteger(price) &&\n price >= 0 &&\n price <= POS_MAX_MODIFIER_PRICE_CENTS\n )\n}\n\n/** Why a product's modifier groups cannot be saved, or null when they can. */\nexport function modifierGroupsProblem(groups: unknown): string | null {\n if (groups == null) return null\n if (!Array.isArray(groups)) return 'Modifiers must be a list'\n if (groups.length > POS_MAX_MODIFIER_GROUPS) {\n return `At most ${POS_MAX_MODIFIER_GROUPS} modifier groups per product`\n }\n const groupIds = new Set<string>()\n for (const group of groups as ProductModifierGroup[]) {\n if (!group?.id || groupIds.has(group.id)) return 'Modifier groups need unique ids'\n groupIds.add(group.id)\n if (!String(group.name ?? '').trim()) return 'Name every modifier group'\n const options = Array.isArray(group.options) ? group.options : []\n if (options.length === 0) return `Add a choice to “${group.name}”`\n if (options.length > POS_MAX_MODIFIER_OPTIONS) {\n return `“${group.name}” has more than ${POS_MAX_MODIFIER_OPTIONS} choices`\n }\n const optionIds = new Set<string>()\n for (const option of options) {\n if (!option?.id || optionIds.has(option.id)) return 'Modifier choices need unique ids'\n optionIds.add(option.id)\n if (!String(option.name ?? '').trim()) return `Name every choice in “${group.name}”`\n const price = option.priceCents\n if (!modifierPriceIsValid(price)) {\n return `“${option.name}” needs a price of $0 to $1,000`\n }\n const byVariant = option.priceCentsByVariant\n if (byVariant != null) {\n if (typeof byVariant !== 'object' || Array.isArray(byVariant)) {\n return `“${option.name}” has prices that are not a list by option`\n }\n if (Object.values(byVariant).some((cents) => !modifierPriceIsValid(cents))) {\n return `“${option.name}” needs a price of $0 to $1,000 for each option it is priced by`\n }\n }\n }\n if (group.online != null && typeof group.online !== 'boolean') {\n return `“${group.name}” is shown on the website or not`\n }\n const { min, max } = group\n if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`\n if (!Number.isInteger(max) || max < 1 || max > options.length) {\n return `“${group.name}” allows 1 to ${options.length} choices`\n }\n if (min > max) return `“${group.name}” requires more choices than it allows`\n }\n return null\n}\n\n/** The product's groups, or none when they are missing or malformed. */\nexport function productModifierGroups(product: {\n modifierGroups?: unknown\n}): ProductModifierGroup[] {\n const groups = product?.modifierGroups\n if (!Array.isArray(groups) || modifierGroupsProblem(groups)) return []\n return groups as ProductModifierGroup[]\n}\n\n/** Whether a group must be answered before the item can be added. */\nexport function modifierGroupRequired(group: ProductModifierGroup): boolean {\n return group.min > 0\n}\n\n/**\n * The groups the product's page on the website offers (AGL-4100): those\n * switched to \"Show on the website\". A register-only group is never sent to\n * a shopper, never asked of one and never priced on an online order.\n */\nexport function websiteModifierGroups(product: { modifierGroups?: unknown }): ProductModifierGroup[] {\n return productModifierGroups(product).filter((group) => group.online === true)\n}\n\n/** Whether the website must ask a shopper a choice before the item goes in a cart. */\nexport function websiteModifiersRequired(product: { modifierGroups?: unknown }): boolean {\n return websiteModifierGroups(product).some(modifierGroupRequired)\n}\n\n/**\n * What one choice adds to one unit of the picked variant (AGL-4100): its\n * price on that variant where it has one, else its own price.\n */\nexport function modifierOptionPriceCents(\n option: Pick<ProductModifierOption, 'priceCents' | 'priceCentsByVariant'>,\n variantId?: string | null,\n): number {\n const byVariant = variantId ? option.priceCentsByVariant?.[variantId] : undefined\n return typeof byVariant === 'number' && Number.isFinite(byVariant) ? byVariant : option.priceCents\n}\n\n/** A line's chosen modifiers, priced. */\nexport interface ResolvedLineModifiers {\n ok: boolean\n /** The chosen modifiers, in group order; empty when not `ok`. */\n modifiers: OrderLineModifier[]\n /** What they add to one unit, in cents. */\n extraCents: number\n /** What to fix, when not `ok`. */\n error?: string\n}\n\n/** The picks a request names, as ids only, bounded; nothing for a value that is not a list. */\nexport function modifierSelectionFrom(raw: unknown): ModifierSelection[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map((entry: any) => ({\n groupId: String(entry?.groupId ?? ''),\n optionId: String(entry?.optionId ?? ''),\n }))\n}\n\n/**\n * Prices a line's chosen modifiers from the PRODUCT, in group order, and\n * checks every group's minimum and maximum. Unknown ids, a repeated choice\n * and a group answered past its maximum are refused with what to fix.\n *\n * `variantId` is the variant the line sells (AGL-4100): a choice priced by\n * variant adds its price on that one. Without it every choice adds its own\n * `priceCents`, as it always did.\n */\nexport function resolveLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): ResolvedLineModifiers {\n const refuse = (error: string): ResolvedLineModifiers => ({\n ok: false,\n modifiers: [],\n extraCents: 0,\n error,\n })\n const groups = productModifierGroups(product)\n const picks = modifierSelectionFrom(raw)\n const seen = new Set<string>()\n for (const pick of picks) {\n const key = `${pick.groupId}:${pick.optionId}`\n if (seen.has(key)) return refuse(`A choice on ${product.name} was picked twice.`)\n seen.add(key)\n const group = groups.find((entry) => entry.id === pick.groupId)\n if (!group || !group.options.some((option) => option.id === pick.optionId)) {\n return refuse(`A choice on ${product.name} is no longer offered. Remove it and add it again.`)\n }\n }\n const modifiers: OrderLineModifier[] = []\n for (const group of groups) {\n const chosen = group.options.filter((option) =>\n picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id),\n )\n if (chosen.length < group.min) {\n return refuse(`Choose ${group.name.toLowerCase()} for ${product.name}.`)\n }\n if (chosen.length > group.max) {\n return refuse(`Choose at most ${group.max} for ${group.name.toLowerCase()} on ${product.name}.`)\n }\n for (const option of chosen) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: modifierOptionPriceCents(option, variantId),\n })\n }\n }\n return {\n ok: true,\n modifiers,\n extraCents: modifiers.reduce((sum, modifier) => sum + modifier.priceCents, 0),\n }\n}\n\n/**\n * {@link resolveLineModifiers} for a sale made online (AGL-4100): only the\n * groups shown on the website are offered, asked or priced. A register-only\n * group, required or not, is not the shopper's to answer.\n */\nexport function resolveWebsiteLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): ResolvedLineModifiers {\n return resolveLineModifiers(\n { name: product.name, modifierGroups: websiteModifierGroups(product) },\n raw,\n variantId,\n )\n}\n\n/**\n * The modifiers a PAID line names, for its order record (AGL-4100): every\n * pick the product still offers, in group order, priced as it is now, with no\n * minimum or maximum asked. The money was taken when the sale was priced;\n * a choice the merchant removed since is left off the record, never a reason\n * to lose the order.\n */\nexport function soldLineModifiers(\n product: { modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): OrderLineModifier[] {\n const picks = modifierSelectionFrom(raw)\n const modifiers: OrderLineModifier[] = []\n for (const group of productModifierGroups(product)) {\n for (const option of group.options) {\n if (picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id)) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: modifierOptionPriceCents(option, variantId),\n })\n }\n }\n }\n return modifiers\n}\n\n/**\n * What a shopper is told about a cart line whose add-ons no longer answer\n * the product (AGL-4100): the resolver's reason, and how to put it right.\n */\nexport function cartLineModifierProblem(error: string | undefined): string {\n const reason = String(error ?? '').trim() || 'The choices on this item have changed.'\n return /Remove it and add it again/.test(reason)\n ? reason\n : `${reason} Remove it and add it again from its page.`\n}\n\n/** The picks a priced line keeps: ids only, in the product's group order. */\nexport function modifierSelectionOf(\n modifiers: readonly Pick<OrderLineModifier, 'groupId' | 'optionId'>[],\n): ModifierSelection[] {\n return modifiers.map((modifier) => ({ groupId: modifier.groupId, optionId: modifier.optionId }))\n}\n\n/**\n * The picks as one metadata value (AGL-4100), for a payment processor that\n * holds strings only: `[[\"group\",\"option\"],…]`. Empty for no picks.\n */\nexport function encodeModifierSelection(selection: readonly ModifierSelection[] | undefined): string {\n if (!selection?.length) return ''\n return JSON.stringify(selection.map((pick) => [pick.groupId, pick.optionId]))\n}\n\n/** {@link encodeModifierSelection} read back; nothing for a value it did not write. */\nexport function decodeModifierSelection(value: unknown): ModifierSelection[] {\n if (typeof value !== 'string' || !value) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter(\n (pair): pair is [string, string] =>\n Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'string',\n )\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map(([groupId, optionId]) => ({ groupId, optionId }))\n } catch {\n return []\n }\n}\n\n/** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */\nexport function lineLabelWithModifiers(\n variantLabel: string | undefined,\n modifiers: readonly Pick<OrderLineModifier, 'name'>[],\n): string {\n const added = modifiers.map((modifier) => modifier.name).join(', ')\n return [variantLabel, added].filter(Boolean).join(' / ')\n}\n\n/** Two lines merge only when they are the same item with the same choices. */\nexport function modifierSelectionKey(selection: readonly ModifierSelection[] | undefined): string {\n return [...(selection ?? [])]\n .map((pick) => `${pick.groupId}:${pick.optionId}`)\n .sort()\n .join('|')\n}\n"],"names":["POS_MAX_MODIFIER_GROUPS","POS_MAX_MODIFIER_OPTIONS","POS_MAX_MODIFIER_PRICE_CENTS","modifierPriceIsValid","price","Number","isInteger","modifierGroupsProblem","groups","Array","isArray","length","groupIds","Set","group","id","has","add","String","name","trim","options","optionIds","option","priceCents","byVariant","priceCentsByVariant","Object","values","some","cents","online","min","max","productModifierGroups","product","modifierGroups","modifierGroupRequired","websiteModifierGroups","filter","websiteModifiersRequired","modifierOptionPriceCents","variantId","undefined","isFinite","modifierSelectionFrom","raw","slice","map","entry","groupId","optionId","resolveLineModifiers","refuse","error","ok","modifiers","extraCents","picks","seen","pick","key","find","chosen","toLowerCase","push","reduce","sum","modifier","resolveWebsiteLineModifiers","soldLineModifiers","cartLineModifierProblem","reason","test","modifierSelectionOf","encodeModifierSelection","selection","JSON","stringify","decodeModifierSelection","value","parsed","parse","pair","lineLabelWithModifiers","variantLabel","added","join","Boolean","modifierSelectionKey","sort"],"mappings":"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"}
@@ -1892,7 +1892,7 @@ async function findOrderForDispute(paymentIntentId) {
1892
1892
  const { hostId, productId } = (_object_metadata15 = object.metadata) != null ? _object_metadata15 : {};
1893
1893
  if (hostId && productId) {
1894
1894
  var _productForSnapshot_data, _ref9, _productForSnapshot_get, _subTotals_totalCents, _productForSnapshot_get1, _ref10;
1895
- var _object_metadata16, _liftedForSnapshot_subscription, _object_customer_details, _object_customer_details1, _object_customer_details2;
1895
+ var _object_metadata16, _object_metadata17, _liftedForSnapshot_subscription, _object_customer_details, _object_customer_details1, _object_customer_details2;
1896
1896
  const firestore = firebaseAdmin.app().firestore();
1897
1897
  const hostRef = firestore.collection('hosts').doc(String(hostId));
1898
1898
  // AGL-1732: this branch used to write the subscription doc and stop —
@@ -1923,10 +1923,16 @@ async function findOrderForDispute(paymentIntentId) {
1923
1923
  });
1924
1924
  const soldVariant = ((_object_metadata16 = object.metadata) == null ? void 0 : _object_metadata16.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
1925
1925
  const variantOptions = Object.values((_ref9 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref9 : {});
1926
+ // The add-ons bought with it (AGL-4100), named from the product; their
1927
+ // price is already in the session's `unitAmountCents`.
1928
+ const subModifiers = CommerceModel.soldLineModifiers(liftedForSnapshot, CommerceModel.decodeModifierSelection((_object_metadata17 = object.metadata) == null ? void 0 : _object_metadata17.modifiers), soldVariant == null ? void 0 : soldVariant.id);
1929
+ const subLabel = CommerceModel.lineLabelWithModifiers(variantOptions.length ? variantOptions.join(' / ') : undefined, subModifiers);
1926
1930
  const { lineItems: subLineItems, totals: subTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
1927
1931
  name: String((_productForSnapshot_get = productForSnapshot.get('name')) != null ? _productForSnapshot_get : 'Product')
1928
- }, variantOptions.length ? {
1929
- variantLabel: variantOptions.join(' / ')
1932
+ }, subLabel ? {
1933
+ variantLabel: subLabel
1934
+ } : {}, subModifiers.length ? {
1935
+ modifiers: subModifiers
1930
1936
  } : {}, (soldVariant == null ? void 0 : soldVariant.sku) ? {
1931
1937
  sku: soldVariant.sku
1932
1938
  } : {}, liftedForSnapshot.type ? {
@@ -2617,11 +2623,11 @@ async function findOrderForDispute(paymentIntentId) {
2617
2623
  }
2618
2624
  // Reservations (AGL-310): payment confirms the pending hold.
2619
2625
  if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata1 = object.metadata) == null ? void 0 : _object_metadata1.type) === 'commerce-reservation' && (object == null ? void 0 : object.payment_status) === 'paid') {
2620
- var _object_metadata17;
2621
- const { hostId, reservationId } = (_object_metadata17 = object.metadata) != null ? _object_metadata17 : {};
2626
+ var _object_metadata18;
2627
+ const { hostId, reservationId } = (_object_metadata18 = object.metadata) != null ? _object_metadata18 : {};
2622
2628
  if (hostId && reservationId) {
2623
2629
  var _ref30, _ref31;
2624
- var _object_metadata18;
2630
+ var _object_metadata19;
2625
2631
  const firestore = firebaseAdmin.app().firestore();
2626
2632
  const reservationRef = firestore.collection('hosts').doc(String(hostId)).collection('reservations').doc(String(reservationId));
2627
2633
  // THE MERCHANT'S LODGING TAX, SEPARATED FROM THE STAY (AGL-1969).
@@ -2633,7 +2639,7 @@ async function findOrderForDispute(paymentIntentId) {
2633
2639
  // `amount_total` is stay-plus-tax and Stripe's own tax fields read
2634
2640
  // zero; the session's metadata is the only witness, exactly as it is
2635
2641
  // for a buy-now sale.
2636
- const taxCents = Math.max(0, Math.round(Number((_ref30 = object == null ? void 0 : (_object_metadata18 = object.metadata) == null ? void 0 : _object_metadata18.taxCents) != null ? _ref30 : 0)));
2642
+ const taxCents = Math.max(0, Math.round(Number((_ref30 = object == null ? void 0 : (_object_metadata19 = object.metadata) == null ? void 0 : _object_metadata19.taxCents) != null ? _ref30 : 0)));
2637
2643
  // What the guest actually handed over, MINUS that tax. This is the
2638
2644
  // DEPOSIT when the resource has one (`reserve.ts` charges
2639
2645
  // `depositCents || totalCents`), never the stay's `totalCents` — see
@@ -2850,11 +2856,11 @@ async function findOrderForDispute(paymentIntentId) {
2850
2856
  // Cart orders (AGL-293): one multi-line order from the cart doc;
2851
2857
  // clears the cart and decrements each line's stock.
2852
2858
  if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata2 = object.metadata) == null ? void 0 : _object_metadata2.type) === 'commerce-cart' && (object == null ? void 0 : object.payment_status) === 'paid') {
2853
- var _object_metadata19;
2854
- const { hostId, cartId, feeCents, couponCode } = (_object_metadata19 = object.metadata) != null ? _object_metadata19 : {};
2859
+ var _object_metadata20;
2860
+ const { hostId, cartId, feeCents, couponCode } = (_object_metadata20 = object.metadata) != null ? _object_metadata20 : {};
2855
2861
  if (hostId && cartId) {
2856
2862
  var _cartSnapshot_data, _ref40, _object_shipping_details_address_postal_code, _object_shipping_details_address_country, _ref41, _ref42, _ref43, _ref44, _ref45;
2857
- var _object_shipping_details, _object_customer_details7, _object_metadata20, _object_customer_details8, _object_customer_details9, _object_customer_details10, _object_metadata21, _this2, _object_metadata22;
2863
+ var _object_shipping_details, _object_customer_details7, _object_metadata21, _object_customer_details8, _object_customer_details9, _object_customer_details10, _object_metadata22, _this2, _object_metadata23;
2858
2864
  const firestore = firebaseAdmin.app().firestore();
2859
2865
  const hostRef = firestore.collection('hosts').doc(String(hostId));
2860
2866
  const cartRef = hostRef.collection('carts').doc(String(cartId));
@@ -2900,6 +2906,12 @@ async function findOrderForDispute(paymentIntentId) {
2900
2906
  const product = productsById.get(line.productId);
2901
2907
  if (!product) return null;
2902
2908
  const variant = line.variantId ? product.variants.find((item)=>item.id === line.variantId) : product.variants[0];
2909
+ // The add-ons on the line (AGL-4100), priced from the product as
2910
+ // `cart-checkout.ts` priced them on the variant sold, and named
2911
+ // on the line as the register names its modifiers.
2912
+ const modifiers = CommerceModel.soldLineModifiers(product, line.modifiers, variant == null ? void 0 : variant.id);
2913
+ const extraCents = modifiers.reduce((sum, modifier)=>sum + modifier.priceCents, 0);
2914
+ const variantLabel = CommerceModel.lineLabelWithModifiers(variant && Object.keys((_variant_options = variant.options) != null ? _variant_options : {}).length ? Object.values((_variant_options1 = variant.options) != null ? _variant_options1 : {}).join(' / ') : undefined, modifiers);
2903
2915
  return _extends({
2904
2916
  productId: line.productId
2905
2917
  }, line.variantId ? {
@@ -2908,8 +2920,10 @@ async function findOrderForDispute(paymentIntentId) {
2908
2920
  useDayKey: line.useDayKey
2909
2921
  } : {}, {
2910
2922
  name: product.name
2911
- }, variant && Object.keys((_variant_options = variant.options) != null ? _variant_options : {}).length ? {
2912
- variantLabel: Object.values((_variant_options1 = variant.options) != null ? _variant_options1 : {}).join(' / ')
2923
+ }, variantLabel ? {
2924
+ variantLabel
2925
+ } : {}, modifiers.length ? {
2926
+ modifiers
2913
2927
  } : {}, (variant == null ? void 0 : variant.sku) ? {
2914
2928
  sku: variant.sku
2915
2929
  } : {}, {
@@ -2918,7 +2932,7 @@ async function findOrderForDispute(paymentIntentId) {
2918
2932
  supplierId: product.supplierId
2919
2933
  } : {}, {
2920
2934
  quantity: line.quantity,
2921
- unitAmountCents: Math.round(Number((_ref = variant == null ? void 0 : variant.priceUsd) != null ? _ref : 0) * 100)
2935
+ unitAmountCents: Math.round(Number((_ref = variant == null ? void 0 : variant.priceUsd) != null ? _ref : 0) * 100) + extraCents
2922
2936
  });
2923
2937
  }).filter(Boolean);
2924
2938
  const shipping = (_ref40 = object == null ? void 0 : object.shipping_details) != null ? _ref40 : object == null ? void 0 : object.customer_details;
@@ -3314,7 +3328,7 @@ async function findOrderForDispute(paymentIntentId) {
3314
3328
  // is byte-identical to what it was before this issue for exactly that
3315
3329
  // reason. The hold refuses the SECOND shopper; it never gates the
3316
3330
  // first one's goods.
3317
- await releaseStockHold(hostRef, String((_ref41 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref41 : ''));
3331
+ await releaseStockHold(hostRef, String((_ref41 = (_object_metadata21 = object.metadata) == null ? void 0 : _object_metadata21.stockHoldKey) != null ? _ref41 : ''));
3318
3332
  const cartOrderTotal = `$${(Number((_ref42 = object == null ? void 0 : object.amount_total) != null ? _ref42 : 0) / 100).toFixed(2)}`;
3319
3333
  const cartUnits = lineItems.reduce((sum, line)=>sum + line.quantity, 0);
3320
3334
  void notifyHostManagers(String(hostId), {
@@ -3357,22 +3371,22 @@ async function findOrderForDispute(paymentIntentId) {
3357
3371
  }));
3358
3372
  if (couponCode) {
3359
3373
  var _ref52;
3360
- var _object_metadata23;
3374
+ var _object_metadata24;
3361
3375
  await settleRedemption({
3362
3376
  firestore,
3363
3377
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3364
- holdKey: String((_ref52 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref52 : ''),
3378
+ holdKey: String((_ref52 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.couponHoldKey) != null ? _ref52 : ''),
3365
3379
  orderRef,
3366
3380
  label: `coupon ${couponCode}`,
3367
3381
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
3368
3382
  });
3369
3383
  }
3370
3384
  // Gift card balance settlement (AGL-322, made a settlement by AGL-2449).
3371
- if ((_object_metadata21 = object.metadata) == null ? void 0 : _object_metadata21.giftCardCode) {
3385
+ if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.giftCardCode) {
3372
3386
  var _ref53, _ref54;
3373
- var _object_metadata24, _object_metadata25;
3374
- const giftCardCents = Number((_ref53 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref53 : 0);
3375
- const holdKey = String((_ref54 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref54 : '');
3387
+ var _object_metadata25, _object_metadata26;
3388
+ const giftCardCents = Number((_ref53 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardCents) != null ? _ref53 : 0);
3389
+ const holdKey = String((_ref54 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.giftCardHoldKey) != null ? _ref54 : '');
3376
3390
  const cardRef = hostRef.collection('giftCards').doc(String(object.metadata.giftCardCode));
3377
3391
  const orphanNote = `$${(giftCardCents / 100).toFixed(2)} was applied from gift card ` + `${object.metadata.giftCardCode}, which no longer exists. The ` + 'balance was not deducted from any card.';
3378
3392
  if (!holdKey) {
@@ -3538,13 +3552,13 @@ async function findOrderForDispute(paymentIntentId) {
3538
3552
  }
3539
3553
  }
3540
3554
  // Discounts engine redemptions (AGL-305).
3541
- if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.discountId) {
3555
+ if ((_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.discountId) {
3542
3556
  var _ref58;
3543
- var _object_metadata26;
3557
+ var _object_metadata27;
3544
3558
  await settleRedemption({
3545
3559
  firestore,
3546
3560
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3547
- holdKey: String((_ref58 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref58 : ''),
3561
+ holdKey: String((_ref58 = (_object_metadata27 = object.metadata) == null ? void 0 : _object_metadata27.discountHoldKey) != null ? _ref58 : ''),
3548
3562
  orderRef,
3549
3563
  label: `discount ${object.metadata.discountId}`,
3550
3564
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
@@ -3558,8 +3572,8 @@ async function findOrderForDispute(paymentIntentId) {
3558
3572
  // this type and the quote's own id as `orderId`, and the quote document is
3559
3573
  // what becomes the order.
3560
3574
  if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata3 = object.metadata) == null ? void 0 : _object_metadata3.type) === 'commerce-draft' && (object == null ? void 0 : object.payment_status) === 'paid') {
3561
- var _object_metadata27;
3562
- const { hostId, orderId, productId } = (_object_metadata27 = object.metadata) != null ? _object_metadata27 : {};
3575
+ var _object_metadata28;
3576
+ const { hostId, orderId, productId } = (_object_metadata28 = object.metadata) != null ? _object_metadata28 : {};
3563
3577
  if (hostId && orderId) {
3564
3578
  var _ref59, _ref60, _ref61;
3565
3579
  var _object_total_details, _object_total_details1;
@@ -3848,7 +3862,7 @@ async function findOrderForDispute(paymentIntentId) {
3848
3862
  }
3849
3863
  if (flipped) {
3850
3864
  var _ref62, _ref63, _ref64, _ref65, _ref66, _ref67, _ref68;
3851
- var _object_metadata28, _object_customer_details15, _order_totals, _object_metadata29, _object_customer_details16;
3865
+ var _object_metadata29, _object_customer_details15, _order_totals, _object_metadata30, _object_customer_details16;
3852
3866
  const order = paidOrder;
3853
3867
  // A legacy register QR sale is a register sale completing (AGL-3607).
3854
3868
  if (order.channel === 'pos') {
@@ -3872,13 +3886,13 @@ async function findOrderForDispute(paymentIntentId) {
3872
3886
  // `increment(1)` for uncapped promotions and sessions minted before
3873
3887
  // the hold existed, which Stripe's at-least-once delivery would run
3874
3888
  // twice. The transition is what makes it once.
3875
- if ((_object_metadata28 = object.metadata) == null ? void 0 : _object_metadata28.discountId) {
3889
+ if ((_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountId) {
3876
3890
  var _ref69;
3877
- var _object_metadata30;
3891
+ var _object_metadata31;
3878
3892
  await settleRedemption({
3879
3893
  firestore,
3880
3894
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3881
- holdKey: String((_ref69 = (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.discountHoldKey) != null ? _ref69 : ''),
3895
+ holdKey: String((_ref69 = (_object_metadata31 = object.metadata) == null ? void 0 : _object_metadata31.discountHoldKey) != null ? _ref69 : ''),
3882
3896
  orderRef,
3883
3897
  label: `discount ${object.metadata.discountId}`,
3884
3898
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
@@ -3889,7 +3903,7 @@ async function findOrderForDispute(paymentIntentId) {
3889
3903
  const draftPaidCents = Number((_ref64 = object == null ? void 0 : object.amount_total) != null ? _ref64 : 0) || Number((_ref65 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref65 : 0);
3890
3904
  const isQuotePayment = CommerceModel.isQuote(order);
3891
3905
  // A draft paid through its invoice's link (AGL-4099).
3892
- const isInvoicePayment = !isQuotePayment && String((_ref66 = object == null ? void 0 : (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.payLinkVersion) != null ? _ref66 : '') !== '';
3906
+ const isInvoicePayment = !isQuotePayment && String((_ref66 = object == null ? void 0 : (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.payLinkVersion) != null ? _ref66 : '') !== '';
3893
3907
  void notifyHostManagers(String(hostId), {
3894
3908
  type: 'content.order',
3895
3909
  title: isQuotePayment ? `Quote paid — ${CommerceModel.formatQuoteNumber(order, String(orderId))}` : isInvoicePayment ? `Invoice paid — ${draftLabel}` : `Draft order paid — ${draftLabel}`,
@@ -3947,13 +3961,13 @@ async function findOrderForDispute(paymentIntentId) {
3947
3961
  });
3948
3962
  if (productId) {
3949
3963
  var _productSnapshot_data, _ref74, _ref75;
3950
- var _object_metadata31, _lifted_variants_, _order_lineItems_, _order_lineItems;
3964
+ var _object_metadata32, _lifted_variants_, _order_lineItems_, _order_lineItems;
3951
3965
  const productRef = hostRef.collection('products').doc(String(productId));
3952
3966
  const productSnapshot = await productRef.get();
3953
3967
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data = productSnapshot.data()) != null ? _productSnapshot_data : {
3954
3968
  name: 'Product'
3955
3969
  });
3956
- const soldVariantId = String((_ref74 = (_object_metadata31 = object.metadata) == null ? void 0 : _object_metadata31.variantId) != null ? _ref74 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3970
+ const soldVariantId = String((_ref74 = (_object_metadata32 = object.metadata) == null ? void 0 : _object_metadata32.variantId) != null ? _ref74 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3957
3971
  const quantity = (_ref75 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref75 : 1;
3958
3972
  if (soldVariantId) {
3959
3973
  // Atomic since AGL-2320.
@@ -4069,11 +4083,11 @@ async function findOrderForDispute(paymentIntentId) {
4069
4083
  }
4070
4084
  // Commerce Starter orders (AGL-90): recorded under the selling host.
4071
4085
  if (type === 'checkout.session.completed' && (object == null ? void 0 : (_object_metadata4 = object.metadata) == null ? void 0 : _object_metadata4.type) === 'commerce-order' && (object == null ? void 0 : object.payment_status) === 'paid') {
4072
- var _object_metadata32;
4073
- const { hostId, productId, feeCents, couponCode } = (_object_metadata32 = object.metadata) != null ? _object_metadata32 : {};
4086
+ var _object_metadata33;
4087
+ const { hostId, productId, feeCents, couponCode } = (_object_metadata33 = object.metadata) != null ? _object_metadata33 : {};
4074
4088
  if (hostId && productId) {
4075
4089
  var _ref78, _productForSnapshot_get2, _productForSnapshot_data1, _ref79, _ref80, _object_shipping_details_address_postal_code1, _object_shipping_details_address_country1, _ref81, _ref82, _ref83, _ref84, _ref85;
4076
- var _object_metadata33, _buyNowLineItems_, _object_shipping_details1, _object_customer_details18, _object_customer_details19, _object_customer_details20, _object_metadata34;
4090
+ var _object_metadata34, _object_metadata35, _buyNowLineItems_, _object_shipping_details1, _object_customer_details18, _object_customer_details19, _object_customer_details20, _object_metadata36;
4077
4091
  const firestore = firebaseAdmin.app().firestore();
4078
4092
  const hostRef = firestore.collection('hosts').doc(String(hostId));
4079
4093
  // Orders v1 (AGL-283): line-item snapshot + totals + timeline with
@@ -4095,12 +4109,19 @@ async function findOrderForDispute(paymentIntentId) {
4095
4109
  const liftedForSnapshot = CommerceModel.liftLegacyProduct((_productForSnapshot_data1 = productForSnapshot.data()) != null ? _productForSnapshot_data1 : {
4096
4110
  name: snapshotName
4097
4111
  });
4098
- const soldVariant = ((_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
4112
+ const soldVariant = ((_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
4099
4113
  const variantOptions = Object.values((_ref79 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref79 : {});
4114
+ // The add-ons bought with it (AGL-4100), named from the product; their
4115
+ // price is already in the session's `unitAmountCents`, so a price
4116
+ // edit since the sale moves no money on the record.
4117
+ const buyNowModifiers = CommerceModel.soldLineModifiers(liftedForSnapshot, CommerceModel.decodeModifierSelection((_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.modifiers), soldVariant == null ? void 0 : soldVariant.id);
4118
+ const buyNowLabel = CommerceModel.lineLabelWithModifiers(variantOptions.length ? variantOptions.join(' / ') : undefined, buyNowModifiers);
4100
4119
  const { lineItems: buyNowLineItems, totals: buyNowTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
4101
4120
  name: snapshotName
4102
- }, variantOptions.length ? {
4103
- variantLabel: variantOptions.join(' / ')
4121
+ }, buyNowLabel ? {
4122
+ variantLabel: buyNowLabel
4123
+ } : {}, buyNowModifiers.length ? {
4124
+ modifiers: buyNowModifiers
4104
4125
  } : {}, (soldVariant == null ? void 0 : soldVariant.sku) ? {
4105
4126
  sku: soldVariant.sku
4106
4127
  } : {}, liftedForSnapshot.type ? {
@@ -4385,11 +4406,11 @@ async function findOrderForDispute(paymentIntentId) {
4385
4406
  // the receipt below, and no longer decides any count.
4386
4407
  {
4387
4408
  var _productSnapshot_data2, _ref86;
4388
- var _object_metadata35, _lifted_variants_1;
4409
+ var _object_metadata37, _lifted_variants_1;
4389
4410
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data2 = productSnapshot.data()) != null ? _productSnapshot_data2 : {
4390
4411
  name: 'Product'
4391
4412
  });
4392
- const soldVariantId = String((_ref86 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.variantId) != null ? _ref86 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
4413
+ const soldVariantId = String((_ref86 = (_object_metadata37 = object.metadata) == null ? void 0 : _object_metadata37.variantId) != null ? _ref86 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
4393
4414
  if (soldVariantId) {
4394
4415
  // AGL-1711: `-1` regardless of how many units were bought, so a
4395
4416
  // 3-unit buy-now sale decremented stock by one and the difference
@@ -4422,14 +4443,14 @@ async function findOrderForDispute(paymentIntentId) {
4422
4443
  // Settlement, the same way round and for the same reason as the cart
4423
4444
  // branch above (AGL-2356): the decrement lands first, then the
4424
4445
  // reservation is dropped.
4425
- await releaseStockHold(hostRef, String((_ref85 = (_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.stockHoldKey) != null ? _ref85 : ''));
4446
+ await releaseStockHold(hostRef, String((_ref85 = (_object_metadata36 = object.metadata) == null ? void 0 : _object_metadata36.stockHoldKey) != null ? _ref85 : ''));
4426
4447
  if (couponCode) {
4427
4448
  var _ref87;
4428
- var _object_metadata36;
4449
+ var _object_metadata38;
4429
4450
  await settleRedemption({
4430
4451
  firestore,
4431
4452
  ref: hostRef.collection('coupons').doc(String(couponCode)),
4432
- holdKey: String((_ref87 = (_object_metadata36 = object.metadata) == null ? void 0 : _object_metadata36.couponHoldKey) != null ? _ref87 : ''),
4453
+ holdKey: String((_ref87 = (_object_metadata38 = object.metadata) == null ? void 0 : _object_metadata38.couponHoldKey) != null ? _ref87 : ''),
4433
4454
  orderRef,
4434
4455
  label: `coupon ${couponCode}`,
4435
4456
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'