@aglyn/plugins-commerce 1.0.0-beta.199 → 1.0.0-beta.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/console/catalog-organization-card.component.js +48 -3
  3. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  4. package/src/lib/components/console/gift-cards-card.component.d.ts +9 -0
  5. package/src/lib/components/console/gift-cards-card.component.js +53 -28
  6. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  7. package/src/lib/components/console/host-orders-card.component.d.ts +26 -0
  8. package/src/lib/components/console/host-orders-card.component.js +264 -194
  9. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  10. package/src/lib/components/console/order-detail-dialog.component.js +12 -0
  11. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  12. package/src/lib/components/console/pos-page.component.d.ts +14 -0
  13. package/src/lib/components/console/pos-page.component.js +86 -45
  14. package/src/lib/components/console/pos-page.component.js.map +1 -1
  15. package/src/lib/components/console/product-editor-dialog.component.js +30 -6
  16. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  17. package/src/lib/components/console/product-slugs.d.ts +43 -0
  18. package/src/lib/components/console/product-slugs.js +60 -0
  19. package/src/lib/components/console/product-slugs.js.map +1 -0
  20. package/src/lib/components/console/products-hub-card.component.d.ts +2 -2
  21. package/src/lib/components/console/products-hub-card.component.js +229 -282
  22. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  23. package/src/lib/components/console/products-hub-zone.component.d.ts +10 -5
  24. package/src/lib/components/console/products-hub-zone.component.js +32 -9
  25. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  26. package/src/lib/components/console/recovery-queue-card.component.d.ts +37 -5
  27. package/src/lib/components/console/recovery-queue-card.component.js +107 -86
  28. package/src/lib/components/console/recovery-queue-card.component.js.map +1 -1
  29. package/src/lib/components/console/reservations-card.component.js +15 -3
  30. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  31. package/src/lib/components/console/smart-collections.d.ts +32 -0
  32. package/src/lib/components/console/smart-collections.js +57 -0
  33. package/src/lib/components/console/smart-collections.js.map +1 -0
  34. package/src/lib/components/console/stock-movements-card.component.d.ts +13 -4
  35. package/src/lib/components/console/stock-movements-card.component.js +142 -180
  36. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  37. package/src/lib/components/product-grid.d.ts +5 -0
  38. package/src/lib/components/product-grid.js +95 -34
  39. package/src/lib/components/product-grid.js.map +1 -1
  40. package/src/lib/constants/orders-list-query.d.ts +40 -0
  41. package/src/lib/constants/orders-list-query.js +205 -0
  42. package/src/lib/constants/orders-list-query.js.map +1 -0
  43. package/src/lib/constants/product-list-query.d.ts +42 -0
  44. package/src/lib/constants/product-list-query.js +217 -0
  45. package/src/lib/constants/product-list-query.js.map +1 -0
  46. package/src/lib/constants/stock-movement-query.d.ts +27 -0
  47. package/src/lib/constants/stock-movement-query.js +110 -0
  48. package/src/lib/constants/stock-movement-query.js.map +1 -0
  49. package/src/lib/constants/storefront-catalog-query.d.ts +103 -0
  50. package/src/lib/constants/storefront-catalog-query.js +312 -0
  51. package/src/lib/constants/storefront-catalog-query.js.map +1 -0
  52. package/src/lib/model/checkout-recovery.d.ts +37 -0
  53. package/src/lib/model/checkout-recovery.js +44 -0
  54. package/src/lib/model/checkout-recovery.js.map +1 -0
  55. package/src/lib/model/commerce-orders.d.ts +7 -0
  56. package/src/lib/model/commerce-orders.js.map +1 -1
  57. package/src/lib/model/commerce.d.ts +80 -0
  58. package/src/lib/model/commerce.js +67 -6
  59. package/src/lib/model/commerce.js.map +1 -1
  60. package/src/lib/model/gift-card-search.d.ts +24 -0
  61. package/src/lib/model/gift-card-search.js +48 -0
  62. package/src/lib/model/gift-card-search.js.map +1 -0
  63. package/src/lib/model/index.d.ts +1 -0
  64. package/src/lib/model/index.js +1 -0
  65. package/src/lib/model/index.js.map +1 -1
  66. package/src/lib/model/order-list-fields.d.ts +80 -0
  67. package/src/lib/model/order-list-fields.js +112 -0
  68. package/src/lib/model/order-list-fields.js.map +1 -0
  69. package/src/lib/server/billing-webhook.js +180 -99
  70. package/src/lib/server/billing-webhook.js.map +1 -1
  71. package/src/lib/server/cancel-order.js +6 -6
  72. package/src/lib/server/cancel-order.js.map +1 -1
  73. package/src/lib/server/cart-checkout.js +9 -2
  74. package/src/lib/server/cart-checkout.js.map +1 -1
  75. package/src/lib/server/catalog.d.ts +46 -30
  76. package/src/lib/server/catalog.js +507 -170
  77. package/src/lib/server/catalog.js.map +1 -1
  78. package/src/lib/server/checkout.js +3 -1
  79. package/src/lib/server/checkout.js.map +1 -1
  80. package/src/lib/server/collection-membership.d.ts +35 -0
  81. package/src/lib/server/collection-membership.js +172 -0
  82. package/src/lib/server/collection-membership.js.map +1 -0
  83. package/src/lib/server/draft-order.js +5 -3
  84. package/src/lib/server/draft-order.js.map +1 -1
  85. package/src/lib/server/gift-cards.js +7 -1
  86. package/src/lib/server/gift-cards.js.map +1 -1
  87. package/src/lib/server/member-name-search.d.ts +17 -0
  88. package/src/lib/server/member-name-search.js +23 -1
  89. package/src/lib/server/member-name-search.js.map +1 -1
  90. package/src/lib/server/membership-account.js +7 -3
  91. package/src/lib/server/membership-account.js.map +1 -1
  92. package/src/lib/server/membership-admin-password.js +8 -2
  93. package/src/lib/server/membership-admin-password.js.map +1 -1
  94. package/src/lib/server/membership-recover.js +4 -1
  95. package/src/lib/server/membership-recover.js.map +1 -1
  96. package/src/lib/server/membership-register.js +3 -1
  97. package/src/lib/server/membership-register.js.map +1 -1
  98. package/src/lib/server/pos-order.js +9 -5
  99. package/src/lib/server/pos-order.js.map +1 -1
  100. package/src/lib/server/process-abandoned.js +20 -7
  101. package/src/lib/server/process-abandoned.js.map +1 -1
  102. package/src/lib/server/process-restock.js +6 -2
  103. package/src/lib/server/process-restock.js.map +1 -1
  104. package/src/lib/server/reserve-stock.js +3 -4
  105. package/src/lib/server/reserve-stock.js.map +1 -1
  106. package/src/lib/server/reserve.js +3 -1
  107. package/src/lib/server/reserve.js.map +1 -1
  108. package/src/lib/server/site-page-enricher.js +3 -3
  109. package/src/lib/server/site-page-enricher.js.map +1 -1
  110. package/src/lib/server.js +4 -0
  111. package/src/lib/server.js.map +1 -1
