@aglyn/plugins-commerce 1.0.0-beta.232 → 1.0.0-beta.234
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-extras.d.ts +6 -0
- package/src/lib/components/cart-extras.js +4 -1
- package/src/lib/components/cart-extras.js.map +1 -1
- package/src/lib/components/cart-fulfillment.d.ts +117 -0
- package/src/lib/components/cart-fulfillment.js +367 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -0
- package/src/lib/components/cart.js +52 -10
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +42 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +38 -10
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
- package/src/lib/components/console/locations-card.component.js +177 -3
- package/src/lib/components/console/locations-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +10 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
- package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
- package/src/lib/components/console/order-offline-notice.component.js +53 -0
- package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-api.d.ts +24 -1
- package/src/lib/components/console/pos/pos-api.js +13 -0
- package/src/lib/components/console/pos/pos-api.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
- package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
- package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
- package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
- package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
- package/src/lib/components/console/pos/use-pos-display.js +3 -2
- package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
- package/src/lib/components/console/pos-devices-card.component.js +2 -2
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
- package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
- package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
- package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
- package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
- package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
- package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
- package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
- package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +174 -27
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +35 -10
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
- package/src/lib/components/console/stock-movements-card.component.js +48 -16
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/console/store-zones.d.ts +8 -1
- package/src/lib/components/console/store-zones.js +11 -1
- package/src/lib/components/console/store-zones.js.map +1 -1
- package/src/lib/components/order-status.js +66 -0
- package/src/lib/components/order-status.js.map +1 -1
- package/src/lib/components/product-detail.js +42 -15
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
- package/src/lib/constants/local-fulfillment-list-query.js +84 -0
- package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
- package/src/lib/constants/orders-list-query.d.ts +7 -2
- package/src/lib/constants/orders-list-query.js +89 -8
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
- package/src/lib/constants/pos-shift-list-query.js +115 -0
- package/src/lib/constants/pos-shift-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +6 -1
- package/src/lib/constants/product-list-query.js +77 -8
- package/src/lib/constants/product-list-query.js.map +1 -1
- package/src/lib/constants/stock-movement-query.d.ts +3 -2
- package/src/lib/constants/stock-movement-query.js +52 -8
- package/src/lib/constants/stock-movement-query.js.map +1 -1
- package/src/lib/model/buyer-notifications.d.ts +5 -3
- package/src/lib/model/buyer-notifications.js +25 -4
- package/src/lib/model/buyer-notifications.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +59 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
- package/src/lib/model/commerce-pos-kiosk.js +147 -0
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +234 -0
- package/src/lib/model/commerce-pos-offline.js +301 -0
- package/src/lib/model/commerce-pos-offline.js.map +1 -0
- package/src/lib/model/commerce-pos-ops.d.ts +16 -6
- package/src/lib/model/commerce-pos-ops.js +12 -4
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +13 -1
- package/src/lib/model/commerce-pos.js +5 -4
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-stock-holds.d.ts +24 -3
- package/src/lib/model/commerce-stock-holds.js +26 -8
- package/src/lib/model/commerce-stock-holds.js.map +1 -1
- package/src/lib/model/commerce.d.ts +3 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +4 -0
- package/src/lib/model/index.js +4 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
- package/src/lib/model/local-fulfillment-settings.js +361 -0
- package/src/lib/model/local-fulfillment-settings.js.map +1 -0
- package/src/lib/model/order-list-fields.d.ts +10 -1
- package/src/lib/model/order-list-fields.js +1 -0
- package/src/lib/model/order-list-fields.js.map +1 -1
- package/src/lib/model/order-local-fulfillment.d.ts +131 -0
- package/src/lib/model/order-local-fulfillment.js +125 -0
- package/src/lib/model/order-local-fulfillment.js.map +1 -0
- package/src/lib/model/order-shipping-export.js +4 -0
- package/src/lib/model/order-shipping-export.js.map +1 -1
- package/src/lib/model/order-status-view.d.ts +20 -1
- package/src/lib/model/order-status-view.js +60 -6
- package/src/lib/model/order-status-view.js.map +1 -1
- package/src/lib/plugin-config.js +6 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +8 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/pos-kiosk-config.d.ts +26 -0
- package/src/lib/pos-kiosk-config.js +59 -0
- package/src/lib/pos-kiosk-config.js.map +1 -0
- package/src/lib/server/api-v1/openapi.js +8 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/api-v1/order-view.d.ts +19 -0
- package/src/lib/server/api-v1/order-view.js +35 -0
- package/src/lib/server/api-v1/order-view.js.map +1 -1
- package/src/lib/server/billing-webhook.js +238 -87
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.d.ts +2 -0
- package/src/lib/server/cart-checkout.js +173 -13
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/checkout-extras.js +18 -4
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +71 -14
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/local-fulfillment-status.d.ts +86 -0
- package/src/lib/server/local-fulfillment-status.js +379 -0
- package/src/lib/server/local-fulfillment-status.js.map +1 -0
- package/src/lib/server/local-fulfillment.d.ts +190 -0
- package/src/lib/server/local-fulfillment.js +580 -0
- package/src/lib/server/local-fulfillment.js.map +1 -0
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/order-notifications.js +61 -0
- package/src/lib/server/order-notifications.js.map +1 -1
- package/src/lib/server/pos-display.d.ts +27 -1
- package/src/lib/server/pos-display.js +50 -9
- package/src/lib/server/pos-display.js.map +1 -1
- package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
- package/src/lib/server/pos-kiosk-principal.js +42 -0
- package/src/lib/server/pos-kiosk-principal.js.map +1 -0
- package/src/lib/server/pos-kiosk.d.ts +54 -0
- package/src/lib/server/pos-kiosk.js +972 -0
- package/src/lib/server/pos-kiosk.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +85 -0
- package/src/lib/server/pos-offline-sync.js +765 -0
- package/src/lib/server/pos-offline-sync.js.map +1 -0
- package/src/lib/server/pos-order.js +22 -4
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.d.ts +6 -0
- package/src/lib/server/pos-payment.js +225 -6
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-print.js +5 -0
- package/src/lib/server/pos-print.js.map +1 -1
- package/src/lib/server/pos-return.js +27 -0
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-sale.d.ts +6 -0
- package/src/lib/server/pos-sale.js +2 -0
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-shift.js +8 -1
- package/src/lib/server/pos-shift.js.map +1 -1
- package/src/lib/server/refund.js +3 -0
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/stock-hold.d.ts +9 -1
- package/src/lib/server/stock-hold.js +15 -7
- package/src/lib/server/stock-hold.js.map +1 -1
- package/src/lib/server.js +16 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +161 -0
- package/src/lib/tenant-emails.js.map +1 -1
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { canPurchase } from "./commerce.js";
|
|
17
18
|
/**
|
|
18
19
|
* HOW LONG A UNIT STAYS SPOKEN FOR.
|
|
19
20
|
*
|
|
@@ -74,10 +75,13 @@ export const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS;
|
|
|
74
75
|
// able to reserve `NaN` units and make every comparison below false.
|
|
75
76
|
if (Number.isFinite(count) && count > 0) units[variantId] = count;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
const locationId = typeof (hold == null ? void 0 : hold.locationId) === 'string' && hold.locationId ? hold.locationId : undefined;
|
|
79
|
+
live[key] = _extends({
|
|
78
80
|
expiresAtMs,
|
|
79
81
|
units
|
|
80
|
-
}
|
|
82
|
+
}, locationId ? {
|
|
83
|
+
locationId
|
|
84
|
+
} : {});
|
|
81
85
|
}
|
|
82
86
|
return live;
|
|
83
87
|
}
|
|
@@ -88,7 +92,7 @@ export const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS;
|
|
|
88
92
|
* the shopper presses the same button twice, the second press re-derives the
|
|
89
93
|
* same hold key, and without this exclusion it would be refused the unit it is
|
|
90
94
|
* already holding.
|
|
91
|
-
*/ export function heldVariantUnits(product, variantId, nowMs, exceptHoldKey) {
|
|
95
|
+
*/ export function heldVariantUnits(product, variantId, nowMs, exceptHoldKey, /** Count only the holds taken at this location (AGL-3624). */ atLocationId) {
|
|
92
96
|
if (!variantId) return 0;
|
|
93
97
|
const live = pruneStockHolds(product == null ? void 0 : product.stockHolds, nowMs);
|
|
94
98
|
if (exceptHoldKey) delete live[exceptHoldKey];
|
|
@@ -96,6 +100,7 @@ export const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS;
|
|
|
96
100
|
for (const hold of Object.values(live)){
|
|
97
101
|
var _ref;
|
|
98
102
|
var _hold_units;
|
|
103
|
+
if (atLocationId && hold.locationId !== atLocationId) continue;
|
|
99
104
|
held += Number((_ref = (_hold_units = hold.units) == null ? void 0 : _hold_units[variantId]) != null ? _ref : 0);
|
|
100
105
|
}
|
|
101
106
|
return held;
|
|
@@ -131,14 +136,27 @@ export const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS;
|
|
|
131
136
|
* NOTE FOR THIS REPO: `strictNullChecks` is off, so `if (!available)` does NOT
|
|
132
137
|
* distinguish `null` from `0` here — and `0` is a legitimate, load-bearing
|
|
133
138
|
* answer meaning "tracked, and none left". Every caller must test `== null`.
|
|
134
|
-
*/ export function availableVariantUnits(product, variantId, nowMs, exceptHoldKey
|
|
139
|
+
*/ export function availableVariantUnits(product, variantId, nowMs, exceptHoldKey, /**
|
|
140
|
+
* The location the buyer is collecting from or being delivered from
|
|
141
|
+
* (AGL-3624). With one, and a variant whose stock is split by location,
|
|
142
|
+
* the answer is the SMALLER of what that location's bucket has free and
|
|
143
|
+
* what the whole variant has free: a shipped cart's hold names no bucket,
|
|
144
|
+
* so it can only be charged against the total, and a location with ten on
|
|
145
|
+
* its shelf cannot sell ten when shipped checkouts already hold the other
|
|
146
|
+
* locations' every unit plus some of these.
|
|
147
|
+
*/ locationId) {
|
|
148
|
+
var _variant_inventoryByLocation_locationId;
|
|
135
149
|
const variant = resolveVariant(product, variantId);
|
|
136
150
|
if (!variant) return null;
|
|
137
151
|
if (variant.inventory == null) return null;
|
|
138
152
|
if ((product == null ? void 0 : product.oversellPolicy) === 'backorder') return null;
|
|
139
153
|
const onShelf = Math.round(Number(variant.inventory));
|
|
140
154
|
if (!Number.isFinite(onShelf)) return null;
|
|
141
|
-
|
|
155
|
+
const total = Math.max(0, onShelf - heldVariantUnits(product, variant.id, nowMs, exceptHoldKey));
|
|
156
|
+
if (!locationId || !variant.inventoryByLocation) return total;
|
|
157
|
+
const bucket = Math.round(Number((_variant_inventoryByLocation_locationId = variant.inventoryByLocation[locationId]) != null ? _variant_inventoryByLocation_locationId : 0));
|
|
158
|
+
const atLocation = Math.max(0, (Number.isFinite(bucket) ? bucket : 0) - heldVariantUnits(product, variant.id, nowMs, exceptHoldKey, locationId));
|
|
159
|
+
return Math.min(total, atLocation);
|
|
142
160
|
}
|
|
143
161
|
/**
|
|
144
162
|
* Is there a hold to take at all?
|
|
@@ -162,10 +180,10 @@ export const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS;
|
|
|
162
180
|
*
|
|
163
181
|
* `canPurchase` is still the first test rather than re-derived arithmetic, so
|
|
164
182
|
* the two cannot drift on the cases they share.
|
|
165
|
-
*/ export function canReserveStock(product, variantId, quantity = 1, nowMs = Date.now(), exceptHoldKey) {
|
|
183
|
+
*/ export function canReserveStock(product, variantId, quantity = 1, nowMs = Date.now(), exceptHoldKey, /** Reserve at this location's bucket (AGL-3624); see `availableVariantUnits`. */ locationId) {
|
|
166
184
|
if (!product) return false;
|
|
167
185
|
if (!canPurchase(product, variantId, quantity)) return false;
|
|
168
|
-
const available = availableVariantUnits(product, variantId, nowMs, exceptHoldKey);
|
|
186
|
+
const available = availableVariantUnits(product, variantId, nowMs, exceptHoldKey, locationId);
|
|
169
187
|
// `== null`, never `!available` — see the note on `availableVariantUnits`.
|
|
170
188
|
if (available == null) return true;
|
|
171
189
|
return available >= Math.max(1, Math.round(Number(quantity) || 1));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-stock-holds.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\nimport type { HostProduct } from './commerce'\nimport { canPurchase } from './commerce'\n\n/**\n * RESERVED STOCK: the arithmetic half of the checkout hold (AGL-2356).\n *\n * ## The defect this closes\n *\n * AGL-2320 made every stock decrement atomic, so a decrement is never LOST and\n * the count never shows phantom stock. It could not make the storefront REFUSE\n * an oversell, and said so: the gate and the decrement are minutes apart with\n * money moving in between.\n *\n * - `checkout.ts` / `cart-checkout.ts` called `canPurchase` against a plain\n * `.get()` when the Stripe Checkout Session was created. Nothing was\n * written, so there was no document to contend on — N concurrent shoppers\n * all read `inventory: 1`, all passed, and no locking discipline anywhere\n * could change that, because a read races nothing.\n * - the decrement happened in `billing-webhook.ts` when the session\n * completed. By then the shopper has paid, so refusing there would take\n * money and withhold goods — worse than the oversell.\n *\n * The only correct place to refuse is session creation, and refusing there\n * requires WRITING. That is what a hold is: a unit spoken for by a live\n * checkout, subtracted from what the next shopper is offered.\n *\n * ## Why the hold does not touch `inventory`\n *\n * `inventory` means UNITS ON THE SHELF and half the product reads it that way —\n * the low-stock crossing alert, the restock queue, `reconcile-stock.ts`, the\n * ledger's `appliedDelta` cap, the console's own count. A hold is not a unit\n * leaving the shelf; it is a unit promised to someone who has not paid yet.\n * Decrementing at hold time and \"converting\" at settlement would make every one\n * of those readers wrong for the duration of a checkout, and would make a\n * released hold indistinguishable from a restock in the ledger.\n *\n * So the shelf count is untouched and AVAILABILITY becomes a derived figure:\n * `inventory` minus the live holds. `decrementVariantStock` is unchanged, which\n * is the point — settlement is still the same single atomic decrement it was\n * before this issue, and the hold is simply dropped afterwards.\n *\n * ## Why a lapsed hold is a release\n *\n * Every read prunes (`pruneStockHolds`), so a hold whose release never arrived\n * lapses on its own the next time anybody asks. That is what makes a leaked\n * hold a bounded inconvenience rather than the unbounded, invisible UNDER-sell\n * this issue was held back for: stock is never stranded waiting on a webhook.\n * The explicit release paths exist so the merchant and the shopper see the unit\n * come back promptly, not because correctness depends on them.\n */\n\n/** One live reservation on a product, keyed by checkout attempt. */\nexport interface StockHold {\n /** Wall-clock ms after which this hold is void. */\n expiresAtMs: number\n /**\n * Variant id → units held. A map and not a single number because one cart\n * can hold two variants of one product, and a product document is the thing\n * the transaction contends on — collapsing them would make a two-variant\n * cart reserve the wrong shelf.\n */\n units?: Record<string, number>\n}\n\n/**\n * The product fields this file reads. Structural on purpose: `HostProduct`\n * carries forty fields and the hold arithmetic must not be able to depend on\n * any of them but these.\n */\nexport interface HeldStockProduct extends Pick<\n HostProduct,\n 'variants' | 'oversellPolicy'\n> {\n /** Hold key → hold. Absent on every product written before AGL-2356. */\n stockHolds?: Record<string, StockHold>\n}\n\n/**\n * HOW LONG A UNIT STAYS SPOKEN FOR.\n *\n * This number is not free to choose. A hold that lapses while its Checkout\n * Session can still be PAID reopens the exact window this file closes: the\n * shopper pays, the webhook decrements, and the unit was sold to someone else\n * in between. So the floor is the session's own lifetime.\n *\n * Stripe's default session lifetime is 24 HOURS, and a 24-hour stock hold is\n * precisely the trade this issue was blocked on — one abandoned cart standing\n * on a merchant's last unit for a day. The resolution is not to shorten the\n * hold below the session; it is to shorten the SESSION. `checkout.ts` and\n * `cart-checkout.ts` now send `expires_at` at `CHECKOUT_SESSION_TTL_MS`, which\n * is Stripe's own minimum and the same half-hour the bookings plugin has always\n * given a held room.\n *\n * The hold then outlives the session by `STOCK_HOLD_GRACE_MS`, so the ordering\n * is always: session dies → `checkout.session.expired` releases → the hold was\n * still standing when it did. The grace is what keeps clock skew between Stripe\n * and this process from lapsing a hold under a session that is still payable.\n */\n/**\n * 31 minutes, not 30. Stripe REJECTS an `expires_at` less than 30 minutes in\n * the future, and it measures against its own clock at the moment the request\n * lands — so a value computed here at exactly the floor fails whenever network\n * latency or clock skew eats a second. The extra minute is the margin; it is\n * not a product opinion, and the product opinion is \"the shortest session\n * Stripe will sell us\".\n */\nexport const CHECKOUT_SESSION_TTL_MS = 31 * 60 * 1000\n\n/** Cushion between the session expiring and its hold lapsing. */\nexport const STOCK_HOLD_GRACE_MS = 10 * 60 * 1000\n\nexport const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS\n\n/**\n * What a shopper is told when the units exist but are spoken for.\n *\n * Deliberately NOT the bare \"Sold out\" the empty-shelf refusal uses. The\n * merchant has stock; someone else is in checkout with it. Naming the wait is\n * what stops a shopper concluding the store is empty and leaving — the hold\n * lapses within the half hour and the unit comes back.\n */\nexport const STOCK_HELD_MESSAGE =\n 'The last of these is in someone else’s checkout. ' +\n 'Try again in a few minutes.'\n\n/**\n * The product's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal. A merchant\n * losing a unit forever to one corrupt map entry is the only direction with no\n * recovery inside the product, and it is exactly the failure mode that kept\n * this issue out of the launch build.\n */\nexport function pruneStockHolds(\n holds: Record<string, StockHold> | undefined,\n nowMs: number,\n): Record<string, StockHold> {\n const live: Record<string, StockHold> = {}\n for (const [key, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const units: Record<string, number> = {}\n for (const [variantId, raw] of Object.entries(hold?.units ?? {})) {\n const count = Math.round(Number(raw))\n // A non-numeric or non-positive count holds nothing. Coerced rather than\n // trusted for the same reason as the expiry: a corrupt entry must not be\n // able to reserve `NaN` units and make every comparison below false.\n if (Number.isFinite(count) && count > 0) units[variantId] = count\n }\n live[key] = { expiresAtMs, units }\n }\n return live\n}\n\n/**\n * Units of one variant spoken for by live checkouts right now.\n *\n * `exceptHoldKey` is how a retry of one attempt does not stand in its own way:\n * the shopper presses the same button twice, the second press re-derives the\n * same hold key, and without this exclusion it would be refused the unit it is\n * already holding.\n */\nexport function heldVariantUnits(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n exceptHoldKey?: string,\n): number {\n if (!variantId) return 0\n const live = pruneStockHolds(product?.stockHolds, nowMs)\n if (exceptHoldKey) delete live[exceptHoldKey]\n let held = 0\n for (const hold of Object.values(live)) {\n held += Number(hold.units?.[variantId] ?? 0)\n }\n return held\n}\n\n/** Units held across every variant of a product — the console's figure. */\nexport function heldProductUnits(\n product: HeldStockProduct | undefined,\n nowMs: number,\n): number {\n let held = 0\n for (const hold of Object.values(\n pruneStockHolds(product?.stockHolds, nowMs),\n )) {\n for (const units of Object.values(hold.units ?? {})) held += Number(units)\n }\n return held\n}\n\n/**\n * Resolve the variant a purchase refers to, EXACTLY as `canPurchase` does.\n *\n * Shared rather than re-derived, because the pre-filter and the in-transaction\n * re-check disagreeing about which variant they are talking about is a defect\n * with no visible symptom until the wrong shelf goes negative.\n */\nfunction resolveVariant(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n) {\n return variantId\n ? product?.variants?.find((item) => item.id === variantId)\n : product?.variants?.[0]\n}\n\n/**\n * Units a NEW checkout could take: the shelf minus the live holds.\n *\n * `null` when there is no number to answer with — an untracked variant, a\n * backorder product (whose merchant chose to sell past zero, so availability is\n * unbounded by construction), or a variant that does not exist. `null` rather\n * than `Infinity` so a caller cannot do arithmetic on \"no limit\" and get a\n * number, and never negative: a shelf that went short under a `backorder`\n * policy reads as zero available, not as a debt.\n *\n * NOTE FOR THIS REPO: `strictNullChecks` is off, so `if (!available)` does NOT\n * distinguish `null` from `0` here — and `0` is a legitimate, load-bearing\n * answer meaning \"tracked, and none left\". Every caller must test `== null`.\n */\nexport function availableVariantUnits(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n exceptHoldKey?: string,\n): number | null {\n const variant = resolveVariant(product, variantId)\n if (!variant) return null\n if (variant.inventory == null) return null\n if (product?.oversellPolicy === 'backorder') return null\n const onShelf = Math.round(Number(variant.inventory))\n if (!Number.isFinite(onShelf)) return null\n return Math.max(\n 0,\n onShelf - heldVariantUnits(product, variant.id, nowMs, exceptHoldKey),\n )\n}\n\n/**\n * Is there a hold to take at all?\n *\n * False for the cases `availableVariantUnits` answers `null` for — an untracked\n * or backorder variant has no finite shelf to reserve against, and writing a\n * hold for one would put a growing map on a document whose availability it can\n * never change. The same shape as `holdPromotionSlot` skipping an UNCAPPED\n * promotion: the unlimited case stays off the write path entirely.\n */\nexport function stockIsReservable(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n): boolean {\n return availableVariantUnits(product, variantId, nowMs) != null\n}\n\n/**\n * Can this attempt reserve `quantity` units?\n *\n * The hold-aware twin of `canPurchase`, and it must agree with it everywhere\n * `canPurchase` says NO: an untracked variant is always purchasable, a\n * backorder product always is, a missing variant never is. The single\n * difference is that a tracked deny-policy variant is measured against the\n * shelf MINUS what live checkouts are already holding.\n *\n * `canPurchase` is still the first test rather than re-derived arithmetic, so\n * the two cannot drift on the cases they share.\n */\nexport function canReserveStock(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n quantity = 1,\n nowMs: number = Date.now(),\n exceptHoldKey?: string,\n): boolean {\n if (!product) return false\n if (!canPurchase(product as HostProduct, variantId, quantity)) return false\n const available = availableVariantUnits(\n product,\n variantId,\n nowMs,\n exceptHoldKey,\n )\n // `== null`, never `!available` — see the note on `availableVariantUnits`.\n if (available == null) return true\n return available >= Math.max(1, Math.round(Number(quantity) || 1))\n}\n\n/**\n * What the console prints beside a stock count (AGL-2356).\n *\n * The merchant-facing half of the decision, and the reason a hold is not just\n * an internal optimisation. `inventory` deliberately does not move while a unit\n * is held, so a merchant looking at \"3 in stock\" while the storefront refuses\n * the third sale has no way to reason about it — the number is right and the\n * behaviour looks wrong. This is what names the gap.\n *\n * Empty string when nothing is held, so the row stays exactly as it reads today\n * on the overwhelming majority of products.\n */\nexport function stockHoldLabel(\n product: HeldStockProduct | undefined,\n nowMs: number = Date.now(),\n): string {\n const held = heldProductUnits(product, nowMs)\n if (held <= 0) return ''\n return `${held} reserved in checkout`\n}\n"],"names":["canPurchase","CHECKOUT_SESSION_TTL_MS","STOCK_HOLD_GRACE_MS","STOCK_HOLD_TTL_MS","STOCK_HELD_MESSAGE","pruneStockHolds","holds","nowMs","live","key","hold","Object","entries","expiresAtMs","Number","isFinite","units","variantId","raw","count","Math","round","heldVariantUnits","product","exceptHoldKey","stockHolds","held","values","heldProductUnits","resolveVariant","variants","find","item","id","availableVariantUnits","variant","inventory","oversellPolicy","onShelf","max","stockIsReservable","canReserveStock","quantity","Date","now","available","stockHoldLabel"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-stock-holds.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\nimport type { HostProduct } from './commerce'\nimport { canPurchase } from './commerce'\n\n/**\n * RESERVED STOCK: the arithmetic half of the checkout hold (AGL-2356).\n *\n * ## The defect this closes\n *\n * AGL-2320 made every stock decrement atomic, so a decrement is never LOST and\n * the count never shows phantom stock. It could not make the storefront REFUSE\n * an oversell, and said so: the gate and the decrement are minutes apart with\n * money moving in between.\n *\n * - `checkout.ts` / `cart-checkout.ts` called `canPurchase` against a plain\n * `.get()` when the Stripe Checkout Session was created. Nothing was\n * written, so there was no document to contend on — N concurrent shoppers\n * all read `inventory: 1`, all passed, and no locking discipline anywhere\n * could change that, because a read races nothing.\n * - the decrement happened in `billing-webhook.ts` when the session\n * completed. By then the shopper has paid, so refusing there would take\n * money and withhold goods — worse than the oversell.\n *\n * The only correct place to refuse is session creation, and refusing there\n * requires WRITING. That is what a hold is: a unit spoken for by a live\n * checkout, subtracted from what the next shopper is offered.\n *\n * ## Why the hold does not touch `inventory`\n *\n * `inventory` means UNITS ON THE SHELF and half the product reads it that way —\n * the low-stock crossing alert, the restock queue, `reconcile-stock.ts`, the\n * ledger's `appliedDelta` cap, the console's own count. A hold is not a unit\n * leaving the shelf; it is a unit promised to someone who has not paid yet.\n * Decrementing at hold time and \"converting\" at settlement would make every one\n * of those readers wrong for the duration of a checkout, and would make a\n * released hold indistinguishable from a restock in the ledger.\n *\n * So the shelf count is untouched and AVAILABILITY becomes a derived figure:\n * `inventory` minus the live holds. `decrementVariantStock` is unchanged, which\n * is the point — settlement is still the same single atomic decrement it was\n * before this issue, and the hold is simply dropped afterwards.\n *\n * ## Why a lapsed hold is a release\n *\n * Every read prunes (`pruneStockHolds`), so a hold whose release never arrived\n * lapses on its own the next time anybody asks. That is what makes a leaked\n * hold a bounded inconvenience rather than the unbounded, invisible UNDER-sell\n * this issue was held back for: stock is never stranded waiting on a webhook.\n * The explicit release paths exist so the merchant and the shopper see the unit\n * come back promptly, not because correctness depends on them.\n */\n\n/** One live reservation on a product, keyed by checkout attempt. */\nexport interface StockHold {\n /** Wall-clock ms after which this hold is void. */\n expiresAtMs: number\n /**\n * Variant id → units held. A map and not a single number because one cart\n * can hold two variants of one product, and a product document is the thing\n * the transaction contends on — collapsing them would make a two-variant\n * cart reserve the wrong shelf.\n */\n units?: Record<string, number>\n /**\n * The location the units are reserved AT (AGL-3624): a pickup or local\n * delivery cart takes its stock off one location's bucket, so its hold\n * spends that bucket. Absent for a shipped cart, whose hold spends only the\n * variant's total.\n */\n locationId?: string\n}\n\n/**\n * The product fields this file reads. Structural on purpose: `HostProduct`\n * carries forty fields and the hold arithmetic must not be able to depend on\n * any of them but these.\n */\nexport interface HeldStockProduct extends Pick<\n HostProduct,\n 'variants' | 'oversellPolicy'\n> {\n /** Hold key → hold. Absent on every product written before AGL-2356. */\n stockHolds?: Record<string, StockHold>\n}\n\n/**\n * HOW LONG A UNIT STAYS SPOKEN FOR.\n *\n * This number is not free to choose. A hold that lapses while its Checkout\n * Session can still be PAID reopens the exact window this file closes: the\n * shopper pays, the webhook decrements, and the unit was sold to someone else\n * in between. So the floor is the session's own lifetime.\n *\n * Stripe's default session lifetime is 24 HOURS, and a 24-hour stock hold is\n * precisely the trade this issue was blocked on — one abandoned cart standing\n * on a merchant's last unit for a day. The resolution is not to shorten the\n * hold below the session; it is to shorten the SESSION. `checkout.ts` and\n * `cart-checkout.ts` now send `expires_at` at `CHECKOUT_SESSION_TTL_MS`, which\n * is Stripe's own minimum and the same half-hour the bookings plugin has always\n * given a held room.\n *\n * The hold then outlives the session by `STOCK_HOLD_GRACE_MS`, so the ordering\n * is always: session dies → `checkout.session.expired` releases → the hold was\n * still standing when it did. The grace is what keeps clock skew between Stripe\n * and this process from lapsing a hold under a session that is still payable.\n */\n/**\n * 31 minutes, not 30. Stripe REJECTS an `expires_at` less than 30 minutes in\n * the future, and it measures against its own clock at the moment the request\n * lands — so a value computed here at exactly the floor fails whenever network\n * latency or clock skew eats a second. The extra minute is the margin; it is\n * not a product opinion, and the product opinion is \"the shortest session\n * Stripe will sell us\".\n */\nexport const CHECKOUT_SESSION_TTL_MS = 31 * 60 * 1000\n\n/** Cushion between the session expiring and its hold lapsing. */\nexport const STOCK_HOLD_GRACE_MS = 10 * 60 * 1000\n\nexport const STOCK_HOLD_TTL_MS = CHECKOUT_SESSION_TTL_MS + STOCK_HOLD_GRACE_MS\n\n/**\n * What a shopper is told when the units exist but are spoken for.\n *\n * Deliberately NOT the bare \"Sold out\" the empty-shelf refusal uses. The\n * merchant has stock; someone else is in checkout with it. Naming the wait is\n * what stops a shopper concluding the store is empty and leaving — the hold\n * lapses within the half hour and the unit comes back.\n */\nexport const STOCK_HELD_MESSAGE =\n 'The last of these is in someone else’s checkout. ' +\n 'Try again in a few minutes.'\n\n/**\n * The product's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal. A merchant\n * losing a unit forever to one corrupt map entry is the only direction with no\n * recovery inside the product, and it is exactly the failure mode that kept\n * this issue out of the launch build.\n */\nexport function pruneStockHolds(\n holds: Record<string, StockHold> | undefined,\n nowMs: number,\n): Record<string, StockHold> {\n const live: Record<string, StockHold> = {}\n for (const [key, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const units: Record<string, number> = {}\n for (const [variantId, raw] of Object.entries(hold?.units ?? {})) {\n const count = Math.round(Number(raw))\n // A non-numeric or non-positive count holds nothing. Coerced rather than\n // trusted for the same reason as the expiry: a corrupt entry must not be\n // able to reserve `NaN` units and make every comparison below false.\n if (Number.isFinite(count) && count > 0) units[variantId] = count\n }\n const locationId =\n typeof hold?.locationId === 'string' && hold.locationId ? hold.locationId : undefined\n live[key] = { expiresAtMs, units, ...(locationId ? { locationId } : {}) }\n }\n return live\n}\n\n/**\n * Units of one variant spoken for by live checkouts right now.\n *\n * `exceptHoldKey` is how a retry of one attempt does not stand in its own way:\n * the shopper presses the same button twice, the second press re-derives the\n * same hold key, and without this exclusion it would be refused the unit it is\n * already holding.\n */\nexport function heldVariantUnits(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n exceptHoldKey?: string,\n /** Count only the holds taken at this location (AGL-3624). */\n atLocationId?: string,\n): number {\n if (!variantId) return 0\n const live = pruneStockHolds(product?.stockHolds, nowMs)\n if (exceptHoldKey) delete live[exceptHoldKey]\n let held = 0\n for (const hold of Object.values(live)) {\n if (atLocationId && hold.locationId !== atLocationId) continue\n held += Number(hold.units?.[variantId] ?? 0)\n }\n return held\n}\n\n/** Units held across every variant of a product — the console's figure. */\nexport function heldProductUnits(\n product: HeldStockProduct | undefined,\n nowMs: number,\n): number {\n let held = 0\n for (const hold of Object.values(\n pruneStockHolds(product?.stockHolds, nowMs),\n )) {\n for (const units of Object.values(hold.units ?? {})) held += Number(units)\n }\n return held\n}\n\n/**\n * Resolve the variant a purchase refers to, EXACTLY as `canPurchase` does.\n *\n * Shared rather than re-derived, because the pre-filter and the in-transaction\n * re-check disagreeing about which variant they are talking about is a defect\n * with no visible symptom until the wrong shelf goes negative.\n */\nfunction resolveVariant(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n) {\n return variantId\n ? product?.variants?.find((item) => item.id === variantId)\n : product?.variants?.[0]\n}\n\n/**\n * Units a NEW checkout could take: the shelf minus the live holds.\n *\n * `null` when there is no number to answer with — an untracked variant, a\n * backorder product (whose merchant chose to sell past zero, so availability is\n * unbounded by construction), or a variant that does not exist. `null` rather\n * than `Infinity` so a caller cannot do arithmetic on \"no limit\" and get a\n * number, and never negative: a shelf that went short under a `backorder`\n * policy reads as zero available, not as a debt.\n *\n * NOTE FOR THIS REPO: `strictNullChecks` is off, so `if (!available)` does NOT\n * distinguish `null` from `0` here — and `0` is a legitimate, load-bearing\n * answer meaning \"tracked, and none left\". Every caller must test `== null`.\n */\nexport function availableVariantUnits(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n exceptHoldKey?: string,\n /**\n * The location the buyer is collecting from or being delivered from\n * (AGL-3624). With one, and a variant whose stock is split by location,\n * the answer is the SMALLER of what that location's bucket has free and\n * what the whole variant has free: a shipped cart's hold names no bucket,\n * so it can only be charged against the total, and a location with ten on\n * its shelf cannot sell ten when shipped checkouts already hold the other\n * locations' every unit plus some of these.\n */\n locationId?: string,\n): number | null {\n const variant = resolveVariant(product, variantId)\n if (!variant) return null\n if (variant.inventory == null) return null\n if (product?.oversellPolicy === 'backorder') return null\n const onShelf = Math.round(Number(variant.inventory))\n if (!Number.isFinite(onShelf)) return null\n const total = Math.max(\n 0,\n onShelf - heldVariantUnits(product, variant.id, nowMs, exceptHoldKey),\n )\n if (!locationId || !variant.inventoryByLocation) return total\n const bucket = Math.round(Number(variant.inventoryByLocation[locationId] ?? 0))\n const atLocation = Math.max(\n 0,\n (Number.isFinite(bucket) ? bucket : 0) -\n heldVariantUnits(product, variant.id, nowMs, exceptHoldKey, locationId),\n )\n return Math.min(total, atLocation)\n}\n\n/**\n * Is there a hold to take at all?\n *\n * False for the cases `availableVariantUnits` answers `null` for — an untracked\n * or backorder variant has no finite shelf to reserve against, and writing a\n * hold for one would put a growing map on a document whose availability it can\n * never change. The same shape as `holdPromotionSlot` skipping an UNCAPPED\n * promotion: the unlimited case stays off the write path entirely.\n */\nexport function stockIsReservable(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n nowMs: number,\n): boolean {\n return availableVariantUnits(product, variantId, nowMs) != null\n}\n\n/**\n * Can this attempt reserve `quantity` units?\n *\n * The hold-aware twin of `canPurchase`, and it must agree with it everywhere\n * `canPurchase` says NO: an untracked variant is always purchasable, a\n * backorder product always is, a missing variant never is. The single\n * difference is that a tracked deny-policy variant is measured against the\n * shelf MINUS what live checkouts are already holding.\n *\n * `canPurchase` is still the first test rather than re-derived arithmetic, so\n * the two cannot drift on the cases they share.\n */\nexport function canReserveStock(\n product: HeldStockProduct | undefined,\n variantId: string | undefined,\n quantity = 1,\n nowMs: number = Date.now(),\n exceptHoldKey?: string,\n /** Reserve at this location's bucket (AGL-3624); see `availableVariantUnits`. */\n locationId?: string,\n): boolean {\n if (!product) return false\n if (!canPurchase(product as HostProduct, variantId, quantity)) return false\n const available = availableVariantUnits(\n product,\n variantId,\n nowMs,\n exceptHoldKey,\n locationId,\n )\n // `== null`, never `!available` — see the note on `availableVariantUnits`.\n if (available == null) return true\n return available >= Math.max(1, Math.round(Number(quantity) || 1))\n}\n\n/**\n * What the console prints beside a stock count (AGL-2356).\n *\n * The merchant-facing half of the decision, and the reason a hold is not just\n * an internal optimisation. `inventory` deliberately does not move while a unit\n * is held, so a merchant looking at \"3 in stock\" while the storefront refuses\n * the third sale has no way to reason about it — the number is right and the\n * behaviour looks wrong. This is what names the gap.\n *\n * Empty string when nothing is held, so the row stays exactly as it reads today\n * on the overwhelming majority of products.\n */\nexport function stockHoldLabel(\n product: HeldStockProduct | undefined,\n nowMs: number = Date.now(),\n): string {\n const held = heldProductUnits(product, nowMs)\n if (held <= 0) return ''\n return `${held} reserved in checkout`\n}\n"],"names":["canPurchase","CHECKOUT_SESSION_TTL_MS","STOCK_HOLD_GRACE_MS","STOCK_HOLD_TTL_MS","STOCK_HELD_MESSAGE","pruneStockHolds","holds","nowMs","live","key","hold","Object","entries","expiresAtMs","Number","isFinite","units","variantId","raw","count","Math","round","locationId","undefined","heldVariantUnits","product","exceptHoldKey","atLocationId","stockHolds","held","values","heldProductUnits","resolveVariant","variants","find","item","id","availableVariantUnits","variant","inventory","oversellPolicy","onShelf","total","max","inventoryByLocation","bucket","atLocation","min","stockIsReservable","canReserveStock","quantity","Date","now","available","stockHoldLabel"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type ProductModifierGroup } from './product-modifiers';
|
|
18
18
|
import { type ProductChannelFacts } from './product-channel';
|
|
19
|
+
import type { PickupLocationSettings } from './order-local-fulfillment';
|
|
19
20
|
/**
|
|
20
21
|
* Commerce catalog v1 (AGL-276): products with options/variants,
|
|
21
22
|
* hierarchical categories, tags, and manual/smart collections. Pure
|
|
@@ -87,6 +88,8 @@ export interface InventoryLocation {
|
|
|
87
88
|
* it. Optional: a location written before this has only `address`.
|
|
88
89
|
*/
|
|
89
90
|
postalAddress?: PostalAddress;
|
|
91
|
+
/** Whether buyers may collect orders here, and when (AGL-3624). */
|
|
92
|
+
pickup?: PickupLocationSettings;
|
|
90
93
|
}
|
|
91
94
|
/** A product's packed size for one unit, when all three sides are known. */
|
|
92
95
|
export declare function productParcelDimensions(product: Pick<HostProduct, 'shipping'> | null | undefined): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce.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\nimport { modifierGroupsProblem, type ProductModifierGroup } from './product-modifiers'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { draftProductChannelFacts, type ProductChannelFacts } from './product-channel'\n\n/**\n * Commerce catalog v1 (AGL-276): products with options/variants,\n * hierarchical categories, tags, and manual/smart collections. Pure\n * model + helpers — no I/O; the products hub and storefront APIs operate\n * over these shapes. Documents live under `hosts/{hostId}/products`,\n * `.../productCategories`, and `.../collections`; the legacy Commerce\n * Starter product doc (AGL-90: name/priceUsd/inventory) lifts into this\n * shape via `liftLegacyProduct`, so existing docs keep working.\n */\n\n/*\n * THERE IS NO STOREFRONT FEE CONSTANT, and that absence is load-bearing\n * (AGL-2295).\n *\n * `COMMERCE_PLATFORM_FEE_PERCENT = 2` lived here with ZERO call sites. It\n * asserted a flat 2% that agrees with the plan table for exactly one cell\n * (Starter, physical) and disagrees with the other fifteen — Starter digital\n * is 5%, Pro digital 3%, Business 2%, Scale 1%, and every physical rate above\n * Starter is 0%. Nothing was wrong while nothing read it; the cost was that\n * the next reader wiring it up would silently re-price every plan, and a\n * constant sitting in the model beside the types reads like the answer.\n *\n * The single source is `resolveTransactionFeePct(org, productType)` in\n * `plan-entitlements.ts`, resolved PER REQUEST from the org — because it\n * depends on the plan, on per-org staff overrides, and on whether the\n * subscription is still alive. A constant cannot express any of that.\n * `commerce-fee-single-source.spec.ts` fails the build if one comes back.\n */\n/** Product price ceiling (whole USD). */\nexport const COMMERCE_MAX_PRICE_USD = 10000\n\nexport type ProductType = 'physical' | 'digital' | 'service'\nexport type ProductStatus = 'draft' | 'active' | 'archived'\n\n/** One axis of variation, e.g. { name: 'Size', values: ['S','M','L'] }. */\nexport interface ProductOption {\n name: string\n values: string[]\n}\n\n/** A sellable configuration of a product (every product has ≥ 1). */\nexport interface ProductVariant {\n /** Stable id unique within the product (never reused after delete). */\n id: string\n /** Option selections keyed by option name; {} for the default variant. */\n options?: Record<string, string>\n sku?: string\n barcode?: string\n priceUsd: number\n /** Strike-through price; a sale badge shows when > priceUsd. */\n compareAtPriceUsd?: number\n weightGrams?: number\n /** null/undefined = untracked; 0 = sold out (matches AGL-96). */\n inventory?: number | null\n /**\n * Per-location stock (AGL-286); `inventory` stays the summed\n * denormalization. Absent = single default location.\n */\n inventoryByLocation?: Record<string, number>\n /** Media-library image URL shown when this variant is selected. */\n imageUrl?: string\n}\n\n/** A product's packed size and customs facts (AGL-3612). */\nexport interface ProductShippingFacts {\n lengthCm?: number | null\n widthCm?: number | null\n heightCm?: number | null\n /** Harmonized System tariff code. */\n hsCode?: string\n /** ISO-3166 alpha-2 country the product was made in. */\n originCountry?: string\n}\n\n/** A place's postal address, in parts (AGL-3612). Country is ISO-3166 alpha-2. */\nexport interface PostalAddress {\n line1?: string\n line2?: string\n city?: string\n state?: string\n postalCode?: string\n country?: string\n phone?: string\n}\n\n/** `hosts/{hostId}/locations/{id}` doc (AGL-286). */\nexport interface InventoryLocation {\n name: string\n isDefault?: boolean\n /** Free text, as the location was first described. */\n address?: string\n /**\n * The same place as a postal address (AGL-3612), so parcels can ship from\n * it. Optional: a location written before this has only `address`.\n */\n postalAddress?: PostalAddress\n}\n\n/** A product's packed size for one unit, when all three sides are known. */\nexport function productParcelDimensions(\n product: Pick<HostProduct, 'shipping'> | null | undefined,\n): { lengthCm: number; widthCm: number; heightCm: number } | undefined {\n const facts = product?.shipping\n const side = (value: unknown) => {\n const number = Number(value)\n return Number.isFinite(number) && number > 0 ? Math.min(number, 300) : 0\n }\n const lengthCm = side(facts?.lengthCm)\n const widthCm = side(facts?.widthCm)\n const heightCm = side(facts?.heightCm)\n return lengthCm && widthCm && heightCm ? { lengthCm, widthCm, heightCm } : undefined\n}\n\n/**\n * Shipping facts as the product editor stages them, keystroke by keystroke\n * (AGL-3647): {@link normalizeProductShippingFacts}, except that a country\n * of origin being typed keeps its first letter. The editor runs this on\n * every keystroke, and the strict form drops anything short of two letters,\n * so the field could never be typed into. Readers normalize what is stored.\n */\nexport function draftProductShippingFacts(value: unknown): ProductShippingFacts | undefined {\n const facts = normalizeProductShippingFacts(value)\n const typed = String((value as Record<string, unknown> | null)?.['originCountry'] ?? '')\n .trim()\n .toUpperCase()\n if (/^[A-Z]$/.test(typed)) return { ...facts, originCountry: typed }\n return facts\n}\n\n/** A stored product's shipping facts made safe (AGL-3612); never throws. */\nexport function normalizeProductShippingFacts(value: unknown): ProductShippingFacts | undefined {\n if (!value || typeof value !== 'object') return undefined\n const raw = value as Record<string, unknown>\n const facts: ProductShippingFacts = {}\n for (const side of ['lengthCm', 'widthCm', 'heightCm'] as const) {\n const number = Number(raw[side])\n if (raw[side] !== null && raw[side] !== undefined && Number.isFinite(number) && number > 0) {\n facts[side] = Math.min(Math.round(number * 10) / 10, 300)\n }\n }\n const hsCode = String(raw['hsCode'] ?? '').replace(/[^0-9.]/g, '').slice(0, 14)\n if (hsCode) facts.hsCode = hsCode\n const originCountry = String(raw['originCountry'] ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(originCountry)) facts.originCountry = originCountry\n return Object.keys(facts).length ? facts : undefined\n}\n\n/** A stored location's postal address made safe (AGL-3612); never throws. */\nexport function normalizePostalAddress(value: unknown): PostalAddress | undefined {\n if (!value || typeof value !== 'object') return undefined\n const raw = value as Record<string, unknown>\n const address: PostalAddress = {}\n for (const field of ['line1', 'line2', 'city', 'state', 'postalCode', 'phone'] as const) {\n const text = String(raw[field] ?? '').trim().slice(0, 120)\n if (text) address[field] = text\n }\n const country = String(raw['country'] ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) address.country = country\n return Object.keys(address).length ? address : undefined\n}\n\n/**\n * `hosts/{hostId}/registers/{id}` doc (AGL-472): a named point-of-sale\n * register. Creation is capped by the plan's `posRegisters` quota (Pro 1,\n * Business 2, Advanced 5; raised by a per-org entitlement override for the\n * $89/mo add-on), enforced server-side by the resources route. Each POS\n * sale stamps its `registerId` so takings are attributable per register.\n */\nexport interface PosRegister {\n name: string\n /** Optional default inventory location this register sells from. */\n locationId?: string\n}\n\n/**\n * The register ids within the plan's `posRegisters` cap (AGL-482), ranked\n * by creation order — the same ordering `pos-order.ts` enforces at sale\n * time. Registers beyond the cap (e.g. after a Business→Pro downgrade) are\n * excluded so the console/POS can dim or hide them instead of surfacing a\n * checkout 403. `cap` of `Infinity` returns all.\n */\nexport function registersWithinCap(\n registers: Array<{ $id: string; createdAt?: any }>,\n cap: number,\n): Set<string> {\n const createdMs = (r: { createdAt?: any }) =>\n r.createdAt?.toMillis?.() ?? r.createdAt?.seconds ?? 0\n const ranked = [...registers].sort(\n (a, b) => createdMs(a) - createdMs(b) || a.$id.localeCompare(b.$id),\n )\n return new Set(ranked.slice(0, Math.max(0, cap)).map((r) => r.$id))\n}\n\n/**\n * `hosts/{hostId}/suppliers/{id}` doc (AGL-289): where dropshipped\n * order lines route on payment. Email and webhook are both optional but\n * one must be set for routing to do anything; webhook payloads are\n * HMAC-SHA256-signed with `webhookSecret`.\n */\nexport interface HostSupplier {\n name: string\n email?: string\n webhookUrl?: string\n webhookSecret?: string\n}\n\n/** `hosts/{hostId}/products/{id}` doc. */\nexport interface HostProduct {\n name: string\n /** Host-unique URL segment for /products/{slug}. */\n slug: string\n description?: string\n type: ProductType\n status: ProductStatus\n /** Ordered media-library image URLs (first = primary). */\n mediaUrls?: string[]\n categoryIds?: string[]\n /**\n * The SMART collections this product's rules answer (AGL-3321), written by\n * every writer — see `productCollectionIds`.\n */\n collectionIds?: string[]\n tags?: string[]\n options?: ProductOption[]\n variants: ProductVariant[]\n /** Per-product overrides for PDP meta tags (AGL-299 consumes). */\n seo?: { title?: string; description?: string; imageUrl?: string }\n /**\n * What shopping channels ask of the product (AGL-3637): brand, GTIN, MPN,\n * condition and Google category. Absent means nothing was entered; the\n * channel feeds fall back to the store's defaults.\n */\n channel?: ProductChannelFacts\n /** Supplier for dropship routing (AGL-289). */\n supplierId?: string\n /** Out-of-stock behavior (AGL-281): deny (default) or allow backorder. */\n oversellPolicy?: 'deny' | 'backorder'\n /** Never taxed regardless of tax settings (AGL-285). */\n taxExempt?: boolean\n /**\n * Digital delivery (AGL-302): downloadable files for `digital`\n * products. Buyers always download the CURRENT list, so uploading a\n * new version re-delivers to everyone.\n */\n digitalFiles?: Array<{ url: string; fileName: string; version?: string }>\n /** Max download attempts per order line; absent = unlimited. */\n downloadLimit?: number\n /**\n * Recurring billing (AGL-303): buyers subscribe instead of buying\n * once; an active subscription is the entitlement content gating\n * checks (AGL-309).\n */\n subscription?: { interval: 'month' | 'year'; trialDays?: number }\n /**\n * Buyer-chosen billing (AGL-545): with `subscription` set, true lets\n * the buyer pick one-time OR subscribe on the PDP (default one-time);\n * absent/false keeps the product subscription-only.\n */\n subscriptionOptional?: boolean\n /** Members-only videos (AGL-315), streamed via short-TTL links. */\n gatedVideos?: Array<{ url: string; title?: string }>\n /** Manual related products for the upsell block (AGL-325). */\n relatedProductIds?: string[]\n /**\n * What one unit ships as (AGL-3612): its packed size, for a carrier to\n * price, and its customs facts, for a parcel that crosses a border. Weight\n * stays per variant. Absent means unknown, and a label is priced in the\n * merchant's default box.\n */\n shipping?: ProductShippingFacts\n /** Buying this issues a gift-card code for its price (AGL-322). */\n giftCard?: boolean\n /** Tracked-total at/below this alerts host managers (AGL-281). */\n lowStockThreshold?: number\n /**\n * Choices added to the item at the register (AGL-3607) — \"Oat milk\",\n * \"Extra shot\" — priced by the server from this list. See\n * `product-modifiers.ts`.\n */\n modifierGroups?: ProductModifierGroup[]\n /** Shown under Quick keys on the register's first screen (AGL-3607). */\n posQuickKey?: boolean\n createdAtMs?: number\n updatedAtMs?: number\n deletedAt?: number | null\n /** Legacy Commerce Starter fields kept so old docs read back (AGL-90). */\n priceUsd?: number\n inventory?: number | null\n imageUrl?: string\n}\n\n/** `hosts/{hostId}/productCategories/{id}` doc (tree via parentId). */\nexport interface ProductCategory {\n name: string\n slug: string\n parentId?: string | null\n order?: number\n}\n\nexport type CollectionRuleField =\n | 'tag'\n | 'categoryId'\n | 'priceUsd'\n | 'name'\n | 'type'\nexport type CollectionRuleOp = 'eq' | 'neq' | 'lt' | 'gt' | 'contains'\n\nexport interface CollectionRule {\n field: CollectionRuleField\n op: CollectionRuleOp\n value: string | number\n}\n\n/** `hosts/{hostId}/collections/{id}` doc. */\nexport interface HostCollection {\n name: string\n slug: string\n description?: string\n mode: 'manual' | 'smart'\n /** Manual mode: explicit ordered membership. */\n productIds?: string[]\n /** Smart mode: rules evaluated over active products. */\n rules?: CollectionRule[]\n /** Smart mode: true = every rule must match (default), false = any. */\n matchAll?: boolean\n /** Media-library image URL for the collection card/landing hero. */\n imageUrl?: string\n order?: number\n}\n\n/**\n * Payment-provider seam (AGL-284): checkout/session creation goes\n * through a provider id so PayPal (etc.) can slot in without reworking\n * callers. Stripe is the only implementation today; store settings\n * (AGL-295) will carry the selection when a second provider exists.\n */\nexport type PaymentProviderId = 'stripe'\n\nexport interface PaymentCheckoutRequest {\n provider: PaymentProviderId\n hostId: string\n amountCents: number\n feeCents: number\n productName: string\n successUrl: string\n cancelUrl: string\n metadata?: Record<string, string>\n}\n\n/** Variant ceiling per product (Shopify parity). */\nexport const COMMERCE_MAX_VARIANTS = 100\nexport const COMMERCE_MAX_OPTIONS = 3\nexport const COMMERCE_MAX_OPTION_VALUES = 25\nexport const COMMERCE_SLUG_MAX_LENGTH = 80\n\n/** Lowercase-kebab slug from a product/category/collection name. */\nexport function commerceSlug(name: string): string {\n return String(name ?? '')\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, COMMERCE_SLUG_MAX_LENGTH)\n}\n\n/**\n * The search keys a product write carries, so the catalog can be searched by\n * the QUERY rather than by the rows a listener happened to fetch (AGL-2501).\n *\n * The hub lists `limit(500)`, and the search compared what that returned — so\n * a product past the window answered \"no products match\", which reads as the\n * product not existing rather than as the search not reaching it. Filtering\n * server-side needs fields Firestore can index, and neither of the two things\n * a merchant searches by is one:\n *\n * - A NAME needs case-insensitive contains, which Firestore has no operator\n * for. `nameSearchFields` denormalizes it into the three shapes that are\n * indexable — an exact key, word-prefix tokens for `array-contains`, and\n * the key reversed so \"ends with\" becomes a prefix range.\n * - A SKU lives inside `variants`, an array of OBJECTS, and Firestore cannot\n * query a field inside one. Flattened here into a top-level array so a SKU\n * is one `array-contains`.\n *\n * ⛔ A SKU therefore matches WHOLE, not as a substring: \"abc-123\" finds it and\n * \"123\" does not. That is the right way round for a SKU, which is a value\n * somebody copies rather than half-remembers — unlike a name, where the tokens\n * above buy word-prefix matching precisely because names are half-remembered.\n *\n * `barcodes` is the same flattening for the same reason, and it is the one the\n * register depends on: a keyboard-wedge scanner types the code and presses\n * Enter, so the lookup has to be exact and has to reach the whole catalog. A\n * scan is never a half-remembered value.\n *\n * Lower-cased because the translator lower-cases the typed query before it\n * builds the `array-contains`; stored and typed have to be normalized the same\n * way or the two silently disagree.\n *\n * ⚠️ Each array is OMITTED when a product has none, rather than written as\n * `[]`. `isNotEmpty` is served as `!= null`, and an empty array is not null —\n * so a product with no SKUs at all would answer \"has a SKU\" for every row.\n *\n * `priceFromCents` is the storefront's price key (AGL-3321): the price a\n * product grid card shows — \"From $X\", the lowest variant price — in whole\n * cents, so the catalog's price slider and its price sorts are a range and an\n * `orderBy` on one stored number rather than a comparison over variants no\n * query can reach. Written only when the variants are, because it is derived\n * from them: a write that carries no variants must not stamp a price.\n *\n * ⚠️ Spread this at EVERY write that sets a product's name or variants. A\n * write that sets the name without it leaves the keys describing the previous\n * name, and the product becomes findable only by what it used to be called.\n */\nexport function productSearchFields(product: {\n name: string\n variants?: ProductVariant[]\n}): {\n name: string\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n skus?: string[]\n barcodes?: string[]\n priceFromCents?: number\n} {\n const flatten = (read: (variant: ProductVariant) => string | undefined) => [\n ...new Set(\n (product.variants ?? [])\n .map((variant) => (read(variant) ?? '').trim().toLowerCase())\n .filter(Boolean),\n ),\n ]\n const skus = flatten((variant) => variant.sku)\n const barcodes = flatten((variant) => variant.barcode)\n return {\n ...nameSearchFields(product.name),\n ...(skus.length ? { skus } : {}),\n ...(barcodes.length ? { barcodes } : {}),\n ...(product.variants\n ? { priceFromCents: productPriceFromCents({ variants: product.variants }) }\n : {}),\n }\n}\n\n/**\n * The price a storefront card shows, in whole cents (AGL-3321): the low end\n * of the variant range, which is what \"From $X\" prints and what the catalog's\n * price filter and price sorts compare. Stored as `priceFromCents` by\n * {@link productSearchFields}; `0` for a product with no priced variant yet.\n */\nexport function productPriceFromCents(\n product: Pick<HostProduct, 'variants'>,\n): number {\n return Math.round(productPriceRange(product)[0] * 100)\n}\n\n/**\n * The stock keys a product write carries (AGL-3321): the flat `inventory`\n * total, and `soldOut` — whether a storefront card reads \"Sold out\", which is\n * what the product grid's In stock chip asks the catalog query for\n * (`soldOut == false`). A grid cannot ask a query about stock inside\n * `variants`, so the verdict is stored beside them.\n *\n * ⚠️ Spread this at EVERY write that changes `variants` or `oversellPolicy`:\n * a sale, a cancellation, a stock adjustment and the editor alike. A write\n * that moves stock without it leaves a sold-out product under In stock, or an\n * in-stock one missing from it. Stored by `backfill-products-list-fields.mjs`\n * on the products written before it.\n */\nexport function productStockFields(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n): { inventory: number | null; soldOut: boolean } {\n const inventory = productInventory(product)\n return {\n inventory,\n soldOut: inventory != null && inventory <= 0 && product.oversellPolicy !== 'backorder',\n }\n}\n\n/**\n * Cartesian product of option values → the variant option combinations\n * the products hub materializes into a variants matrix. Empty/absent\n * options yield a single default combination ({}).\n */\nexport function expandVariantMatrix(\n options: ProductOption[] | undefined,\n): Array<Record<string, string>> {\n const usable = (options ?? []).filter(\n (option) => option.name && (option.values?.length ?? 0) > 0,\n )\n if (usable.length === 0) return [{}]\n let combos: Array<Record<string, string>> = [{}]\n for (const option of usable) {\n const next: Array<Record<string, string>> = []\n for (const combo of combos) {\n for (const value of option.values) {\n next.push({ ...combo, [option.name]: value })\n }\n }\n combos = next\n if (combos.length > COMMERCE_MAX_VARIANTS) {\n return combos.slice(0, COMMERCE_MAX_VARIANTS)\n }\n }\n return combos\n}\n\n/**\n * The option axes renamed, with every variant's selection carried to the new\n * name in place (AGL-3066).\n *\n * A variant keeps its selections keyed by option NAME, so a rename has to move\n * each key rather than rebuild the matrix: a rebuild matches variants by name\n * and value, finds none under the new name, and replaces every variant with a\n * new id holding the first variant's price and stock and no SKU, barcode,\n * compare-at price or weight. Moving the key keeps the id every order line\n * and stock movement refers to, and every field a person set.\n *\n * `names` holds one entry per option, in order; an entry that is not a string\n * keeps that option's name. Each selection keeps its position among a\n * variant's selections, because a variant is labeled by them in order.\n *\n * Two options may share a name for a moment while a person types. A variant\n * cannot hold two selections under one name, so while the new names are not\n * all different nothing is moved, and `validateProduct` refuses the product.\n * The selection left under the old name is found again by its value once the\n * names differ, so typing through a duplicate loses nothing.\n */\nexport function renameProductOptions(\n product: Pick<HostProduct, 'options' | 'variants'>,\n names: ReadonlyArray<string | null | undefined>,\n): Pick<HostProduct, 'options' | 'variants'> {\n const current = product.options ?? []\n const options = current.map((option, index) => {\n const name = names[index]\n return typeof name === 'string' ? { ...option, name } : option\n })\n const next = options.map((option) => option.name)\n if (new Set(next).size !== next.length) return { options, variants: product.variants }\n const variants = (product.variants ?? []).map((variant) => {\n const selections = variant.options\n if (!selections) return variant\n const axisOf = optionAxesOf(current, selections)\n const moved: Record<string, string> = {}\n let changed = false\n for (const [key, value] of Object.entries(selections)) {\n const axis = axisOf.get(key)\n const name = axis === undefined ? key : next[axis]\n if (name !== key) changed = true\n moved[name] = value\n }\n return changed ? { ...variant, options: moved } : variant\n })\n return { options, variants }\n}\n\n/**\n * Which option each of a variant's selections belongs to: the option of the\n * same name first, and then, for a selection under a name no option has any\n * longer, the one remaining option whose values hold its value. A selection\n * that fits neither belongs to no option and is left as it is.\n */\nfunction optionAxesOf(\n options: readonly ProductOption[],\n selections: Readonly<Record<string, string>>,\n): Map<string, number> {\n const axisOf = new Map<string, number>()\n const taken = new Set<number>()\n options.forEach((option, index) => {\n if (Object.prototype.hasOwnProperty.call(selections, option.name) && !axisOf.has(option.name)) {\n axisOf.set(option.name, index)\n taken.add(index)\n }\n })\n for (const [key, value] of Object.entries(selections)) {\n if (axisOf.has(key)) continue\n const axis = options.findIndex(\n (option, index) => !taken.has(index) && (option.values ?? []).includes(value),\n )\n if (axis === -1) continue\n axisOf.set(key, axis)\n taken.add(axis)\n }\n return axisOf\n}\n\n/**\n * Whether a variant has a price a sale can charge (AGL-2916).\n *\n * A product proposed by AI is created with every price left empty for the\n * merchant to set, so a stored variant can lack one. `Number(undefined)` is\n * no price, and neither is `null`, which `Number` reads as zero: a sale door\n * that priced either would charge nothing, or fail at the processor.\n */\nexport function variantHasPrice(variant: object | null | undefined): boolean {\n const price = (variant as { priceUsd?: unknown } | null | undefined)?.priceUsd\n return typeof price === 'number' && Number.isFinite(price) && price >= 0\n}\n\n/** Whether any variant of a product still needs its price set (AGL-2916). */\nexport function productPriceMissing(product: { variants?: readonly object[] }): boolean {\n return (product.variants ?? []).some((variant) => !variantHasPrice(variant))\n}\n\n/** Copy proposed for a product, by field (AGL-2916). A field left out is left alone. */\nexport interface ProductCopyValues {\n description?: string\n tags?: string[]\n categoryIds?: string[]\n /** One name per option the product has, in order. */\n optionNames?: string[]\n seoTitle?: string\n seoDescription?: string\n /**\n * Packed size and customs facts (AGL-3612), merged over what the product\n * has; a side set to `null` is cleared.\n */\n shipping?: Partial<ProductShippingFacts>\n /**\n * Shopping-channel facts (AGL-3637), merged over what the product has; a\n * field set to `''` is cleared.\n */\n channel?: { [Field in keyof ProductChannelFacts]?: string }\n}\n\n/**\n * The fields proposed copy changes on a product (AGL-2916), in the shape the\n * editor stores them: only the fields `values` names. Tags and categories are\n * each kept once; the search listing keeps its share image; and option names\n * rename the product's options through `renameProductOptions`, keeping every\n * variant, only when they name each option once with a name of its own.\n */\nexport function productCopyPatch(\n product: Pick<HostProduct, 'options' | 'variants' | 'seo' | 'shipping'> &\n Pick<Partial<HostProduct>, 'channel'>,\n values: ProductCopyValues,\n): Partial<\n Pick<HostProduct, 'description' | 'tags' | 'categoryIds' | 'seo' | 'options' | 'variants' | 'shipping' | 'channel'>\n> {\n const patch: Partial<\n Pick<HostProduct, 'description' | 'tags' | 'categoryIds' | 'seo' | 'options' | 'variants' | 'shipping' | 'channel'>\n > = {}\n if (values.shipping && typeof values.shipping === 'object') {\n // Stored whole, never `undefined`: the editor's save is a full `setDoc`,\n // which refuses an undefined field. Nothing known is an empty map.\n patch.shipping = draftProductShippingFacts({ ...product.shipping, ...values.shipping }) ?? {}\n }\n if (values.channel && typeof values.channel === 'object') {\n // Stored whole, never `undefined`, for the same reason. Nothing entered\n // is an empty map. Kept as typed: this runs on every keystroke.\n patch.channel = draftProductChannelFacts({ ...product.channel, ...values.channel }) ?? {}\n }\n if (typeof values.description === 'string') patch.description = values.description\n if (Array.isArray(values.tags)) {\n patch.tags = [...new Set(values.tags.map((tag) => tag.trim()).filter(Boolean))]\n }\n if (Array.isArray(values.categoryIds)) {\n patch.categoryIds = [...new Set(values.categoryIds.filter(Boolean))]\n }\n if (typeof values.seoTitle === 'string' || typeof values.seoDescription === 'string') {\n patch.seo = {\n ...product.seo,\n ...(typeof values.seoTitle === 'string' ? { title: values.seoTitle } : {}),\n ...(typeof values.seoDescription === 'string' ? { description: values.seoDescription } : {}),\n }\n }\n const options = product.options ?? []\n const names = values.optionNames?.map((name) => name.trim())\n if (\n names &&\n options.length > 0 &&\n names.length === options.length &&\n names.every(Boolean) &&\n new Set(names).size === names.length &&\n names.some((name, index) => name !== options[index].name)\n ) {\n Object.assign(patch, renameProductOptions(product, names))\n }\n return patch\n}\n\n/** A product proposed for a store, before anyone has priced it (AGL-2916). */\nexport interface ProposedProduct {\n name: string\n type: ProductType\n description: string\n tags: string[]\n options: ProductOption[]\n seoTitle: string\n seoDescription: string\n}\n\n/** A new product whose variants carry no price yet. */\nexport type UnpricedProductDraft = Omit<HostProduct, 'variants'> & {\n variants: Array<Omit<ProductVariant, 'priceUsd'>>\n}\n\n/**\n * The document a proposed product is created as (AGL-2916): a `draft`, under\n * a slug no product in `takenSlugs` has, with one variant for each\n * combination of its options and NO price on any of them. Prices are the\n * merchant's to set: the editor refuses to save the product until every\n * variant has one, and no sale door sells a variant without one\n * (`variantHasPrice`). The search keys travel with the name, as on every\n * create path.\n */\nexport function unpricedProductDraft(\n proposal: ProposedProduct,\n takenSlugs: ReadonlySet<string>,\n nowMs: number,\n): UnpricedProductDraft {\n const name = proposal.name.trim().slice(0, 120)\n const base = commerceSlug(name) || 'product'\n let slug = base\n for (let suffix = 2; takenSlugs.has(slug); suffix += 1) slug = `${base}-${suffix}`\n const seen = new Set<string>()\n const options = proposal.options\n .map((option) => ({\n name: option.name.trim(),\n values: [...new Set(option.values.map((value) => value.trim()).filter(Boolean))].slice(\n 0,\n COMMERCE_MAX_OPTION_VALUES,\n ),\n }))\n .filter((option) => {\n if (!option.name || !option.values.length || seen.has(option.name)) return false\n seen.add(option.name)\n return true\n })\n .slice(0, COMMERCE_MAX_OPTIONS)\n const variants = options.length\n ? expandVariantMatrix(options).map((combo, index) => ({\n id: `v${nowMs.toString(36)}${index}`,\n options: combo,\n }))\n : [{ id: 'default' }]\n const seoTitle = proposal.seoTitle.trim()\n const seoDescription = proposal.seoDescription.trim()\n return {\n // The keys for the variants as created: no price is `priceFromCents: 0`\n // and no tracked stock is never sold out, so the draft is ordered and\n // filtered like every other product once it is priced and activated.\n ...productSearchFields({ name, variants: variants as ProductVariant[] }),\n ...productStockFields({ variants: variants as ProductVariant[] }),\n slug,\n type: proposal.type,\n status: 'draft',\n description: proposal.description,\n tags: [...new Set(proposal.tags.map((tag) => tag.trim()).filter(Boolean))],\n ...(options.length ? { options } : {}),\n variants,\n ...(seoTitle || seoDescription\n ? { seo: { ...(seoTitle ? { title: seoTitle } : {}), ...(seoDescription ? { description: seoDescription } : {}) } }\n : {}),\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n}\n\n/** Variant whose option selections match exactly; undefined if none. */\nexport function findVariant(\n product: Pick<HostProduct, 'variants'>,\n selections: Record<string, string>,\n): ProductVariant | undefined {\n return product.variants?.find((variant) => {\n const options = variant.options ?? {}\n const keys = new Set([...Object.keys(options), ...Object.keys(selections)])\n for (const key of keys) {\n if (options[key] !== selections[key]) return false\n }\n return true\n })\n}\n\n/** [min, max] price across variants; [0, 0] when there are none. */\nexport function productPriceRange(\n product: Pick<HostProduct, 'variants'>,\n): [number, number] {\n const prices = (product.variants ?? [])\n .map((variant) => Number(variant.priceUsd))\n .filter((price) => Number.isFinite(price) && price >= 0)\n if (prices.length === 0) return [0, 0]\n return [Math.min(...prices), Math.max(...prices)]\n}\n\n/** Sum of tracked inventory; null when every variant is untracked. */\nexport function productInventory(\n product: Pick<HostProduct, 'variants'>,\n): number | null {\n let total: number | null = null\n for (const variant of product.variants ?? []) {\n if (variant.inventory == null) continue\n total = (total ?? 0) + Number(variant.inventory)\n }\n return total\n}\n\n/**\n * Reasons an inventory adjustment doc may carry (AGL-281).\n *\n * `cancellation` is the only one written automatically in the PLUS direction\n * (AGL-1808): cancelling an order the transition rules prove was never\n * fulfilled puts back exactly what the sale took. It is deliberately not\n * `refund` — no money moved — and not `correction`, which is what a merchant\n * picks when the count disagreed with the shelf for a reason nobody recorded.\n * Kept out of the \"Adjust stock\" dialog's menu on purpose: a merchant reaching\n * for it by hand means something else happened.\n *\n * `sync` is a count another warehouse reported (AGL-3634): a fulfillment\n * network holding the goods said how many it can ship, and the count was set\n * to that. Written only through core's `core.stock-levels`, with `source`\n * naming who counted, and kept out of the dialog's menu for the same reason.\n */\nexport type InventoryAdjustmentReason =\n 'sale' | 'refund' | 'restock' | 'correction' | 'damage' | 'cancellation' | 'sync'\n\n/** `hosts/{hostId}/inventoryAdjustments/{id}` doc. */\nexport interface InventoryAdjustment {\n productId: string\n variantId: string\n /** Positive = stock added, negative = removed. */\n delta: number\n /**\n * What the count ACTUALLY moved by, when the floor in\n * `adjustVariantInventory` absorbed part of `delta` (AGL-2149). Written only\n * when the two differ, which for a sale means a backorder product that sold\n * past zero: `delta` stays the units sold — that is the merchant's stock\n * history and it must keep saying \"3 went out the door\" — while this says how\n * many of them the count could give up.\n *\n * A REVERSAL MUST READ THIS, NOT `delta`. Restoring `delta` on a backorder\n * sale that the floor swallowed invents inventory that never existed: 0 sells\n * 3, the count stays 0, and a `+3` on cancellation leaves 3 units nobody has.\n * Absent on every row written before AGL-2149 and on every row where nothing\n * was clamped, so `appliedDelta ?? delta` is the reader.\n */\n appliedDelta?: number\n reason: InventoryAdjustmentReason\n /** Order id for sale/refund adjustments. */\n orderId?: string\n /** Location for multi-location stock (AGL-286); absent = default. */\n locationId?: string\n /**\n * Who counted, for a `sync` row: the warehouse that reported it (AGL-3634);\n * for a `sale` another channel made, that channel (AGL-3638).\n */\n source?: string\n atMs: number\n}\n\n/**\n * Whether `quantity` of a variant is purchasable (AGL-281): untracked\n * stock always is; tracked stock honors the product's oversell policy.\n */\nexport function canPurchase(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n variantId: string | undefined,\n quantity = 1,\n): boolean {\n const variant = variantId\n ? product.variants?.find((item) => item.id === variantId)\n : product.variants?.[0]\n if (!variant) return false\n if (variant.inventory == null) return true\n if (product.oversellPolicy === 'backorder') return true\n return Number(variant.inventory) >= quantity\n}\n\n/**\n * What the register has to SAY about a line it is about to sell (AGL-2357).\n *\n * ## The defect\n *\n * `pos-order.ts` contains no `canPurchase` call. Every storefront door gates on\n * it; the register does not, so a merchant who set `oversellPolicy: 'deny'` in\n * the product editor silently got `backorder` at the counter — the count floors\n * at zero underneath and nothing anywhere says so.\n *\n * ## The decision, and why it is a shortfall and not a refusal\n *\n * Warn, never block. A till is the wrong place for a stale number to stop a\n * real sale: the cashier is holding the goods, the physical shelf is the truth,\n * and refusing loses a sale over data that is behind. But \"deny\" must at least\n * SAY something. So this reports the shortfall and the caller sells anyway.\n * Honouring the policy with a manager-level override is the post-launch shape\n * (AGL-2372) and wants an audit trail this deliberately does not build.\n *\n * ## Why the test is `canPurchase`\n *\n * Exactly the gate the register was missing, and no wider. An untracked variant\n * has no number to be short against, and a `backorder` product's merchant chose\n * to sell past zero — warning them would be noise about a setting they set on\n * purpose. So a shortfall is reported only where a purchase would have been\n * refused anywhere else in the product.\n */\nexport interface StockShortfall {\n productId: string\n variantId?: string\n /** Product name at the time of sale, for a message the cashier can read. */\n name: string\n variantLabel?: string\n /** Units the cashier is ringing up. */\n requested: number\n /** Units the count says are on the shelf; never negative. */\n available: number\n}\n\nexport function stockShortfall(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n variantId: string | undefined,\n quantity = 1,\n): { available: number } | null {\n if (canPurchase(product, variantId, quantity)) return null\n const variant = variantId\n ? product.variants?.find((item) => item.id === variantId)\n : product.variants?.[0]\n // `canPurchase` answers false for a variant that does not exist at all. That\n // is not a stock shortfall and must not be reported as \"0 in stock\" — the\n // register's own line builder falls back to `variants[0]`, so the case is\n // unreachable from the counter, and inventing a count for it would be the\n // one failure mode this feature must not have.\n if (!variant || variant.inventory == null) return null\n return { available: Math.max(0, Math.round(Number(variant.inventory) || 0)) }\n}\n\n/**\n * Whether stock tracking MEANS anything on a product (AGL-1744).\n *\n * `canPurchase` above gates every checkout on stock, subscription sessions\n * included, but nothing ever decrements for a subscription — not on the\n * initial charge (the `commerce-subscription` webhook branch deliberately\n * writes no order and touches no inventory, AGL-1732) and not on renewal\n * (`invoice.payment_succeeded` is unhandled repo-wide, AGL-1743). So on a\n * subscription-only product that gate is permanently satisfied: one unit of\n * stock sells an unlimited number of subscriptions, and the count never\n * moves however many boxes ship. The merchant set a number, the system\n * checks it on every sale, and the number is never true.\n *\n * AGL-1750 resolved the half AGL-1744 had to leave open: every paid invoice\n * of a PHYSICAL subscription now mints an order and decrements its variant's\n * stock (the `invoice.paid` branch of the commerce webhook), opening cycle\n * included. So the honest split is now three-way:\n *\n * - `subscriptionOptional` (\"Both\") products have a real one-time path —\n * the cart only ever builds `mode: 'payment'` sessions, and buy-now with\n * `billing: 'once'` records a plain order — and that path decrements\n * honestly. Withdrawing stock tracking there would delete a working\n * control and re-open the AGL-1711 oversell on the half that works.\n * - a PHYSICAL subscription box consumes a unit every cycle, and since\n * AGL-1750 the cycle's own order decrements it — the count moves as the\n * boxes ship, so the number the merchant sets is kept true again.\n * - a DIGITAL or SERVICE subscription-only product still has no\n * decrementing path anywhere, so the field stays withdrawn there: one\n * unit would still sell unlimited subscriptions.\n *\n * This says nothing about what `canPurchase` returns — that is the gate for\n * every other purchase path and is deliberately untouched. This is a console\n * predicate: it stops the editor inviting a merchant to set a number the\n * system will never keep true.\n */\nexport function stockTrackingApplies(\n product: Pick<HostProduct, 'subscription' | 'subscriptionOptional' | 'type'>,\n): boolean {\n return (\n !product.subscription ||\n product.subscriptionOptional === true ||\n product.type === 'physical'\n )\n}\n\n/** Buyer's requested billing mode on a checkout POST (AGL-545). */\nexport type CheckoutBillingChoice = 'once' | 'subscribe'\n\n/**\n * Resolves the Stripe Checkout mode for a product + the buyer's\n * requested billing choice (AGL-545). The product doc is authoritative:\n * - no `subscription` → always one-time (the request field is ignored)\n * - `subscription` without `subscriptionOptional` → always subscription\n * (a forged `billing: 'once'` cannot buy a subscription product once)\n * - `subscriptionOptional` → the buyer chooses; absent/invalid choices\n * default to one-time, matching the PDP default.\n */\nexport function resolveCheckoutBillingMode(\n product: Pick<HostProduct, 'subscription' | 'subscriptionOptional'>,\n requested?: string | null,\n): 'payment' | 'subscription' {\n if (!product.subscription) return 'payment'\n if (!product.subscriptionOptional) return 'subscription'\n return requested === 'subscribe' ? 'subscription' : 'payment'\n}\n\n/**\n * Applies a stock delta to one variant, flooring at zero (race-window\n * sales can't drive the display negative), and returns the new variants\n * array — callers persist it plus the `productInventory` denormalization.\n * With `locationId`, the delta lands in that location's bucket and the\n * flat `inventory` re-sums across locations (AGL-286).\n */\nexport function adjustVariantInventory(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n delta: number,\n locationId?: string,\n): ProductVariant[] {\n return (product.variants ?? []).map((variant) => {\n if (variant.id !== variantId || variant.inventory == null) return variant\n if (locationId && variant.inventoryByLocation) {\n const buckets = {\n ...variant.inventoryByLocation,\n [locationId]: Math.max(\n 0,\n Number(variant.inventoryByLocation[locationId] ?? 0) + delta,\n ),\n }\n return {\n ...variant,\n inventoryByLocation: buckets,\n inventory: Object.values(buckets).reduce(\n (sum, count) => sum + Number(count),\n 0,\n ),\n }\n }\n return {\n ...variant,\n inventory: Math.max(0, Number(variant.inventory) + delta),\n }\n })\n}\n\n/**\n * What `adjustVariantInventory` will ACTUALLY move the count by (AGL-2149).\n *\n * The floor in that helper is not a rounding detail, it is a silent absorber. A\n * backorder product (`oversellPolicy: 'backorder'`, which `canPurchase` admits\n * at any stock level) sitting at 0 sells 3: `Math.max(0, 0 + -3)` is 0, so the\n * count does not move — correctly, since inventory is a shelf count and shelves\n * do not go negative — but the ledger row said `-3` and every reversal read it\n * as three units to put back. Cancelling that order handed the merchant 3 units\n * that never existed and a stock badge that offers them for sale.\n *\n * Removing the floor is not the alternative: every reader of `inventory` — the\n * badges, `canPurchase`, `productInventory`, `isLowStock` — assumes it cannot\n * go negative, and a negative count would be a far wider change than the bug.\n * So the clamp stays and the amount it absorbed is recorded instead.\n *\n * Deliberately mirrors `adjustVariantInventory`'s own arithmetic, including\n * which field the clamp applies to in the location-tracked case, so the two\n * cannot disagree about what happened.\n */\nexport function appliedVariantInventoryDelta(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n delta: number,\n locationId?: string,\n): number {\n const variant = (product.variants ?? []).find(\n (item) => item.id === variantId,\n )\n if (!variant || variant.inventory == null) return 0\n if (locationId && variant.inventoryByLocation) {\n const before = Number(variant.inventoryByLocation[locationId] ?? 0)\n return Math.max(0, before + delta) - before\n }\n const before = Number(variant.inventory)\n return Math.max(0, before + delta) - before\n}\n\n/**\n * Moves stock between two locations of a tracked variant (AGL-286).\n * Quantity clamps to what the source location holds; the flat total is\n * unchanged by construction.\n */\nexport function transferVariantInventory(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n fromLocationId: string,\n toLocationId: string,\n quantity: number,\n): ProductVariant[] {\n return (product.variants ?? []).map((variant) => {\n if (variant.id !== variantId || variant.inventory == null) return variant\n const buckets = { ...(variant.inventoryByLocation ?? {}) }\n const available = Math.max(0, Number(buckets[fromLocationId] ?? 0))\n const moved = Math.min(available, Math.max(0, Math.round(quantity)))\n if (moved === 0) return variant\n buckets[fromLocationId] = available - moved\n buckets[toLocationId] = Math.max(0, Number(buckets[toLocationId] ?? 0)) + moved\n return { ...variant, inventoryByLocation: buckets }\n })\n}\n\n/** True when tracked stock is at/below the product's alert threshold. */\nexport function isLowStock(\n product: Pick<HostProduct, 'variants' | 'lowStockThreshold'>,\n): boolean {\n const threshold = product.lowStockThreshold\n if (threshold == null || !(threshold >= 0)) return false\n const total = productInventory(product)\n return total != null && total <= threshold\n}\n\nfunction ruleMatches(product: HostProduct, rule: CollectionRule): boolean {\n const value = rule.value\n switch (rule.field) {\n case 'tag': {\n const tags = product.tags ?? []\n const has = tags.includes(String(value))\n return rule.op === 'neq' ? !has : has\n }\n case 'categoryId': {\n const ids = product.categoryIds ?? []\n const has = ids.includes(String(value))\n return rule.op === 'neq' ? !has : has\n }\n case 'priceUsd': {\n const [min, max] = productPriceRange(product)\n const numeric = Number(value)\n if (!Number.isFinite(numeric)) return false\n if (rule.op === 'lt') return min < numeric\n if (rule.op === 'gt') return max > numeric\n if (rule.op === 'neq') return min !== numeric || max !== numeric\n return min <= numeric && numeric <= max\n }\n case 'name': {\n const name = (product.name ?? '').toLowerCase()\n const needle = String(value).toLowerCase()\n if (rule.op === 'contains') return name.includes(needle)\n if (rule.op === 'neq') return name !== needle\n return name === needle\n }\n case 'type': {\n const matches = product.type === value\n return rule.op === 'neq' ? !matches : matches\n }\n default:\n return false\n }\n}\n\n/**\n * Whether a product's own fields answer a SMART collection's rules — its\n * tags, categories, prices, name and type, and nothing about whether it is\n * on sale. That is the storefront query's scope (`deletedAt == null`,\n * `status == 'active'`), so membership stored on the product never goes stale\n * when a product is archived or brought back.\n *\n * A collection with no rules holds nothing.\n */\n/** What smart-collection membership reads of a product — a draft's too. */\nexport interface MembershipProduct {\n name?: string\n type?: ProductType\n tags?: string[]\n categoryIds?: string[]\n variants?: ReadonlyArray<{ id?: string; priceUsd?: number | null }>\n}\n\nexport function smartCollectionMatches(\n product: MembershipProduct,\n collection: Pick<HostCollection, 'rules' | 'matchAll'>,\n): boolean {\n const rules = collection.rules ?? []\n if (rules.length === 0) return false\n const matcher = (rule: CollectionRule) => ruleMatches(product as HostProduct, rule)\n return collection.matchAll === false\n ? rules.some(matcher)\n : rules.every(matcher)\n}\n\n/**\n * Smart-collection membership: draft/archived/deleted products never\n * match; manual collections answer from productIds.\n */\nexport function matchesCollection(\n product: HostProduct,\n collection: HostCollection,\n productId?: string,\n): boolean {\n if (product.deletedAt || product.status !== 'active') return false\n if (collection.mode === 'manual') {\n return productId != null &&\n (collection.productIds ?? []).includes(productId)\n }\n return smartCollectionMatches(product, collection)\n}\n\n/**\n * The stored membership of a product in the host's SMART collections\n * (AGL-3321): the ids of those whose rules it answers\n * ({@link smartCollectionMatches}), sorted.\n *\n * The storefront reads a smart collection whose rules no query can hold (a\n * NOT, an OR, a name rule, a price over) as `collectionIds array-contains\n * <id>` — so the membership is WRITTEN, by every product writer from the\n * host's smart collections as they stand, and by the collection-membership\n * route for every product whenever a smart collection's rules change or it\n * is deleted. `tools/scripts/backfill-product-collection-ids.mjs` stamps the\n * products written before; the two answer\n * `tools/scripts/lib/product-collection-ids.fixtures.json`.\n */\nexport const PRODUCT_COLLECTION_IDS = 'collectionIds'\n\n/** A smart collection as membership is computed from it. */\nexport interface SmartCollectionRules {\n id: string\n rules?: CollectionRule[]\n matchAll?: boolean\n}\n\nexport function productCollectionIds(\n product: MembershipProduct,\n smartCollections: readonly SmartCollectionRules[],\n): string[] {\n return smartCollections\n .filter((collection) => smartCollectionMatches(product, collection))\n .map((collection) => collection.id)\n .sort()\n}\n\n/**\n * Lifts a legacy Commerce Starter doc (AGL-90: flat name/priceUsd/\n * inventory/imageUrl) into the catalog shape with a single default\n * variant. Already-lifted docs pass through unchanged.\n */\nexport function liftLegacyProduct(\n raw: Partial<HostProduct> & { name?: string },\n): HostProduct {\n if (Array.isArray(raw.variants) && raw.variants.length > 0) {\n return raw as HostProduct\n }\n const priceUsd = Number(raw.priceUsd ?? 0)\n return {\n ...raw,\n name: raw.name ?? 'Product',\n slug: raw.slug || commerceSlug(raw.name ?? 'product'),\n type: raw.type ?? 'physical',\n status: raw.status ?? 'active',\n variants: [\n {\n id: 'default',\n priceUsd: Number.isFinite(priceUsd) ? priceUsd : 0,\n inventory: raw.inventory ?? null,\n },\n ],\n }\n}\n\n/** Actionable error, or null when the product is storable. */\nexport function validateProduct(product: HostProduct): string | null {\n if (!product.name?.trim()) return 'Product name is required'\n if (!product.slug || product.slug !== commerceSlug(product.slug)) {\n return 'Slug must be lowercase letters, numbers, and dashes'\n }\n if (!['physical', 'digital', 'service'].includes(product.type)) {\n return 'Unknown product type'\n }\n if (!['draft', 'active', 'archived'].includes(product.status)) {\n return 'Unknown product status'\n }\n const options = product.options ?? []\n if (options.length > COMMERCE_MAX_OPTIONS) {\n return `At most ${COMMERCE_MAX_OPTIONS} options per product`\n }\n for (const option of options) {\n if (!option.name?.trim()) return 'Option names are required'\n if ((option.values?.length ?? 0) === 0) {\n return `Option \"${option.name}\" needs at least one value`\n }\n if (option.values.length > COMMERCE_MAX_OPTION_VALUES) {\n return `Option \"${option.name}\" has too many values`\n }\n if (new Set(option.values).size !== option.values.length) {\n return `Option \"${option.name}\" has duplicate values`\n }\n }\n // A variant keys its selections by option name, so two options with one\n // name cannot both be selected (AGL-3066).\n if (new Set(options.map((option) => option.name.trim())).size !== options.length) {\n return 'Each option needs its own name'\n }\n const modifierProblem = modifierGroupsProblem(product.modifierGroups)\n if (modifierProblem) return modifierProblem\n const variants = product.variants ?? []\n if (variants.length === 0) return 'Products need at least one variant'\n if (variants.length > COMMERCE_MAX_VARIANTS) {\n return `At most ${COMMERCE_MAX_VARIANTS} variants per product`\n }\n const ids = new Set<string>()\n const skus = new Set<string>()\n for (const variant of variants) {\n if (!variant.id) return 'Variants need stable ids'\n if (ids.has(variant.id)) return 'Variant ids must be unique'\n ids.add(variant.id)\n if (variant.priceUsd === undefined || variant.priceUsd === null) {\n return 'Set a price for every variant'\n }\n const price = Number(variant.priceUsd)\n if (!Number.isFinite(price) || price < 0) {\n return 'Variant prices must be zero or more'\n }\n if (price > COMMERCE_MAX_PRICE_USD) {\n return `Prices are capped at $${COMMERCE_MAX_PRICE_USD}`\n }\n if (variant.sku) {\n if (skus.has(variant.sku)) return 'Variant SKUs must be unique'\n skus.add(variant.sku)\n }\n if (\n variant.compareAtPriceUsd != null &&\n Number(variant.compareAtPriceUsd) <= price\n ) {\n return 'Compare-at price must exceed the price'\n }\n }\n return null\n}\n\n/** Actionable error, or null when the collection is storable. */\nexport function validateCollection(collection: HostCollection): string | null {\n if (!collection.name?.trim()) return 'Collection name is required'\n if (\n !collection.slug ||\n collection.slug !== commerceSlug(collection.slug)\n ) {\n return 'Slug must be lowercase letters, numbers, and dashes'\n }\n if (collection.mode !== 'manual' && collection.mode !== 'smart') {\n return 'Unknown collection mode'\n }\n if (collection.mode === 'smart') {\n const rules = collection.rules ?? []\n if (rules.length === 0) return 'Smart collections need at least one rule'\n for (const rule of rules) {\n if (\n !['tag', 'categoryId', 'priceUsd', 'name', 'type'].includes(rule.field)\n ) {\n return 'Unknown rule field'\n }\n if (!['eq', 'neq', 'lt', 'gt', 'contains'].includes(rule.op)) {\n return 'Unknown rule operator'\n }\n if (rule.value === '' || rule.value == null) {\n return 'Rule values are required'\n }\n }\n }\n return null\n}\n"],"names":["modifierGroupsProblem","nameSearchFields","draftProductChannelFacts","COMMERCE_MAX_PRICE_USD","productParcelDimensions","product","facts","shipping","side","value","number","Number","isFinite","Math","min","lengthCm","widthCm","heightCm","undefined","draftProductShippingFacts","normalizeProductShippingFacts","typed","String","trim","toUpperCase","test","originCountry","raw","round","hsCode","replace","slice","Object","keys","length","normalizePostalAddress","address","field","text","country","registersWithinCap","registers","cap","createdMs","r","createdAt","toMillis","seconds","ranked","sort","a","b","$id","localeCompare","Set","max","map","COMMERCE_MAX_VARIANTS","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_OPTION_VALUES","COMMERCE_SLUG_MAX_LENGTH","commerceSlug","name","normalize","toLowerCase","productSearchFields","flatten","read","variants","variant","filter","Boolean","skus","sku","barcodes","barcode","priceFromCents","productPriceFromCents","productPriceRange","productStockFields","inventory","productInventory","soldOut","oversellPolicy","expandVariantMatrix","options","usable","option","values","combos","next","combo","push","renameProductOptions","names","current","index","size","selections","axisOf","optionAxesOf","moved","changed","key","entries","axis","get","Map","taken","forEach","prototype","hasOwnProperty","call","has","set","add","findIndex","includes","variantHasPrice","price","priceUsd","productPriceMissing","some","productCopyPatch","patch","channel","description","Array","isArray","tags","tag","categoryIds","seoTitle","seoDescription","seo","title","optionNames","every","assign","unpricedProductDraft","proposal","takenSlugs","nowMs","base","slug","suffix","seen","id","toString","type","status","createdAtMs","updatedAtMs","findVariant","find","prices","total","canPurchase","variantId","quantity","item","stockShortfall","available","stockTrackingApplies","subscription","subscriptionOptional","resolveCheckoutBillingMode","requested","adjustVariantInventory","delta","locationId","inventoryByLocation","buckets","reduce","sum","count","appliedVariantInventoryDelta","before","transferVariantInventory","fromLocationId","toLocationId","isLowStock","threshold","lowStockThreshold","ruleMatches","rule","op","ids","numeric","needle","matches","smartCollectionMatches","collection","rules","matcher","matchAll","matchesCollection","productId","deletedAt","mode","productIds","PRODUCT_COLLECTION_IDS","productCollectionIds","smartCollections","liftLegacyProduct","validateProduct","modifierProblem","modifierGroups","compareAtPriceUsd","validateCollection"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAmC,yBAAqB;AACtF,SAASC,gBAAgB,QAAQ,qCAAoC;AACrE,SAASC,wBAAwB,QAAkC,uBAAmB;AAEtF;;;;;;;;CAQC,GAED;;;;;;;;;;;;;;;;;CAiBC,GACD,uCAAuC,GACvC,OAAO,MAAMC,yBAAyB,MAAK;AAqE3C,0EAA0E,GAC1E,OAAO,SAASC,wBACdC,OAAyD;IAEzD,MAAMC,QAAQD,2BAAAA,QAASE,QAAQ;IAC/B,MAAMC,OAAO,CAACC;QACZ,MAAMC,SAASC,OAAOF;QACtB,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIG,KAAKC,GAAG,CAACJ,QAAQ,OAAO;IACzE;IACA,MAAMK,WAAWP,KAAKF,yBAAAA,MAAOS,QAAQ;IACrC,MAAMC,UAAUR,KAAKF,yBAAAA,MAAOU,OAAO;IACnC,MAAMC,WAAWT,KAAKF,yBAAAA,MAAOW,QAAQ;IACrC,OAAOF,YAAYC,WAAWC,WAAW;QAAEF;QAAUC;QAASC;IAAS,IAAIC;AAC7E;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,0BAA0BV,KAAc;;IACtD,MAAMH,QAAQc,8BAA8BX;IAC5C,MAAMY,QAAQC,eAAQb,yBAAD,AAACA,KAA0C,CAAC,gBAAgB,mBAAI,IAClFc,IAAI,GACJC,WAAW;IACd,IAAI,UAAUC,IAAI,CAACJ,QAAQ,OAAO,aAAKf;QAAOoB,eAAeL;;IAC7D,OAAOf;AACT;AAEA,0EAA0E,GAC1E,OAAO,SAASc,8BAA8BX,KAAc;QAUpCkB,aAEOA;IAX7B,IAAI,CAAClB,SAAS,OAAOA,UAAU,UAAU,OAAOS;IAChD,MAAMS,MAAMlB;IACZ,MAAMH,QAA8B,CAAC;IACrC,KAAK,MAAME,QAAQ;QAAC;QAAY;QAAW;KAAW,CAAW;QAC/D,MAAME,SAASC,OAAOgB,GAAG,CAACnB,KAAK;QAC/B,IAAImB,GAAG,CAACnB,KAAK,KAAK,QAAQmB,GAAG,CAACnB,KAAK,KAAKU,aAAaP,OAAOC,QAAQ,CAACF,WAAWA,SAAS,GAAG;YAC1FJ,KAAK,CAACE,KAAK,GAAGK,KAAKC,GAAG,CAACD,KAAKe,KAAK,CAAClB,SAAS,MAAM,IAAI;QACvD;IACF;IACA,MAAMmB,SAASP,QAAOK,cAAAA,GAAG,CAAC,SAAS,YAAbA,cAAiB,IAAIG,OAAO,CAAC,YAAY,IAAIC,KAAK,CAAC,GAAG;IAC5E,IAAIF,QAAQvB,MAAMuB,MAAM,GAAGA;IAC3B,MAAMH,gBAAgBJ,QAAOK,qBAAAA,GAAG,CAAC,gBAAgB,YAApBA,qBAAwB,IAAIJ,IAAI,GAAGC,WAAW;IAC3E,IAAI,aAAaC,IAAI,CAACC,gBAAgBpB,MAAMoB,aAAa,GAAGA;IAC5D,OAAOM,OAAOC,IAAI,CAAC3B,OAAO4B,MAAM,GAAG5B,QAAQY;AAC7C;AAEA,2EAA2E,GAC3E,OAAO,SAASiB,uBAAuB1B,KAAc;QAQ5BkB;IAPvB,IAAI,CAAClB,SAAS,OAAOA,UAAU,UAAU,OAAOS;IAChD,MAAMS,MAAMlB;IACZ,MAAM2B,UAAyB,CAAC;IAChC,KAAK,MAAMC,SAAS;QAAC;QAAS;QAAS;QAAQ;QAAS;QAAc;KAAQ,CAAW;YACnEV;QAApB,MAAMW,OAAOhB,QAAOK,aAAAA,GAAG,CAACU,MAAM,YAAVV,aAAc,IAAIJ,IAAI,GAAGQ,KAAK,CAAC,GAAG;QACtD,IAAIO,MAAMF,OAAO,CAACC,MAAM,GAAGC;IAC7B;IACA,MAAMC,UAAUjB,QAAOK,eAAAA,GAAG,CAAC,UAAU,YAAdA,eAAkB,IAAIJ,IAAI,GAAGC,WAAW;IAC/D,IAAI,aAAaC,IAAI,CAACc,UAAUH,QAAQG,OAAO,GAAGA;IAClD,OAAOP,OAAOC,IAAI,CAACG,SAASF,MAAM,GAAGE,UAAUlB;AACjD;AAeA;;;;;;CAMC,GACD,OAAO,SAASsB,mBACdC,SAAkD,EAClDC,GAAW;IAEX,MAAMC,YAAY,CAACC;YACjBA;YAAAA,uBAAAA,cAA6BA;gBAA7BA,iBAAAA,eAAAA,EAAEC,SAAS,sBAAXD,wBAAAA,aAAaE,QAAQ,qBAArBF,2BAAAA,kCAA6BA,gBAAAA,EAAEC,SAAS,qBAAXD,cAAaG,OAAO,YAAjDH,OAAqD;;IACvD,MAAMI,SAAS;WAAIP;KAAU,CAACQ,IAAI,CAChC,CAACC,GAAGC,IAAMR,UAAUO,KAAKP,UAAUQ,MAAMD,EAAEE,GAAG,CAACC,aAAa,CAACF,EAAEC,GAAG;IAEpE,OAAO,IAAIE,IAAIN,OAAOjB,KAAK,CAAC,GAAGlB,KAAK0C,GAAG,CAAC,GAAGb,MAAMc,GAAG,CAAC,CAACZ,IAAMA,EAAEQ,GAAG;AACnE;AA8JA,kDAAkD,GAClD,OAAO,MAAMK,wBAAwB,IAAG;AACxC,OAAO,MAAMC,uBAAuB,EAAC;AACrC,OAAO,MAAMC,6BAA6B,GAAE;AAC5C,OAAO,MAAMC,2BAA2B,GAAE;AAE1C,kEAAkE,GAClE,OAAO,SAASC,aAAaC,IAAY;IACvC,OAAOxC,OAAOwC,eAAAA,OAAQ,IACnBC,SAAS,CAAC,QACVjC,OAAO,CAAC,UAAU,IAClBkC,WAAW,GACXlC,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,YAAY,IACpBC,KAAK,CAAC,GAAG6B;AACd;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CC,GACD,OAAO,SAASK,oBAAoB5D,OAGnC;IASC,MAAM6D,UAAU,CAACC;YAEZ9D;eAFsE;eACtE,IAAIiD,IACL,EAACjD,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EACpBmD,GAAG,CAAC,CAACa;oBAAaF;uBAAD,EAACA,QAAAA,KAAKE,oBAALF,QAAiB,IAAI5C,IAAI,GAAGyC,WAAW;eACzDM,MAAM,CAACC;SAEb;;IACD,MAAMC,OAAON,QAAQ,CAACG,UAAYA,QAAQI,GAAG;IAC7C,MAAMC,WAAWR,QAAQ,CAACG,UAAYA,QAAQM,OAAO;IACrD,OAAO,aACF1E,iBAAiBI,QAAQyD,IAAI,GAC5BU,KAAKtC,MAAM,GAAG;QAAEsC;IAAK,IAAI,CAAC,GAC1BE,SAASxC,MAAM,GAAG;QAAEwC;IAAS,IAAI,CAAC,GAClCrE,QAAQ+D,QAAQ,GAChB;QAAEQ,gBAAgBC,sBAAsB;YAAET,UAAU/D,QAAQ+D,QAAQ;QAAC;IAAG,IACxE,CAAC;AAET;AAEA;;;;;CAKC,GACD,OAAO,SAASS,sBACdxE,OAAsC;IAEtC,OAAOQ,KAAKe,KAAK,CAACkD,kBAAkBzE,QAAQ,CAAC,EAAE,GAAG;AACpD;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAAS0E,mBACd1E,OAAyD;IAEzD,MAAM2E,YAAYC,iBAAiB5E;IACnC,OAAO;QACL2E;QACAE,SAASF,aAAa,QAAQA,aAAa,KAAK3E,QAAQ8E,cAAc,KAAK;IAC7E;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASC,oBACdC,OAAoC;IAEpC,MAAMC,SAAS,CAACD,kBAAAA,UAAW,EAAE,EAAEf,MAAM,CACnC,CAACiB;;YAA2BA;eAAhBA,OAAOzB,IAAI,IAAI,UAACyB,iBAAAA,OAAOC,MAAM,qBAAbD,eAAerD,MAAM,mBAAI,KAAK;;IAE5D,IAAIoD,OAAOpD,MAAM,KAAK,GAAG,OAAO;QAAC,CAAC;KAAE;IACpC,IAAIuD,SAAwC;QAAC,CAAC;KAAE;IAChD,KAAK,MAAMF,UAAUD,OAAQ;QAC3B,MAAMI,OAAsC,EAAE;QAC9C,KAAK,MAAMC,SAASF,OAAQ;YAC1B,KAAK,MAAMhF,SAAS8E,OAAOC,MAAM,CAAE;gBACjCE,KAAKE,IAAI,CAAC,aAAKD;oBAAO,CAACJ,OAAOzB,IAAI,CAAC,EAAErD;;YACvC;QACF;QACAgF,SAASC;QACT,IAAID,OAAOvD,MAAM,GAAGuB,uBAAuB;YACzC,OAAOgC,OAAO1D,KAAK,CAAC,GAAG0B;QACzB;IACF;IACA,OAAOgC;AACT;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASI,qBACdxF,OAAkD,EAClDyF,KAA+C;QAE/BzF,kBAOEA;IAPlB,MAAM0F,WAAU1F,mBAAAA,QAAQgF,OAAO,YAAfhF,mBAAmB,EAAE;IACrC,MAAMgF,UAAUU,QAAQvC,GAAG,CAAC,CAAC+B,QAAQS;QACnC,MAAMlC,OAAOgC,KAAK,CAACE,MAAM;QACzB,OAAO,OAAOlC,SAAS,WAAW,aAAKyB;YAAQzB;aAASyB;IAC1D;IACA,MAAMG,OAAOL,QAAQ7B,GAAG,CAAC,CAAC+B,SAAWA,OAAOzB,IAAI;IAChD,IAAI,IAAIR,IAAIoC,MAAMO,IAAI,KAAKP,KAAKxD,MAAM,EAAE,OAAO;QAAEmD;QAASjB,UAAU/D,QAAQ+D,QAAQ;IAAC;IACrF,MAAMA,WAAW,EAAC/D,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EAAEmD,GAAG,CAAC,CAACa;QAC7C,MAAM6B,aAAa7B,QAAQgB,OAAO;QAClC,IAAI,CAACa,YAAY,OAAO7B;QACxB,MAAM8B,SAASC,aAAaL,SAASG;QACrC,MAAMG,QAAgC,CAAC;QACvC,IAAIC,UAAU;QACd,KAAK,MAAM,CAACC,KAAK9F,MAAM,IAAIuB,OAAOwE,OAAO,CAACN,YAAa;YACrD,MAAMO,OAAON,OAAOO,GAAG,CAACH;YACxB,MAAMzC,OAAO2C,SAASvF,YAAYqF,MAAMb,IAAI,CAACe,KAAK;YAClD,IAAI3C,SAASyC,KAAKD,UAAU;YAC5BD,KAAK,CAACvC,KAAK,GAAGrD;QAChB;QACA,OAAO6F,UAAU,aAAKjC;YAASgB,SAASgB;aAAUhC;IACpD;IACA,OAAO;QAAEgB;QAASjB;IAAS;AAC7B;AAEA;;;;;CAKC,GACD,SAASgC,aACPf,OAAiC,EACjCa,UAA4C;IAE5C,MAAMC,SAAS,IAAIQ;IACnB,MAAMC,QAAQ,IAAItD;IAClB+B,QAAQwB,OAAO,CAAC,CAACtB,QAAQS;QACvB,IAAIhE,OAAO8E,SAAS,CAACC,cAAc,CAACC,IAAI,CAACd,YAAYX,OAAOzB,IAAI,KAAK,CAACqC,OAAOc,GAAG,CAAC1B,OAAOzB,IAAI,GAAG;YAC7FqC,OAAOe,GAAG,CAAC3B,OAAOzB,IAAI,EAAEkC;YACxBY,MAAMO,GAAG,CAACnB;QACZ;IACF;IACA,KAAK,MAAM,CAACO,KAAK9F,MAAM,IAAIuB,OAAOwE,OAAO,CAACN,YAAa;QACrD,IAAIC,OAAOc,GAAG,CAACV,MAAM;QACrB,MAAME,OAAOpB,QAAQ+B,SAAS,CAC5B,CAAC7B,QAAQS;gBAAgCT;mBAAtB,CAACqB,MAAMK,GAAG,CAACjB,UAAU,EAACT,iBAAAA,OAAOC,MAAM,YAAbD,iBAAiB,EAAE,EAAE8B,QAAQ,CAAC5G;;QAEzE,IAAIgG,SAAS,CAAC,GAAG;QACjBN,OAAOe,GAAG,CAACX,KAAKE;QAChBG,MAAMO,GAAG,CAACV;IACZ;IACA,OAAON;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,SAASmB,gBAAgBjD,OAAkC;IAChE,MAAMkD,QAASlD,2BAAD,AAACA,QAAuDmD,QAAQ;IAC9E,OAAO,OAAOD,UAAU,YAAY5G,OAAOC,QAAQ,CAAC2G,UAAUA,SAAS;AACzE;AAEA,2EAA2E,GAC3E,OAAO,SAASE,oBAAoBpH,OAAyC;QACnEA;IAAR,OAAO,EAACA,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EAAEqH,IAAI,CAAC,CAACrD,UAAY,CAACiD,gBAAgBjD;AACrE;AAuBA;;;;;;CAMC,GACD,OAAO,SAASsD,iBACdtH,OACuC,EACvCmF,MAAyB;QA+BTnF;QACFmF;IA5Bd,MAAMoC,QAEF,CAAC;IACL,IAAIpC,OAAOjF,QAAQ,IAAI,OAAOiF,OAAOjF,QAAQ,KAAK,UAAU;YAGzCY;QAFjB,yEAAyE;QACzE,mEAAmE;QACnEyG,MAAMrH,QAAQ,IAAGY,6BAAAA,0BAA0B,aAAKd,QAAQE,QAAQ,EAAKiF,OAAOjF,QAAQ,cAAnEY,6BAA0E,CAAC;IAC9F;IACA,IAAIqE,OAAOqC,OAAO,IAAI,OAAOrC,OAAOqC,OAAO,KAAK,UAAU;YAGxC3H;QAFhB,wEAAwE;QACxE,gEAAgE;QAChE0H,MAAMC,OAAO,IAAG3H,4BAAAA,yBAAyB,aAAKG,QAAQwH,OAAO,EAAKrC,OAAOqC,OAAO,cAAhE3H,4BAAuE,CAAC;IAC1F;IACA,IAAI,OAAOsF,OAAOsC,WAAW,KAAK,UAAUF,MAAME,WAAW,GAAGtC,OAAOsC,WAAW;IAClF,IAAIC,MAAMC,OAAO,CAACxC,OAAOyC,IAAI,GAAG;QAC9BL,MAAMK,IAAI,GAAG;eAAI,IAAI3E,IAAIkC,OAAOyC,IAAI,CAACzE,GAAG,CAAC,CAAC0E,MAAQA,IAAI3G,IAAI,IAAI+C,MAAM,CAACC;SAAU;IACjF;IACA,IAAIwD,MAAMC,OAAO,CAACxC,OAAO2C,WAAW,GAAG;QACrCP,MAAMO,WAAW,GAAG;eAAI,IAAI7E,IAAIkC,OAAO2C,WAAW,CAAC7D,MAAM,CAACC;SAAU;IACtE;IACA,IAAI,OAAOiB,OAAO4C,QAAQ,KAAK,YAAY,OAAO5C,OAAO6C,cAAc,KAAK,UAAU;QACpFT,MAAMU,GAAG,GAAG,aACPjI,QAAQiI,GAAG,EACV,OAAO9C,OAAO4C,QAAQ,KAAK,WAAW;YAAEG,OAAO/C,OAAO4C,QAAQ;QAAC,IAAI,CAAC,GACpE,OAAO5C,OAAO6C,cAAc,KAAK,WAAW;YAAEP,aAAatC,OAAO6C,cAAc;QAAC,IAAI,CAAC;IAE9F;IACA,MAAMhD,WAAUhF,mBAAAA,QAAQgF,OAAO,YAAfhF,mBAAmB,EAAE;IACrC,MAAMyF,SAAQN,sBAAAA,OAAOgD,WAAW,qBAAlBhD,oBAAoBhC,GAAG,CAAC,CAACM,OAASA,KAAKvC,IAAI;IACzD,IACEuE,SACAT,QAAQnD,MAAM,GAAG,KACjB4D,MAAM5D,MAAM,KAAKmD,QAAQnD,MAAM,IAC/B4D,MAAM2C,KAAK,CAAClE,YACZ,IAAIjB,IAAIwC,OAAOG,IAAI,KAAKH,MAAM5D,MAAM,IACpC4D,MAAM4B,IAAI,CAAC,CAAC5D,MAAMkC,QAAUlC,SAASuB,OAAO,CAACW,MAAM,CAAClC,IAAI,GACxD;QACA9B,OAAO0G,MAAM,CAACd,OAAO/B,qBAAqBxF,SAASyF;IACrD;IACA,OAAO8B;AACT;AAkBA;;;;;;;;CAQC,GACD,OAAO,SAASe,qBACdC,QAAyB,EACzBC,UAA+B,EAC/BC,KAAa;IAEb,MAAMhF,OAAO8E,SAAS9E,IAAI,CAACvC,IAAI,GAAGQ,KAAK,CAAC,GAAG;IAC3C,MAAMgH,OAAOlF,aAAaC,SAAS;IACnC,IAAIkF,OAAOD;IACX,IAAK,IAAIE,SAAS,GAAGJ,WAAW5B,GAAG,CAAC+B,OAAOC,UAAU,EAAGD,OAAO,GAAGD,KAAK,CAAC,EAAEE,QAAQ;IAClF,MAAMC,OAAO,IAAI5F;IACjB,MAAM+B,UAAUuD,SAASvD,OAAO,CAC7B7B,GAAG,CAAC,CAAC+B,SAAY,CAAA;YAChBzB,MAAMyB,OAAOzB,IAAI,CAACvC,IAAI;YACtBiE,QAAQ;mBAAI,IAAIlC,IAAIiC,OAAOC,MAAM,CAAChC,GAAG,CAAC,CAAC/C,QAAUA,MAAMc,IAAI,IAAI+C,MAAM,CAACC;aAAU,CAACxC,KAAK,CACpF,GACA4B;QAEJ,CAAA,GACCW,MAAM,CAAC,CAACiB;QACP,IAAI,CAACA,OAAOzB,IAAI,IAAI,CAACyB,OAAOC,MAAM,CAACtD,MAAM,IAAIgH,KAAKjC,GAAG,CAAC1B,OAAOzB,IAAI,GAAG,OAAO;QAC3EoF,KAAK/B,GAAG,CAAC5B,OAAOzB,IAAI;QACpB,OAAO;IACT,GACC/B,KAAK,CAAC,GAAG2B;IACZ,MAAMU,WAAWiB,QAAQnD,MAAM,GAC3BkD,oBAAoBC,SAAS7B,GAAG,CAAC,CAACmC,OAAOK,QAAW,CAAA;YAClDmD,IAAI,CAAC,CAAC,EAAEL,MAAMM,QAAQ,CAAC,MAAMpD,OAAO;YACpCX,SAASM;QACX,CAAA,KACA;QAAC;YAAEwD,IAAI;QAAU;KAAE;IACvB,MAAMf,WAAWQ,SAASR,QAAQ,CAAC7G,IAAI;IACvC,MAAM8G,iBAAiBO,SAASP,cAAc,CAAC9G,IAAI;IACnD,OAAO,aAIF0C,oBAAoB;QAAEH;QAAMM,UAAUA;IAA6B,IACnEW,mBAAmB;QAAEX,UAAUA;IAA6B;QAC/D4E;QACAK,MAAMT,SAASS,IAAI;QACnBC,QAAQ;QACRxB,aAAac,SAASd,WAAW;QACjCG,MAAM;eAAI,IAAI3E,IAAIsF,SAASX,IAAI,CAACzE,GAAG,CAAC,CAAC0E,MAAQA,IAAI3G,IAAI,IAAI+C,MAAM,CAACC;SAAU;OACtEc,QAAQnD,MAAM,GAAG;QAAEmD;IAAQ,IAAI,CAAC;QACpCjB;OACIgE,YAAYC,iBACZ;QAAEC,KAAK,aAAMF,WAAW;YAAEG,OAAOH;QAAS,IAAI,CAAC,GAAQC,iBAAiB;YAAEP,aAAaO;QAAe,IAAI,CAAC;IAAK,IAChH,CAAC;QACLkB,aAAaT;QACbU,aAAaV;;AAEjB;AAEA,sEAAsE,GACtE,OAAO,SAASW,YACdpJ,OAAsC,EACtC6F,UAAkC;QAE3B7F;IAAP,QAAOA,oBAAAA,QAAQ+D,QAAQ,qBAAhB/D,kBAAkBqJ,IAAI,CAAC,CAACrF;YACbA;QAAhB,MAAMgB,WAAUhB,mBAAAA,QAAQgB,OAAO,YAAfhB,mBAAmB,CAAC;QACpC,MAAMpC,OAAO,IAAIqB,IAAI;eAAItB,OAAOC,IAAI,CAACoD;eAAarD,OAAOC,IAAI,CAACiE;SAAY;QAC1E,KAAK,MAAMK,OAAOtE,KAAM;YACtB,IAAIoD,OAAO,CAACkB,IAAI,KAAKL,UAAU,CAACK,IAAI,EAAE,OAAO;QAC/C;QACA,OAAO;IACT;AACF;AAEA,kEAAkE,GAClE,OAAO,SAASzB,kBACdzE,OAAsC;QAEtBA;IAAhB,MAAMsJ,SAAS,EAACtJ,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EACnCmD,GAAG,CAAC,CAACa,UAAY1D,OAAO0D,QAAQmD,QAAQ,GACxClD,MAAM,CAAC,CAACiD,QAAU5G,OAAOC,QAAQ,CAAC2G,UAAUA,SAAS;IACxD,IAAIoC,OAAOzH,MAAM,KAAK,GAAG,OAAO;QAAC;QAAG;KAAE;IACtC,OAAO;QAACrB,KAAKC,GAAG,IAAI6I;QAAS9I,KAAK0C,GAAG,IAAIoG;KAAQ;AACnD;AAEA,oEAAoE,GACpE,OAAO,SAAS1E,iBACd5E,OAAsC;QAGhBA;IADtB,IAAIuJ,QAAuB;IAC3B,KAAK,MAAMvF,YAAWhE,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,CAAE;QAC5C,IAAIgE,QAAQW,SAAS,IAAI,MAAM;QAC/B4E,QAAQ,CAACA,gBAAAA,QAAS,KAAKjJ,OAAO0D,QAAQW,SAAS;IACjD;IACA,OAAO4E;AACT;AAuDA;;;CAGC,GACD,OAAO,SAASC,YACdxJ,OAAyD,EACzDyJ,SAA6B,EAC7BC,WAAW,CAAC;QAGR1J,mBACAA;IAFJ,MAAMgE,UAAUyF,aACZzJ,oBAAAA,QAAQ+D,QAAQ,qBAAhB/D,kBAAkBqJ,IAAI,CAAC,CAACM,OAASA,KAAKb,EAAE,KAAKW,cAC7CzJ,qBAAAA,QAAQ+D,QAAQ,qBAAhB/D,kBAAkB,CAAC,EAAE;IACzB,IAAI,CAACgE,SAAS,OAAO;IACrB,IAAIA,QAAQW,SAAS,IAAI,MAAM,OAAO;IACtC,IAAI3E,QAAQ8E,cAAc,KAAK,aAAa,OAAO;IACnD,OAAOxE,OAAO0D,QAAQW,SAAS,KAAK+E;AACtC;AAyCA,OAAO,SAASE,eACd5J,OAAyD,EACzDyJ,SAA6B,EAC7BC,WAAW,CAAC;QAIR1J,mBACAA;IAHJ,IAAIwJ,YAAYxJ,SAASyJ,WAAWC,WAAW,OAAO;IACtD,MAAM1F,UAAUyF,aACZzJ,oBAAAA,QAAQ+D,QAAQ,qBAAhB/D,kBAAkBqJ,IAAI,CAAC,CAACM,OAASA,KAAKb,EAAE,KAAKW,cAC7CzJ,qBAAAA,QAAQ+D,QAAQ,qBAAhB/D,kBAAkB,CAAC,EAAE;IACzB,6EAA6E;IAC7E,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1E,+CAA+C;IAC/C,IAAI,CAACgE,WAAWA,QAAQW,SAAS,IAAI,MAAM,OAAO;IAClD,OAAO;QAAEkF,WAAWrJ,KAAK0C,GAAG,CAAC,GAAG1C,KAAKe,KAAK,CAACjB,OAAO0D,QAAQW,SAAS,KAAK;IAAI;AAC9E;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,SAASmF,qBACd9J,OAA4E;IAE5E,OACE,CAACA,QAAQ+J,YAAY,IACrB/J,QAAQgK,oBAAoB,KAAK,QACjChK,QAAQgJ,IAAI,KAAK;AAErB;AAKA;;;;;;;;CAQC,GACD,OAAO,SAASiB,2BACdjK,OAAmE,EACnEkK,SAAyB;IAEzB,IAAI,CAAClK,QAAQ+J,YAAY,EAAE,OAAO;IAClC,IAAI,CAAC/J,QAAQgK,oBAAoB,EAAE,OAAO;IAC1C,OAAOE,cAAc,cAAc,iBAAiB;AACtD;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,uBACdnK,OAAsC,EACtCyJ,SAAiB,EACjBW,KAAa,EACbC,UAAmB;QAEXrK;IAAR,OAAO,EAACA,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EAAEmD,GAAG,CAAC,CAACa;QACnC,IAAIA,QAAQ8E,EAAE,KAAKW,aAAazF,QAAQW,SAAS,IAAI,MAAM,OAAOX;QAClE,IAAIqG,cAAcrG,QAAQsG,mBAAmB,EAAE;gBAKlCtG;YAJX,MAAMuG,UAAU,aACXvG,QAAQsG,mBAAmB;gBAC9B,CAACD,WAAW,EAAE7J,KAAK0C,GAAG,CACpB,GACA5C,QAAO0D,0CAAAA,QAAQsG,mBAAmB,CAACD,WAAW,YAAvCrG,0CAA2C,KAAKoG;;YAG3D,OAAO,aACFpG;gBACHsG,qBAAqBC;gBACrB5F,WAAWhD,OAAOwD,MAAM,CAACoF,SAASC,MAAM,CACtC,CAACC,KAAKC,QAAUD,MAAMnK,OAAOoK,QAC7B;;QAGN;QACA,OAAO,aACF1G;YACHW,WAAWnE,KAAK0C,GAAG,CAAC,GAAG5C,OAAO0D,QAAQW,SAAS,IAAIyF;;IAEvD;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASO,6BACd3K,OAAsC,EACtCyJ,SAAiB,EACjBW,KAAa,EACbC,UAAmB;QAEFrK;IAAjB,MAAMgE,UAAU,EAAChE,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EAAEqJ,IAAI,CAC3C,CAACM,OAASA,KAAKb,EAAE,KAAKW;IAExB,IAAI,CAACzF,WAAWA,QAAQW,SAAS,IAAI,MAAM,OAAO;IAClD,IAAI0F,cAAcrG,QAAQsG,mBAAmB,EAAE;YACvBtG;QAAtB,MAAM4G,SAAStK,QAAO0D,0CAAAA,QAAQsG,mBAAmB,CAACD,WAAW,YAAvCrG,0CAA2C;QACjE,OAAOxD,KAAK0C,GAAG,CAAC,GAAG0H,SAASR,SAASQ;IACvC;IACA,MAAMA,SAAStK,OAAO0D,QAAQW,SAAS;IACvC,OAAOnE,KAAK0C,GAAG,CAAC,GAAG0H,SAASR,SAASQ;AACvC;AAEA;;;;CAIC,GACD,OAAO,SAASC,yBACd7K,OAAsC,EACtCyJ,SAAiB,EACjBqB,cAAsB,EACtBC,YAAoB,EACpBrB,QAAgB;QAER1J;IAAR,OAAO,EAACA,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE,EAAEmD,GAAG,CAAC,CAACa;YAEbA,8BACeuG,yBAIMA;QAN3C,IAAIvG,QAAQ8E,EAAE,KAAKW,aAAazF,QAAQW,SAAS,IAAI,MAAM,OAAOX;QAClE,MAAMuG,UAAU,cAAMvG,+BAAAA,QAAQsG,mBAAmB,YAA3BtG,+BAA+B,CAAC;QACtD,MAAM6F,YAAYrJ,KAAK0C,GAAG,CAAC,GAAG5C,QAAOiK,0BAAAA,OAAO,CAACO,eAAe,YAAvBP,0BAA2B;QAChE,MAAMvE,QAAQxF,KAAKC,GAAG,CAACoJ,WAAWrJ,KAAK0C,GAAG,CAAC,GAAG1C,KAAKe,KAAK,CAACmI;QACzD,IAAI1D,UAAU,GAAG,OAAOhC;QACxBuG,OAAO,CAACO,eAAe,GAAGjB,YAAY7D;QACtCuE,OAAO,CAACQ,aAAa,GAAGvK,KAAK0C,GAAG,CAAC,GAAG5C,QAAOiK,wBAAAA,OAAO,CAACQ,aAAa,YAArBR,wBAAyB,MAAMvE;QAC1E,OAAO,aAAKhC;YAASsG,qBAAqBC;;IAC5C;AACF;AAEA,uEAAuE,GACvE,OAAO,SAASS,WACdhL,OAA4D;IAE5D,MAAMiL,YAAYjL,QAAQkL,iBAAiB;IAC3C,IAAID,aAAa,QAAQ,CAAEA,CAAAA,aAAa,CAAA,GAAI,OAAO;IACnD,MAAM1B,QAAQ3E,iBAAiB5E;IAC/B,OAAOuJ,SAAS,QAAQA,SAAS0B;AACnC;AAEA,SAASE,YAAYnL,OAAoB,EAAEoL,IAAoB;IAC7D,MAAMhL,QAAQgL,KAAKhL,KAAK;IACxB,OAAQgL,KAAKpJ,KAAK;QAChB,KAAK;YAAO;oBACGhC;gBAAb,MAAM4H,QAAO5H,gBAAAA,QAAQ4H,IAAI,YAAZ5H,gBAAgB,EAAE;gBAC/B,MAAM4G,MAAMgB,KAAKZ,QAAQ,CAAC/F,OAAOb;gBACjC,OAAOgL,KAAKC,EAAE,KAAK,QAAQ,CAACzE,MAAMA;YACpC;QACA,KAAK;YAAc;oBACL5G;gBAAZ,MAAMsL,OAAMtL,uBAAAA,QAAQ8H,WAAW,YAAnB9H,uBAAuB,EAAE;gBACrC,MAAM4G,MAAM0E,IAAItE,QAAQ,CAAC/F,OAAOb;gBAChC,OAAOgL,KAAKC,EAAE,KAAK,QAAQ,CAACzE,MAAMA;YACpC;QACA,KAAK;YAAY;gBACf,MAAM,CAACnG,KAAKyC,IAAI,GAAGuB,kBAAkBzE;gBACrC,MAAMuL,UAAUjL,OAAOF;gBACvB,IAAI,CAACE,OAAOC,QAAQ,CAACgL,UAAU,OAAO;gBACtC,IAAIH,KAAKC,EAAE,KAAK,MAAM,OAAO5K,MAAM8K;gBACnC,IAAIH,KAAKC,EAAE,KAAK,MAAM,OAAOnI,MAAMqI;gBACnC,IAAIH,KAAKC,EAAE,KAAK,OAAO,OAAO5K,QAAQ8K,WAAWrI,QAAQqI;gBACzD,OAAO9K,OAAO8K,WAAWA,WAAWrI;YACtC;QACA,KAAK;YAAQ;oBACGlD;gBAAd,MAAMyD,OAAO,EAACzD,gBAAAA,QAAQyD,IAAI,YAAZzD,gBAAgB,IAAI2D,WAAW;gBAC7C,MAAM6H,SAASvK,OAAOb,OAAOuD,WAAW;gBACxC,IAAIyH,KAAKC,EAAE,KAAK,YAAY,OAAO5H,KAAKuD,QAAQ,CAACwE;gBACjD,IAAIJ,KAAKC,EAAE,KAAK,OAAO,OAAO5H,SAAS+H;gBACvC,OAAO/H,SAAS+H;YAClB;QACA,KAAK;YAAQ;gBACX,MAAMC,UAAUzL,QAAQgJ,IAAI,KAAK5I;gBACjC,OAAOgL,KAAKC,EAAE,KAAK,QAAQ,CAACI,UAAUA;YACxC;QACA;YACE,OAAO;IACX;AACF;AAoBA,OAAO,SAASC,uBACd1L,OAA0B,EAC1B2L,UAAsD;QAExCA;IAAd,MAAMC,SAAQD,oBAAAA,WAAWC,KAAK,YAAhBD,oBAAoB,EAAE;IACpC,IAAIC,MAAM/J,MAAM,KAAK,GAAG,OAAO;IAC/B,MAAMgK,UAAU,CAACT,OAAyBD,YAAYnL,SAAwBoL;IAC9E,OAAOO,WAAWG,QAAQ,KAAK,QAC3BF,MAAMvE,IAAI,CAACwE,WACXD,MAAMxD,KAAK,CAACyD;AAClB;AAEA;;;CAGC,GACD,OAAO,SAASE,kBACd/L,OAAoB,EACpB2L,UAA0B,EAC1BK,SAAkB;IAElB,IAAIhM,QAAQiM,SAAS,IAAIjM,QAAQiJ,MAAM,KAAK,UAAU,OAAO;IAC7D,IAAI0C,WAAWO,IAAI,KAAK,UAAU;YAE7BP;QADH,OAAOK,aAAa,QAClB,EAACL,yBAAAA,WAAWQ,UAAU,YAArBR,yBAAyB,EAAE,EAAE3E,QAAQ,CAACgF;IAC3C;IACA,OAAON,uBAAuB1L,SAAS2L;AACzC;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMS,yBAAyB,gBAAe;AASrD,OAAO,SAASC,qBACdrM,OAA0B,EAC1BsM,gBAAiD;IAEjD,OAAOA,iBACJrI,MAAM,CAAC,CAAC0H,aAAeD,uBAAuB1L,SAAS2L,aACvDxI,GAAG,CAAC,CAACwI,aAAeA,WAAW7C,EAAE,EACjClG,IAAI;AACT;AAEA;;;;CAIC,GACD,OAAO,SAAS2J,kBACdjL,GAA6C;QAKrBA,eAGhBA,WACyBA,YACzBA,WACEA,aAKOA;IAdjB,IAAIoG,MAAMC,OAAO,CAACrG,IAAIyC,QAAQ,KAAKzC,IAAIyC,QAAQ,CAAClC,MAAM,GAAG,GAAG;QAC1D,OAAOP;IACT;IACA,MAAM6F,WAAW7G,QAAOgB,gBAAAA,IAAI6F,QAAQ,YAAZ7F,gBAAgB;IACxC,OAAO,aACFA;QACHmC,IAAI,GAAEnC,YAAAA,IAAImC,IAAI,YAARnC,YAAY;QAClBqH,MAAMrH,IAAIqH,IAAI,IAAInF,cAAalC,aAAAA,IAAImC,IAAI,YAARnC,aAAY;QAC3C0H,IAAI,GAAE1H,YAAAA,IAAI0H,IAAI,YAAR1H,YAAY;QAClB2H,MAAM,GAAE3H,cAAAA,IAAI2H,MAAM,YAAV3H,cAAc;QACtByC,UAAU;YACR;gBACE+E,IAAI;gBACJ3B,UAAU7G,OAAOC,QAAQ,CAAC4G,YAAYA,WAAW;gBACjDxC,SAAS,GAAErD,iBAAAA,IAAIqD,SAAS,YAAbrD,iBAAiB;YAC9B;SACD;;AAEL;AAEA,4DAA4D,GAC5D,OAAO,SAASkL,gBAAgBxM,OAAoB;QAWlCA,kBAuBCA;QAjCZA;IAAL,IAAI,GAACA,gBAAAA,QAAQyD,IAAI,qBAAZzD,cAAckB,IAAI,KAAI,OAAO;IAClC,IAAI,CAAClB,QAAQ2I,IAAI,IAAI3I,QAAQ2I,IAAI,KAAKnF,aAAaxD,QAAQ2I,IAAI,GAAG;QAChE,OAAO;IACT;IACA,IAAI,CAAC;QAAC;QAAY;QAAW;KAAU,CAAC3B,QAAQ,CAAChH,QAAQgJ,IAAI,GAAG;QAC9D,OAAO;IACT;IACA,IAAI,CAAC;QAAC;QAAS;QAAU;KAAW,CAAChC,QAAQ,CAAChH,QAAQiJ,MAAM,GAAG;QAC7D,OAAO;IACT;IACA,MAAMjE,WAAUhF,mBAAAA,QAAQgF,OAAO,YAAfhF,mBAAmB,EAAE;IACrC,IAAIgF,QAAQnD,MAAM,GAAGwB,sBAAsB;QACzC,OAAO,CAAC,QAAQ,EAAEA,qBAAqB,oBAAoB,CAAC;IAC9D;IACA,KAAK,MAAM6B,UAAUF,QAAS;;YACvBE,cACAA;QADL,IAAI,GAACA,eAAAA,OAAOzB,IAAI,qBAAXyB,aAAahE,IAAI,KAAI,OAAO;QACjC,IAAI,UAACgE,iBAAAA,OAAOC,MAAM,qBAAbD,eAAerD,MAAM,mBAAI,OAAO,GAAG;YACtC,OAAO,CAAC,QAAQ,EAAEqD,OAAOzB,IAAI,CAAC,0BAA0B,CAAC;QAC3D;QACA,IAAIyB,OAAOC,MAAM,CAACtD,MAAM,GAAGyB,4BAA4B;YACrD,OAAO,CAAC,QAAQ,EAAE4B,OAAOzB,IAAI,CAAC,qBAAqB,CAAC;QACtD;QACA,IAAI,IAAIR,IAAIiC,OAAOC,MAAM,EAAES,IAAI,KAAKV,OAAOC,MAAM,CAACtD,MAAM,EAAE;YACxD,OAAO,CAAC,QAAQ,EAAEqD,OAAOzB,IAAI,CAAC,sBAAsB,CAAC;QACvD;IACF;IACA,wEAAwE;IACxE,2CAA2C;IAC3C,IAAI,IAAIR,IAAI+B,QAAQ7B,GAAG,CAAC,CAAC+B,SAAWA,OAAOzB,IAAI,CAACvC,IAAI,KAAK0E,IAAI,KAAKZ,QAAQnD,MAAM,EAAE;QAChF,OAAO;IACT;IACA,MAAM4K,kBAAkB9M,sBAAsBK,QAAQ0M,cAAc;IACpE,IAAID,iBAAiB,OAAOA;IAC5B,MAAM1I,YAAW/D,oBAAAA,QAAQ+D,QAAQ,YAAhB/D,oBAAoB,EAAE;IACvC,IAAI+D,SAASlC,MAAM,KAAK,GAAG,OAAO;IAClC,IAAIkC,SAASlC,MAAM,GAAGuB,uBAAuB;QAC3C,OAAO,CAAC,QAAQ,EAAEA,sBAAsB,qBAAqB,CAAC;IAChE;IACA,MAAMkI,MAAM,IAAIrI;IAChB,MAAMkB,OAAO,IAAIlB;IACjB,KAAK,MAAMe,WAAWD,SAAU;QAC9B,IAAI,CAACC,QAAQ8E,EAAE,EAAE,OAAO;QACxB,IAAIwC,IAAI1E,GAAG,CAAC5C,QAAQ8E,EAAE,GAAG,OAAO;QAChCwC,IAAIxE,GAAG,CAAC9C,QAAQ8E,EAAE;QAClB,IAAI9E,QAAQmD,QAAQ,KAAKtG,aAAamD,QAAQmD,QAAQ,KAAK,MAAM;YAC/D,OAAO;QACT;QACA,MAAMD,QAAQ5G,OAAO0D,QAAQmD,QAAQ;QACrC,IAAI,CAAC7G,OAAOC,QAAQ,CAAC2G,UAAUA,QAAQ,GAAG;YACxC,OAAO;QACT;QACA,IAAIA,QAAQpH,wBAAwB;YAClC,OAAO,CAAC,sBAAsB,EAAEA,wBAAwB;QAC1D;QACA,IAAIkE,QAAQI,GAAG,EAAE;YACf,IAAID,KAAKyC,GAAG,CAAC5C,QAAQI,GAAG,GAAG,OAAO;YAClCD,KAAK2C,GAAG,CAAC9C,QAAQI,GAAG;QACtB;QACA,IACEJ,QAAQ2I,iBAAiB,IAAI,QAC7BrM,OAAO0D,QAAQ2I,iBAAiB,KAAKzF,OACrC;YACA,OAAO;QACT;IACF;IACA,OAAO;AACT;AAEA,+DAA+D,GAC/D,OAAO,SAAS0F,mBAAmBjB,UAA0B;QACtDA;IAAL,IAAI,GAACA,mBAAAA,WAAWlI,IAAI,qBAAfkI,iBAAiBzK,IAAI,KAAI,OAAO;IACrC,IACE,CAACyK,WAAWhD,IAAI,IAChBgD,WAAWhD,IAAI,KAAKnF,aAAamI,WAAWhD,IAAI,GAChD;QACA,OAAO;IACT;IACA,IAAIgD,WAAWO,IAAI,KAAK,YAAYP,WAAWO,IAAI,KAAK,SAAS;QAC/D,OAAO;IACT;IACA,IAAIP,WAAWO,IAAI,KAAK,SAAS;YACjBP;QAAd,MAAMC,SAAQD,oBAAAA,WAAWC,KAAK,YAAhBD,oBAAoB,EAAE;QACpC,IAAIC,MAAM/J,MAAM,KAAK,GAAG,OAAO;QAC/B,KAAK,MAAMuJ,QAAQQ,MAAO;YACxB,IACE,CAAC;gBAAC;gBAAO;gBAAc;gBAAY;gBAAQ;aAAO,CAAC5E,QAAQ,CAACoE,KAAKpJ,KAAK,GACtE;gBACA,OAAO;YACT;YACA,IAAI,CAAC;gBAAC;gBAAM;gBAAO;gBAAM;gBAAM;aAAW,CAACgF,QAAQ,CAACoE,KAAKC,EAAE,GAAG;gBAC5D,OAAO;YACT;YACA,IAAID,KAAKhL,KAAK,KAAK,MAAMgL,KAAKhL,KAAK,IAAI,MAAM;gBAC3C,OAAO;YACT;QACF;IACF;IACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce.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\nimport { modifierGroupsProblem, type ProductModifierGroup } from './product-modifiers'\nimport { nameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { draftProductChannelFacts, type ProductChannelFacts } from './product-channel'\nimport type { PickupLocationSettings } from './order-local-fulfillment'\n\n/**\n * Commerce catalog v1 (AGL-276): products with options/variants,\n * hierarchical categories, tags, and manual/smart collections. Pure\n * model + helpers — no I/O; the products hub and storefront APIs operate\n * over these shapes. Documents live under `hosts/{hostId}/products`,\n * `.../productCategories`, and `.../collections`; the legacy Commerce\n * Starter product doc (AGL-90: name/priceUsd/inventory) lifts into this\n * shape via `liftLegacyProduct`, so existing docs keep working.\n */\n\n/*\n * THERE IS NO STOREFRONT FEE CONSTANT, and that absence is load-bearing\n * (AGL-2295).\n *\n * `COMMERCE_PLATFORM_FEE_PERCENT = 2` lived here with ZERO call sites. It\n * asserted a flat 2% that agrees with the plan table for exactly one cell\n * (Starter, physical) and disagrees with the other fifteen — Starter digital\n * is 5%, Pro digital 3%, Business 2%, Scale 1%, and every physical rate above\n * Starter is 0%. Nothing was wrong while nothing read it; the cost was that\n * the next reader wiring it up would silently re-price every plan, and a\n * constant sitting in the model beside the types reads like the answer.\n *\n * The single source is `resolveTransactionFeePct(org, productType)` in\n * `plan-entitlements.ts`, resolved PER REQUEST from the org — because it\n * depends on the plan, on per-org staff overrides, and on whether the\n * subscription is still alive. A constant cannot express any of that.\n * `commerce-fee-single-source.spec.ts` fails the build if one comes back.\n */\n/** Product price ceiling (whole USD). */\nexport const COMMERCE_MAX_PRICE_USD = 10000\n\nexport type ProductType = 'physical' | 'digital' | 'service'\nexport type ProductStatus = 'draft' | 'active' | 'archived'\n\n/** One axis of variation, e.g. { name: 'Size', values: ['S','M','L'] }. */\nexport interface ProductOption {\n name: string\n values: string[]\n}\n\n/** A sellable configuration of a product (every product has ≥ 1). */\nexport interface ProductVariant {\n /** Stable id unique within the product (never reused after delete). */\n id: string\n /** Option selections keyed by option name; {} for the default variant. */\n options?: Record<string, string>\n sku?: string\n barcode?: string\n priceUsd: number\n /** Strike-through price; a sale badge shows when > priceUsd. */\n compareAtPriceUsd?: number\n weightGrams?: number\n /** null/undefined = untracked; 0 = sold out (matches AGL-96). */\n inventory?: number | null\n /**\n * Per-location stock (AGL-286); `inventory` stays the summed\n * denormalization. Absent = single default location.\n */\n inventoryByLocation?: Record<string, number>\n /** Media-library image URL shown when this variant is selected. */\n imageUrl?: string\n}\n\n/** A product's packed size and customs facts (AGL-3612). */\nexport interface ProductShippingFacts {\n lengthCm?: number | null\n widthCm?: number | null\n heightCm?: number | null\n /** Harmonized System tariff code. */\n hsCode?: string\n /** ISO-3166 alpha-2 country the product was made in. */\n originCountry?: string\n}\n\n/** A place's postal address, in parts (AGL-3612). Country is ISO-3166 alpha-2. */\nexport interface PostalAddress {\n line1?: string\n line2?: string\n city?: string\n state?: string\n postalCode?: string\n country?: string\n phone?: string\n}\n\n/** `hosts/{hostId}/locations/{id}` doc (AGL-286). */\nexport interface InventoryLocation {\n name: string\n isDefault?: boolean\n /** Free text, as the location was first described. */\n address?: string\n /**\n * The same place as a postal address (AGL-3612), so parcels can ship from\n * it. Optional: a location written before this has only `address`.\n */\n postalAddress?: PostalAddress\n /** Whether buyers may collect orders here, and when (AGL-3624). */\n pickup?: PickupLocationSettings\n}\n\n/** A product's packed size for one unit, when all three sides are known. */\nexport function productParcelDimensions(\n product: Pick<HostProduct, 'shipping'> | null | undefined,\n): { lengthCm: number; widthCm: number; heightCm: number } | undefined {\n const facts = product?.shipping\n const side = (value: unknown) => {\n const number = Number(value)\n return Number.isFinite(number) && number > 0 ? Math.min(number, 300) : 0\n }\n const lengthCm = side(facts?.lengthCm)\n const widthCm = side(facts?.widthCm)\n const heightCm = side(facts?.heightCm)\n return lengthCm && widthCm && heightCm ? { lengthCm, widthCm, heightCm } : undefined\n}\n\n/**\n * Shipping facts as the product editor stages them, keystroke by keystroke\n * (AGL-3647): {@link normalizeProductShippingFacts}, except that a country\n * of origin being typed keeps its first letter. The editor runs this on\n * every keystroke, and the strict form drops anything short of two letters,\n * so the field could never be typed into. Readers normalize what is stored.\n */\nexport function draftProductShippingFacts(value: unknown): ProductShippingFacts | undefined {\n const facts = normalizeProductShippingFacts(value)\n const typed = String((value as Record<string, unknown> | null)?.['originCountry'] ?? '')\n .trim()\n .toUpperCase()\n if (/^[A-Z]$/.test(typed)) return { ...facts, originCountry: typed }\n return facts\n}\n\n/** A stored product's shipping facts made safe (AGL-3612); never throws. */\nexport function normalizeProductShippingFacts(value: unknown): ProductShippingFacts | undefined {\n if (!value || typeof value !== 'object') return undefined\n const raw = value as Record<string, unknown>\n const facts: ProductShippingFacts = {}\n for (const side of ['lengthCm', 'widthCm', 'heightCm'] as const) {\n const number = Number(raw[side])\n if (raw[side] !== null && raw[side] !== undefined && Number.isFinite(number) && number > 0) {\n facts[side] = Math.min(Math.round(number * 10) / 10, 300)\n }\n }\n const hsCode = String(raw['hsCode'] ?? '').replace(/[^0-9.]/g, '').slice(0, 14)\n if (hsCode) facts.hsCode = hsCode\n const originCountry = String(raw['originCountry'] ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(originCountry)) facts.originCountry = originCountry\n return Object.keys(facts).length ? facts : undefined\n}\n\n/** A stored location's postal address made safe (AGL-3612); never throws. */\nexport function normalizePostalAddress(value: unknown): PostalAddress | undefined {\n if (!value || typeof value !== 'object') return undefined\n const raw = value as Record<string, unknown>\n const address: PostalAddress = {}\n for (const field of ['line1', 'line2', 'city', 'state', 'postalCode', 'phone'] as const) {\n const text = String(raw[field] ?? '').trim().slice(0, 120)\n if (text) address[field] = text\n }\n const country = String(raw['country'] ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) address.country = country\n return Object.keys(address).length ? address : undefined\n}\n\n/**\n * `hosts/{hostId}/registers/{id}` doc (AGL-472): a named point-of-sale\n * register. Creation is capped by the plan's `posRegisters` quota (Pro 1,\n * Business 2, Advanced 5; raised by a per-org entitlement override for the\n * $89/mo add-on), enforced server-side by the resources route. Each POS\n * sale stamps its `registerId` so takings are attributable per register.\n */\nexport interface PosRegister {\n name: string\n /** Optional default inventory location this register sells from. */\n locationId?: string\n}\n\n/**\n * The register ids within the plan's `posRegisters` cap (AGL-482), ranked\n * by creation order — the same ordering `pos-order.ts` enforces at sale\n * time. Registers beyond the cap (e.g. after a Business→Pro downgrade) are\n * excluded so the console/POS can dim or hide them instead of surfacing a\n * checkout 403. `cap` of `Infinity` returns all.\n */\nexport function registersWithinCap(\n registers: Array<{ $id: string; createdAt?: any }>,\n cap: number,\n): Set<string> {\n const createdMs = (r: { createdAt?: any }) =>\n r.createdAt?.toMillis?.() ?? r.createdAt?.seconds ?? 0\n const ranked = [...registers].sort(\n (a, b) => createdMs(a) - createdMs(b) || a.$id.localeCompare(b.$id),\n )\n return new Set(ranked.slice(0, Math.max(0, cap)).map((r) => r.$id))\n}\n\n/**\n * `hosts/{hostId}/suppliers/{id}` doc (AGL-289): where dropshipped\n * order lines route on payment. Email and webhook are both optional but\n * one must be set for routing to do anything; webhook payloads are\n * HMAC-SHA256-signed with `webhookSecret`.\n */\nexport interface HostSupplier {\n name: string\n email?: string\n webhookUrl?: string\n webhookSecret?: string\n}\n\n/** `hosts/{hostId}/products/{id}` doc. */\nexport interface HostProduct {\n name: string\n /** Host-unique URL segment for /products/{slug}. */\n slug: string\n description?: string\n type: ProductType\n status: ProductStatus\n /** Ordered media-library image URLs (first = primary). */\n mediaUrls?: string[]\n categoryIds?: string[]\n /**\n * The SMART collections this product's rules answer (AGL-3321), written by\n * every writer — see `productCollectionIds`.\n */\n collectionIds?: string[]\n tags?: string[]\n options?: ProductOption[]\n variants: ProductVariant[]\n /** Per-product overrides for PDP meta tags (AGL-299 consumes). */\n seo?: { title?: string; description?: string; imageUrl?: string }\n /**\n * What shopping channels ask of the product (AGL-3637): brand, GTIN, MPN,\n * condition and Google category. Absent means nothing was entered; the\n * channel feeds fall back to the store's defaults.\n */\n channel?: ProductChannelFacts\n /** Supplier for dropship routing (AGL-289). */\n supplierId?: string\n /** Out-of-stock behavior (AGL-281): deny (default) or allow backorder. */\n oversellPolicy?: 'deny' | 'backorder'\n /** Never taxed regardless of tax settings (AGL-285). */\n taxExempt?: boolean\n /**\n * Digital delivery (AGL-302): downloadable files for `digital`\n * products. Buyers always download the CURRENT list, so uploading a\n * new version re-delivers to everyone.\n */\n digitalFiles?: Array<{ url: string; fileName: string; version?: string }>\n /** Max download attempts per order line; absent = unlimited. */\n downloadLimit?: number\n /**\n * Recurring billing (AGL-303): buyers subscribe instead of buying\n * once; an active subscription is the entitlement content gating\n * checks (AGL-309).\n */\n subscription?: { interval: 'month' | 'year'; trialDays?: number }\n /**\n * Buyer-chosen billing (AGL-545): with `subscription` set, true lets\n * the buyer pick one-time OR subscribe on the PDP (default one-time);\n * absent/false keeps the product subscription-only.\n */\n subscriptionOptional?: boolean\n /** Members-only videos (AGL-315), streamed via short-TTL links. */\n gatedVideos?: Array<{ url: string; title?: string }>\n /** Manual related products for the upsell block (AGL-325). */\n relatedProductIds?: string[]\n /**\n * What one unit ships as (AGL-3612): its packed size, for a carrier to\n * price, and its customs facts, for a parcel that crosses a border. Weight\n * stays per variant. Absent means unknown, and a label is priced in the\n * merchant's default box.\n */\n shipping?: ProductShippingFacts\n /** Buying this issues a gift-card code for its price (AGL-322). */\n giftCard?: boolean\n /** Tracked-total at/below this alerts host managers (AGL-281). */\n lowStockThreshold?: number\n /**\n * Choices added to the item at the register (AGL-3607) — \"Oat milk\",\n * \"Extra shot\" — priced by the server from this list. See\n * `product-modifiers.ts`.\n */\n modifierGroups?: ProductModifierGroup[]\n /** Shown under Quick keys on the register's first screen (AGL-3607). */\n posQuickKey?: boolean\n createdAtMs?: number\n updatedAtMs?: number\n deletedAt?: number | null\n /** Legacy Commerce Starter fields kept so old docs read back (AGL-90). */\n priceUsd?: number\n inventory?: number | null\n imageUrl?: string\n}\n\n/** `hosts/{hostId}/productCategories/{id}` doc (tree via parentId). */\nexport interface ProductCategory {\n name: string\n slug: string\n parentId?: string | null\n order?: number\n}\n\nexport type CollectionRuleField =\n | 'tag'\n | 'categoryId'\n | 'priceUsd'\n | 'name'\n | 'type'\nexport type CollectionRuleOp = 'eq' | 'neq' | 'lt' | 'gt' | 'contains'\n\nexport interface CollectionRule {\n field: CollectionRuleField\n op: CollectionRuleOp\n value: string | number\n}\n\n/** `hosts/{hostId}/collections/{id}` doc. */\nexport interface HostCollection {\n name: string\n slug: string\n description?: string\n mode: 'manual' | 'smart'\n /** Manual mode: explicit ordered membership. */\n productIds?: string[]\n /** Smart mode: rules evaluated over active products. */\n rules?: CollectionRule[]\n /** Smart mode: true = every rule must match (default), false = any. */\n matchAll?: boolean\n /** Media-library image URL for the collection card/landing hero. */\n imageUrl?: string\n order?: number\n}\n\n/**\n * Payment-provider seam (AGL-284): checkout/session creation goes\n * through a provider id so PayPal (etc.) can slot in without reworking\n * callers. Stripe is the only implementation today; store settings\n * (AGL-295) will carry the selection when a second provider exists.\n */\nexport type PaymentProviderId = 'stripe'\n\nexport interface PaymentCheckoutRequest {\n provider: PaymentProviderId\n hostId: string\n amountCents: number\n feeCents: number\n productName: string\n successUrl: string\n cancelUrl: string\n metadata?: Record<string, string>\n}\n\n/** Variant ceiling per product (Shopify parity). */\nexport const COMMERCE_MAX_VARIANTS = 100\nexport const COMMERCE_MAX_OPTIONS = 3\nexport const COMMERCE_MAX_OPTION_VALUES = 25\nexport const COMMERCE_SLUG_MAX_LENGTH = 80\n\n/** Lowercase-kebab slug from a product/category/collection name. */\nexport function commerceSlug(name: string): string {\n return String(name ?? '')\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, COMMERCE_SLUG_MAX_LENGTH)\n}\n\n/**\n * The search keys a product write carries, so the catalog can be searched by\n * the QUERY rather than by the rows a listener happened to fetch (AGL-2501).\n *\n * The hub lists `limit(500)`, and the search compared what that returned — so\n * a product past the window answered \"no products match\", which reads as the\n * product not existing rather than as the search not reaching it. Filtering\n * server-side needs fields Firestore can index, and neither of the two things\n * a merchant searches by is one:\n *\n * - A NAME needs case-insensitive contains, which Firestore has no operator\n * for. `nameSearchFields` denormalizes it into the three shapes that are\n * indexable — an exact key, word-prefix tokens for `array-contains`, and\n * the key reversed so \"ends with\" becomes a prefix range.\n * - A SKU lives inside `variants`, an array of OBJECTS, and Firestore cannot\n * query a field inside one. Flattened here into a top-level array so a SKU\n * is one `array-contains`.\n *\n * ⛔ A SKU therefore matches WHOLE, not as a substring: \"abc-123\" finds it and\n * \"123\" does not. That is the right way round for a SKU, which is a value\n * somebody copies rather than half-remembers — unlike a name, where the tokens\n * above buy word-prefix matching precisely because names are half-remembered.\n *\n * `barcodes` is the same flattening for the same reason, and it is the one the\n * register depends on: a keyboard-wedge scanner types the code and presses\n * Enter, so the lookup has to be exact and has to reach the whole catalog. A\n * scan is never a half-remembered value.\n *\n * Lower-cased because the translator lower-cases the typed query before it\n * builds the `array-contains`; stored and typed have to be normalized the same\n * way or the two silently disagree.\n *\n * ⚠️ Each array is OMITTED when a product has none, rather than written as\n * `[]`. `isNotEmpty` is served as `!= null`, and an empty array is not null —\n * so a product with no SKUs at all would answer \"has a SKU\" for every row.\n *\n * `priceFromCents` is the storefront's price key (AGL-3321): the price a\n * product grid card shows — \"From $X\", the lowest variant price — in whole\n * cents, so the catalog's price slider and its price sorts are a range and an\n * `orderBy` on one stored number rather than a comparison over variants no\n * query can reach. Written only when the variants are, because it is derived\n * from them: a write that carries no variants must not stamp a price.\n *\n * ⚠️ Spread this at EVERY write that sets a product's name or variants. A\n * write that sets the name without it leaves the keys describing the previous\n * name, and the product becomes findable only by what it used to be called.\n */\nexport function productSearchFields(product: {\n name: string\n variants?: ProductVariant[]\n}): {\n name: string\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n skus?: string[]\n barcodes?: string[]\n priceFromCents?: number\n} {\n const flatten = (read: (variant: ProductVariant) => string | undefined) => [\n ...new Set(\n (product.variants ?? [])\n .map((variant) => (read(variant) ?? '').trim().toLowerCase())\n .filter(Boolean),\n ),\n ]\n const skus = flatten((variant) => variant.sku)\n const barcodes = flatten((variant) => variant.barcode)\n return {\n ...nameSearchFields(product.name),\n ...(skus.length ? { skus } : {}),\n ...(barcodes.length ? { barcodes } : {}),\n ...(product.variants\n ? { priceFromCents: productPriceFromCents({ variants: product.variants }) }\n : {}),\n }\n}\n\n/**\n * The price a storefront card shows, in whole cents (AGL-3321): the low end\n * of the variant range, which is what \"From $X\" prints and what the catalog's\n * price filter and price sorts compare. Stored as `priceFromCents` by\n * {@link productSearchFields}; `0` for a product with no priced variant yet.\n */\nexport function productPriceFromCents(\n product: Pick<HostProduct, 'variants'>,\n): number {\n return Math.round(productPriceRange(product)[0] * 100)\n}\n\n/**\n * The stock keys a product write carries (AGL-3321): the flat `inventory`\n * total, and `soldOut` — whether a storefront card reads \"Sold out\", which is\n * what the product grid's In stock chip asks the catalog query for\n * (`soldOut == false`). A grid cannot ask a query about stock inside\n * `variants`, so the verdict is stored beside them.\n *\n * ⚠️ Spread this at EVERY write that changes `variants` or `oversellPolicy`:\n * a sale, a cancellation, a stock adjustment and the editor alike. A write\n * that moves stock without it leaves a sold-out product under In stock, or an\n * in-stock one missing from it. Stored by `backfill-products-list-fields.mjs`\n * on the products written before it.\n */\nexport function productStockFields(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n): { inventory: number | null; soldOut: boolean } {\n const inventory = productInventory(product)\n return {\n inventory,\n soldOut: inventory != null && inventory <= 0 && product.oversellPolicy !== 'backorder',\n }\n}\n\n/**\n * Cartesian product of option values → the variant option combinations\n * the products hub materializes into a variants matrix. Empty/absent\n * options yield a single default combination ({}).\n */\nexport function expandVariantMatrix(\n options: ProductOption[] | undefined,\n): Array<Record<string, string>> {\n const usable = (options ?? []).filter(\n (option) => option.name && (option.values?.length ?? 0) > 0,\n )\n if (usable.length === 0) return [{}]\n let combos: Array<Record<string, string>> = [{}]\n for (const option of usable) {\n const next: Array<Record<string, string>> = []\n for (const combo of combos) {\n for (const value of option.values) {\n next.push({ ...combo, [option.name]: value })\n }\n }\n combos = next\n if (combos.length > COMMERCE_MAX_VARIANTS) {\n return combos.slice(0, COMMERCE_MAX_VARIANTS)\n }\n }\n return combos\n}\n\n/**\n * The option axes renamed, with every variant's selection carried to the new\n * name in place (AGL-3066).\n *\n * A variant keeps its selections keyed by option NAME, so a rename has to move\n * each key rather than rebuild the matrix: a rebuild matches variants by name\n * and value, finds none under the new name, and replaces every variant with a\n * new id holding the first variant's price and stock and no SKU, barcode,\n * compare-at price or weight. Moving the key keeps the id every order line\n * and stock movement refers to, and every field a person set.\n *\n * `names` holds one entry per option, in order; an entry that is not a string\n * keeps that option's name. Each selection keeps its position among a\n * variant's selections, because a variant is labeled by them in order.\n *\n * Two options may share a name for a moment while a person types. A variant\n * cannot hold two selections under one name, so while the new names are not\n * all different nothing is moved, and `validateProduct` refuses the product.\n * The selection left under the old name is found again by its value once the\n * names differ, so typing through a duplicate loses nothing.\n */\nexport function renameProductOptions(\n product: Pick<HostProduct, 'options' | 'variants'>,\n names: ReadonlyArray<string | null | undefined>,\n): Pick<HostProduct, 'options' | 'variants'> {\n const current = product.options ?? []\n const options = current.map((option, index) => {\n const name = names[index]\n return typeof name === 'string' ? { ...option, name } : option\n })\n const next = options.map((option) => option.name)\n if (new Set(next).size !== next.length) return { options, variants: product.variants }\n const variants = (product.variants ?? []).map((variant) => {\n const selections = variant.options\n if (!selections) return variant\n const axisOf = optionAxesOf(current, selections)\n const moved: Record<string, string> = {}\n let changed = false\n for (const [key, value] of Object.entries(selections)) {\n const axis = axisOf.get(key)\n const name = axis === undefined ? key : next[axis]\n if (name !== key) changed = true\n moved[name] = value\n }\n return changed ? { ...variant, options: moved } : variant\n })\n return { options, variants }\n}\n\n/**\n * Which option each of a variant's selections belongs to: the option of the\n * same name first, and then, for a selection under a name no option has any\n * longer, the one remaining option whose values hold its value. A selection\n * that fits neither belongs to no option and is left as it is.\n */\nfunction optionAxesOf(\n options: readonly ProductOption[],\n selections: Readonly<Record<string, string>>,\n): Map<string, number> {\n const axisOf = new Map<string, number>()\n const taken = new Set<number>()\n options.forEach((option, index) => {\n if (Object.prototype.hasOwnProperty.call(selections, option.name) && !axisOf.has(option.name)) {\n axisOf.set(option.name, index)\n taken.add(index)\n }\n })\n for (const [key, value] of Object.entries(selections)) {\n if (axisOf.has(key)) continue\n const axis = options.findIndex(\n (option, index) => !taken.has(index) && (option.values ?? []).includes(value),\n )\n if (axis === -1) continue\n axisOf.set(key, axis)\n taken.add(axis)\n }\n return axisOf\n}\n\n/**\n * Whether a variant has a price a sale can charge (AGL-2916).\n *\n * A product proposed by AI is created with every price left empty for the\n * merchant to set, so a stored variant can lack one. `Number(undefined)` is\n * no price, and neither is `null`, which `Number` reads as zero: a sale door\n * that priced either would charge nothing, or fail at the processor.\n */\nexport function variantHasPrice(variant: object | null | undefined): boolean {\n const price = (variant as { priceUsd?: unknown } | null | undefined)?.priceUsd\n return typeof price === 'number' && Number.isFinite(price) && price >= 0\n}\n\n/** Whether any variant of a product still needs its price set (AGL-2916). */\nexport function productPriceMissing(product: { variants?: readonly object[] }): boolean {\n return (product.variants ?? []).some((variant) => !variantHasPrice(variant))\n}\n\n/** Copy proposed for a product, by field (AGL-2916). A field left out is left alone. */\nexport interface ProductCopyValues {\n description?: string\n tags?: string[]\n categoryIds?: string[]\n /** One name per option the product has, in order. */\n optionNames?: string[]\n seoTitle?: string\n seoDescription?: string\n /**\n * Packed size and customs facts (AGL-3612), merged over what the product\n * has; a side set to `null` is cleared.\n */\n shipping?: Partial<ProductShippingFacts>\n /**\n * Shopping-channel facts (AGL-3637), merged over what the product has; a\n * field set to `''` is cleared.\n */\n channel?: { [Field in keyof ProductChannelFacts]?: string }\n}\n\n/**\n * The fields proposed copy changes on a product (AGL-2916), in the shape the\n * editor stores them: only the fields `values` names. Tags and categories are\n * each kept once; the search listing keeps its share image; and option names\n * rename the product's options through `renameProductOptions`, keeping every\n * variant, only when they name each option once with a name of its own.\n */\nexport function productCopyPatch(\n product: Pick<HostProduct, 'options' | 'variants' | 'seo' | 'shipping'> &\n Pick<Partial<HostProduct>, 'channel'>,\n values: ProductCopyValues,\n): Partial<\n Pick<HostProduct, 'description' | 'tags' | 'categoryIds' | 'seo' | 'options' | 'variants' | 'shipping' | 'channel'>\n> {\n const patch: Partial<\n Pick<HostProduct, 'description' | 'tags' | 'categoryIds' | 'seo' | 'options' | 'variants' | 'shipping' | 'channel'>\n > = {}\n if (values.shipping && typeof values.shipping === 'object') {\n // Stored whole, never `undefined`: the editor's save is a full `setDoc`,\n // which refuses an undefined field. Nothing known is an empty map.\n patch.shipping = draftProductShippingFacts({ ...product.shipping, ...values.shipping }) ?? {}\n }\n if (values.channel && typeof values.channel === 'object') {\n // Stored whole, never `undefined`, for the same reason. Nothing entered\n // is an empty map. Kept as typed: this runs on every keystroke.\n patch.channel = draftProductChannelFacts({ ...product.channel, ...values.channel }) ?? {}\n }\n if (typeof values.description === 'string') patch.description = values.description\n if (Array.isArray(values.tags)) {\n patch.tags = [...new Set(values.tags.map((tag) => tag.trim()).filter(Boolean))]\n }\n if (Array.isArray(values.categoryIds)) {\n patch.categoryIds = [...new Set(values.categoryIds.filter(Boolean))]\n }\n if (typeof values.seoTitle === 'string' || typeof values.seoDescription === 'string') {\n patch.seo = {\n ...product.seo,\n ...(typeof values.seoTitle === 'string' ? { title: values.seoTitle } : {}),\n ...(typeof values.seoDescription === 'string' ? { description: values.seoDescription } : {}),\n }\n }\n const options = product.options ?? []\n const names = values.optionNames?.map((name) => name.trim())\n if (\n names &&\n options.length > 0 &&\n names.length === options.length &&\n names.every(Boolean) &&\n new Set(names).size === names.length &&\n names.some((name, index) => name !== options[index].name)\n ) {\n Object.assign(patch, renameProductOptions(product, names))\n }\n return patch\n}\n\n/** A product proposed for a store, before anyone has priced it (AGL-2916). */\nexport interface ProposedProduct {\n name: string\n type: ProductType\n description: string\n tags: string[]\n options: ProductOption[]\n seoTitle: string\n seoDescription: string\n}\n\n/** A new product whose variants carry no price yet. */\nexport type UnpricedProductDraft = Omit<HostProduct, 'variants'> & {\n variants: Array<Omit<ProductVariant, 'priceUsd'>>\n}\n\n/**\n * The document a proposed product is created as (AGL-2916): a `draft`, under\n * a slug no product in `takenSlugs` has, with one variant for each\n * combination of its options and NO price on any of them. Prices are the\n * merchant's to set: the editor refuses to save the product until every\n * variant has one, and no sale door sells a variant without one\n * (`variantHasPrice`). The search keys travel with the name, as on every\n * create path.\n */\nexport function unpricedProductDraft(\n proposal: ProposedProduct,\n takenSlugs: ReadonlySet<string>,\n nowMs: number,\n): UnpricedProductDraft {\n const name = proposal.name.trim().slice(0, 120)\n const base = commerceSlug(name) || 'product'\n let slug = base\n for (let suffix = 2; takenSlugs.has(slug); suffix += 1) slug = `${base}-${suffix}`\n const seen = new Set<string>()\n const options = proposal.options\n .map((option) => ({\n name: option.name.trim(),\n values: [...new Set(option.values.map((value) => value.trim()).filter(Boolean))].slice(\n 0,\n COMMERCE_MAX_OPTION_VALUES,\n ),\n }))\n .filter((option) => {\n if (!option.name || !option.values.length || seen.has(option.name)) return false\n seen.add(option.name)\n return true\n })\n .slice(0, COMMERCE_MAX_OPTIONS)\n const variants = options.length\n ? expandVariantMatrix(options).map((combo, index) => ({\n id: `v${nowMs.toString(36)}${index}`,\n options: combo,\n }))\n : [{ id: 'default' }]\n const seoTitle = proposal.seoTitle.trim()\n const seoDescription = proposal.seoDescription.trim()\n return {\n // The keys for the variants as created: no price is `priceFromCents: 0`\n // and no tracked stock is never sold out, so the draft is ordered and\n // filtered like every other product once it is priced and activated.\n ...productSearchFields({ name, variants: variants as ProductVariant[] }),\n ...productStockFields({ variants: variants as ProductVariant[] }),\n slug,\n type: proposal.type,\n status: 'draft',\n description: proposal.description,\n tags: [...new Set(proposal.tags.map((tag) => tag.trim()).filter(Boolean))],\n ...(options.length ? { options } : {}),\n variants,\n ...(seoTitle || seoDescription\n ? { seo: { ...(seoTitle ? { title: seoTitle } : {}), ...(seoDescription ? { description: seoDescription } : {}) } }\n : {}),\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n}\n\n/** Variant whose option selections match exactly; undefined if none. */\nexport function findVariant(\n product: Pick<HostProduct, 'variants'>,\n selections: Record<string, string>,\n): ProductVariant | undefined {\n return product.variants?.find((variant) => {\n const options = variant.options ?? {}\n const keys = new Set([...Object.keys(options), ...Object.keys(selections)])\n for (const key of keys) {\n if (options[key] !== selections[key]) return false\n }\n return true\n })\n}\n\n/** [min, max] price across variants; [0, 0] when there are none. */\nexport function productPriceRange(\n product: Pick<HostProduct, 'variants'>,\n): [number, number] {\n const prices = (product.variants ?? [])\n .map((variant) => Number(variant.priceUsd))\n .filter((price) => Number.isFinite(price) && price >= 0)\n if (prices.length === 0) return [0, 0]\n return [Math.min(...prices), Math.max(...prices)]\n}\n\n/** Sum of tracked inventory; null when every variant is untracked. */\nexport function productInventory(\n product: Pick<HostProduct, 'variants'>,\n): number | null {\n let total: number | null = null\n for (const variant of product.variants ?? []) {\n if (variant.inventory == null) continue\n total = (total ?? 0) + Number(variant.inventory)\n }\n return total\n}\n\n/**\n * Reasons an inventory adjustment doc may carry (AGL-281).\n *\n * `cancellation` is the only one written automatically in the PLUS direction\n * (AGL-1808): cancelling an order the transition rules prove was never\n * fulfilled puts back exactly what the sale took. It is deliberately not\n * `refund` — no money moved — and not `correction`, which is what a merchant\n * picks when the count disagreed with the shelf for a reason nobody recorded.\n * Kept out of the \"Adjust stock\" dialog's menu on purpose: a merchant reaching\n * for it by hand means something else happened.\n *\n * `sync` is a count another warehouse reported (AGL-3634): a fulfillment\n * network holding the goods said how many it can ship, and the count was set\n * to that. Written only through core's `core.stock-levels`, with `source`\n * naming who counted, and kept out of the dialog's menu for the same reason.\n */\nexport type InventoryAdjustmentReason =\n 'sale' | 'refund' | 'restock' | 'correction' | 'damage' | 'cancellation' | 'sync'\n\n/** `hosts/{hostId}/inventoryAdjustments/{id}` doc. */\nexport interface InventoryAdjustment {\n productId: string\n variantId: string\n /** Positive = stock added, negative = removed. */\n delta: number\n /**\n * What the count ACTUALLY moved by, when the floor in\n * `adjustVariantInventory` absorbed part of `delta` (AGL-2149). Written only\n * when the two differ, which for a sale means a backorder product that sold\n * past zero: `delta` stays the units sold — that is the merchant's stock\n * history and it must keep saying \"3 went out the door\" — while this says how\n * many of them the count could give up.\n *\n * A REVERSAL MUST READ THIS, NOT `delta`. Restoring `delta` on a backorder\n * sale that the floor swallowed invents inventory that never existed: 0 sells\n * 3, the count stays 0, and a `+3` on cancellation leaves 3 units nobody has.\n * Absent on every row written before AGL-2149 and on every row where nothing\n * was clamped, so `appliedDelta ?? delta` is the reader.\n */\n appliedDelta?: number\n reason: InventoryAdjustmentReason\n /** Order id for sale/refund adjustments. */\n orderId?: string\n /** Location for multi-location stock (AGL-286); absent = default. */\n locationId?: string\n /**\n * Who counted, for a `sync` row: the warehouse that reported it (AGL-3634);\n * for a `sale` another channel made, that channel (AGL-3638).\n */\n source?: string\n atMs: number\n}\n\n/**\n * Whether `quantity` of a variant is purchasable (AGL-281): untracked\n * stock always is; tracked stock honors the product's oversell policy.\n */\nexport function canPurchase(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n variantId: string | undefined,\n quantity = 1,\n): boolean {\n const variant = variantId\n ? product.variants?.find((item) => item.id === variantId)\n : product.variants?.[0]\n if (!variant) return false\n if (variant.inventory == null) return true\n if (product.oversellPolicy === 'backorder') return true\n return Number(variant.inventory) >= quantity\n}\n\n/**\n * What the register has to SAY about a line it is about to sell (AGL-2357).\n *\n * ## The defect\n *\n * `pos-order.ts` contains no `canPurchase` call. Every storefront door gates on\n * it; the register does not, so a merchant who set `oversellPolicy: 'deny'` in\n * the product editor silently got `backorder` at the counter — the count floors\n * at zero underneath and nothing anywhere says so.\n *\n * ## The decision, and why it is a shortfall and not a refusal\n *\n * Warn, never block. A till is the wrong place for a stale number to stop a\n * real sale: the cashier is holding the goods, the physical shelf is the truth,\n * and refusing loses a sale over data that is behind. But \"deny\" must at least\n * SAY something. So this reports the shortfall and the caller sells anyway.\n * Honouring the policy with a manager-level override is the post-launch shape\n * (AGL-2372) and wants an audit trail this deliberately does not build.\n *\n * ## Why the test is `canPurchase`\n *\n * Exactly the gate the register was missing, and no wider. An untracked variant\n * has no number to be short against, and a `backorder` product's merchant chose\n * to sell past zero — warning them would be noise about a setting they set on\n * purpose. So a shortfall is reported only where a purchase would have been\n * refused anywhere else in the product.\n */\nexport interface StockShortfall {\n productId: string\n variantId?: string\n /** Product name at the time of sale, for a message the cashier can read. */\n name: string\n variantLabel?: string\n /** Units the cashier is ringing up. */\n requested: number\n /** Units the count says are on the shelf; never negative. */\n available: number\n}\n\nexport function stockShortfall(\n product: Pick<HostProduct, 'variants' | 'oversellPolicy'>,\n variantId: string | undefined,\n quantity = 1,\n): { available: number } | null {\n if (canPurchase(product, variantId, quantity)) return null\n const variant = variantId\n ? product.variants?.find((item) => item.id === variantId)\n : product.variants?.[0]\n // `canPurchase` answers false for a variant that does not exist at all. That\n // is not a stock shortfall and must not be reported as \"0 in stock\" — the\n // register's own line builder falls back to `variants[0]`, so the case is\n // unreachable from the counter, and inventing a count for it would be the\n // one failure mode this feature must not have.\n if (!variant || variant.inventory == null) return null\n return { available: Math.max(0, Math.round(Number(variant.inventory) || 0)) }\n}\n\n/**\n * Whether stock tracking MEANS anything on a product (AGL-1744).\n *\n * `canPurchase` above gates every checkout on stock, subscription sessions\n * included, but nothing ever decrements for a subscription — not on the\n * initial charge (the `commerce-subscription` webhook branch deliberately\n * writes no order and touches no inventory, AGL-1732) and not on renewal\n * (`invoice.payment_succeeded` is unhandled repo-wide, AGL-1743). So on a\n * subscription-only product that gate is permanently satisfied: one unit of\n * stock sells an unlimited number of subscriptions, and the count never\n * moves however many boxes ship. The merchant set a number, the system\n * checks it on every sale, and the number is never true.\n *\n * AGL-1750 resolved the half AGL-1744 had to leave open: every paid invoice\n * of a PHYSICAL subscription now mints an order and decrements its variant's\n * stock (the `invoice.paid` branch of the commerce webhook), opening cycle\n * included. So the honest split is now three-way:\n *\n * - `subscriptionOptional` (\"Both\") products have a real one-time path —\n * the cart only ever builds `mode: 'payment'` sessions, and buy-now with\n * `billing: 'once'` records a plain order — and that path decrements\n * honestly. Withdrawing stock tracking there would delete a working\n * control and re-open the AGL-1711 oversell on the half that works.\n * - a PHYSICAL subscription box consumes a unit every cycle, and since\n * AGL-1750 the cycle's own order decrements it — the count moves as the\n * boxes ship, so the number the merchant sets is kept true again.\n * - a DIGITAL or SERVICE subscription-only product still has no\n * decrementing path anywhere, so the field stays withdrawn there: one\n * unit would still sell unlimited subscriptions.\n *\n * This says nothing about what `canPurchase` returns — that is the gate for\n * every other purchase path and is deliberately untouched. This is a console\n * predicate: it stops the editor inviting a merchant to set a number the\n * system will never keep true.\n */\nexport function stockTrackingApplies(\n product: Pick<HostProduct, 'subscription' | 'subscriptionOptional' | 'type'>,\n): boolean {\n return (\n !product.subscription ||\n product.subscriptionOptional === true ||\n product.type === 'physical'\n )\n}\n\n/** Buyer's requested billing mode on a checkout POST (AGL-545). */\nexport type CheckoutBillingChoice = 'once' | 'subscribe'\n\n/**\n * Resolves the Stripe Checkout mode for a product + the buyer's\n * requested billing choice (AGL-545). The product doc is authoritative:\n * - no `subscription` → always one-time (the request field is ignored)\n * - `subscription` without `subscriptionOptional` → always subscription\n * (a forged `billing: 'once'` cannot buy a subscription product once)\n * - `subscriptionOptional` → the buyer chooses; absent/invalid choices\n * default to one-time, matching the PDP default.\n */\nexport function resolveCheckoutBillingMode(\n product: Pick<HostProduct, 'subscription' | 'subscriptionOptional'>,\n requested?: string | null,\n): 'payment' | 'subscription' {\n if (!product.subscription) return 'payment'\n if (!product.subscriptionOptional) return 'subscription'\n return requested === 'subscribe' ? 'subscription' : 'payment'\n}\n\n/**\n * Applies a stock delta to one variant, flooring at zero (race-window\n * sales can't drive the display negative), and returns the new variants\n * array — callers persist it plus the `productInventory` denormalization.\n * With `locationId`, the delta lands in that location's bucket and the\n * flat `inventory` re-sums across locations (AGL-286).\n */\nexport function adjustVariantInventory(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n delta: number,\n locationId?: string,\n): ProductVariant[] {\n return (product.variants ?? []).map((variant) => {\n if (variant.id !== variantId || variant.inventory == null) return variant\n if (locationId && variant.inventoryByLocation) {\n const buckets = {\n ...variant.inventoryByLocation,\n [locationId]: Math.max(\n 0,\n Number(variant.inventoryByLocation[locationId] ?? 0) + delta,\n ),\n }\n return {\n ...variant,\n inventoryByLocation: buckets,\n inventory: Object.values(buckets).reduce(\n (sum, count) => sum + Number(count),\n 0,\n ),\n }\n }\n return {\n ...variant,\n inventory: Math.max(0, Number(variant.inventory) + delta),\n }\n })\n}\n\n/**\n * What `adjustVariantInventory` will ACTUALLY move the count by (AGL-2149).\n *\n * The floor in that helper is not a rounding detail, it is a silent absorber. A\n * backorder product (`oversellPolicy: 'backorder'`, which `canPurchase` admits\n * at any stock level) sitting at 0 sells 3: `Math.max(0, 0 + -3)` is 0, so the\n * count does not move — correctly, since inventory is a shelf count and shelves\n * do not go negative — but the ledger row said `-3` and every reversal read it\n * as three units to put back. Cancelling that order handed the merchant 3 units\n * that never existed and a stock badge that offers them for sale.\n *\n * Removing the floor is not the alternative: every reader of `inventory` — the\n * badges, `canPurchase`, `productInventory`, `isLowStock` — assumes it cannot\n * go negative, and a negative count would be a far wider change than the bug.\n * So the clamp stays and the amount it absorbed is recorded instead.\n *\n * Deliberately mirrors `adjustVariantInventory`'s own arithmetic, including\n * which field the clamp applies to in the location-tracked case, so the two\n * cannot disagree about what happened.\n */\nexport function appliedVariantInventoryDelta(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n delta: number,\n locationId?: string,\n): number {\n const variant = (product.variants ?? []).find(\n (item) => item.id === variantId,\n )\n if (!variant || variant.inventory == null) return 0\n if (locationId && variant.inventoryByLocation) {\n const before = Number(variant.inventoryByLocation[locationId] ?? 0)\n return Math.max(0, before + delta) - before\n }\n const before = Number(variant.inventory)\n return Math.max(0, before + delta) - before\n}\n\n/**\n * Moves stock between two locations of a tracked variant (AGL-286).\n * Quantity clamps to what the source location holds; the flat total is\n * unchanged by construction.\n */\nexport function transferVariantInventory(\n product: Pick<HostProduct, 'variants'>,\n variantId: string,\n fromLocationId: string,\n toLocationId: string,\n quantity: number,\n): ProductVariant[] {\n return (product.variants ?? []).map((variant) => {\n if (variant.id !== variantId || variant.inventory == null) return variant\n const buckets = { ...(variant.inventoryByLocation ?? {}) }\n const available = Math.max(0, Number(buckets[fromLocationId] ?? 0))\n const moved = Math.min(available, Math.max(0, Math.round(quantity)))\n if (moved === 0) return variant\n buckets[fromLocationId] = available - moved\n buckets[toLocationId] = Math.max(0, Number(buckets[toLocationId] ?? 0)) + moved\n return { ...variant, inventoryByLocation: buckets }\n })\n}\n\n/** True when tracked stock is at/below the product's alert threshold. */\nexport function isLowStock(\n product: Pick<HostProduct, 'variants' | 'lowStockThreshold'>,\n): boolean {\n const threshold = product.lowStockThreshold\n if (threshold == null || !(threshold >= 0)) return false\n const total = productInventory(product)\n return total != null && total <= threshold\n}\n\nfunction ruleMatches(product: HostProduct, rule: CollectionRule): boolean {\n const value = rule.value\n switch (rule.field) {\n case 'tag': {\n const tags = product.tags ?? []\n const has = tags.includes(String(value))\n return rule.op === 'neq' ? !has : has\n }\n case 'categoryId': {\n const ids = product.categoryIds ?? []\n const has = ids.includes(String(value))\n return rule.op === 'neq' ? !has : has\n }\n case 'priceUsd': {\n const [min, max] = productPriceRange(product)\n const numeric = Number(value)\n if (!Number.isFinite(numeric)) return false\n if (rule.op === 'lt') return min < numeric\n if (rule.op === 'gt') return max > numeric\n if (rule.op === 'neq') return min !== numeric || max !== numeric\n return min <= numeric && numeric <= max\n }\n case 'name': {\n const name = (product.name ?? '').toLowerCase()\n const needle = String(value).toLowerCase()\n if (rule.op === 'contains') return name.includes(needle)\n if (rule.op === 'neq') return name !== needle\n return name === needle\n }\n case 'type': {\n const matches = product.type === value\n return rule.op === 'neq' ? !matches : matches\n }\n default:\n return false\n }\n}\n\n/**\n * Whether a product's own fields answer a SMART collection's rules — its\n * tags, categories, prices, name and type, and nothing about whether it is\n * on sale. That is the storefront query's scope (`deletedAt == null`,\n * `status == 'active'`), so membership stored on the product never goes stale\n * when a product is archived or brought back.\n *\n * A collection with no rules holds nothing.\n */\n/** What smart-collection membership reads of a product — a draft's too. */\nexport interface MembershipProduct {\n name?: string\n type?: ProductType\n tags?: string[]\n categoryIds?: string[]\n variants?: ReadonlyArray<{ id?: string; priceUsd?: number | null }>\n}\n\nexport function smartCollectionMatches(\n product: MembershipProduct,\n collection: Pick<HostCollection, 'rules' | 'matchAll'>,\n): boolean {\n const rules = collection.rules ?? []\n if (rules.length === 0) return false\n const matcher = (rule: CollectionRule) => ruleMatches(product as HostProduct, rule)\n return collection.matchAll === false\n ? rules.some(matcher)\n : rules.every(matcher)\n}\n\n/**\n * Smart-collection membership: draft/archived/deleted products never\n * match; manual collections answer from productIds.\n */\nexport function matchesCollection(\n product: HostProduct,\n collection: HostCollection,\n productId?: string,\n): boolean {\n if (product.deletedAt || product.status !== 'active') return false\n if (collection.mode === 'manual') {\n return productId != null &&\n (collection.productIds ?? []).includes(productId)\n }\n return smartCollectionMatches(product, collection)\n}\n\n/**\n * The stored membership of a product in the host's SMART collections\n * (AGL-3321): the ids of those whose rules it answers\n * ({@link smartCollectionMatches}), sorted.\n *\n * The storefront reads a smart collection whose rules no query can hold (a\n * NOT, an OR, a name rule, a price over) as `collectionIds array-contains\n * <id>` — so the membership is WRITTEN, by every product writer from the\n * host's smart collections as they stand, and by the collection-membership\n * route for every product whenever a smart collection's rules change or it\n * is deleted. `tools/scripts/backfill-product-collection-ids.mjs` stamps the\n * products written before; the two answer\n * `tools/scripts/lib/product-collection-ids.fixtures.json`.\n */\nexport const PRODUCT_COLLECTION_IDS = 'collectionIds'\n\n/** A smart collection as membership is computed from it. */\nexport interface SmartCollectionRules {\n id: string\n rules?: CollectionRule[]\n matchAll?: boolean\n}\n\nexport function productCollectionIds(\n product: MembershipProduct,\n smartCollections: readonly SmartCollectionRules[],\n): string[] {\n return smartCollections\n .filter((collection) => smartCollectionMatches(product, collection))\n .map((collection) => collection.id)\n .sort()\n}\n\n/**\n * Lifts a legacy Commerce Starter doc (AGL-90: flat name/priceUsd/\n * inventory/imageUrl) into the catalog shape with a single default\n * variant. Already-lifted docs pass through unchanged.\n */\nexport function liftLegacyProduct(\n raw: Partial<HostProduct> & { name?: string },\n): HostProduct {\n if (Array.isArray(raw.variants) && raw.variants.length > 0) {\n return raw as HostProduct\n }\n const priceUsd = Number(raw.priceUsd ?? 0)\n return {\n ...raw,\n name: raw.name ?? 'Product',\n slug: raw.slug || commerceSlug(raw.name ?? 'product'),\n type: raw.type ?? 'physical',\n status: raw.status ?? 'active',\n variants: [\n {\n id: 'default',\n priceUsd: Number.isFinite(priceUsd) ? priceUsd : 0,\n inventory: raw.inventory ?? null,\n },\n ],\n }\n}\n\n/** Actionable error, or null when the product is storable. */\nexport function validateProduct(product: HostProduct): string | null {\n if (!product.name?.trim()) return 'Product name is required'\n if (!product.slug || product.slug !== commerceSlug(product.slug)) {\n return 'Slug must be lowercase letters, numbers, and dashes'\n }\n if (!['physical', 'digital', 'service'].includes(product.type)) {\n return 'Unknown product type'\n }\n if (!['draft', 'active', 'archived'].includes(product.status)) {\n return 'Unknown product status'\n }\n const options = product.options ?? []\n if (options.length > COMMERCE_MAX_OPTIONS) {\n return `At most ${COMMERCE_MAX_OPTIONS} options per product`\n }\n for (const option of options) {\n if (!option.name?.trim()) return 'Option names are required'\n if ((option.values?.length ?? 0) === 0) {\n return `Option \"${option.name}\" needs at least one value`\n }\n if (option.values.length > COMMERCE_MAX_OPTION_VALUES) {\n return `Option \"${option.name}\" has too many values`\n }\n if (new Set(option.values).size !== option.values.length) {\n return `Option \"${option.name}\" has duplicate values`\n }\n }\n // A variant keys its selections by option name, so two options with one\n // name cannot both be selected (AGL-3066).\n if (new Set(options.map((option) => option.name.trim())).size !== options.length) {\n return 'Each option needs its own name'\n }\n const modifierProblem = modifierGroupsProblem(product.modifierGroups)\n if (modifierProblem) return modifierProblem\n const variants = product.variants ?? []\n if (variants.length === 0) return 'Products need at least one variant'\n if (variants.length > COMMERCE_MAX_VARIANTS) {\n return `At most ${COMMERCE_MAX_VARIANTS} variants per product`\n }\n const ids = new Set<string>()\n const skus = new Set<string>()\n for (const variant of variants) {\n if (!variant.id) return 'Variants need stable ids'\n if (ids.has(variant.id)) return 'Variant ids must be unique'\n ids.add(variant.id)\n if (variant.priceUsd === undefined || variant.priceUsd === null) {\n return 'Set a price for every variant'\n }\n const price = Number(variant.priceUsd)\n if (!Number.isFinite(price) || price < 0) {\n return 'Variant prices must be zero or more'\n }\n if (price > COMMERCE_MAX_PRICE_USD) {\n return `Prices are capped at $${COMMERCE_MAX_PRICE_USD}`\n }\n if (variant.sku) {\n if (skus.has(variant.sku)) return 'Variant SKUs must be unique'\n skus.add(variant.sku)\n }\n if (\n variant.compareAtPriceUsd != null &&\n Number(variant.compareAtPriceUsd) <= price\n ) {\n return 'Compare-at price must exceed the price'\n }\n }\n return null\n}\n\n/** Actionable error, or null when the collection is storable. */\nexport function validateCollection(collection: HostCollection): string | null {\n if (!collection.name?.trim()) return 'Collection name is required'\n if (\n !collection.slug ||\n collection.slug !== commerceSlug(collection.slug)\n ) {\n return 'Slug must be lowercase letters, numbers, and dashes'\n }\n if (collection.mode !== 'manual' && collection.mode !== 'smart') {\n return 'Unknown collection mode'\n }\n if (collection.mode === 'smart') {\n const rules = collection.rules ?? []\n if (rules.length === 0) return 'Smart collections need at least one rule'\n for (const rule of rules) {\n if (\n !['tag', 'categoryId', 'priceUsd', 'name', 'type'].includes(rule.field)\n ) {\n return 'Unknown rule field'\n }\n if (!['eq', 'neq', 'lt', 'gt', 'contains'].includes(rule.op)) {\n return 'Unknown rule operator'\n }\n if (rule.value === '' || rule.value == null) {\n return 'Rule values are required'\n }\n }\n }\n return null\n}\n"],"names":["modifierGroupsProblem","nameSearchFields","draftProductChannelFacts","COMMERCE_MAX_PRICE_USD","productParcelDimensions","product","facts","shipping","side","value","number","Number","isFinite","Math","min","lengthCm","widthCm","heightCm","undefined","draftProductShippingFacts","normalizeProductShippingFacts","typed","String","trim","toUpperCase","test","originCountry","raw","round","hsCode","replace","slice","Object","keys","length","normalizePostalAddress","address","field","text","country","registersWithinCap","registers","cap","createdMs","r","createdAt","toMillis","seconds","ranked","sort","a","b","$id","localeCompare","Set","max","map","COMMERCE_MAX_VARIANTS","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_OPTION_VALUES","COMMERCE_SLUG_MAX_LENGTH","commerceSlug","name","normalize","toLowerCase","productSearchFields","flatten","read","variants","variant","filter","Boolean","skus","sku","barcodes","barcode","priceFromCents","productPriceFromCents","productPriceRange","productStockFields","inventory","productInventory","soldOut","oversellPolicy","expandVariantMatrix","options","usable","option","values","combos","next","combo","push","renameProductOptions","names","current","index","size","selections","axisOf","optionAxesOf","moved","changed","key","entries","axis","get","Map","taken","forEach","prototype","hasOwnProperty","call","has","set","add","findIndex","includes","variantHasPrice","price","priceUsd","productPriceMissing","some","productCopyPatch","patch","channel","description","Array","isArray","tags","tag","categoryIds","seoTitle","seoDescription","seo","title","optionNames","every","assign","unpricedProductDraft","proposal","takenSlugs","nowMs","base","slug","suffix","seen","id","toString","type","status","createdAtMs","updatedAtMs","findVariant","find","prices","total","canPurchase","variantId","quantity","item","stockShortfall","available","stockTrackingApplies","subscription","subscriptionOptional","resolveCheckoutBillingMode","requested","adjustVariantInventory","delta","locationId","inventoryByLocation","buckets","reduce","sum","count","appliedVariantInventoryDelta","before","transferVariantInventory","fromLocationId","toLocationId","isLowStock","threshold","lowStockThreshold","ruleMatches","rule","op","ids","numeric","needle","matches","smartCollectionMatches","collection","rules","matcher","matchAll","matchesCollection","productId","deletedAt","mode","productIds","PRODUCT_COLLECTION_IDS","productCollectionIds","smartCollections","liftLegacyProduct","validateProduct","modifierProblem","modifierGroups","compareAtPriceUsd","validateCollection"],"mappings":"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"}
|