@aglyn/plugins-commerce 1.0.0-beta.277 → 1.0.0-beta.282
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 +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- package/src/lib/testing/fake-firestore.js.map +1 -1
package/src/lib/server/cart.js
CHANGED
|
@@ -97,6 +97,17 @@ import { cartCookieName, mintCartId, readCartId } from "./cart-cookie.js";
|
|
|
97
97
|
} else if (action === 'remove') {
|
|
98
98
|
cart.lines = CommerceModel.removeCartLine(cart, line);
|
|
99
99
|
} else {
|
|
100
|
+
// A product sold in person only (AGL-4060) never enters a basket: it
|
|
101
|
+
// answers as no product at all, as its page does. A line already in
|
|
102
|
+
// the cart stays removable and resolves as unavailable below.
|
|
103
|
+
if (line.quantity > 0 && isDocumentId(line.productId)) {
|
|
104
|
+
const productSnapshot = await hostRef.collection('products').doc(line.productId).get();
|
|
105
|
+
if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {
|
|
106
|
+
return res.status(404).json({
|
|
107
|
+
error: 'Unknown product'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
100
111
|
cart.lines = CommerceModel.upsertCartLine(cart, line, action === 'set' ? 'set' : 'add');
|
|
101
112
|
}
|
|
102
113
|
// AGL-1769: never mint an EMPTY cart. `clear` returns above before the
|
|
@@ -134,7 +145,7 @@ import { cartCookieName, mintCartId, readCartId } from "./cart-cookie.js";
|
|
|
134
145
|
var _variant_options, _variant_options1, _ref, _variant_imageUrl;
|
|
135
146
|
var _product_mediaUrls, _product_mediaUrls1;
|
|
136
147
|
const product = productsById.get(line.productId);
|
|
137
|
-
if (!product ||
|
|
148
|
+
if (!product || !CommerceModel.productListedOnline(product)) {
|
|
138
149
|
var _ref1;
|
|
139
150
|
return _extends({}, line, {
|
|
140
151
|
name: (_ref1 = product == null ? void 0 : product.name) != null ? _ref1 : 'Unavailable product',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/cart.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { cartCookieName, mintCartId, readCartId } from './cart-cookie'\n\nexport interface ResolvedCartLine extends CommerceModel.CartLine {\n name: string\n variantLabel?: string\n unitAmountCents: number\n imageUrl?: string\n /** Line no longer purchasable (deleted/draft/sold out). */\n unavailable?: boolean\n}\n\n/**\n * Server-backed cart (AGL-293): cookie-keyed doc under\n * `hosts/{hostId}/carts`. GET returns the resolved cart; POST mutates\n * with {action: add|set|remove|clear}. Lines resolve against product\n * docs on every read so prices/names never go stale, and unavailable\n * lines surface instead of silently charging.\n */\nexport const cartHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n if (!isPost && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isPost\n ? typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n : {}\n // AGL-1769: BOTH components of the path this handler writes come from the\n // caller — `hostId` off the body, `cartId` off a cookie whose name embeds\n // `hostId` — and `.doc()` appends a slash-separated PATH rather than taking\n // one opaque id. So an unvalidated pair named the nesting as well as the\n // document: a `hostId` of `a/b/c` wrote under `hosts/a/b/c`, invisible to\n // every console list because they resolve the host doc first.\n //\n // This handler is the one visitor-facing write in the AGL-1763 sweep that\n // needs NO credentials — no session, no member gate, no signature — so it is\n // the one place the id rule has to hold on its own.\n const hostId = String((isPost ? body.hostId : req.query.hostId) ?? '')\n // The message names BOTH causes it now covers (`762621581`): the guard reads\n // as \"absent\" but also refuses a hostId that is a path rather than an id, and\n // a caller told only \"missing\" would go looking for the wrong mistake.\n // AGL-1771: `isDocumentId`, not the cart module's old `isCartId` alias — this\n // value is a HOST id, and the rule it has to satisfy was never about carts.\n if (!isDocumentId(hostId))\n return res.status(400).json({ error: 'Missing or invalid hostId' })\n\n const cookieName = cartCookieName(hostId)\n // A cookie that is not a single document id reads as NO cart: a GET returns\n // an empty one and a POST mints a fresh id below, exactly as for a first-time\n // visitor. Nothing is stranded — a path that was never a cart id was never a\n // basket anyone filled.\n let cartId = readCartId(req.cookies, hostId)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n\n try {\n if (!cartId) {\n if (!isPost) return res.status(200).json({ lines: [], count: 0 })\n cartId = mintCartId()\n res.setHeader(\n 'Set-Cookie',\n `${cookieName}=${cartId}; Path=/; Max-Age=${60 * 60 * 24 * 90}; ` +\n 'HttpOnly; SameSite=Lax; Secure',\n )\n }\n const cartRef = hostRef.collection('carts').doc(cartId)\n const cartSnapshot = await cartRef.get()\n const cart: CommerceModel.HostCart = (cartSnapshot.data() as any) ?? { lines: [] }\n\n if (isPost) {\n const action = String(body.action ?? 'add')\n const line: CommerceModel.CartLine = {\n productId: String(body.productId ?? ''),\n ...(body.variantId ? { variantId: String(body.variantId) } : {}),\n // Clamped at the door as well as in the model (AGL-2285): the route\n // used `Math.round(Number(...))` alone, which yields `NaN` for\n // `{\"quantity\":\"x\"}` and slipped past `upsertCartLine`'s `<= 0` guard.\n quantity: Math.min(\n CommerceModel.CART_MAX_QUANTITY,\n Math.max(\n -CommerceModel.CART_MAX_QUANTITY,\n Number.isFinite(Number(body.quantity ?? 1))\n ? Math.round(Number(body.quantity ?? 1))\n : 0,\n ),\n ),\n }\n if (action === 'clear') {\n cart.lines = []\n } else if (!line.productId) {\n return res.status(400).json({ error: 'Missing productId' })\n } else if (action === 'remove') {\n cart.lines = CommerceModel.removeCartLine(cart, line)\n } else {\n cart.lines = CommerceModel.upsertCartLine(\n cart,\n line,\n action === 'set' ? 'set' : 'add',\n )\n }\n // AGL-1769: never mint an EMPTY cart. `clear` returns above before the\n // `!line.productId` check, so `{ hostId, action: 'clear' }` — no\n // product, valid or otherwise, and no credentials — used to create a\n // document per request holding `lines: []` and two timestamps. A `remove`\n // or a `set 0` that empties an absent basket is the same request wearing\n // a productId. A cart that would store no lines is not a cart.\n //\n // AGL-1760's test — does refusing discard money or work that already\n // occurred — is passed trivially here: there is no basket to strand. The\n // caller still gets its cookie and its empty-cart response, so the next\n // add lands on this id and creates the document properly.\n if (cartSnapshot.exists || cart.lines.length > 0) {\n await cartRef.set(\n {\n lines: cart.lines,\n updatedAtMs: Date.now(),\n ...(cartSnapshot.exists ? {} : { createdAtMs: Date.now() }),\n },\n { merge: true },\n )\n }\n }\n\n // Resolve display lines from product docs — never trust stored data.\n const uniqueProductIds = [...new Set(cart.lines.map((l) => l.productId))]\n const productSnapshots = await Promise.all(\n uniqueProductIds.map((id) =>\n hostRef.collection('products').doc(id).get(),\n ),\n )\n const productsById = new Map(\n productSnapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as any) : null,\n ]),\n )\n const lines: ResolvedCartLine[] = cart.lines.map((line) => {\n const product = productsById.get(line.productId)\n if (!product || product.deletedAt || product.status !== 'active') {\n return {\n ...line,\n name: product?.name ?? 'Unavailable product',\n unitAmountCents: 0,\n unavailable: true,\n }\n }\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n // A variant with no price yet (AGL-2916) is not for sale, and never a\n // line worth nothing.\n if (!variant || !CommerceModel.variantHasPrice(variant)) {\n return {\n ...line,\n name: product.name,\n unitAmountCents: 0,\n unavailable: true,\n }\n }\n return {\n ...line,\n name: product.name,\n ...(Object.keys(variant.options ?? {}).length\n ? { variantLabel: Object.values(variant.options ?? {}).join(' / ') }\n : {}),\n unitAmountCents: Math.round(Number(variant.priceUsd) * 100),\n ...(variant.imageUrl || product.mediaUrls?.[0] || product.imageUrl\n ? {\n imageUrl:\n variant.imageUrl ??\n product.mediaUrls?.[0] ??\n product.imageUrl,\n }\n : {}),\n ...(CommerceModel.canPurchase(product, variant.id, line.quantity)\n ? {}\n : { unavailable: true }),\n }\n })\n const subtotalCents = lines\n .filter((line) => !line.unavailable)\n .reduce((sum, line) => sum + line.unitAmountCents * line.quantity, 0)\n return res.status(200).json({\n lines,\n count: CommerceModel.cartCount(cart),\n subtotalCents,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Cart unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","isDocumentId","cartCookieName","mintCartId","readCartId","cartHandler","req","res","isPost","method","status","json","error","body","JSON","parse","hostId","String","query","cookieName","cartId","cookies","firestore","app","hostRef","collection","doc","cartSnapshot","lines","count","setHeader","cartRef","get","cart","data","action","line","productId","variantId","quantity","Math","min","CART_MAX_QUANTITY","max","Number","isFinite","round","removeCartLine","upsertCartLine","exists","length","set","updatedAtMs","Date","now","createdAtMs","merge","uniqueProductIds","Set","map","l","productSnapshots","Promise","all","id","productsById","Map","snapshot","liftLegacyProduct","variant","product","deletedAt","name","unitAmountCents","unavailable","variants","find","item","variantHasPrice","Object","keys","options","variantLabel","values","join","priceUsd","imageUrl","mediaUrls","canPurchase","subtotalCents","filter","reduce","sum","cartCount","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/cart.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { cartCookieName, mintCartId, readCartId } from './cart-cookie'\n\nexport interface ResolvedCartLine extends CommerceModel.CartLine {\n name: string\n variantLabel?: string\n unitAmountCents: number\n imageUrl?: string\n /** Line no longer purchasable (deleted/draft/sold out). */\n unavailable?: boolean\n}\n\n/**\n * Server-backed cart (AGL-293): cookie-keyed doc under\n * `hosts/{hostId}/carts`. GET returns the resolved cart; POST mutates\n * with {action: add|set|remove|clear}. Lines resolve against product\n * docs on every read so prices/names never go stale, and unavailable\n * lines surface instead of silently charging.\n */\nexport const cartHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n if (!isPost && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isPost\n ? typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n : {}\n // AGL-1769: BOTH components of the path this handler writes come from the\n // caller — `hostId` off the body, `cartId` off a cookie whose name embeds\n // `hostId` — and `.doc()` appends a slash-separated PATH rather than taking\n // one opaque id. So an unvalidated pair named the nesting as well as the\n // document: a `hostId` of `a/b/c` wrote under `hosts/a/b/c`, invisible to\n // every console list because they resolve the host doc first.\n //\n // This handler is the one visitor-facing write in the AGL-1763 sweep that\n // needs NO credentials — no session, no member gate, no signature — so it is\n // the one place the id rule has to hold on its own.\n const hostId = String((isPost ? body.hostId : req.query.hostId) ?? '')\n // The message names BOTH causes it now covers (`762621581`): the guard reads\n // as \"absent\" but also refuses a hostId that is a path rather than an id, and\n // a caller told only \"missing\" would go looking for the wrong mistake.\n // AGL-1771: `isDocumentId`, not the cart module's old `isCartId` alias — this\n // value is a HOST id, and the rule it has to satisfy was never about carts.\n if (!isDocumentId(hostId))\n return res.status(400).json({ error: 'Missing or invalid hostId' })\n\n const cookieName = cartCookieName(hostId)\n // A cookie that is not a single document id reads as NO cart: a GET returns\n // an empty one and a POST mints a fresh id below, exactly as for a first-time\n // visitor. Nothing is stranded — a path that was never a cart id was never a\n // basket anyone filled.\n let cartId = readCartId(req.cookies, hostId)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n\n try {\n if (!cartId) {\n if (!isPost) return res.status(200).json({ lines: [], count: 0 })\n cartId = mintCartId()\n res.setHeader(\n 'Set-Cookie',\n `${cookieName}=${cartId}; Path=/; Max-Age=${60 * 60 * 24 * 90}; ` +\n 'HttpOnly; SameSite=Lax; Secure',\n )\n }\n const cartRef = hostRef.collection('carts').doc(cartId)\n const cartSnapshot = await cartRef.get()\n const cart: CommerceModel.HostCart = (cartSnapshot.data() as any) ?? { lines: [] }\n\n if (isPost) {\n const action = String(body.action ?? 'add')\n const line: CommerceModel.CartLine = {\n productId: String(body.productId ?? ''),\n ...(body.variantId ? { variantId: String(body.variantId) } : {}),\n // Clamped at the door as well as in the model (AGL-2285): the route\n // used `Math.round(Number(...))` alone, which yields `NaN` for\n // `{\"quantity\":\"x\"}` and slipped past `upsertCartLine`'s `<= 0` guard.\n quantity: Math.min(\n CommerceModel.CART_MAX_QUANTITY,\n Math.max(\n -CommerceModel.CART_MAX_QUANTITY,\n Number.isFinite(Number(body.quantity ?? 1))\n ? Math.round(Number(body.quantity ?? 1))\n : 0,\n ),\n ),\n }\n if (action === 'clear') {\n cart.lines = []\n } else if (!line.productId) {\n return res.status(400).json({ error: 'Missing productId' })\n } else if (action === 'remove') {\n cart.lines = CommerceModel.removeCartLine(cart, line)\n } else {\n // A product sold in person only (AGL-4060) never enters a basket: it\n // answers as no product at all, as its page does. A line already in\n // the cart stays removable and resolves as unavailable below.\n if (line.quantity > 0 && isDocumentId(line.productId)) {\n const productSnapshot = await hostRef.collection('products').doc(line.productId).get()\n if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {\n return res.status(404).json({ error: 'Unknown product' })\n }\n }\n cart.lines = CommerceModel.upsertCartLine(\n cart,\n line,\n action === 'set' ? 'set' : 'add',\n )\n }\n // AGL-1769: never mint an EMPTY cart. `clear` returns above before the\n // `!line.productId` check, so `{ hostId, action: 'clear' }` — no\n // product, valid or otherwise, and no credentials — used to create a\n // document per request holding `lines: []` and two timestamps. A `remove`\n // or a `set 0` that empties an absent basket is the same request wearing\n // a productId. A cart that would store no lines is not a cart.\n //\n // AGL-1760's test — does refusing discard money or work that already\n // occurred — is passed trivially here: there is no basket to strand. The\n // caller still gets its cookie and its empty-cart response, so the next\n // add lands on this id and creates the document properly.\n if (cartSnapshot.exists || cart.lines.length > 0) {\n await cartRef.set(\n {\n lines: cart.lines,\n updatedAtMs: Date.now(),\n ...(cartSnapshot.exists ? {} : { createdAtMs: Date.now() }),\n },\n { merge: true },\n )\n }\n }\n\n // Resolve display lines from product docs — never trust stored data.\n const uniqueProductIds = [...new Set(cart.lines.map((l) => l.productId))]\n const productSnapshots = await Promise.all(\n uniqueProductIds.map((id) =>\n hostRef.collection('products').doc(id).get(),\n ),\n )\n const productsById = new Map(\n productSnapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as any) : null,\n ]),\n )\n const lines: ResolvedCartLine[] = cart.lines.map((line) => {\n const product = productsById.get(line.productId)\n if (!product || !CommerceModel.productListedOnline(product)) {\n return {\n ...line,\n name: product?.name ?? 'Unavailable product',\n unitAmountCents: 0,\n unavailable: true,\n }\n }\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n // A variant with no price yet (AGL-2916) is not for sale, and never a\n // line worth nothing.\n if (!variant || !CommerceModel.variantHasPrice(variant)) {\n return {\n ...line,\n name: product.name,\n unitAmountCents: 0,\n unavailable: true,\n }\n }\n return {\n ...line,\n name: product.name,\n ...(Object.keys(variant.options ?? {}).length\n ? { variantLabel: Object.values(variant.options ?? {}).join(' / ') }\n : {}),\n unitAmountCents: Math.round(Number(variant.priceUsd) * 100),\n ...(variant.imageUrl || product.mediaUrls?.[0] || product.imageUrl\n ? {\n imageUrl:\n variant.imageUrl ??\n product.mediaUrls?.[0] ??\n product.imageUrl,\n }\n : {}),\n ...(CommerceModel.canPurchase(product, variant.id, line.quantity)\n ? {}\n : { unavailable: true }),\n }\n })\n const subtotalCents = lines\n .filter((line) => !line.unavailable)\n .reduce((sum, line) => sum + line.unitAmountCents * line.quantity, 0)\n return res.status(200).json({\n lines,\n count: CommerceModel.cartCount(cart),\n subtotalCents,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Cart unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","isDocumentId","cartCookieName","mintCartId","readCartId","cartHandler","req","res","isPost","method","status","json","error","body","JSON","parse","hostId","String","query","cookieName","cartId","cookies","firestore","app","hostRef","collection","doc","cartSnapshot","lines","count","setHeader","cartRef","get","cart","data","action","line","productId","variantId","quantity","Math","min","CART_MAX_QUANTITY","max","Number","isFinite","round","removeCartLine","productSnapshot","exists","productSellsOnline","upsertCartLine","length","set","updatedAtMs","Date","now","createdAtMs","merge","uniqueProductIds","Set","map","l","productSnapshots","Promise","all","id","productsById","Map","snapshot","liftLegacyProduct","variant","product","productListedOnline","name","unitAmountCents","unavailable","variants","find","item","variantHasPrice","Object","keys","options","variantLabel","values","join","priceUsd","imageUrl","mediaUrls","canPurchase","subtotalCents","filter","reduce","sum","cartCount","console"],"mappings":"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"}
|
|
@@ -57,6 +57,18 @@ import * as CommerceModel from "../model/index.js";
|
|
|
57
57
|
* all of it rather than over a window of the catalog.
|
|
58
58
|
*/ const MANUAL_COLLECTION_MAX = 1000;
|
|
59
59
|
/** Ids per `getAll`, well inside the Admin SDK's comfortable batch. */ const GET_ALL_CHUNK = 100;
|
|
60
|
+
/**
|
|
61
|
+
* Reads one page may make to fill itself past products sold in person only
|
|
62
|
+
* (AGL-4060). `sellOnline` is absent on every product stored before the
|
|
63
|
+
* switch, and a query cannot ask for "absent or true", so the query reads
|
|
64
|
+
* the catalog's scope and the page drops what is not sold online — and reads
|
|
65
|
+
* on, up to this many times, so a run of in-person-only products does not
|
|
66
|
+
* leave a short page in the middle of the grid.
|
|
67
|
+
*/ const CATALOG_FILL_READS = 4;
|
|
68
|
+
/**
|
|
69
|
+
* Rows each price-bound read scans for the cheapest (or dearest) product
|
|
70
|
+
* sold online, past any sold in person only.
|
|
71
|
+
*/ const PRICE_BOUND_SCAN = 10;
|
|
60
72
|
function toRow(docSnapshot) {
|
|
61
73
|
var _raw_deletedAt, _product_name;
|
|
62
74
|
const raw = docSnapshot.data();
|
|
@@ -78,7 +90,10 @@ function toRow(docSnapshot) {
|
|
|
78
90
|
/** A row's value at a queried path, as the query sees it. */ function rowValue(row, path) {
|
|
79
91
|
return row.values[path];
|
|
80
92
|
}
|
|
81
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* A held row a shopper may be shown: live and active, as the query's scope,
|
|
95
|
+
* and sold online (AGL-4060).
|
|
96
|
+
*/ const onSale = (row)=>STOREFRONT_CATALOG_BASE.every((filter)=>rowMatches(row, filter)) && CommerceModel.productSellsOnline(row.product);
|
|
82
97
|
/** One plan predicate over one row, with Firestore's meaning of each operator. */ function rowMatches(row, filter) {
|
|
83
98
|
const actual = rowValue(row, filter.path);
|
|
84
99
|
if (actual === undefined) return false;
|
|
@@ -302,7 +317,7 @@ const compareValues = (a, b)=>{
|
|
|
302
317
|
}
|
|
303
318
|
async function runPublicCatalog(params, reads) {
|
|
304
319
|
var _params_collectionSlug, _params_categorySlug, _params_collectionId, _params_categoryId, _params_tag, _params_query, _params_ids, _params_limit;
|
|
305
|
-
var _categoriesSnapshot_docs_
|
|
320
|
+
var _categoriesSnapshot_docs_;
|
|
306
321
|
const { hostId } = params;
|
|
307
322
|
const collectionSlug = (_params_collectionSlug = params.collectionSlug) != null ? _params_collectionSlug : '';
|
|
308
323
|
const categorySlug = (_params_categorySlug = params.categorySlug) != null ? _params_categorySlug : '';
|
|
@@ -450,7 +465,9 @@ async function runPublicCatalog(params, reads) {
|
|
|
450
465
|
* rules no query can hold. The plan still decides what is applied — the
|
|
451
466
|
* same refusals as the query path, so a grid behaves the same whatever it
|
|
452
467
|
* lists — and its predicates are answered over every held row.
|
|
453
|
-
*/ const answerHeld = (
|
|
468
|
+
*/ const answerHeld = (rows, options)=>{
|
|
469
|
+
// A member sold in person only is no row of the grid (AGL-4060).
|
|
470
|
+
const held = rows.filter((row)=>CommerceModel.productSellsOnline(row.product));
|
|
454
471
|
const request = {
|
|
455
472
|
base: STOREFRONT_CATALOG_BASE,
|
|
456
473
|
clauses: [
|
|
@@ -576,18 +593,38 @@ async function runPublicCatalog(params, reads) {
|
|
|
576
593
|
path: 'priceFromCents',
|
|
577
594
|
direction
|
|
578
595
|
}
|
|
579
|
-
})).limit(
|
|
580
|
-
|
|
581
|
-
|
|
596
|
+
})).limit(PRICE_BOUND_SCAN).get();
|
|
597
|
+
// The first price sold online (AGL-4060): an in-person-only product's price
|
|
598
|
+
// never anchors the slider.
|
|
599
|
+
const boundOf = (snapshot)=>{
|
|
600
|
+
var _snapshot_docs_find;
|
|
601
|
+
return snapshot == null ? void 0 : (_snapshot_docs_find = snapshot.docs.find((docSnapshot)=>CommerceModel.productSellsOnline(docSnapshot.data()))) == null ? void 0 : _snapshot_docs_find.get('priceFromCents');
|
|
602
|
+
};
|
|
603
|
+
const bounds = Promise.all([
|
|
582
604
|
unpriced ? pricedBy(unpriced, 'asc') : null,
|
|
583
605
|
unpriced ? pricedBy(unpriced, 'desc') : null
|
|
584
606
|
]);
|
|
585
|
-
|
|
586
|
-
const rows =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
607
|
+
// A page of rows sold online, read on past the ones sold in person only.
|
|
608
|
+
const rows = [];
|
|
609
|
+
let last;
|
|
610
|
+
let more = false;
|
|
611
|
+
for(let read = 0; read < CATALOG_FILL_READS && rows.length < max; read += 1){
|
|
612
|
+
const need = max - rows.length;
|
|
613
|
+
const snapshot = await pageQuery.limit(need + 1).get();
|
|
614
|
+
const docs = snapshot.docs.slice(0, need);
|
|
615
|
+
for (const docSnapshot of docs){
|
|
616
|
+
last = docSnapshot;
|
|
617
|
+
const row = toRow(docSnapshot);
|
|
618
|
+
if (row && CommerceModel.productSellsOnline(row.product)) rows.push(row);
|
|
619
|
+
}
|
|
620
|
+
more = snapshot.docs.length > need;
|
|
621
|
+
if (!more || !last) break;
|
|
622
|
+
pageQuery = ordered(plan).startAfter(last.get(plan.orderBy.path), last.id);
|
|
623
|
+
}
|
|
624
|
+
const [lowest, highest] = await bounds;
|
|
625
|
+
const low = boundOf(lowest);
|
|
626
|
+
const high = boundOf(highest);
|
|
627
|
+
return result(rows, plan, _extends({}, last && more ? {
|
|
591
628
|
nextCursor: encodeCursor(last.get(plan.orderBy.path), last.id)
|
|
592
629
|
} : {}, typeof low === 'number' && typeof high === 'number' ? {
|
|
593
630
|
priceBounds: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/catalog.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 { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryFilter,\n type ListQueryPlan,\n type ListQueryRefusal,\n type ListQueryRequest,\n type ListQuerySort,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { applyListQuery } from '@aglyn/tenant-data-admin/server/list-query'\nimport {\n smartCollectionScope,\n STOREFRONT_CATALOG_BASE,\n STOREFRONT_CATALOG_DECLARATION,\n STOREFRONT_CATALOG_SORTS,\n STOREFRONT_PRODUCT_TYPES,\n storefrontCatalogSort,\n} from '../constants/storefront-catalog-query'\nimport * as CommerceModel from '../model'\n\nexport interface PublicCatalogItem {\n id: string\n name: string\n slug: string\n type: CommerceModel.ProductType\n priceUsd: number\n maxPriceUsd: number\n compareAtPriceUsd?: number\n imageUrl?: string\n soldOut: boolean\n tags?: string[]\n /**\n * Listed before any variant has a price (AGL-3676): the card says \"Price\n * coming soon\" instead of a price, and nothing sells it until one is set.\n * Absent for every product with a price.\n */\n priceComingSoon?: true\n /** How many variants the product sells as: one adds straight to a cart. */\n variantCount: number\n /**\n * The one variant a card's quick add puts in the cart — present only for\n * a product with exactly one variant; several need the product page.\n */\n defaultVariantId?: string\n}\n\n/**\n * The photo a product's card shows: the first non-blank of its media, its\n * legacy single image, then the first variant photo. Reading only the first\n * media entry passed a blank one through as the card's image — and a blank\n * resolves to no `src`, so a product WITH a photo drew an empty tile.\n */\nexport function productCardImage(\n product: Pick<CommerceModel.HostProduct, 'mediaUrls' | 'imageUrl' | 'variants'>,\n): string | undefined {\n const candidates = [\n ...(product.mediaUrls ?? []),\n product.imageUrl,\n ...(product.variants ?? []).map((variant) => variant.imageUrl),\n ]\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim()\n }\n return undefined\n}\n\n/** Host category surfaced to grids for filter chips (AGL-561). */\nexport interface PublicCatalogCategory {\n id: string\n name: string\n slug: string\n}\n\n/** Price facet surfaced to grids to bound a range slider (AGL-564). */\nexport interface PublicCatalogPriceBounds {\n minCents: number\n maxCents: number\n}\n\n/**\n * A control the query could not apply, and why — the plan's own refusal,\n * which the grid names through `listQueryRefusals` with the declaration's\n * fields and the categories it shows.\n */\nexport type PublicCatalogRefusal = ListQueryRefusal\n\n/** Non-negative integer cents from a query param; undefined otherwise. */\nfunction parsePriceCents(value: unknown): number | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n const cents = Number(raw)\n if (!Number.isFinite(cents) || cents < 0) return undefined\n return Math.round(cents)\n}\n\n/** A catalog query, already parsed and bounded. */\nexport interface PublicCatalogQuery {\n hostId: string\n collectionId?: string\n collectionSlug?: string\n categoryId?: string\n categorySlug?: string\n tag?: string\n /** Search: the first word, matched as a word prefix of the product name. */\n query?: string\n type?: CommerceModel.ProductType | ''\n /** The In stock chip: only products a card would not mark sold out. */\n inStock?: boolean\n /** Explicit id list in the given order (wishlist rendering, AGL-297). */\n ids?: string[]\n minPriceCents?: number\n maxPriceCents?: number\n sort?: string\n /** Page size, 1–100. */\n limit?: number\n /** The `nextCursor` a previous page returned — continue after it. */\n after?: string\n /** Include category + price facets in the result. */\n facets?: boolean\n /**\n * Optional per-render dedupe (see {@link CatalogReadScope}). Absent for a\n * one-off call such as the API route, where there is nothing to share with.\n */\n reads?: CatalogReadScope\n}\n\n/**\n * Reads shared by every catalog query in ONE render, for ONE host.\n *\n * Two grids on one page asking the SAME question share one answer, and every\n * grid that shows category chips shares one read of the taxonomy. A grid\n * asking a different question — another collection, another sort — runs its\n * own query, because each query now reads only the page it returns rather\n * than the whole catalog (AGL-3321).\n *\n * Scoped to one host and one render deliberately: it holds promises, not\n * data, so nothing is cached across requests and no staleness window opens.\n * Reusing a scope for a second host would serve that host the first one's\n * catalog, which is why it is created per render rather than per module.\n */\nexport interface CatalogReadScope {\n categories?: Promise<FirebaseFirestore.QuerySnapshot>\n results?: Map<string, Promise<PublicCatalogResult>>\n}\n\n/** A fresh read scope. One per render, never reused across hosts. */\nexport function createCatalogReadScope(): CatalogReadScope {\n return {}\n}\n\n/** What both the API route and the SSR seed hand to a product grid. */\nexport interface PublicCatalogResult {\n items: PublicCatalogItem[]\n /** Pass back as `after` for the next page; absent on the last one. */\n nextCursor?: string\n categories?: PublicCatalogCategory[]\n priceBounds?: PublicCatalogPriceBounds\n /** Controls in force that the query could not apply (AGL-3321). */\n refused?: PublicCatalogRefusal[]\n /** Said about what WAS applied, e.g. that search reads one word. */\n notices?: string[]\n}\n\n/** Most products one page returns. */\nconst CATALOG_PAGE_MAX = 100\n/** Page size when a grid names none. */\nconst CATALOG_PAGE_DEFAULT = 24\n/**\n * Members of a MANUAL collection read by id. A hand-curated list is its own\n * complete set, so it is read whole and every visitor control is answered over\n * all of it rather than over a window of the catalog.\n */\nconst MANUAL_COLLECTION_MAX = 1000\n/** Ids per `getAll`, well inside the Admin SDK's comfortable batch. */\nconst GET_ALL_CHUNK = 100\n\n/** One product as the catalog reads it: stored, lifted, and keyed. */\ninterface CatalogRow {\n id: string\n product: CommerceModel.HostProduct\n /**\n * The fields the catalog query reads, as the writers stamp them — derived\n * through the same library functions the writers spread, so a held row\n * (a manual collection's member, an id list's entry) answers a predicate\n * exactly as the stored document will once it is backfilled.\n */\n values: Record<string, unknown>\n}\n\nfunction toRow(docSnapshot: FirebaseFirestore.DocumentSnapshot): CatalogRow | null {\n const raw = docSnapshot.data() as Record<string, unknown> | undefined\n if (!raw) return null\n const product = CommerceModel.liftLegacyProduct(raw as never)\n return {\n id: docSnapshot.id,\n product,\n values: {\n ...raw,\n status: product.status,\n type: product.type,\n deletedAt: raw['deletedAt'] ?? null,\n ...CommerceModel.productSearchFields({\n name: String(product.name ?? ''),\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n },\n }\n}\n\n/** A row's value at a queried path, as the query sees it. */\nfunction rowValue(row: CatalogRow, path: string): unknown {\n return row.values[path]\n}\n\n/** A held row a shopper may be shown: live and active, as the query's scope. */\nconst onSale = (row: CatalogRow) =>\n STOREFRONT_CATALOG_BASE.every((filter) => rowMatches(row, filter))\n\n/** One plan predicate over one row, with Firestore's meaning of each operator. */\nfunction rowMatches(row: CatalogRow, filter: ListQueryFilter): boolean {\n const actual = rowValue(row, filter.path)\n if (actual === undefined) return false\n const value = filter.value\n const list = Array.isArray(value) ? (value as readonly unknown[]) : [value]\n switch (filter.op) {\n case '==':\n return actual === value\n case '!=':\n return actual !== null && actual !== value\n case 'in':\n return list.includes(actual)\n case 'array-contains':\n return Array.isArray(actual) && actual.includes(value)\n case 'array-contains-any':\n return Array.isArray(actual) && actual.some((member) => list.includes(member))\n case '<':\n return typeof actual === typeof value && (actual as number) < (value as number)\n case '<=':\n return typeof actual === typeof value && (actual as number) <= (value as number)\n case '>':\n return typeof actual === typeof value && (actual as number) > (value as number)\n case '>=':\n return typeof actual === typeof value && (actual as number) >= (value as number)\n default:\n return false\n }\n}\n\n/** A cursor: the last row's order value and id, opaque to the grid. */\nfunction encodeCursor(value: unknown, id: string): string {\n return Buffer.from(JSON.stringify([value ?? null, id])).toString('base64url')\n}\n\nfunction decodeCursor(raw: string | undefined): { value: unknown; id: string } | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'))\n if (!Array.isArray(parsed) || parsed.length !== 2) return null\n const [value, id] = parsed\n if (typeof id !== 'string' || !id) return null\n if (value !== null && typeof value !== 'string' && typeof value !== 'number') return null\n return { value, id }\n } catch {\n return null\n }\n}\n\nconst compareValues = (a: unknown, b: unknown): number => {\n if (a === b) return 0\n // A row without the ordered value sorts after every row with one.\n if (a === undefined || a === null) return 1\n if (b === undefined || b === null) return -1\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * One page of rows already held WHOLE — a manual collection, an id list, a\n * smart collection's scan — ordered by `key` then id, after the cursor.\n */\nfunction pageHeldRows(\n rows: readonly CatalogRow[],\n key: (row: CatalogRow) => unknown,\n direction: 'asc' | 'desc',\n after: string | undefined,\n max: number,\n): { rows: CatalogRow[]; nextCursor?: string } {\n const signed = (value: number) => (direction === 'desc' ? -value : value)\n const ordered = [...rows].sort(\n (a, b) =>\n signed(compareValues(key(a), key(b))) || signed(compareValues(a.id, b.id)),\n )\n const cursor = decodeCursor(after)\n const start = cursor\n ? ordered.findIndex(\n (row) =>\n signed(compareValues(key(row), cursor.value)) > 0 ||\n (compareValues(key(row), cursor.value) === 0 &&\n signed(compareValues(row.id, cursor.id)) > 0),\n )\n : 0\n if (start === -1) return { rows: [] }\n const page = ordered.slice(start, start + max)\n const last = page[page.length - 1]\n return {\n rows: page,\n ...(last && start + max < ordered.length\n ? { nextCursor: encodeCursor(key(last), last.id) }\n : {}),\n }\n}\n\n/** Documents by id, in chunks, dropping the ones that do not exist. */\nasync function readProductsById(\n productsRef: FirebaseFirestore.CollectionReference,\n ids: readonly string[],\n): Promise<CatalogRow[]> {\n const firestore = firebaseAdmin.app().firestore()\n const rows: CatalogRow[] = []\n for (let start = 0; start < ids.length; start += GET_ALL_CHUNK) {\n const refs = ids.slice(start, start + GET_ALL_CHUNK).map((id) => productsRef.doc(id))\n if (!refs.length) continue\n const snapshots = await firestore.getAll(...refs)\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n const row = toRow(snapshot)\n if (row) rows.push(row)\n }\n }\n return rows\n}\n\n/** The card a grid renders for one product. */\nfunction toItem(row: CatalogRow): PublicCatalogItem {\n const { product } = row\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n const primary = product.variants[0]\n return {\n id: row.id,\n name: product.name,\n slug: product.slug,\n type: product.type,\n priceUsd: minPrice,\n maxPriceUsd: maxPrice,\n ...(primary?.compareAtPriceUsd\n ? { compareAtPriceUsd: primary.compareAtPriceUsd }\n : {}),\n ...(productCardImage(product) ? { imageUrl: productCardImage(product) } : {}),\n // The same verdict the In stock chip queries (`soldOut`), from the one\n // function every stock writer stores it through.\n soldOut: CommerceModel.productStockFields(product).soldOut,\n ...(product.tags?.length ? { tags: product.tags } : {}),\n ...(product.variants.some((variant) => CommerceModel.variantHasPrice(variant))\n ? {}\n : { priceComingSoon: true as const }),\n variantCount: product.variants.length,\n ...(product.variants.length === 1 && primary?.id ? { defaultVariantId: primary.id } : {}),\n }\n}\n\n/**\n * The plan's refusals, as the plan made them; the grid words each one. A\n * search refused because a smart collection already holds the query's one\n * array clause gets a reason a shopper can read.\n */\nfunction refusalsOf(plan: ListQueryPlan): PublicCatalogRefusal[] {\n return plan.refused.map((refusal) =>\n refusal.clause === 'search' && refusal.reason.startsWith('this list is already narrowed')\n ? {\n ...refusal,\n reason: 'this collection is already narrowed by a tag or category, which search cannot combine with',\n }\n : refusal,\n )\n}\n\n/** How a sort reads in the grid's select, for the notice below. */\nconst SORT_LABELS: Readonly<Record<string, string>> = {\n nameLower: 'Name',\n createdAtMs: 'Newest',\n}\n\n/** The operators that make a field a range, which must lead the order. */\nconst RANGE_OPS: ReadonlySet<string> = new Set(['<', '<=', '>', '>=', '!='])\n\n/**\n * The plan, ordered the way the visitor asked when a range forced its field.\n *\n * A range leads the order, so while the price slider (or a smart collection's\n * \"price under\" rule) is set the query is ordered by price: \"high to low\"\n * stays high to low, and any other sort — Name, Newest — becomes low to high\n * and is SAID to, rather than silently replaced (AGL-3321).\n *\n * The notice names the cause. The slider is the visitor's to clear, so the\n * sort they chose returns when they do; a collection's rule is in the scope\n * (`request.base`), which no visitor can clear, so that notice says the\n * collection is always ordered by price.\n */\nfunction planFor(request: ListQueryRequest, sort: ListQuerySort): ListQueryPlan {\n const plan = planListQuery(STOREFRONT_CATALOG_DECLARATION, request, nameSearchNormalizers)\n if (plan.orderBy.path !== sort.path) {\n const asked = SORT_LABELS[sort.path]\n if (!asked) return plan\n const byRule = (request.base ?? []).some(\n (filter) => filter.path === plan.orderBy.path && RANGE_OPS.has(filter.op),\n )\n return {\n ...plan,\n notices: [\n ...plan.notices,\n byRule\n ? `Sorted by price, low to high — this collection picks its products by price, so it cannot be sorted by ${asked}.`\n : `Sorted by price, low to high, while a price range is set — ${asked} applies again when it is cleared.`,\n ],\n }\n }\n return plan.orderBy.direction !== sort.direction ? { ...plan, orderBy: sort } : plan\n}\n\n/** A price bound as the grid's slider shows it, for a refusal's sentence. */\nconst dollars = (cents: number) => `$${(cents / 100).toFixed(cents % 100 ? 2 : 0)}`\n\n/**\n * Public catalog listing (AGL-291): active products for storefront blocks —\n * a collection, a category or a tag, searched, filtered by type and price,\n * sorted, and paged by cursor (AGL-561, AGL-564).\n *\n * EVERY CONTROL IS ON THE QUERY (AGL-3321). This used to read `limit(500)`\n * products and narrow them in memory, so a catalog past five hundred answered\n * every search, chip and slider from an arbitrary five hundred of them. Now:\n *\n * - the catalog, a category or a tag — ONE query built by the list query\n * plan (`STOREFRONT_CATALOG_DECLARATION`), paged by its own cursor;\n * - a SMART collection whose rules a query can hold — the same query, with\n * the rules as its scope (`smartCollectionScope`);\n * - a MANUAL collection, or an explicit id list — read whole by id, which\n * is a complete set, and answered over all of it;\n * - a smart collection whose rules no query can hold (a NOT, an OR, a name\n * rule, a price over) — the same query, scoped by the membership every\n * product writer stores (`collectionIds array-contains <id>`), which the\n * collection-membership route re-stamps whenever the rules change.\n *\n * What one query cannot hold — two array clauses, two ranges — is refused by\n * name in `refused` and not applied, never applied to some rows and not\n * others. Read-only public data; prices here are display-only (charges\n * always come from the docs server-side).\n *\n * Extracted from the handler (AGL-659) so the site-page enricher seeds a\n * grid's first page through THIS function rather than a reimplementation\n * of it. A seed that filters or sorts differently from the API would\n * server-render one set of products and hydrate to another.\n */\nexport async function queryPublicCatalog(\n params: PublicCatalogQuery,\n): Promise<PublicCatalogResult> {\n const { reads, ...question } = params\n if (!reads) return runPublicCatalog(question)\n const key = JSON.stringify(question)\n reads.results ??= new Map()\n const shared = reads.results.get(key)\n if (shared) return shared\n // Stored BEFORE it is awaited, so grids running together under one\n // `Promise.all` share the one in-flight read rather than racing to store it.\n const pending = runPublicCatalog(question, reads)\n reads.results.set(key, pending)\n return pending\n}\n\nasync function runPublicCatalog(\n params: Omit<PublicCatalogQuery, 'reads'>,\n reads?: CatalogReadScope,\n): Promise<PublicCatalogResult> {\n const { hostId } = params\n const collectionSlug = params.collectionSlug ?? ''\n const categorySlug = params.categorySlug ?? ''\n // Id-based filters are preferred (rename-safe, AGL-343); slugs remain\n // for legacy screens and template-URL resolution.\n const collectionIdParam = params.collectionId ?? ''\n const categoryIdParam = params.categoryId ?? ''\n const tag = (params.tag ?? '').trim()\n const query = (params.query ?? '').trim()\n const type = STOREFRONT_PRODUCT_TYPES.includes(params.type as CommerceModel.ProductType)\n ? (params.type as CommerceModel.ProductType)\n : ''\n const ids = (params.ids ?? []).slice(0, CATALOG_PAGE_MAX)\n const asked = Math.floor(Number(params.limit ?? CATALOG_PAGE_DEFAULT))\n const max =\n Number.isFinite(asked) && asked > 0\n ? Math.min(CATALOG_PAGE_MAX, asked)\n : CATALOG_PAGE_DEFAULT\n const sort = storefrontCatalogSort(params.sort)\n const wantFacets = Boolean(params.facets)\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productsRef = hostRef.collection('products')\n const sharedCategories = () =>\n (reads\n ? (reads.categories ??=\n hostRef.collection('productCategories').limit(200).get())\n : hostRef.collection('productCategories').limit(200).get())\n\n // Wishlist and related rendering (AGL-297): an explicit id list is its own\n // complete set — read it by id, keep what is on sale, in the given order.\n if (ids.length) {\n const order = new Map(ids.map((id, index) => [id, index]))\n const rows = (await readProductsById(productsRef, ids)).filter(onSale)\n rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))\n return { items: rows.slice(0, max).map(toItem) }\n }\n\n const [collectionsSnapshot, categoriesSnapshot, collectionByIdSnapshot, facetsSnapshot] =\n await Promise.all([\n // limit(5) not 1: content collections share this path and a slug is\n // unique only within a kind (AGL-954) — the catalog match is picked\n // out below rather than trusting whichever doc came back first.\n !collectionIdParam && collectionSlug\n ? hostRef\n .collection('collections')\n .where('slug', '==', collectionSlug)\n .limit(5)\n .get()\n : null,\n !categoryIdParam && categorySlug\n ? hostRef\n .collection('productCategories')\n .where('slug', '==', categorySlug)\n .limit(1)\n .get()\n : null,\n collectionIdParam\n ? hostRef.collection('collections').doc(collectionIdParam).get()\n : null,\n wantFacets ? sharedCategories() : null,\n ])\n const collectionById =\n collectionByIdSnapshot?.exists &&\n Aglyn.hostCollectionKind(collectionByIdSnapshot.data()) === 'catalog'\n ? collectionByIdSnapshot\n : undefined\n const collectionDoc =\n collectionById ??\n collectionsSnapshot?.docs.find(\n (docSnapshot) => Aglyn.hostCollectionKind(docSnapshot.data()) === 'catalog',\n )\n const collection = collectionDoc\n ? (collectionDoc.data() as CommerceModel.HostCollection)\n : undefined\n const categoryId = categoryIdParam || categoriesSnapshot?.docs[0]?.id || ''\n\n // Category facets (AGL-561): id/name/slug for grid filter chips,\n // parent-tree order preserved (order, then name). The taxonomy is a\n // bounded picker, read whole.\n const categories: PublicCatalogCategory[] | undefined = facetsSnapshot\n ? facetsSnapshot.docs\n .map((docSnapshot) => {\n const data = docSnapshot.data() as CommerceModel.ProductCategory\n return {\n id: docSnapshot.id,\n name: String(data.name ?? ''),\n slug: String(data.slug ?? ''),\n order: Number(data.order ?? 0),\n }\n })\n .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name))\n .map(({ id, name, slug }) => ({ id, name, slug }))\n : undefined\n\n // The visitor's controls, as clauses on the one declaration. A price\n // clause carries the dollars the slider showed, for a refusal's sentence.\n const priceClauses: ListFilterClause[] = [\n ...(params.minPriceCents != null\n ? [{\n field: 'price',\n op: '>=',\n value: String(params.minPriceCents),\n label: dollars(params.minPriceCents),\n }]\n : []),\n ...(params.maxPriceCents != null\n ? [{\n field: 'price',\n op: '<=',\n value: String(params.maxPriceCents),\n label: dollars(params.maxPriceCents),\n }]\n : []),\n ]\n const otherClauses: ListFilterClause[] = [\n ...(categoryId ? [{ field: 'category', op: 'contains', value: categoryId }] : []),\n ...(tag ? [{ field: 'tag', op: 'contains', value: tag }] : []),\n ...(type ? [{ field: 'type', op: 'equals', value: type }] : []),\n ...(params.inStock ? [{ field: 'stock', op: 'is', value: 'false' }] : []),\n ]\n // The words typed, as a quick search hands them over: the plan searches by\n // the first and says so when there were more.\n const search = query.split(/\\s+/).filter(Boolean)\n\n const result = (\n rows: CatalogRow[],\n plan: ListQueryPlan,\n extra: Partial<PublicCatalogResult> = {},\n ): PublicCatalogResult => {\n const refused = refusalsOf(plan)\n const notices = [...plan.notices, ...(extra.notices ?? [])]\n return {\n items: rows.map(toItem),\n ...(extra.nextCursor ? { nextCursor: extra.nextCursor } : {}),\n ...(categories ? { categories } : {}),\n ...(extra.priceBounds ? { priceBounds: extra.priceBounds } : {}),\n ...(refused.length ? { refused } : {}),\n ...(notices.length ? { notices } : {}),\n }\n }\n\n /*\n * A collection held WHOLE: a manual one's members, or a smart one whose\n * rules no query can hold. The plan still decides what is applied — the\n * same refusals as the query path, so a grid behaves the same whatever it\n * lists — and its predicates are answered over every held row.\n */\n const answerHeld = (\n held: CatalogRow[],\n options: { manualOrder?: Map<string, number>; notices?: string[] },\n ): PublicCatalogResult => {\n const request: ListQueryRequest = {\n base: STOREFRONT_CATALOG_BASE,\n clauses: [...otherClauses, ...priceClauses],\n search,\n sort,\n }\n const plan = planFor(request, sort)\n const unpriced = planFor({ ...request, clauses: otherClauses }, sort)\n const matched = held.filter((row) => plan.filters.every((filter) => rowMatches(row, filter)))\n // Bounds span every OTHER control, so the slider stays anchored while the\n // visitor narrows it.\n const bounding = held.filter((row) =>\n unpriced.filters.every((filter) => rowMatches(row, filter)),\n )\n const priceBounds =\n wantFacets && bounding.length\n ? {\n minCents: Math.min(...bounding.map((row) => Number(rowValue(row, 'priceFromCents')))),\n maxCents: Math.max(...bounding.map((row) => Number(rowValue(row, 'priceFromCents')))),\n }\n : undefined\n // A manual collection keeps the merchant's order unless the visitor (or\n // the author) chose a price or recency sort.\n const keepManual =\n options.manualOrder && plan.orderBy.path === STOREFRONT_CATALOG_SORTS.name.path\n const page = keepManual\n ? pageHeldRows(\n matched,\n (row) => options.manualOrder?.get(row.id) ?? Number.MAX_SAFE_INTEGER,\n 'asc',\n params.after,\n max,\n )\n : pageHeldRows(\n matched,\n (row) => rowValue(row, plan.orderBy.path),\n plan.orderBy.direction,\n params.after,\n max,\n )\n return result(page.rows, plan, {\n ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),\n ...(priceBounds ? { priceBounds } : {}),\n ...(options.notices ? { notices: options.notices } : {}),\n })\n }\n\n let scope: ListQueryFilter[] = []\n if (collection) {\n if (collection.mode === 'manual') {\n const memberIds = (collection.productIds ?? []).slice(0, MANUAL_COLLECTION_MAX)\n const manualOrder = new Map(memberIds.map((id, index) => [id, index]))\n return answerHeld(await readProductsById(productsRef, memberIds), { manualOrder })\n }\n const smart = smartCollectionScope(collection)\n if (!(collection.rules ?? []).length) {\n // A smart collection with no rules holds nothing (`matchesCollection`).\n return result([], planFor({ base: STOREFRONT_CATALOG_BASE, clauses: [], sort }, sort))\n }\n if ('unservable' in smart) {\n // Rules no query can express (a NOT, an OR, a name rule, a price over)\n // are answered by the membership every product writer stores\n // (`collectionIds`, AGL-3321): one array clause on the same query, so\n // the grid's other array controls are refused by name beside it.\n scope = [\n {\n path: CommerceModel.PRODUCT_COLLECTION_IDS,\n op: 'array-contains',\n value: String(collectionDoc?.id ?? ''),\n },\n ]\n } else {\n scope = smart.filters\n // A \"type is not\" rule is `type in […]`; the visitor's Type chip is the\n // narrower question, answered inside it or not at all.\n const typeRule = scope.find((filter) => filter.path === 'type')\n if (typeRule && type) {\n const allowed = Array.isArray(typeRule.value) ? typeRule.value : [typeRule.value]\n if (!allowed.includes(type)) {\n return result([], planFor({ base: STOREFRONT_CATALOG_BASE, clauses: [], sort }, sort))\n }\n scope = scope.filter((filter) => filter !== typeRule)\n }\n }\n }\n\n // THE QUERY: every predicate, one order, a page and one row to spare.\n const request: ListQueryRequest = {\n base: [...STOREFRONT_CATALOG_BASE, ...scope],\n clauses: [...otherClauses, ...priceClauses],\n search,\n sort,\n }\n const plan = planFor(request, sort)\n const byId = firebaseAdmin.firestore.FieldPath.documentId()\n const ordered = (target: ListQueryPlan) =>\n applyListQuery(productsRef, target).orderBy(byId, target.orderBy.direction)\n let pageQuery = ordered(plan)\n const cursor = decodeCursor(params.after)\n if (cursor) pageQuery = pageQuery.startAfter(cursor.value, cursor.id)\n\n // Bounds span every OTHER control, so the slider stays anchored while the\n // visitor narrows it: the lowest and highest price the rest allows, as two\n // one-row reads ordered by price.\n const byPrice = STOREFRONT_CATALOG_SORTS['price-asc']\n const unpriced = wantFacets\n ? planFor({ ...request, clauses: otherClauses, sort: byPrice }, byPrice)\n : null\n const pricedBy = (target: ListQueryPlan, direction: 'asc' | 'desc') =>\n ordered({ ...target, orderBy: { path: 'priceFromCents', direction } }).limit(1).get()\n\n const [snapshot, lowest, highest] = await Promise.all([\n pageQuery.limit(max + 1).get(),\n unpriced ? pricedBy(unpriced, 'asc') : null,\n unpriced ? pricedBy(unpriced, 'desc') : null,\n ])\n const docs = snapshot.docs.slice(0, max)\n const rows = docs.map(toRow).filter((row): row is CatalogRow => Boolean(row))\n const last = docs[docs.length - 1]\n const low = lowest?.docs[0]?.get('priceFromCents')\n const high = highest?.docs[0]?.get('priceFromCents')\n return result(rows, plan, {\n ...(last && snapshot.docs.length > max\n ? { nextCursor: encodeCursor(last.get(plan.orderBy.path), last.id) }\n : {}),\n ...(typeof low === 'number' && typeof high === 'number'\n ? { priceBounds: { minCents: low, maxCents: high } }\n : {}),\n })\n}\n\n/** Public catalog listing endpoint — a thin parse over queryPublicCatalog. */\nexport const catalogHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n\n try {\n const result = await queryPublicCatalog({\n hostId,\n collectionSlug: String(req.query.collection ?? ''),\n categorySlug: String(req.query.category ?? ''),\n collectionId: String(req.query.collectionId ?? ''),\n categoryId: String(req.query.categoryId ?? ''),\n tag: String(req.query.tag ?? ''),\n query: String(req.query.q ?? ''),\n type: String(req.query.type ?? '') as CommerceModel.ProductType | '',\n inStock: String(req.query.inStock ?? '') === '1',\n ids: String(req.query.ids ?? '')\n .split(',')\n .map((id) => id.trim())\n .filter(Boolean),\n // Price-range filter (AGL-564): integer cents on the displayed price.\n minPriceCents: parsePriceCents(req.query.minPriceCents),\n maxPriceCents: parsePriceCents(req.query.maxPriceCents),\n sort: String(req.query.sort ?? 'name'),\n limit: Number(req.query.limit ?? CATALOG_PAGE_DEFAULT),\n after: String(req.query.after ?? ''),\n facets: String(req.query.facets ?? '') === '1',\n })\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=60, stale-while-revalidate=300',\n )\n return res.status(200).json(result)\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Catalog unavailable' })\n }\n}\n"],"names":["nameSearchNormalizers","Aglyn","planListQuery","firebaseAdmin","applyListQuery","smartCollectionScope","STOREFRONT_CATALOG_BASE","STOREFRONT_CATALOG_DECLARATION","STOREFRONT_CATALOG_SORTS","STOREFRONT_PRODUCT_TYPES","storefrontCatalogSort","CommerceModel","productCardImage","product","candidates","mediaUrls","imageUrl","variants","map","variant","candidate","trim","undefined","parsePriceCents","value","raw","String","cents","Number","isFinite","Math","round","createCatalogReadScope","CATALOG_PAGE_MAX","CATALOG_PAGE_DEFAULT","MANUAL_COLLECTION_MAX","GET_ALL_CHUNK","toRow","docSnapshot","data","liftLegacyProduct","id","values","status","type","deletedAt","productSearchFields","name","productStockFields","rowValue","row","path","onSale","every","filter","rowMatches","actual","list","Array","isArray","op","includes","some","member","encodeCursor","Buffer","from","JSON","stringify","toString","decodeCursor","parsed","parse","length","compareValues","a","b","pageHeldRows","rows","key","direction","after","max","signed","ordered","sort","cursor","start","findIndex","page","slice","last","nextCursor","readProductsById","productsRef","ids","firestore","app","refs","doc","snapshots","getAll","snapshot","exists","push","toItem","minPrice","maxPrice","productPriceRange","primary","slug","priceUsd","maxPriceUsd","compareAtPriceUsd","soldOut","tags","variantHasPrice","priceComingSoon","variantCount","defaultVariantId","refusalsOf","plan","refused","refusal","clause","reason","startsWith","SORT_LABELS","nameLower","createdAtMs","RANGE_OPS","Set","planFor","request","orderBy","asked","byRule","base","has","notices","dollars","toFixed","queryPublicCatalog","params","reads","question","runPublicCatalog","results","Map","shared","get","pending","set","categoriesSnapshot","lowest","highest","hostId","collectionSlug","categorySlug","collectionIdParam","collectionId","categoryIdParam","categoryId","tag","query","floor","limit","min","wantFacets","Boolean","facets","hostRef","collection","sharedCategories","categories","order","index","items","collectionsSnapshot","collectionByIdSnapshot","facetsSnapshot","Promise","all","where","collectionById","hostCollectionKind","collectionDoc","docs","find","localeCompare","priceClauses","minPriceCents","field","label","maxPriceCents","otherClauses","inStock","search","split","result","extra","priceBounds","answerHeld","held","options","clauses","unpriced","matched","filters","bounding","minCents","maxCents","keepManual","manualOrder","MAX_SAFE_INTEGER","scope","mode","memberIds","productIds","smart","rules","PRODUCT_COLLECTION_IDS","typeRule","allowed","byId","FieldPath","documentId","target","pageQuery","startAfter","byPrice","pricedBy","low","high","catalogHandler","req","res","json","error","category","q","setHeader","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/catalog.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 { nameSearchNormalizers } from '@aglyn/aglyn/app-utils/name-search'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n type ListQueryFilter,\n type ListQueryPlan,\n type ListQueryRefusal,\n type ListQueryRequest,\n type ListQuerySort,\n planListQuery,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { applyListQuery } from '@aglyn/tenant-data-admin/server/list-query'\nimport {\n smartCollectionScope,\n STOREFRONT_CATALOG_BASE,\n STOREFRONT_CATALOG_DECLARATION,\n STOREFRONT_CATALOG_SORTS,\n STOREFRONT_PRODUCT_TYPES,\n storefrontCatalogSort,\n} from '../constants/storefront-catalog-query'\nimport * as CommerceModel from '../model'\n\nexport interface PublicCatalogItem {\n id: string\n name: string\n slug: string\n type: CommerceModel.ProductType\n priceUsd: number\n maxPriceUsd: number\n compareAtPriceUsd?: number\n imageUrl?: string\n soldOut: boolean\n tags?: string[]\n /**\n * Listed before any variant has a price (AGL-3676): the card says \"Price\n * coming soon\" instead of a price, and nothing sells it until one is set.\n * Absent for every product with a price.\n */\n priceComingSoon?: true\n /** How many variants the product sells as: one adds straight to a cart. */\n variantCount: number\n /**\n * The one variant a card's quick add puts in the cart — present only for\n * a product with exactly one variant; several need the product page.\n */\n defaultVariantId?: string\n}\n\n/**\n * The photo a product's card shows: the first non-blank of its media, its\n * legacy single image, then the first variant photo. Reading only the first\n * media entry passed a blank one through as the card's image — and a blank\n * resolves to no `src`, so a product WITH a photo drew an empty tile.\n */\nexport function productCardImage(\n product: Pick<CommerceModel.HostProduct, 'mediaUrls' | 'imageUrl' | 'variants'>,\n): string | undefined {\n const candidates = [\n ...(product.mediaUrls ?? []),\n product.imageUrl,\n ...(product.variants ?? []).map((variant) => variant.imageUrl),\n ]\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim()\n }\n return undefined\n}\n\n/** Host category surfaced to grids for filter chips (AGL-561). */\nexport interface PublicCatalogCategory {\n id: string\n name: string\n slug: string\n}\n\n/** Price facet surfaced to grids to bound a range slider (AGL-564). */\nexport interface PublicCatalogPriceBounds {\n minCents: number\n maxCents: number\n}\n\n/**\n * A control the query could not apply, and why — the plan's own refusal,\n * which the grid names through `listQueryRefusals` with the declaration's\n * fields and the categories it shows.\n */\nexport type PublicCatalogRefusal = ListQueryRefusal\n\n/** Non-negative integer cents from a query param; undefined otherwise. */\nfunction parsePriceCents(value: unknown): number | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n const cents = Number(raw)\n if (!Number.isFinite(cents) || cents < 0) return undefined\n return Math.round(cents)\n}\n\n/** A catalog query, already parsed and bounded. */\nexport interface PublicCatalogQuery {\n hostId: string\n collectionId?: string\n collectionSlug?: string\n categoryId?: string\n categorySlug?: string\n tag?: string\n /** Search: the first word, matched as a word prefix of the product name. */\n query?: string\n type?: CommerceModel.ProductType | ''\n /** The In stock chip: only products a card would not mark sold out. */\n inStock?: boolean\n /** Explicit id list in the given order (wishlist rendering, AGL-297). */\n ids?: string[]\n minPriceCents?: number\n maxPriceCents?: number\n sort?: string\n /** Page size, 1–100. */\n limit?: number\n /** The `nextCursor` a previous page returned — continue after it. */\n after?: string\n /** Include category + price facets in the result. */\n facets?: boolean\n /**\n * Optional per-render dedupe (see {@link CatalogReadScope}). Absent for a\n * one-off call such as the API route, where there is nothing to share with.\n */\n reads?: CatalogReadScope\n}\n\n/**\n * Reads shared by every catalog query in ONE render, for ONE host.\n *\n * Two grids on one page asking the SAME question share one answer, and every\n * grid that shows category chips shares one read of the taxonomy. A grid\n * asking a different question — another collection, another sort — runs its\n * own query, because each query now reads only the page it returns rather\n * than the whole catalog (AGL-3321).\n *\n * Scoped to one host and one render deliberately: it holds promises, not\n * data, so nothing is cached across requests and no staleness window opens.\n * Reusing a scope for a second host would serve that host the first one's\n * catalog, which is why it is created per render rather than per module.\n */\nexport interface CatalogReadScope {\n categories?: Promise<FirebaseFirestore.QuerySnapshot>\n results?: Map<string, Promise<PublicCatalogResult>>\n}\n\n/** A fresh read scope. One per render, never reused across hosts. */\nexport function createCatalogReadScope(): CatalogReadScope {\n return {}\n}\n\n/** What both the API route and the SSR seed hand to a product grid. */\nexport interface PublicCatalogResult {\n items: PublicCatalogItem[]\n /** Pass back as `after` for the next page; absent on the last one. */\n nextCursor?: string\n categories?: PublicCatalogCategory[]\n priceBounds?: PublicCatalogPriceBounds\n /** Controls in force that the query could not apply (AGL-3321). */\n refused?: PublicCatalogRefusal[]\n /** Said about what WAS applied, e.g. that search reads one word. */\n notices?: string[]\n}\n\n/** Most products one page returns. */\nconst CATALOG_PAGE_MAX = 100\n/** Page size when a grid names none. */\nconst CATALOG_PAGE_DEFAULT = 24\n/**\n * Members of a MANUAL collection read by id. A hand-curated list is its own\n * complete set, so it is read whole and every visitor control is answered over\n * all of it rather than over a window of the catalog.\n */\nconst MANUAL_COLLECTION_MAX = 1000\n/** Ids per `getAll`, well inside the Admin SDK's comfortable batch. */\nconst GET_ALL_CHUNK = 100\n/**\n * Reads one page may make to fill itself past products sold in person only\n * (AGL-4060). `sellOnline` is absent on every product stored before the\n * switch, and a query cannot ask for \"absent or true\", so the query reads\n * the catalog's scope and the page drops what is not sold online — and reads\n * on, up to this many times, so a run of in-person-only products does not\n * leave a short page in the middle of the grid.\n */\nconst CATALOG_FILL_READS = 4\n/**\n * Rows each price-bound read scans for the cheapest (or dearest) product\n * sold online, past any sold in person only.\n */\nconst PRICE_BOUND_SCAN = 10\n\n/** One product as the catalog reads it: stored, lifted, and keyed. */\ninterface CatalogRow {\n id: string\n product: CommerceModel.HostProduct\n /**\n * The fields the catalog query reads, as the writers stamp them — derived\n * through the same library functions the writers spread, so a held row\n * (a manual collection's member, an id list's entry) answers a predicate\n * exactly as the stored document will once it is backfilled.\n */\n values: Record<string, unknown>\n}\n\nfunction toRow(docSnapshot: FirebaseFirestore.DocumentSnapshot): CatalogRow | null {\n const raw = docSnapshot.data() as Record<string, unknown> | undefined\n if (!raw) return null\n const product = CommerceModel.liftLegacyProduct(raw as never)\n return {\n id: docSnapshot.id,\n product,\n values: {\n ...raw,\n status: product.status,\n type: product.type,\n deletedAt: raw['deletedAt'] ?? null,\n ...CommerceModel.productSearchFields({\n name: String(product.name ?? ''),\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n },\n }\n}\n\n/** A row's value at a queried path, as the query sees it. */\nfunction rowValue(row: CatalogRow, path: string): unknown {\n return row.values[path]\n}\n\n/**\n * A held row a shopper may be shown: live and active, as the query's scope,\n * and sold online (AGL-4060).\n */\nconst onSale = (row: CatalogRow) =>\n STOREFRONT_CATALOG_BASE.every((filter) => rowMatches(row, filter)) &&\n CommerceModel.productSellsOnline(row.product)\n\n/** One plan predicate over one row, with Firestore's meaning of each operator. */\nfunction rowMatches(row: CatalogRow, filter: ListQueryFilter): boolean {\n const actual = rowValue(row, filter.path)\n if (actual === undefined) return false\n const value = filter.value\n const list = Array.isArray(value) ? (value as readonly unknown[]) : [value]\n switch (filter.op) {\n case '==':\n return actual === value\n case '!=':\n return actual !== null && actual !== value\n case 'in':\n return list.includes(actual)\n case 'array-contains':\n return Array.isArray(actual) && actual.includes(value)\n case 'array-contains-any':\n return Array.isArray(actual) && actual.some((member) => list.includes(member))\n case '<':\n return typeof actual === typeof value && (actual as number) < (value as number)\n case '<=':\n return typeof actual === typeof value && (actual as number) <= (value as number)\n case '>':\n return typeof actual === typeof value && (actual as number) > (value as number)\n case '>=':\n return typeof actual === typeof value && (actual as number) >= (value as number)\n default:\n return false\n }\n}\n\n/** A cursor: the last row's order value and id, opaque to the grid. */\nfunction encodeCursor(value: unknown, id: string): string {\n return Buffer.from(JSON.stringify([value ?? null, id])).toString('base64url')\n}\n\nfunction decodeCursor(raw: string | undefined): { value: unknown; id: string } | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'))\n if (!Array.isArray(parsed) || parsed.length !== 2) return null\n const [value, id] = parsed\n if (typeof id !== 'string' || !id) return null\n if (value !== null && typeof value !== 'string' && typeof value !== 'number') return null\n return { value, id }\n } catch {\n return null\n }\n}\n\nconst compareValues = (a: unknown, b: unknown): number => {\n if (a === b) return 0\n // A row without the ordered value sorts after every row with one.\n if (a === undefined || a === null) return 1\n if (b === undefined || b === null) return -1\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * One page of rows already held WHOLE — a manual collection, an id list, a\n * smart collection's scan — ordered by `key` then id, after the cursor.\n */\nfunction pageHeldRows(\n rows: readonly CatalogRow[],\n key: (row: CatalogRow) => unknown,\n direction: 'asc' | 'desc',\n after: string | undefined,\n max: number,\n): { rows: CatalogRow[]; nextCursor?: string } {\n const signed = (value: number) => (direction === 'desc' ? -value : value)\n const ordered = [...rows].sort(\n (a, b) =>\n signed(compareValues(key(a), key(b))) || signed(compareValues(a.id, b.id)),\n )\n const cursor = decodeCursor(after)\n const start = cursor\n ? ordered.findIndex(\n (row) =>\n signed(compareValues(key(row), cursor.value)) > 0 ||\n (compareValues(key(row), cursor.value) === 0 &&\n signed(compareValues(row.id, cursor.id)) > 0),\n )\n : 0\n if (start === -1) return { rows: [] }\n const page = ordered.slice(start, start + max)\n const last = page[page.length - 1]\n return {\n rows: page,\n ...(last && start + max < ordered.length\n ? { nextCursor: encodeCursor(key(last), last.id) }\n : {}),\n }\n}\n\n/** Documents by id, in chunks, dropping the ones that do not exist. */\nasync function readProductsById(\n productsRef: FirebaseFirestore.CollectionReference,\n ids: readonly string[],\n): Promise<CatalogRow[]> {\n const firestore = firebaseAdmin.app().firestore()\n const rows: CatalogRow[] = []\n for (let start = 0; start < ids.length; start += GET_ALL_CHUNK) {\n const refs = ids.slice(start, start + GET_ALL_CHUNK).map((id) => productsRef.doc(id))\n if (!refs.length) continue\n const snapshots = await firestore.getAll(...refs)\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n const row = toRow(snapshot)\n if (row) rows.push(row)\n }\n }\n return rows\n}\n\n/** The card a grid renders for one product. */\nfunction toItem(row: CatalogRow): PublicCatalogItem {\n const { product } = row\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n const primary = product.variants[0]\n return {\n id: row.id,\n name: product.name,\n slug: product.slug,\n type: product.type,\n priceUsd: minPrice,\n maxPriceUsd: maxPrice,\n ...(primary?.compareAtPriceUsd\n ? { compareAtPriceUsd: primary.compareAtPriceUsd }\n : {}),\n ...(productCardImage(product) ? { imageUrl: productCardImage(product) } : {}),\n // The same verdict the In stock chip queries (`soldOut`), from the one\n // function every stock writer stores it through.\n soldOut: CommerceModel.productStockFields(product).soldOut,\n ...(product.tags?.length ? { tags: product.tags } : {}),\n ...(product.variants.some((variant) => CommerceModel.variantHasPrice(variant))\n ? {}\n : { priceComingSoon: true as const }),\n variantCount: product.variants.length,\n ...(product.variants.length === 1 && primary?.id ? { defaultVariantId: primary.id } : {}),\n }\n}\n\n/**\n * The plan's refusals, as the plan made them; the grid words each one. A\n * search refused because a smart collection already holds the query's one\n * array clause gets a reason a shopper can read.\n */\nfunction refusalsOf(plan: ListQueryPlan): PublicCatalogRefusal[] {\n return plan.refused.map((refusal) =>\n refusal.clause === 'search' && refusal.reason.startsWith('this list is already narrowed')\n ? {\n ...refusal,\n reason: 'this collection is already narrowed by a tag or category, which search cannot combine with',\n }\n : refusal,\n )\n}\n\n/** How a sort reads in the grid's select, for the notice below. */\nconst SORT_LABELS: Readonly<Record<string, string>> = {\n nameLower: 'Name',\n createdAtMs: 'Newest',\n}\n\n/** The operators that make a field a range, which must lead the order. */\nconst RANGE_OPS: ReadonlySet<string> = new Set(['<', '<=', '>', '>=', '!='])\n\n/**\n * The plan, ordered the way the visitor asked when a range forced its field.\n *\n * A range leads the order, so while the price slider (or a smart collection's\n * \"price under\" rule) is set the query is ordered by price: \"high to low\"\n * stays high to low, and any other sort — Name, Newest — becomes low to high\n * and is SAID to, rather than silently replaced (AGL-3321).\n *\n * The notice names the cause. The slider is the visitor's to clear, so the\n * sort they chose returns when they do; a collection's rule is in the scope\n * (`request.base`), which no visitor can clear, so that notice says the\n * collection is always ordered by price.\n */\nfunction planFor(request: ListQueryRequest, sort: ListQuerySort): ListQueryPlan {\n const plan = planListQuery(STOREFRONT_CATALOG_DECLARATION, request, nameSearchNormalizers)\n if (plan.orderBy.path !== sort.path) {\n const asked = SORT_LABELS[sort.path]\n if (!asked) return plan\n const byRule = (request.base ?? []).some(\n (filter) => filter.path === plan.orderBy.path && RANGE_OPS.has(filter.op),\n )\n return {\n ...plan,\n notices: [\n ...plan.notices,\n byRule\n ? `Sorted by price, low to high — this collection picks its products by price, so it cannot be sorted by ${asked}.`\n : `Sorted by price, low to high, while a price range is set — ${asked} applies again when it is cleared.`,\n ],\n }\n }\n return plan.orderBy.direction !== sort.direction ? { ...plan, orderBy: sort } : plan\n}\n\n/** A price bound as the grid's slider shows it, for a refusal's sentence. */\nconst dollars = (cents: number) => `$${(cents / 100).toFixed(cents % 100 ? 2 : 0)}`\n\n/**\n * Public catalog listing (AGL-291): active products for storefront blocks —\n * a collection, a category or a tag, searched, filtered by type and price,\n * sorted, and paged by cursor (AGL-561, AGL-564).\n *\n * EVERY CONTROL IS ON THE QUERY (AGL-3321). This used to read `limit(500)`\n * products and narrow them in memory, so a catalog past five hundred answered\n * every search, chip and slider from an arbitrary five hundred of them. Now:\n *\n * - the catalog, a category or a tag — ONE query built by the list query\n * plan (`STOREFRONT_CATALOG_DECLARATION`), paged by its own cursor;\n * - a SMART collection whose rules a query can hold — the same query, with\n * the rules as its scope (`smartCollectionScope`);\n * - a MANUAL collection, or an explicit id list — read whole by id, which\n * is a complete set, and answered over all of it;\n * - a smart collection whose rules no query can hold (a NOT, an OR, a name\n * rule, a price over) — the same query, scoped by the membership every\n * product writer stores (`collectionIds array-contains <id>`), which the\n * collection-membership route re-stamps whenever the rules change.\n *\n * What one query cannot hold — two array clauses, two ranges — is refused by\n * name in `refused` and not applied, never applied to some rows and not\n * others. Read-only public data; prices here are display-only (charges\n * always come from the docs server-side).\n *\n * Extracted from the handler (AGL-659) so the site-page enricher seeds a\n * grid's first page through THIS function rather than a reimplementation\n * of it. A seed that filters or sorts differently from the API would\n * server-render one set of products and hydrate to another.\n */\nexport async function queryPublicCatalog(\n params: PublicCatalogQuery,\n): Promise<PublicCatalogResult> {\n const { reads, ...question } = params\n if (!reads) return runPublicCatalog(question)\n const key = JSON.stringify(question)\n reads.results ??= new Map()\n const shared = reads.results.get(key)\n if (shared) return shared\n // Stored BEFORE it is awaited, so grids running together under one\n // `Promise.all` share the one in-flight read rather than racing to store it.\n const pending = runPublicCatalog(question, reads)\n reads.results.set(key, pending)\n return pending\n}\n\nasync function runPublicCatalog(\n params: Omit<PublicCatalogQuery, 'reads'>,\n reads?: CatalogReadScope,\n): Promise<PublicCatalogResult> {\n const { hostId } = params\n const collectionSlug = params.collectionSlug ?? ''\n const categorySlug = params.categorySlug ?? ''\n // Id-based filters are preferred (rename-safe, AGL-343); slugs remain\n // for legacy screens and template-URL resolution.\n const collectionIdParam = params.collectionId ?? ''\n const categoryIdParam = params.categoryId ?? ''\n const tag = (params.tag ?? '').trim()\n const query = (params.query ?? '').trim()\n const type = STOREFRONT_PRODUCT_TYPES.includes(params.type as CommerceModel.ProductType)\n ? (params.type as CommerceModel.ProductType)\n : ''\n const ids = (params.ids ?? []).slice(0, CATALOG_PAGE_MAX)\n const asked = Math.floor(Number(params.limit ?? CATALOG_PAGE_DEFAULT))\n const max =\n Number.isFinite(asked) && asked > 0\n ? Math.min(CATALOG_PAGE_MAX, asked)\n : CATALOG_PAGE_DEFAULT\n const sort = storefrontCatalogSort(params.sort)\n const wantFacets = Boolean(params.facets)\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productsRef = hostRef.collection('products')\n const sharedCategories = () =>\n (reads\n ? (reads.categories ??=\n hostRef.collection('productCategories').limit(200).get())\n : hostRef.collection('productCategories').limit(200).get())\n\n // Wishlist and related rendering (AGL-297): an explicit id list is its own\n // complete set — read it by id, keep what is on sale, in the given order.\n if (ids.length) {\n const order = new Map(ids.map((id, index) => [id, index]))\n const rows = (await readProductsById(productsRef, ids)).filter(onSale)\n rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))\n return { items: rows.slice(0, max).map(toItem) }\n }\n\n const [collectionsSnapshot, categoriesSnapshot, collectionByIdSnapshot, facetsSnapshot] =\n await Promise.all([\n // limit(5) not 1: content collections share this path and a slug is\n // unique only within a kind (AGL-954) — the catalog match is picked\n // out below rather than trusting whichever doc came back first.\n !collectionIdParam && collectionSlug\n ? hostRef\n .collection('collections')\n .where('slug', '==', collectionSlug)\n .limit(5)\n .get()\n : null,\n !categoryIdParam && categorySlug\n ? hostRef\n .collection('productCategories')\n .where('slug', '==', categorySlug)\n .limit(1)\n .get()\n : null,\n collectionIdParam\n ? hostRef.collection('collections').doc(collectionIdParam).get()\n : null,\n wantFacets ? sharedCategories() : null,\n ])\n const collectionById =\n collectionByIdSnapshot?.exists &&\n Aglyn.hostCollectionKind(collectionByIdSnapshot.data()) === 'catalog'\n ? collectionByIdSnapshot\n : undefined\n const collectionDoc =\n collectionById ??\n collectionsSnapshot?.docs.find(\n (docSnapshot) => Aglyn.hostCollectionKind(docSnapshot.data()) === 'catalog',\n )\n const collection = collectionDoc\n ? (collectionDoc.data() as CommerceModel.HostCollection)\n : undefined\n const categoryId = categoryIdParam || categoriesSnapshot?.docs[0]?.id || ''\n\n // Category facets (AGL-561): id/name/slug for grid filter chips,\n // parent-tree order preserved (order, then name). The taxonomy is a\n // bounded picker, read whole.\n const categories: PublicCatalogCategory[] | undefined = facetsSnapshot\n ? facetsSnapshot.docs\n .map((docSnapshot) => {\n const data = docSnapshot.data() as CommerceModel.ProductCategory\n return {\n id: docSnapshot.id,\n name: String(data.name ?? ''),\n slug: String(data.slug ?? ''),\n order: Number(data.order ?? 0),\n }\n })\n .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name))\n .map(({ id, name, slug }) => ({ id, name, slug }))\n : undefined\n\n // The visitor's controls, as clauses on the one declaration. A price\n // clause carries the dollars the slider showed, for a refusal's sentence.\n const priceClauses: ListFilterClause[] = [\n ...(params.minPriceCents != null\n ? [{\n field: 'price',\n op: '>=',\n value: String(params.minPriceCents),\n label: dollars(params.minPriceCents),\n }]\n : []),\n ...(params.maxPriceCents != null\n ? [{\n field: 'price',\n op: '<=',\n value: String(params.maxPriceCents),\n label: dollars(params.maxPriceCents),\n }]\n : []),\n ]\n const otherClauses: ListFilterClause[] = [\n ...(categoryId ? [{ field: 'category', op: 'contains', value: categoryId }] : []),\n ...(tag ? [{ field: 'tag', op: 'contains', value: tag }] : []),\n ...(type ? [{ field: 'type', op: 'equals', value: type }] : []),\n ...(params.inStock ? [{ field: 'stock', op: 'is', value: 'false' }] : []),\n ]\n // The words typed, as a quick search hands them over: the plan searches by\n // the first and says so when there were more.\n const search = query.split(/\\s+/).filter(Boolean)\n\n const result = (\n rows: CatalogRow[],\n plan: ListQueryPlan,\n extra: Partial<PublicCatalogResult> = {},\n ): PublicCatalogResult => {\n const refused = refusalsOf(plan)\n const notices = [...plan.notices, ...(extra.notices ?? [])]\n return {\n items: rows.map(toItem),\n ...(extra.nextCursor ? { nextCursor: extra.nextCursor } : {}),\n ...(categories ? { categories } : {}),\n ...(extra.priceBounds ? { priceBounds: extra.priceBounds } : {}),\n ...(refused.length ? { refused } : {}),\n ...(notices.length ? { notices } : {}),\n }\n }\n\n /*\n * A collection held WHOLE: a manual one's members, or a smart one whose\n * rules no query can hold. The plan still decides what is applied — the\n * same refusals as the query path, so a grid behaves the same whatever it\n * lists — and its predicates are answered over every held row.\n */\n const answerHeld = (\n rows: CatalogRow[],\n options: { manualOrder?: Map<string, number>; notices?: string[] },\n ): PublicCatalogResult => {\n // A member sold in person only is no row of the grid (AGL-4060).\n const held = rows.filter((row) => CommerceModel.productSellsOnline(row.product))\n const request: ListQueryRequest = {\n base: STOREFRONT_CATALOG_BASE,\n clauses: [...otherClauses, ...priceClauses],\n search,\n sort,\n }\n const plan = planFor(request, sort)\n const unpriced = planFor({ ...request, clauses: otherClauses }, sort)\n const matched = held.filter((row) => plan.filters.every((filter) => rowMatches(row, filter)))\n // Bounds span every OTHER control, so the slider stays anchored while the\n // visitor narrows it.\n const bounding = held.filter((row) =>\n unpriced.filters.every((filter) => rowMatches(row, filter)),\n )\n const priceBounds =\n wantFacets && bounding.length\n ? {\n minCents: Math.min(...bounding.map((row) => Number(rowValue(row, 'priceFromCents')))),\n maxCents: Math.max(...bounding.map((row) => Number(rowValue(row, 'priceFromCents')))),\n }\n : undefined\n // A manual collection keeps the merchant's order unless the visitor (or\n // the author) chose a price or recency sort.\n const keepManual =\n options.manualOrder && plan.orderBy.path === STOREFRONT_CATALOG_SORTS.name.path\n const page = keepManual\n ? pageHeldRows(\n matched,\n (row) => options.manualOrder?.get(row.id) ?? Number.MAX_SAFE_INTEGER,\n 'asc',\n params.after,\n max,\n )\n : pageHeldRows(\n matched,\n (row) => rowValue(row, plan.orderBy.path),\n plan.orderBy.direction,\n params.after,\n max,\n )\n return result(page.rows, plan, {\n ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),\n ...(priceBounds ? { priceBounds } : {}),\n ...(options.notices ? { notices: options.notices } : {}),\n })\n }\n\n let scope: ListQueryFilter[] = []\n if (collection) {\n if (collection.mode === 'manual') {\n const memberIds = (collection.productIds ?? []).slice(0, MANUAL_COLLECTION_MAX)\n const manualOrder = new Map(memberIds.map((id, index) => [id, index]))\n return answerHeld(await readProductsById(productsRef, memberIds), { manualOrder })\n }\n const smart = smartCollectionScope(collection)\n if (!(collection.rules ?? []).length) {\n // A smart collection with no rules holds nothing (`matchesCollection`).\n return result([], planFor({ base: STOREFRONT_CATALOG_BASE, clauses: [], sort }, sort))\n }\n if ('unservable' in smart) {\n // Rules no query can express (a NOT, an OR, a name rule, a price over)\n // are answered by the membership every product writer stores\n // (`collectionIds`, AGL-3321): one array clause on the same query, so\n // the grid's other array controls are refused by name beside it.\n scope = [\n {\n path: CommerceModel.PRODUCT_COLLECTION_IDS,\n op: 'array-contains',\n value: String(collectionDoc?.id ?? ''),\n },\n ]\n } else {\n scope = smart.filters\n // A \"type is not\" rule is `type in […]`; the visitor's Type chip is the\n // narrower question, answered inside it or not at all.\n const typeRule = scope.find((filter) => filter.path === 'type')\n if (typeRule && type) {\n const allowed = Array.isArray(typeRule.value) ? typeRule.value : [typeRule.value]\n if (!allowed.includes(type)) {\n return result([], planFor({ base: STOREFRONT_CATALOG_BASE, clauses: [], sort }, sort))\n }\n scope = scope.filter((filter) => filter !== typeRule)\n }\n }\n }\n\n // THE QUERY: every predicate, one order, a page and one row to spare.\n const request: ListQueryRequest = {\n base: [...STOREFRONT_CATALOG_BASE, ...scope],\n clauses: [...otherClauses, ...priceClauses],\n search,\n sort,\n }\n const plan = planFor(request, sort)\n const byId = firebaseAdmin.firestore.FieldPath.documentId()\n const ordered = (target: ListQueryPlan) =>\n applyListQuery(productsRef, target).orderBy(byId, target.orderBy.direction)\n let pageQuery = ordered(plan)\n const cursor = decodeCursor(params.after)\n if (cursor) pageQuery = pageQuery.startAfter(cursor.value, cursor.id)\n\n // Bounds span every OTHER control, so the slider stays anchored while the\n // visitor narrows it: the lowest and highest price the rest allows, as two\n // one-row reads ordered by price.\n const byPrice = STOREFRONT_CATALOG_SORTS['price-asc']\n const unpriced = wantFacets\n ? planFor({ ...request, clauses: otherClauses, sort: byPrice }, byPrice)\n : null\n const pricedBy = (target: ListQueryPlan, direction: 'asc' | 'desc') =>\n ordered({ ...target, orderBy: { path: 'priceFromCents', direction } }).limit(PRICE_BOUND_SCAN).get()\n // The first price sold online (AGL-4060): an in-person-only product's price\n // never anchors the slider.\n const boundOf = (snapshot: FirebaseFirestore.QuerySnapshot | null) =>\n snapshot?.docs\n .find((docSnapshot) => CommerceModel.productSellsOnline(docSnapshot.data()))\n ?.get('priceFromCents')\n\n const bounds = Promise.all([\n unpriced ? pricedBy(unpriced, 'asc') : null,\n unpriced ? pricedBy(unpriced, 'desc') : null,\n ])\n // A page of rows sold online, read on past the ones sold in person only.\n const rows: CatalogRow[] = []\n let last: FirebaseFirestore.QueryDocumentSnapshot | undefined\n let more = false\n for (let read = 0; read < CATALOG_FILL_READS && rows.length < max; read += 1) {\n const need = max - rows.length\n const snapshot = await pageQuery.limit(need + 1).get()\n const docs = snapshot.docs.slice(0, need)\n for (const docSnapshot of docs) {\n last = docSnapshot\n const row = toRow(docSnapshot)\n if (row && CommerceModel.productSellsOnline(row.product)) rows.push(row)\n }\n more = snapshot.docs.length > need\n if (!more || !last) break\n pageQuery = ordered(plan).startAfter(last.get(plan.orderBy.path), last.id)\n }\n const [lowest, highest] = await bounds\n const low = boundOf(lowest)\n const high = boundOf(highest)\n return result(rows, plan, {\n ...(last && more\n ? { nextCursor: encodeCursor(last.get(plan.orderBy.path), last.id) }\n : {}),\n ...(typeof low === 'number' && typeof high === 'number'\n ? { priceBounds: { minCents: low, maxCents: high } }\n : {}),\n })\n}\n\n/** Public catalog listing endpoint — a thin parse over queryPublicCatalog. */\nexport const catalogHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n\n try {\n const result = await queryPublicCatalog({\n hostId,\n collectionSlug: String(req.query.collection ?? ''),\n categorySlug: String(req.query.category ?? ''),\n collectionId: String(req.query.collectionId ?? ''),\n categoryId: String(req.query.categoryId ?? ''),\n tag: String(req.query.tag ?? ''),\n query: String(req.query.q ?? ''),\n type: String(req.query.type ?? '') as CommerceModel.ProductType | '',\n inStock: String(req.query.inStock ?? '') === '1',\n ids: String(req.query.ids ?? '')\n .split(',')\n .map((id) => id.trim())\n .filter(Boolean),\n // Price-range filter (AGL-564): integer cents on the displayed price.\n minPriceCents: parsePriceCents(req.query.minPriceCents),\n maxPriceCents: parsePriceCents(req.query.maxPriceCents),\n sort: String(req.query.sort ?? 'name'),\n limit: Number(req.query.limit ?? CATALOG_PAGE_DEFAULT),\n after: String(req.query.after ?? ''),\n facets: String(req.query.facets ?? '') === '1',\n })\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=60, stale-while-revalidate=300',\n )\n return res.status(200).json(result)\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Catalog unavailable' })\n }\n}\n"],"names":["nameSearchNormalizers","Aglyn","planListQuery","firebaseAdmin","applyListQuery","smartCollectionScope","STOREFRONT_CATALOG_BASE","STOREFRONT_CATALOG_DECLARATION","STOREFRONT_CATALOG_SORTS","STOREFRONT_PRODUCT_TYPES","storefrontCatalogSort","CommerceModel","productCardImage","product","candidates","mediaUrls","imageUrl","variants","map","variant","candidate","trim","undefined","parsePriceCents","value","raw","String","cents","Number","isFinite","Math","round","createCatalogReadScope","CATALOG_PAGE_MAX","CATALOG_PAGE_DEFAULT","MANUAL_COLLECTION_MAX","GET_ALL_CHUNK","CATALOG_FILL_READS","PRICE_BOUND_SCAN","toRow","docSnapshot","data","liftLegacyProduct","id","values","status","type","deletedAt","productSearchFields","name","productStockFields","rowValue","row","path","onSale","every","filter","rowMatches","productSellsOnline","actual","list","Array","isArray","op","includes","some","member","encodeCursor","Buffer","from","JSON","stringify","toString","decodeCursor","parsed","parse","length","compareValues","a","b","pageHeldRows","rows","key","direction","after","max","signed","ordered","sort","cursor","start","findIndex","page","slice","last","nextCursor","readProductsById","productsRef","ids","firestore","app","refs","doc","snapshots","getAll","snapshot","exists","push","toItem","minPrice","maxPrice","productPriceRange","primary","slug","priceUsd","maxPriceUsd","compareAtPriceUsd","soldOut","tags","variantHasPrice","priceComingSoon","variantCount","defaultVariantId","refusalsOf","plan","refused","refusal","clause","reason","startsWith","SORT_LABELS","nameLower","createdAtMs","RANGE_OPS","Set","planFor","request","orderBy","asked","byRule","base","has","notices","dollars","toFixed","queryPublicCatalog","params","reads","question","runPublicCatalog","results","Map","shared","get","pending","set","categoriesSnapshot","hostId","collectionSlug","categorySlug","collectionIdParam","collectionId","categoryIdParam","categoryId","tag","query","floor","limit","min","wantFacets","Boolean","facets","hostRef","collection","sharedCategories","categories","order","index","items","collectionsSnapshot","collectionByIdSnapshot","facetsSnapshot","Promise","all","where","collectionById","hostCollectionKind","collectionDoc","docs","find","localeCompare","priceClauses","minPriceCents","field","label","maxPriceCents","otherClauses","inStock","search","split","result","extra","priceBounds","answerHeld","options","held","clauses","unpriced","matched","filters","bounding","minCents","maxCents","keepManual","manualOrder","MAX_SAFE_INTEGER","scope","mode","memberIds","productIds","smart","rules","PRODUCT_COLLECTION_IDS","typeRule","allowed","byId","FieldPath","documentId","target","pageQuery","startAfter","byPrice","pricedBy","boundOf","bounds","more","read","need","lowest","highest","low","high","catalogHandler","req","res","json","error","category","q","setHeader","console"],"mappings":"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"}
|
|
@@ -151,7 +151,7 @@ export function cartExtraView(extra) {
|
|
|
151
151
|
for (const line of cartLines){
|
|
152
152
|
var _product_type;
|
|
153
153
|
const product = products.get(line.productId);
|
|
154
|
-
if (!
|
|
154
|
+
if (!CommerceModel.productListedOnline(product)) continue;
|
|
155
155
|
const variant = line.variantId ? product.variants.find((item)=>item.id === line.variantId) : product.variants[0];
|
|
156
156
|
if (!variant || !CommerceModel.variantHasPrice(variant)) continue;
|
|
157
157
|
lines.push(_extends({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout-extras.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n hasPluginCheckoutExtras,\n quotePluginCheckoutExtras,\n type QuotedPluginCheckoutExtra,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-extras'\nimport {\n hasPluginCheckoutCredits,\n offeredCheckoutCredits,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-credits'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport * as CommerceModel from '../model'\nimport { readCartId } from './cart-cookie'\n\n/**\n * Optional lines another plugin offers at the cart (AGL-3635) — package\n * protection is the first — through core's checkout-extras seam. Commerce\n * names no insurer: it asks the seam, shows each offer as a box, and charges\n * the ones the buyer ticked as ordinary lines on the session.\n *\n * Asked only for a basket that ships: an extra is about a parcel, and a cart\n * of downloads has none.\n */\n\n/** How long the cart and checkout wait for an offer before selling without it. */\nexport const CHECKOUT_EXTRAS_TIMEOUT_MS = 2_500\n\n/** One cart line, priced, as the extras quote reads it. */\nexport interface CheckoutExtrasLine {\n productId: string\n name: string\n sku?: string\n quantity: number\n unitCents: number\n physical: boolean\n}\n\n/** Every offer for a basket, or none. Never throws. */\nexport async function quoteCartExtras(input: {\n hostId: string\n lines: CheckoutExtrasLine[]\n destination?: { country?: string; postalCode?: string }\n}): Promise<QuotedPluginCheckoutExtra[]> {\n if (!hasPluginCheckoutExtras()) return []\n const lines = input.lines.filter((line) => line.quantity > 0 && line.unitCents >= 0)\n if (!lines.some((line) => line.physical)) return []\n const itemsCents = lines.reduce((sum, line) => sum + line.unitCents * line.quantity, 0)\n if (itemsCents <= 0) return []\n const country = String(input.destination?.country ?? '').trim().toUpperCase().slice(0, 2)\n const postalCode = String(input.destination?.postalCode ?? '').trim().slice(0, 12)\n return quotePluginCheckoutExtras(\n {\n hostId: input.hostId,\n // Every storefront session is charged in dollars today (`cart-checkout.ts`).\n currency: 'usd',\n itemsCents,\n lines: lines.map((line) => ({\n itemId: line.productId,\n name: line.name,\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: line.quantity,\n unitCents: line.unitCents,\n ships: line.physical,\n })),\n ...(country || postalCode\n ? { destination: { ...(country ? { country } : {}), ...(postalCode ? { postalCode } : {}) } }\n : {}),\n },\n { timeoutMs: CHECKOUT_EXTRAS_TIMEOUT_MS },\n )\n}\n\n/** What the cart shows of an offer: never the quote id. */\nexport interface CartExtraView {\n id: string\n label: string\n description?: string\n amountCents: number\n defaultSelected: boolean\n termsUrl?: string\n}\n\nexport function cartExtraView(extra: QuotedPluginCheckoutExtra): CartExtraView {\n return {\n id: extra.id,\n label: extra.label,\n ...(extra.description ? { description: extra.description } : {}),\n amountCents: extra.amountCents,\n defaultSelected: extra.defaultSelected,\n ...(extra.termsUrl ? { termsUrl: extra.termsUrl } : {}),\n }\n}\n\n/**\n * `GET /api/commerce/cart-extras?hostId=…` — the offers for the visitor's own\n * cart (cookie). Asked by the cart when it is SHOWN, never by the badge on\n * every page. A store with no offering plugin answers `[]` without reading a\n * document. `private, no-store`: the answer is this basket's.\n */\nexport const cartExtrasHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const hostId = String(req.query.hostId ?? '')\n if (!isDocumentId(hostId)) return res.status(400).json({ error: 'Missing or invalid hostId' })\n // Which store-credit codes the cart takes (AGL-3640) — rewards, referral\n // credit — by provider and name only, so the cart draws the field.\n const credits = hasPluginCheckoutCredits()\n ? (await offeredCheckoutCredits({ hostId, channel: 'online' })).map((credit) => ({\n providerId: credit.providerId,\n label: credit.label,\n }))\n : []\n if (!hasPluginCheckoutExtras()) return res.status(200).json({ extras: [], credits })\n const cartId = readCartId(req.cookies, hostId)\n if (!cartId) return res.status(200).json({ extras: [], credits })\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const cart = ((await hostRef.collection('carts').doc(cartId).get()).data() as CommerceModel.HostCart | undefined) ?? {\n lines: [],\n }\n const lines = await resolveCartExtrasLines(hostRef, cart.lines ?? [])\n const extras = await quoteCartExtras({\n hostId,\n lines,\n destination: {\n country: String(req.query.country ?? ''),\n postalCode: String(req.query.postalCode ?? ''),\n },\n })\n return res.status(200).json({ extras: extras.map(cartExtraView), credits })\n } catch (error) {\n console.error('cart extras failed', error)\n // An offer is never worth a broken cart.\n return res.status(200).json({ extras: [], credits })\n }\n}\n\n/** The cart's purchasable lines, priced from the product documents. */\nasync function resolveCartExtrasLines(\n hostRef: FirebaseFirestore.DocumentReference,\n cartLines: CommerceModel.CartLine[],\n): Promise<CheckoutExtrasLine[]> {\n const ids = [...new Set(cartLines.map((line) => line.productId))].filter((id) => isDocumentId(id))\n const snapshots = await Promise.all(ids.map((id) => hostRef.collection('products').doc(id).get()))\n const products = new Map(\n snapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as never) : null,\n ]),\n )\n const lines: CheckoutExtrasLine[] = []\n for (const line of cartLines) {\n const product = products.get(line.productId)\n if (!product || product.deletedAt || product.status !== 'active') continue\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n if (!variant || !CommerceModel.variantHasPrice(variant)) continue\n lines.push({\n productId: line.productId,\n name: product.name,\n ...(variant.sku ? { sku: variant.sku } : {}),\n quantity: Math.max(0, Math.round(Number(line.quantity) || 0)),\n unitCents: Math.round(Number(variant.priceUsd) * 100),\n physical: (product.type ?? 'physical') === 'physical',\n })\n }\n return lines\n}\n"],"names":["hasPluginCheckoutExtras","quotePluginCheckoutExtras","hasPluginCheckoutCredits","offeredCheckoutCredits","firebaseAdmin","isDocumentId","CommerceModel","readCartId","CHECKOUT_EXTRAS_TIMEOUT_MS","quoteCartExtras","input","lines","filter","line","quantity","unitCents","some","physical","itemsCents","reduce","sum","country","String","destination","trim","toUpperCase","slice","postalCode","hostId","currency","map","itemId","productId","name","sku","ships","timeoutMs","cartExtraView","extra","id","label","description","amountCents","defaultSelected","termsUrl","cartExtrasHandler","req","res","setHeader","method","status","json","error","query","credits","channel","credit","providerId","extras","cartId","cookies","cart","hostRef","app","firestore","collection","doc","get","data","resolveCartExtrasLines","console","cartLines","ids","Set","snapshots","Promise","all","products","Map","snapshot","exists","liftLegacyProduct","product","deletedAt","variant","variantId","variants","find","item","variantHasPrice","push","Math","max","round","Number","priceUsd","type"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout-extras.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n hasPluginCheckoutExtras,\n quotePluginCheckoutExtras,\n type QuotedPluginCheckoutExtra,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-extras'\nimport {\n hasPluginCheckoutCredits,\n offeredCheckoutCredits,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-credits'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport * as CommerceModel from '../model'\nimport { readCartId } from './cart-cookie'\n\n/**\n * Optional lines another plugin offers at the cart (AGL-3635) — package\n * protection is the first — through core's checkout-extras seam. Commerce\n * names no insurer: it asks the seam, shows each offer as a box, and charges\n * the ones the buyer ticked as ordinary lines on the session.\n *\n * Asked only for a basket that ships: an extra is about a parcel, and a cart\n * of downloads has none.\n */\n\n/** How long the cart and checkout wait for an offer before selling without it. */\nexport const CHECKOUT_EXTRAS_TIMEOUT_MS = 2_500\n\n/** One cart line, priced, as the extras quote reads it. */\nexport interface CheckoutExtrasLine {\n productId: string\n name: string\n sku?: string\n quantity: number\n unitCents: number\n physical: boolean\n}\n\n/** Every offer for a basket, or none. Never throws. */\nexport async function quoteCartExtras(input: {\n hostId: string\n lines: CheckoutExtrasLine[]\n destination?: { country?: string; postalCode?: string }\n}): Promise<QuotedPluginCheckoutExtra[]> {\n if (!hasPluginCheckoutExtras()) return []\n const lines = input.lines.filter((line) => line.quantity > 0 && line.unitCents >= 0)\n if (!lines.some((line) => line.physical)) return []\n const itemsCents = lines.reduce((sum, line) => sum + line.unitCents * line.quantity, 0)\n if (itemsCents <= 0) return []\n const country = String(input.destination?.country ?? '').trim().toUpperCase().slice(0, 2)\n const postalCode = String(input.destination?.postalCode ?? '').trim().slice(0, 12)\n return quotePluginCheckoutExtras(\n {\n hostId: input.hostId,\n // Every storefront session is charged in dollars today (`cart-checkout.ts`).\n currency: 'usd',\n itemsCents,\n lines: lines.map((line) => ({\n itemId: line.productId,\n name: line.name,\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: line.quantity,\n unitCents: line.unitCents,\n ships: line.physical,\n })),\n ...(country || postalCode\n ? { destination: { ...(country ? { country } : {}), ...(postalCode ? { postalCode } : {}) } }\n : {}),\n },\n { timeoutMs: CHECKOUT_EXTRAS_TIMEOUT_MS },\n )\n}\n\n/** What the cart shows of an offer: never the quote id. */\nexport interface CartExtraView {\n id: string\n label: string\n description?: string\n amountCents: number\n defaultSelected: boolean\n termsUrl?: string\n}\n\nexport function cartExtraView(extra: QuotedPluginCheckoutExtra): CartExtraView {\n return {\n id: extra.id,\n label: extra.label,\n ...(extra.description ? { description: extra.description } : {}),\n amountCents: extra.amountCents,\n defaultSelected: extra.defaultSelected,\n ...(extra.termsUrl ? { termsUrl: extra.termsUrl } : {}),\n }\n}\n\n/**\n * `GET /api/commerce/cart-extras?hostId=…` — the offers for the visitor's own\n * cart (cookie). Asked by the cart when it is SHOWN, never by the badge on\n * every page. A store with no offering plugin answers `[]` without reading a\n * document. `private, no-store`: the answer is this basket's.\n */\nexport const cartExtrasHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const hostId = String(req.query.hostId ?? '')\n if (!isDocumentId(hostId)) return res.status(400).json({ error: 'Missing or invalid hostId' })\n // Which store-credit codes the cart takes (AGL-3640) — rewards, referral\n // credit — by provider and name only, so the cart draws the field.\n const credits = hasPluginCheckoutCredits()\n ? (await offeredCheckoutCredits({ hostId, channel: 'online' })).map((credit) => ({\n providerId: credit.providerId,\n label: credit.label,\n }))\n : []\n if (!hasPluginCheckoutExtras()) return res.status(200).json({ extras: [], credits })\n const cartId = readCartId(req.cookies, hostId)\n if (!cartId) return res.status(200).json({ extras: [], credits })\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const cart = ((await hostRef.collection('carts').doc(cartId).get()).data() as CommerceModel.HostCart | undefined) ?? {\n lines: [],\n }\n const lines = await resolveCartExtrasLines(hostRef, cart.lines ?? [])\n const extras = await quoteCartExtras({\n hostId,\n lines,\n destination: {\n country: String(req.query.country ?? ''),\n postalCode: String(req.query.postalCode ?? ''),\n },\n })\n return res.status(200).json({ extras: extras.map(cartExtraView), credits })\n } catch (error) {\n console.error('cart extras failed', error)\n // An offer is never worth a broken cart.\n return res.status(200).json({ extras: [], credits })\n }\n}\n\n/** The cart's purchasable lines, priced from the product documents. */\nasync function resolveCartExtrasLines(\n hostRef: FirebaseFirestore.DocumentReference,\n cartLines: CommerceModel.CartLine[],\n): Promise<CheckoutExtrasLine[]> {\n const ids = [...new Set(cartLines.map((line) => line.productId))].filter((id) => isDocumentId(id))\n const snapshots = await Promise.all(ids.map((id) => hostRef.collection('products').doc(id).get()))\n const products = new Map(\n snapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as never) : null,\n ]),\n )\n const lines: CheckoutExtrasLine[] = []\n for (const line of cartLines) {\n const product = products.get(line.productId)\n if (!CommerceModel.productListedOnline(product)) continue\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n if (!variant || !CommerceModel.variantHasPrice(variant)) continue\n lines.push({\n productId: line.productId,\n name: product.name,\n ...(variant.sku ? { sku: variant.sku } : {}),\n quantity: Math.max(0, Math.round(Number(line.quantity) || 0)),\n unitCents: Math.round(Number(variant.priceUsd) * 100),\n physical: (product.type ?? 'physical') === 'physical',\n })\n }\n return lines\n}\n"],"names":["hasPluginCheckoutExtras","quotePluginCheckoutExtras","hasPluginCheckoutCredits","offeredCheckoutCredits","firebaseAdmin","isDocumentId","CommerceModel","readCartId","CHECKOUT_EXTRAS_TIMEOUT_MS","quoteCartExtras","input","lines","filter","line","quantity","unitCents","some","physical","itemsCents","reduce","sum","country","String","destination","trim","toUpperCase","slice","postalCode","hostId","currency","map","itemId","productId","name","sku","ships","timeoutMs","cartExtraView","extra","id","label","description","amountCents","defaultSelected","termsUrl","cartExtrasHandler","req","res","setHeader","method","status","json","error","query","credits","channel","credit","providerId","extras","cartId","cookies","cart","hostRef","app","firestore","collection","doc","get","data","resolveCartExtrasLines","console","cartLines","ids","Set","snapshots","Promise","all","products","Map","snapshot","exists","liftLegacyProduct","product","productListedOnline","variant","variantId","variants","find","item","variantHasPrice","push","Math","max","round","Number","priceUsd","type"],"mappings":"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"}
|
|
@@ -127,6 +127,13 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
|
|
|
127
127
|
error: 'Unknown product'
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
// Sold in person only (AGL-4060): bought at the register, the kiosk or
|
|
131
|
+
// the POS apps, never through the site's Buy now.
|
|
132
|
+
if (!CommerceModel.productSellsOnline(product)) {
|
|
133
|
+
return res.status(404).json({
|
|
134
|
+
error: 'Unknown product'
|
|
135
|
+
});
|
|
136
|
+
}
|
|
130
137
|
// Variant pricing (AGL-292): the buyer's selection maps to a variant;
|
|
131
138
|
// absent variantId keeps the legacy default-variant behavior.
|
|
132
139
|
const lifted = CommerceModel.liftLegacyProduct(product);
|