@@ -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 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'\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\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 * Displayed price in cents (AGL-564): the low end of the variant range —\n * the number grid cards render (`$X` / `From $X`) — so price filtering\n * matches what visitors see.\n */\nexport function displayedPriceCents(\n product: Pick<CommerceModel.HostProduct, 'variants'>,\n): number {\n return Math.round(CommerceModel.productPriceRange(product)[0] * 100)\n}\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/**\n * Case-insensitive catalog search (AGL-561): matches the product name,\n * description, or any tag. An empty/blank query matches everything.\n */\nexport function matchesCatalogQuery(\n product: Pick<CommerceModel.HostProduct, 'name' | 'description' | 'tags'>,\n query: string,\n): boolean {\n const needle = query.trim().toLowerCase()\n if (!needle) return true\n return (\n product.name.toLowerCase().includes(needle) ||\n (product.description ?? '').toLowerCase().includes(needle) ||\n (product.tags ?? []).some((tag) => tag.toLowerCase().includes(needle))\n )\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 /** Free-text search over name, description and tags. */\n query?: string\n type?: CommerceModel.ProductType | ''\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 offset?: number\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 * The products read does not vary with the query — the whole catalog is\n * fetched and then filtered in memory by collection, category, tag, price and\n * search text. A page carrying four product grids therefore issued four\n * identical 500-document reads and threw three of them away.\n *\n * This shares the READ, never the RESULT. Each grid still filters and sorts\n * the shared snapshot independently, because a collection-scoped grid and an\n * all-products grid on the same page must not receive each other's items.\n *\n * Scoped to one host and one render deliberately: it holds a promise, 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 products?: Promise<FirebaseFirestore.QuerySnapshot>\n categories?: Promise<FirebaseFirestore.QuerySnapshot>\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 nextOffset?: number\n categories?: PublicCatalogCategory[]\n priceBounds?: PublicCatalogPriceBounds\n}\n\n/**\n * Public catalog listing (AGL-291): active products for storefront\n * blocks — filterable by collection/category slug or tag, sortable,\n * capped at 100 items per request. Storefront catalog UX (AGL-561)\n * adds text search, a product-type filter, offset paging (for Load\n * more), and facets for filter chips. The price-range filter (AGL-564)\n * bounds on the displayed price and reports `priceBounds`.\n * Read-only public data; prices here are display-only (charges always\n * 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 { 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 ?? ''\n const query = params.query ?? ''\n const type = ['physical', 'digital', 'service'].includes(params.type ?? '')\n ? (params.type as CommerceModel.ProductType)\n : ''\n const ids = (params.ids ?? []).slice(0, 100)\n const minPriceCents = params.minPriceCents\n const maxPriceCents = params.maxPriceCents\n const sort = params.sort || 'name'\n const max = Math.min(100, Math.max(1, Number(params.limit ?? 24)))\n const offset = Math.max(0, Math.round(Number(params.offset ?? 0)) || 0)\n const wantFacets = Boolean(params.facets)\n\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const reads = params.reads\n // Assigned into the scope BEFORE it is awaited, so grids running together\n // under one `Promise.all` share the single in-flight read rather than each\n // starting their own and racing to store it.\n const sharedProducts = () =>\n (reads\n ? (reads.products ??= hostRef.collection('products').limit(500).get())\n : hostRef.collection('products').limit(500).get())\n const sharedCategories = () =>\n (reads\n ? (reads.categories ??=\n hostRef.collection('productCategories').limit(200).get())\n : hostRef.collection('productCategories').limit(200).get())\n const [\n productsSnapshot,\n collectionsSnapshot,\n categoriesSnapshot,\n collectionByIdSnapshot,\n facetsSnapshot,\n ] =\n await Promise.all([\n sharedProducts(),\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 collection =\n collectionById ??\n collectionsSnapshot?.docs.find(\n (docSnapshot) => Aglyn.hostCollectionKind(docSnapshot.data()) === 'catalog',\n )\n const categoryId = categoryIdParam || categoriesSnapshot?.docs[0]?.id\n\n let products = productsSnapshot.docs\n .map((docSnapshot) => ({\n ...CommerceModel.liftLegacyProduct(docSnapshot.data() as any),\n $id: docSnapshot.id,\n }))\n .filter((product) => !product.deletedAt && product.status === 'active')\n if (collection) {\n const shaped = collection.data() as CommerceModel.HostCollection\n products = products.filter((product) =>\n CommerceModel.matchesCollection(product, shaped, product.$id),\n )\n if (shaped.mode === 'manual') {\n const order = new Map(\n (shaped.productIds ?? []).map((id, index) => [id, index]),\n )\n products.sort(\n (a, b) => (order.get(a.$id) ?? 999) - (order.get(b.$id) ?? 999),\n )\n }\n }\n if (categoryId) {\n products = products.filter((product) =>\n (product.categoryIds ?? []).includes(categoryId),\n )\n }\n if (tag) {\n products = products.filter((product) =>\n (product.tags ?? []).includes(tag),\n )\n }\n // Storefront catalog UX (AGL-561): type filter + text search.\n if (type) {\n products = products.filter((product) => product.type === type)\n }\n if (query.trim()) {\n products = products.filter((product) =>\n matchesCatalogQuery(product, query),\n )\n }\n // Wishlist rendering (AGL-297): explicit id list, given order.\n if (ids.length) {\n const order = new Map(ids.map((id, index) => [id, index]))\n products = products\n .filter((product) => order.has(product.$id))\n .sort((a, b) => (order.get(a.$id) ?? 0) - (order.get(b.$id) ?? 0))\n }\n // Price facets + filter (AGL-564): bounds are computed before the\n // price filter (with everything else applied) so the grid's slider\n // stays anchored while the visitor narrows it. Both use the\n // displayed price — the low end of the variant range grid cards\n // show — so filtering matches what visitors see.\n const priceBounds: PublicCatalogPriceBounds | undefined =\n wantFacets && products.length\n ? {\n minCents: Math.min(...products.map(displayedPriceCents)),\n maxCents: Math.max(...products.map(displayedPriceCents)),\n }\n : undefined\n if (minPriceCents != null || maxPriceCents != null) {\n products = products.filter((product) => {\n const cents = displayedPriceCents(product)\n return (\n (minPriceCents == null || cents >= minPriceCents) &&\n (maxPriceCents == null || cents <= maxPriceCents)\n )\n })\n }\n if (sort === 'price-asc' || sort === 'price-desc') {\n products.sort((a, b) => {\n const [minA] = CommerceModel.productPriceRange(a)\n const [minB] = CommerceModel.productPriceRange(b)\n return sort === 'price-asc' ? minA - minB : minB - minA\n })\n } else if (sort === 'newest') {\n products.sort((a, b) => (b.createdAtMs ?? 0) - (a.createdAtMs ?? 0))\n } else if (\n !ids.length &&\n (!collection || (collection.data() as any).mode !== 'manual')\n ) {\n products.sort((a, b) => a.name.localeCompare(b.name))\n }\n\n // Offset paging (AGL-561): grids pass `offset` for Load more;\n // `nextOffset` comes back only while more filtered items remain.\n const page = products.slice(offset, offset + max)\n const nextOffset =\n offset + max < products.length ? offset + max : undefined\n\n const items: PublicCatalogItem[] = page.map((product) => {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n const primary = product.variants[0]\n const inventory = CommerceModel.productInventory(product)\n return {\n id: product.$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 ...(product.mediaUrls?.[0] || product.imageUrl\n ? { imageUrl: product.mediaUrls?.[0] ?? product.imageUrl }\n : {}),\n soldOut:\n inventory != null &&\n inventory <= 0 &&\n product.oversellPolicy !== 'backorder',\n ...(product.tags?.length ? { tags: product.tags } : {}),\n }\n })\n // Category facets (AGL-561): id/name/slug for grid filter chips,\n // parent-tree order preserved (order, then name).\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 return {\n items,\n ...(nextOffset != null ? { nextOffset } : {}),\n ...(categories ? { categories } : {}),\n ...(priceBounds ? { priceBounds } : {}),\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 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 ?? 24),\n offset: Number(req.query.offset ?? 0),\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":["Aglyn","CommerceModel","firebaseAdmin","displayedPriceCents","product","Math","round","productPriceRange","parsePriceCents","value","raw","String","trim","undefined","cents","Number","isFinite","matchesCatalogQuery","query","needle","toLowerCase","name","includes","description","tags","some","tag","createCatalogReadScope","queryPublicCatalog","params","categoriesSnapshot","hostId","collectionSlug","categorySlug","collectionIdParam","collectionId","categoryIdParam","categoryId","type","ids","slice","minPriceCents","maxPriceCents","sort","max","min","limit","offset","wantFacets","Boolean","facets","firestore","app","hostRef","collection","doc","reads","sharedProducts","products","get","sharedCategories","categories","productsSnapshot","collectionsSnapshot","collectionByIdSnapshot","facetsSnapshot","Promise","all","where","collectionById","exists","hostCollectionKind","data","docs","find","docSnapshot","id","map","liftLegacyProduct","$id","filter","deletedAt","status","shaped","matchesCollection","mode","order","Map","productIds","index","a","b","categoryIds","length","has","priceBounds","minCents","maxCents","minA","minB","createdAtMs","localeCompare","page","nextOffset","items","minPrice","maxPrice","primary","variants","inventory","productInventory","slug","priceUsd","maxPriceUsd","compareAtPriceUsd","mediaUrls","imageUrl","soldOut","oversellPolicy","catalogHandler","req","res","json","error","result","category","q","split","setHeader","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,YAAYA,WAAW,sBAAqB;AAC5C,YAAYC,mBAAmB,oBAAU;AACzC,SAASC,aAAa,QAAQ,2BAA0B;AA4BxD;;;;CAIC,GACD,OAAO,SAASC,oBACdC,OAAoD;IAEpD,OAAOC,KAAKC,KAAK,CAACL,cAAcM,iBAAiB,CAACH,QAAQ,CAAC,EAAE,GAAG;AAClE;AAEA,wEAAwE,GACxE,SAASI,gBAAgBC,KAAc;IACrC,MAAMC,MAAMC,OAAOF,gBAAAA,QAAS,IAAIG,IAAI;IACpC,IAAI,CAACF,KAAK,OAAOG;IACjB,MAAMC,QAAQC,OAAOL;IACrB,IAAI,CAACK,OAAOC,QAAQ,CAACF,UAAUA,QAAQ,GAAG,OAAOD;IACjD,OAAOR,KAAKC,KAAK,CAACQ;AACpB;AAEA;;;CAGC,GACD,OAAO,SAASG,oBACdb,OAAyE,EACzEc,KAAa;QAMVd,sBACAA;IALH,MAAMe,SAASD,MAAMN,IAAI,GAAGQ,WAAW;IACvC,IAAI,CAACD,QAAQ,OAAO;IACpB,OACEf,QAAQiB,IAAI,CAACD,WAAW,GAAGE,QAAQ,CAACH,WACpC,EAACf,uBAAAA,QAAQmB,WAAW,YAAnBnB,uBAAuB,IAAIgB,WAAW,GAAGE,QAAQ,CAACH,WACnD,EAACf,gBAAAA,QAAQoB,IAAI,YAAZpB,gBAAgB,EAAE,EAAEqB,IAAI,CAAC,CAACC,MAAQA,IAAIN,WAAW,GAAGE,QAAQ,CAACH;AAElE;AAoDA,mEAAmE,GACnE,OAAO,SAASQ;IACd,OAAO,CAAC;AACV;AAUA;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,mBACpBC,MAA0B;QAGHA,wBACFA,sBAGKA,sBACFA,oBACZA,aACEA,eAC2CA,cAG5CA,aAIgCA,eACAA;QA2DPC;IA5EtC,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,MAAMG,kBAAiBH,yBAAAA,OAAOG,cAAc,YAArBH,yBAAyB;IAChD,MAAMI,gBAAeJ,uBAAAA,OAAOI,YAAY,YAAnBJ,uBAAuB;IAC5C,sEAAsE;IACtE,kDAAkD;IAClD,MAAMK,qBAAoBL,uBAAAA,OAAOM,YAAY,YAAnBN,uBAAuB;IACjD,MAAMO,mBAAkBP,qBAAAA,OAAOQ,UAAU,YAAjBR,qBAAqB;IAC7C,MAAMH,OAAMG,cAAAA,OAAOH,GAAG,YAAVG,cAAc;IAC1B,MAAMX,SAAQW,gBAAAA,OAAOX,KAAK,YAAZW,gBAAgB;IAC9B,MAAMS,OAAO;QAAC;QAAY;QAAW;KAAU,CAAChB,QAAQ,EAACO,eAAAA,OAAOS,IAAI,YAAXT,eAAe,MACnEA,OAAOS,IAAI,GACZ;IACJ,MAAMC,MAAM,EAACV,cAAAA,OAAOU,GAAG,YAAVV,cAAc,EAAE,EAAEW,KAAK,CAAC,GAAG;IACxC,MAAMC,gBAAgBZ,OAAOY,aAAa;IAC1C,MAAMC,gBAAgBb,OAAOa,aAAa;IAC1C,MAAMC,OAAOd,OAAOc,IAAI,IAAI;IAC5B,MAAMC,MAAMvC,KAAKwC,GAAG,CAAC,KAAKxC,KAAKuC,GAAG,CAAC,GAAG7B,QAAOc,gBAAAA,OAAOiB,KAAK,YAAZjB,gBAAgB;IAC7D,MAAMkB,SAAS1C,KAAKuC,GAAG,CAAC,GAAGvC,KAAKC,KAAK,CAACS,QAAOc,iBAAAA,OAAOkB,MAAM,YAAblB,iBAAiB,OAAO;IACrE,MAAMmB,aAAaC,QAAQpB,OAAOqB,MAAM;IAExC,MAAMC,YAAYjD,cAAckD,GAAG,GAAGD,SAAS;IAC/C,MAAME,UAAUF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACxB;IAClD,MAAMyB,QAAQ3B,OAAO2B,KAAK;IAC1B,0EAA0E;IAC1E,2EAA2E;IAC3E,6CAA6C;IAC7C,MAAMC,iBAAiB;YAEhBD,QAAAA;eADJA,SACIA,YAAAA,CAAAA,SAAAA,OAAME,QAAQ,YAAdF,YAAAA,OAAME,QAAQ,GAAKL,QAAQC,UAAU,CAAC,YAAYR,KAAK,CAAC,KAAKa,GAAG,KACjEN,QAAQC,UAAU,CAAC,YAAYR,KAAK,CAAC,KAAKa,GAAG;;IACnD,MAAMC,mBAAmB;YAElBJ,QAAAA;eADJA,SACIA,cAAAA,CAAAA,SAAAA,OAAMK,UAAU,YAAhBL,cAAAA,OAAMK,UAAU,GACfR,QAAQC,UAAU,CAAC,qBAAqBR,KAAK,CAAC,KAAKa,GAAG,KACxDN,QAAQC,UAAU,CAAC,qBAAqBR,KAAK,CAAC,KAAKa,GAAG;;IAC5D,MAAM,CACJG,kBACAC,qBACAjC,oBACAkC,wBACAC,eACD,GACC,MAAMC,QAAQC,GAAG,CAAC;QAChBV;QACA,oEAAoE;QACpE,oEAAoE;QACpE,gEAAgE;QAChE,CAACvB,qBAAqBF,iBAClBqB,QACGC,UAAU,CAAC,eACXc,KAAK,CAAC,QAAQ,MAAMpC,gBACpBc,KAAK,CAAC,GACNa,GAAG,KACN;QACJ,CAACvB,mBAAmBH,eAChBoB,QACGC,UAAU,CAAC,qBACXc,KAAK,CAAC,QAAQ,MAAMnC,cACpBa,KAAK,CAAC,GACNa,GAAG,KACN;QACJzB,oBACImB,QAAQC,UAAU,CAAC,eAAeC,GAAG,CAACrB,mBAAmByB,GAAG,KAC5D;QACJX,aAAaY,qBAAqB;KACnC;IACH,MAAMS,iBACJL,CAAAA,0CAAAA,uBAAwBM,MAAM,KAC9BtE,MAAMuE,kBAAkB,CAACP,uBAAuBQ,IAAI,QAAQ,YACxDR,yBACAnD;IACN,MAAMyC,aACJe,yBAAAA,iBACAN,uCAAAA,oBAAqBU,IAAI,CAACC,IAAI,CAC5B,CAACC,cAAgB3E,MAAMuE,kBAAkB,CAACI,YAAYH,IAAI,QAAQ;IAEtE,MAAMnC,aAAaD,oBAAmBN,uCAAAA,4BAAAA,mBAAoB2C,IAAI,CAAC,EAAE,qBAA3B3C,0BAA6B8C,EAAE;IAErE,IAAIlB,WAAWI,iBAAiBW,IAAI,CACjCI,GAAG,CAAC,CAACF,cAAiB,aAClB1E,cAAc6E,iBAAiB,CAACH,YAAYH,IAAI;YACnDO,KAAKJ,YAAYC,EAAE;YAEpBI,MAAM,CAAC,CAAC5E,UAAY,CAACA,QAAQ6E,SAAS,IAAI7E,QAAQ8E,MAAM,KAAK;IAChE,IAAI5B,YAAY;QACd,MAAM6B,SAAS7B,WAAWkB,IAAI;QAC9Bd,WAAWA,SAASsB,MAAM,CAAC,CAAC5E,UAC1BH,cAAcmF,iBAAiB,CAAChF,SAAS+E,QAAQ/E,QAAQ2E,GAAG;QAE9D,IAAII,OAAOE,IAAI,KAAK,UAAU;gBAEzBF;YADH,MAAMG,QAAQ,IAAIC,IAChB,EAACJ,qBAAAA,OAAOK,UAAU,YAAjBL,qBAAqB,EAAE,EAAEN,GAAG,CAAC,CAACD,IAAIa,QAAU;oBAACb;oBAAIa;iBAAM;YAE1D/B,SAASf,IAAI,CACX,CAAC+C,GAAGC;oBAAOL,YAA4BA;uBAA7B,EAACA,aAAAA,MAAM3B,GAAG,CAAC+B,EAAEX,GAAG,aAAfO,aAAoB,SAAQA,cAAAA,MAAM3B,GAAG,CAACgC,EAAEZ,GAAG,aAAfO,cAAoB;;QAE/D;IACF;IACA,IAAIjD,YAAY;QACdqB,WAAWA,SAASsB,MAAM,CAAC,CAAC5E;gBACzBA;mBAAD,EAACA,uBAAAA,QAAQwF,WAAW,YAAnBxF,uBAAuB,EAAE,EAAEkB,QAAQ,CAACe;;IAEzC;IACA,IAAIX,KAAK;QACPgC,WAAWA,SAASsB,MAAM,CAAC,CAAC5E;gBACzBA;mBAAD,EAACA,gBAAAA,QAAQoB,IAAI,YAAZpB,gBAAgB,EAAE,EAAEkB,QAAQ,CAACI;;IAElC;IACA,8DAA8D;IAC9D,IAAIY,MAAM;QACRoB,WAAWA,SAASsB,MAAM,CAAC,CAAC5E,UAAYA,QAAQkC,IAAI,KAAKA;IAC3D;IACA,IAAIpB,MAAMN,IAAI,IAAI;QAChB8C,WAAWA,SAASsB,MAAM,CAAC,CAAC5E,UAC1Ba,oBAAoBb,SAASc;IAEjC;IACA,+DAA+D;IAC/D,IAAIqB,IAAIsD,MAAM,EAAE;QACd,MAAMP,QAAQ,IAAIC,IAAIhD,IAAIsC,GAAG,CAAC,CAACD,IAAIa,QAAU;gBAACb;gBAAIa;aAAM;QACxD/B,WAAWA,SACRsB,MAAM,CAAC,CAAC5E,UAAYkF,MAAMQ,GAAG,CAAC1F,QAAQ2E,GAAG,GACzCpC,IAAI,CAAC,CAAC+C,GAAGC;gBAAOL,YAA0BA;mBAA3B,EAACA,aAAAA,MAAM3B,GAAG,CAAC+B,EAAEX,GAAG,aAAfO,aAAoB,OAAMA,cAAAA,MAAM3B,GAAG,CAACgC,EAAEZ,GAAG,aAAfO,cAAoB;;IACnE;IACA,kEAAkE;IAClE,mEAAmE;IACnE,4DAA4D;IAC5D,gEAAgE;IAChE,iDAAiD;IACjD,MAAMS,cACJ/C,cAAcU,SAASmC,MAAM,GACzB;QACEG,UAAU3F,KAAKwC,GAAG,IAAIa,SAASmB,GAAG,CAAC1E;QACnC8F,UAAU5F,KAAKuC,GAAG,IAAIc,SAASmB,GAAG,CAAC1E;IACrC,IACAU;IACN,IAAI4B,iBAAiB,QAAQC,iBAAiB,MAAM;QAClDgB,WAAWA,SAASsB,MAAM,CAAC,CAAC5E;YAC1B,MAAMU,QAAQX,oBAAoBC;YAClC,OACE,AAACqC,CAAAA,iBAAiB,QAAQ3B,SAAS2B,aAAY,KAC9CC,CAAAA,iBAAiB,QAAQ5B,SAAS4B,aAAY;QAEnD;IACF;IACA,IAAIC,SAAS,eAAeA,SAAS,cAAc;QACjDe,SAASf,IAAI,CAAC,CAAC+C,GAAGC;YAChB,MAAM,CAACO,KAAK,GAAGjG,cAAcM,iBAAiB,CAACmF;YAC/C,MAAM,CAACS,KAAK,GAAGlG,cAAcM,iBAAiB,CAACoF;YAC/C,OAAOhD,SAAS,cAAcuD,OAAOC,OAAOA,OAAOD;QACrD;IACF,OAAO,IAAIvD,SAAS,UAAU;QAC5Be,SAASf,IAAI,CAAC,CAAC+C,GAAGC;gBAAOA,gBAAuBD;mBAAxB,EAACC,iBAAAA,EAAES,WAAW,YAAbT,iBAAiB,OAAMD,iBAAAA,EAAEU,WAAW,YAAbV,iBAAiB;;IACnE,OAAO,IACL,CAACnD,IAAIsD,MAAM,IACV,CAAA,CAACvC,cAAc,AAACA,WAAWkB,IAAI,GAAWa,IAAI,KAAK,QAAO,GAC3D;QACA3B,SAASf,IAAI,CAAC,CAAC+C,GAAGC,IAAMD,EAAErE,IAAI,CAACgF,aAAa,CAACV,EAAEtE,IAAI;IACrD;IAEA,8DAA8D;IAC9D,iEAAiE;IACjE,MAAMiF,OAAO5C,SAASlB,KAAK,CAACO,QAAQA,SAASH;IAC7C,MAAM2D,aACJxD,SAASH,MAAMc,SAASmC,MAAM,GAAG9C,SAASH,MAAM/B;IAElD,MAAM2F,QAA6BF,KAAKzB,GAAG,CAAC,CAACzE;;YAcrCA,oBACYA,qBAMZA;QApBN,MAAM,CAACqG,UAAUC,SAAS,GAAGzG,cAAcM,iBAAiB,CAACH;QAC7D,MAAMuG,UAAUvG,QAAQwG,QAAQ,CAAC,EAAE;QACnC,MAAMC,YAAY5G,cAAc6G,gBAAgB,CAAC1G;QACjD,OAAO;YACLwE,IAAIxE,QAAQ2E,GAAG;YACf1D,MAAMjB,QAAQiB,IAAI;YAClB0F,MAAM3G,QAAQ2G,IAAI;YAClBzE,MAAMlC,QAAQkC,IAAI;YAClB0E,UAAUP;YACVQ,aAAaP;WACTC,CAAAA,2BAAAA,QAASO,iBAAiB,IAC1B;YAAEA,mBAAmBP,QAAQO,iBAAiB;QAAC,IAC/C,CAAC,GACD9G,EAAAA,qBAAAA,QAAQ+G,SAAS,qBAAjB/G,kBAAmB,CAAC,EAAE,KAAIA,QAAQgH,QAAQ,GAC1C;YAAEA,QAAQ,WAAEhH,sBAAAA,QAAQ+G,SAAS,qBAAjB/G,mBAAmB,CAAC,EAAE,mBAAIA,QAAQgH,QAAQ;QAAC,IACvD,CAAC;YACLC,SACER,aAAa,QACbA,aAAa,KACbzG,QAAQkH,cAAc,KAAK;WACzBlH,EAAAA,gBAAAA,QAAQoB,IAAI,qBAAZpB,cAAcyF,MAAM,IAAG;YAAErE,MAAMpB,QAAQoB,IAAI;QAAC,IAAI,CAAC;IAEzD;IACA,iEAAiE;IACjE,kDAAkD;IAClD,MAAMqC,aAAkDI,iBACpDA,eAAeQ,IAAI,CAChBI,GAAG,CAAC,CAACF;YAIWH,YACAA,YACCA;QALhB,MAAMA,OAAOG,YAAYH,IAAI;QAC7B,OAAO;YACLI,IAAID,YAAYC,EAAE;YAClBvD,MAAMV,QAAO6D,aAAAA,KAAKnD,IAAI,YAATmD,aAAa;YAC1BuC,MAAMpG,QAAO6D,aAAAA,KAAKuC,IAAI,YAATvC,aAAa;YAC1Bc,OAAOvE,QAAOyD,cAAAA,KAAKc,KAAK,YAAVd,cAAc;QAC9B;IACF,GACC7B,IAAI,CAAC,CAAC+C,GAAGC,IAAMD,EAAEJ,KAAK,GAAGK,EAAEL,KAAK,IAAII,EAAErE,IAAI,CAACgF,aAAa,CAACV,EAAEtE,IAAI,GAC/DwD,GAAG,CAAC,CAAC,EAAED,EAAE,EAAEvD,IAAI,EAAE0F,IAAI,EAAE,GAAM,CAAA;YAAEnC;YAAIvD;YAAM0F;QAAK,CAAA,KACjDlG;IAEJ,OAAO;QACL2F;OACID,cAAc,OAAO;QAAEA;IAAW,IAAI,CAAC,GACvC1C,aAAa;QAAEA;IAAW,IAAI,CAAC,GAC/BkC,cAAc;QAAEA;IAAY,IAAI,CAAC;AAEzC;AAEA,4EAA4E,GAC5E,OAAO,MAAMwB,iBAAmC,OAAOC,KAAKC;QACpCD;IAAtB,MAAMzF,SAASpB,QAAO6G,oBAAAA,IAAItG,KAAK,CAACa,MAAM,YAAhByF,oBAAoB;IAC1C,IAAI,CAACzF,QAAQ,OAAO0F,IAAIvC,MAAM,CAAC,KAAKwC,IAAI,CAAC;QAAEC,OAAO;IAAiB;IAEnE,IAAI;YAGuBH,uBACFA,qBACAA,yBACFA,uBACPA,gBACEA,cACDA,iBACDA,gBAOCA,iBACCA,kBACCA,mBACAA;QAnBjB,MAAMI,SAAS,MAAMhG,mBAAmB;YACtCG;YACAC,gBAAgBrB,QAAO6G,wBAAAA,IAAItG,KAAK,CAACoC,UAAU,YAApBkE,wBAAwB;YAC/CvF,cAActB,QAAO6G,sBAAAA,IAAItG,KAAK,CAAC2G,QAAQ,YAAlBL,sBAAsB;YAC3CrF,cAAcxB,QAAO6G,0BAAAA,IAAItG,KAAK,CAACiB,YAAY,YAAtBqF,0BAA0B;YAC/CnF,YAAY1B,QAAO6G,wBAAAA,IAAItG,KAAK,CAACmB,UAAU,YAApBmF,wBAAwB;YAC3C9F,KAAKf,QAAO6G,iBAAAA,IAAItG,KAAK,CAACQ,GAAG,YAAb8F,iBAAiB;YAC7BtG,OAAOP,QAAO6G,eAAAA,IAAItG,KAAK,CAAC4G,CAAC,YAAXN,eAAe;YAC7BlF,MAAM3B,QAAO6G,kBAAAA,IAAItG,KAAK,CAACoB,IAAI,YAAdkF,kBAAkB;YAC/BjF,KAAK5B,QAAO6G,iBAAAA,IAAItG,KAAK,CAACqB,GAAG,YAAbiF,iBAAiB,IAC1BO,KAAK,CAAC,KACNlD,GAAG,CAAC,CAACD,KAAOA,GAAGhE,IAAI,IACnBoE,MAAM,CAAC/B;YACV,sEAAsE;YACtER,eAAejC,gBAAgBgH,IAAItG,KAAK,CAACuB,aAAa;YACtDC,eAAelC,gBAAgBgH,IAAItG,KAAK,CAACwB,aAAa;YACtDC,MAAMhC,QAAO6G,kBAAAA,IAAItG,KAAK,CAACyB,IAAI,YAAd6E,kBAAkB;YAC/B1E,OAAO/B,QAAOyG,mBAAAA,IAAItG,KAAK,CAAC4B,KAAK,YAAf0E,mBAAmB;YACjCzE,QAAQhC,QAAOyG,oBAAAA,IAAItG,KAAK,CAAC6B,MAAM,YAAhByE,oBAAoB;YACnCtE,QAAQvC,QAAO6G,oBAAAA,IAAItG,KAAK,CAACgC,MAAM,YAAhBsE,oBAAoB,QAAQ;QAC7C;QACAC,IAAIO,SAAS,CACX,iBACA;QAEF,OAAOP,IAAIvC,MAAM,CAAC,KAAKwC,IAAI,CAACE;IAC9B,EAAE,OAAOD,OAAO;QACdM,QAAQN,KAAK,CAACA;QACd,OAAOF,IAAIvC,MAAM,CAAC,KAAKwC,IAAI,CAAC;YAAEC,OAAO;QAAsB;IAC7D;AACF,EAAC"}
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\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 ...(product.mediaUrls?.[0] || product.imageUrl\n ? { imageUrl: product.mediaUrls?.[0] ?? product.imageUrl }\n : {}),\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 }\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","parsePriceCents","value","raw","String","trim","undefined","cents","Number","isFinite","Math","round","createCatalogReadScope","CATALOG_PAGE_MAX","CATALOG_PAGE_DEFAULT","MANUAL_COLLECTION_MAX","GET_ALL_CHUNK","toRow","docSnapshot","product","data","liftLegacyProduct","id","values","status","type","deletedAt","productSearchFields","name","variants","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","map","doc","snapshots","getAll","snapshot","exists","push","toItem","minPrice","maxPrice","productPriceRange","primary","slug","priceUsd","maxPriceUsd","compareAtPriceUsd","mediaUrls","imageUrl","soldOut","tags","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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"}
@@ -19,6 +19,7 @@ import * as CommerceModel from "../model/index.js";
19
19
  import { claimAttempt, deriveStripeObjectKey } from "@aglyn/aglyn/server";
20
20
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
21
21
  import { connectLinkageIsReady } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
22
+ import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
22
23
  import { readActiveMemberSession } from "./membership.js";
23
24
  import { resolveManualTaxRateId } from "./manual-tax-rate.js";
24
25
  import { holdPromotionSlot, promotionHoldKey } from "./promotion-hold.js";
@@ -737,7 +738,8 @@ import { applyNativeCheckoutParams, nativeCheckoutStripeHeaders, readCheckoutSes
737
738
  // a destination transfer + percent fee; the webhook records the sub.
738
739
  // `isSubscription` resolved above (AGL-545).
739
740
  const params = new URLSearchParams(_extends({
740
- mode: isSubscription ? 'subscription' : 'payment',
741
+ mode: isSubscription ? 'subscription' : 'payment'
742
+ }, checkoutSessionCardAuthenticationParams(isSubscription ? 'subscription' : 'payment'), {
741
743
  'line_items[0][quantity]': String(chargedQuantity),
742
744
  'line_items[0][price_data][currency]': 'usd',
743
745
  'line_items[0][price_data][unit_amount]': String(chargedUnitAmountCents),