@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.
- package/package.json +13 -13
- package/src/lib/components/cart-fulfillment.d.ts +3 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -1
- package/src/lib/components/cart.js +14 -4
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.js +1 -1
- package/src/lib/components/console/invoice-send.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +1 -1
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
- package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
- package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +31 -5
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
- package/src/lib/components/console/product-register-fields.component.js +8 -197
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.js +6 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
- package/src/lib/components/console/quote-pay-link.component.js +29 -3
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.js +1 -4
- package/src/lib/components/console/quote-send.component.js.map +1 -1
- package/src/lib/components/product-add-ons.d.ts +28 -0
- package/src/lib/components/product-add-ons.js +136 -0
- package/src/lib/components/product-add-ons.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +16 -0
- package/src/lib/components/product-detail.js +112 -19
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +8 -2
- package/src/lib/components/product-grid.js +6 -1
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/model/commerce-cart.d.ts +15 -3
- package/src/lib/model/commerce-cart.js +10 -3
- package/src/lib/model/commerce-cart.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +7 -0
- package/src/lib/model/commerce-invoices.js +11 -0
- package/src/lib/model/commerce-invoices.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +6 -3
- package/src/lib/model/commerce-orders.js +3 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
- package/src/lib/model/commerce-pos-kiosk.js +3 -2
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
- package/src/lib/model/commerce-quote-email.d.ts +14 -0
- package/src/lib/model/commerce-quote-email.js +25 -9
- package/src/lib/model/commerce-quote-email.js.map +1 -1
- package/src/lib/model/commerce-quotes.js +1 -1
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/commerce.d.ts +18 -0
- package/src/lib/model/commerce.js +26 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/product-modifiers.d.ts +74 -4
- package/src/lib/model/product-modifiers.js +131 -10
- package/src/lib/model/product-modifiers.js.map +1 -1
- package/src/lib/server/billing-webhook.js +64 -43
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +14 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.d.ts +5 -0
- package/src/lib/server/cart.js +41 -4
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.d.ts +6 -0
- package/src/lib/server/catalog.js +2 -0
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +3 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +28 -3
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.js.map +1 -1
- package/src/lib/server/pos-offline-sync.js +1 -1
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-order.js +1 -1
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/product.d.ts +26 -0
- package/src/lib/server/product.js +18 -0
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.js +5 -2
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/quote-email.js.map +1 -1
- package/src/lib/utils/add-to-cart.d.ts +8 -0
- package/src/lib/utils/add-to-cart.js +10 -2
- package/src/lib/utils/add-to-cart.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +1 -0
- package/src/lib/utils/quote-email-content.js +7 -4
- 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 (!
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
},
|
|
1929
|
-
variantLabel:
|
|
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
|
|
2621
|
-
const { hostId, reservationId } = (
|
|
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
|
|
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 : (
|
|
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
|
|
2854
|
-
const { hostId, cartId, feeCents, couponCode } = (
|
|
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,
|
|
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
|
-
},
|
|
2912
|
-
variantLabel
|
|
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 = (
|
|
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
|
|
3374
|
+
var _object_metadata24;
|
|
3361
3375
|
await settleRedemption({
|
|
3362
3376
|
firestore,
|
|
3363
3377
|
ref: hostRef.collection('coupons').doc(String(couponCode)),
|
|
3364
|
-
holdKey: String((_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 ((
|
|
3385
|
+
if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.giftCardCode) {
|
|
3372
3386
|
var _ref53, _ref54;
|
|
3373
|
-
var
|
|
3374
|
-
const giftCardCents = Number((_ref53 = (
|
|
3375
|
-
const holdKey = String((_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 ((
|
|
3555
|
+
if ((_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.discountId) {
|
|
3542
3556
|
var _ref58;
|
|
3543
|
-
var
|
|
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 = (
|
|
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
|
|
3562
|
-
const { hostId, orderId, productId } = (
|
|
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
|
|
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 ((
|
|
3889
|
+
if ((_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountId) {
|
|
3876
3890
|
var _ref69;
|
|
3877
|
-
var
|
|
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 = (
|
|
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 : (
|
|
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
|
|
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 = (
|
|
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
|
|
4073
|
-
const { hostId, productId, feeCents, couponCode } = (
|
|
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
|
|
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 = ((
|
|
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
|
-
},
|
|
4103
|
-
variantLabel:
|
|
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
|
|
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 = (
|
|
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 = (
|
|
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
|
|
4449
|
+
var _object_metadata38;
|
|
4429
4450
|
await settleRedemption({
|
|
4430
4451
|
firestore,
|
|
4431
4452
|
ref: hostRef.collection('coupons').doc(String(couponCode)),
|
|
4432
|
-
holdKey: String((_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.'
|