@aglyn/plugins-commerce 1.0.0-beta.288 → 1.0.0-beta.289

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 (87) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +29 -2
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
  6. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  7. package/src/lib/components/console/host-orders-card.component.js +5 -1
  8. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
  10. package/src/lib/components/console/host-quotes-card.component.js +247 -0
  11. package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
  12. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
  13. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  14. package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
  15. package/src/lib/components/console/quote-editor-dialog.component.js +1266 -0
  16. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
  17. package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
  18. package/src/lib/components/console/quote-pay-link.component.js +213 -0
  19. package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
  20. package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
  21. package/src/lib/components/console/quote-settings-card.component.js +121 -0
  22. package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
  23. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  24. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  25. package/src/lib/components/quote-page-body.d.ts +33 -0
  26. package/src/lib/components/quote-page-body.js +696 -0
  27. package/src/lib/components/quote-page-body.js.map +1 -0
  28. package/src/lib/components/quote-page.d.ts +29 -0
  29. package/src/lib/components/quote-page.js +96 -0
  30. package/src/lib/components/quote-page.js.map +1 -0
  31. package/src/lib/components/storefront-payment-element.d.ts +7 -1
  32. package/src/lib/components/storefront-payment-element.js +8 -3
  33. package/src/lib/components/storefront-payment-element.js.map +1 -1
  34. package/src/lib/constants/orders-list-query.js +6 -1
  35. package/src/lib/constants/orders-list-query.js.map +1 -1
  36. package/src/lib/constants/quote-page.d.ts +30 -0
  37. package/src/lib/constants/quote-page.js +35 -0
  38. package/src/lib/constants/quote-page.js.map +1 -0
  39. package/src/lib/model/commerce-orders.d.ts +25 -2
  40. package/src/lib/model/commerce-orders.js +10 -1
  41. package/src/lib/model/commerce-orders.js.map +1 -1
  42. package/src/lib/model/commerce-quote-pay.d.ts +229 -0
  43. package/src/lib/model/commerce-quote-pay.js +393 -0
  44. package/src/lib/model/commerce-quote-pay.js.map +1 -0
  45. package/src/lib/model/commerce-quotes.d.ts +279 -0
  46. package/src/lib/model/commerce-quotes.js +522 -0
  47. package/src/lib/model/commerce-quotes.js.map +1 -0
  48. package/src/lib/model/index.d.ts +2 -0
  49. package/src/lib/model/index.js +2 -0
  50. package/src/lib/model/index.js.map +1 -1
  51. package/src/lib/model/plugin-permissions.d.ts +1 -1
  52. package/src/lib/model/plugin-permissions.js +20 -0
  53. package/src/lib/model/plugin-permissions.js.map +1 -1
  54. package/src/lib/server/api-v1/orders-and-products.js +6 -2
  55. package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
  56. package/src/lib/server/billing-webhook.js +127 -52
  57. package/src/lib/server/billing-webhook.js.map +1 -1
  58. package/src/lib/server/checkout-session-expiry.d.ts +36 -0
  59. package/src/lib/server/checkout-session-expiry.js +63 -0
  60. package/src/lib/server/checkout-session-expiry.js.map +1 -0
  61. package/src/lib/server/draft-order.js +16 -8
  62. package/src/lib/server/draft-order.js.map +1 -1
  63. package/src/lib/server/person-eraser.d.ts +7 -0
  64. package/src/lib/server/person-eraser.js +24 -2
  65. package/src/lib/server/person-eraser.js.map +1 -1
  66. package/src/lib/server/pos-return.js +3 -1
  67. package/src/lib/server/pos-return.js.map +1 -1
  68. package/src/lib/server/quote-link-token.d.ts +39 -0
  69. package/src/lib/server/quote-link-token.js +97 -0
  70. package/src/lib/server/quote-link-token.js.map +1 -0
  71. package/src/lib/server/quote-pay.d.ts +83 -0
  72. package/src/lib/server/quote-pay.js +606 -0
  73. package/src/lib/server/quote-pay.js.map +1 -0
  74. package/src/lib/server/quote.d.ts +49 -0
  75. package/src/lib/server/quote.js +513 -0
  76. package/src/lib/server/quote.js.map +1 -0
  77. package/src/lib/server/related.js +2 -0
  78. package/src/lib/server/related.js.map +1 -1
  79. package/src/lib/server/site-page-resolver.d.ts +2 -0
  80. package/src/lib/server/site-page-resolver.js +75 -0
  81. package/src/lib/server/site-page-resolver.js.map +1 -1
  82. package/src/lib/server.js +14 -0
  83. package/src/lib/server.js.map +1 -1
  84. package/src/lib/site.js +6 -0
  85. package/src/lib/site.js.map +1 -1
  86. package/src/lib/transfer/tracking.server.js +6 -0
  87. package/src/lib/transfer/tracking.server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/related.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\n/**\n * Related products (AGL-325), best source first: the product's manual\n * list, then co-purchase pairs mined from recent orders (\"frequently\n * bought together\"), then tag/category neighbors. Returns catalog ids —\n * the block renders them through the ids-filtered catalog API.\n */\nexport const relatedHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const productId = String(req.query.productId ?? '')\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n // A product sold in person only (AGL-4060) has no page to show neighbors\n // on, and naming them would say what it is bought with.\n if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {\n return res.status(200).json({ productIds: [] })\n }\n const product = CommerceModel.liftLegacyProduct(\n (productSnapshot.data() as any) ?? {},\n )\n\n let ids = (product.relatedProductIds ?? []).slice(0, 8)\n\n if (ids.length === 0) {\n // Co-purchase pairs mined from a bounded sample of multi-line orders.\n //\n // Deliberately NOT \"recent\", which is what this said before: there is\n // no orderable recency field to ask for. Orders carry their timestamp\n // as `createdAtMs` on some documents and `createdAt: {seconds}` on\n // others — `orderCreatedAtMs` exists precisely to paper over that — and\n // an `orderBy` on either one DROPS every order written in the other\n // shape. Ordering by document id instead makes the sample the SAME\n // hundred orders on every call rather than an arbitrary hundred the\n // backend chose, so a product's \"bought together\" list stops changing\n // between requests for no reason. Genuine recency needs one timestamp\n // field across the collection first.\n const orders = await hostRef\n .collection('orders')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(100)\n .get()\n const counts = new Map<string, number>()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n const lineIds = (order.lineItems ?? []).map((line) => line.productId)\n if (!lineIds.includes(productId)) continue\n for (const id of lineIds) {\n if (id === productId) continue\n counts.set(id, (counts.get(id) ?? 0) + 1)\n }\n }\n ids = [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8)\n .map(([id]) => id)\n }\n\n if (ids.length === 0 && (product.tags?.length || product.categoryIds?.length)) {\n // Tag/category neighbors, from the same bounded deterministic sample:\n // an unordered `limit` let a catalog larger than 300 answer one request\n // from one arbitrary slice and the next from another, so a product's\n // neighbours appeared and disappeared without anything changing.\n const catalog = await hostRef\n .collection('products')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(300)\n .get()\n ids = catalog.docs\n .filter((docSnapshot) => {\n if (docSnapshot.id === productId) return false\n const candidate = docSnapshot.data() as any\n if (!CommerceModel.productListedOnline(candidate)) return false\n const sharedTag = (candidate.tags ?? []).some((tag: string) =>\n (product.tags ?? []).includes(tag),\n )\n const sharedCategory = (candidate.categoryIds ?? []).some(\n (id: string) => (product.categoryIds ?? []).includes(id),\n )\n return sharedTag || sharedCategory\n })\n .slice(0, 8)\n .map((docSnapshot) => docSnapshot.id)\n }\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=300, stale-while-revalidate=600',\n )\n return res.status(200).json({ productIds: ids })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Related unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","relatedHandler","req","res","hostId","String","query","productId","status","json","error","productSnapshot","product","firestore","app","hostRef","collection","doc","get","exists","productSellsOnline","data","productIds","liftLegacyProduct","ids","relatedProductIds","slice","length","orders","orderBy","FieldPath","documentId","limit","counts","Map","docSnapshot","docs","order","liftLegacyOrder","lineIds","lineItems","map","line","includes","id","set","entries","sort","a","b","tags","categoryIds","catalog","filter","candidate","productListedOnline","sharedTag","some","tag","sharedCategory","setHeader","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/related.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\n/**\n * Related products (AGL-325), best source first: the product's manual\n * list, then co-purchase pairs mined from recent orders (\"frequently\n * bought together\"), then tag/category neighbors. Returns catalog ids —\n * the block renders them through the ids-filtered catalog API.\n */\nexport const relatedHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const productId = String(req.query.productId ?? '')\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n // A product sold in person only (AGL-4060) has no page to show neighbors\n // on, and naming them would say what it is bought with.\n if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {\n return res.status(200).json({ productIds: [] })\n }\n const product = CommerceModel.liftLegacyProduct(\n (productSnapshot.data() as any) ?? {},\n )\n\n let ids = (product.relatedProductIds ?? []).slice(0, 8)\n\n if (ids.length === 0) {\n // Co-purchase pairs mined from a bounded sample of multi-line orders.\n //\n // Deliberately NOT \"recent\", which is what this said before: there is\n // no orderable recency field to ask for. Orders carry their timestamp\n // as `createdAtMs` on some documents and `createdAt: {seconds}` on\n // others — `orderCreatedAtMs` exists precisely to paper over that — and\n // an `orderBy` on either one DROPS every order written in the other\n // shape. Ordering by document id instead makes the sample the SAME\n // hundred orders on every call rather than an arbitrary hundred the\n // backend chose, so a product's \"bought together\" list stops changing\n // between requests for no reason. Genuine recency needs one timestamp\n // field across the collection first.\n const orders = await hostRef\n .collection('orders')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(100)\n .get()\n const counts = new Map<string, number>()\n for (const docSnapshot of orders.docs) {\n // A quote nobody has paid is not something bought together (AGL-4093).\n if (CommerceModel.isUnpaidQuote(docSnapshot.data())) continue\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n const lineIds = (order.lineItems ?? []).map((line) => line.productId)\n if (!lineIds.includes(productId)) continue\n for (const id of lineIds) {\n if (id === productId) continue\n counts.set(id, (counts.get(id) ?? 0) + 1)\n }\n }\n ids = [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8)\n .map(([id]) => id)\n }\n\n if (ids.length === 0 && (product.tags?.length || product.categoryIds?.length)) {\n // Tag/category neighbors, from the same bounded deterministic sample:\n // an unordered `limit` let a catalog larger than 300 answer one request\n // from one arbitrary slice and the next from another, so a product's\n // neighbours appeared and disappeared without anything changing.\n const catalog = await hostRef\n .collection('products')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(300)\n .get()\n ids = catalog.docs\n .filter((docSnapshot) => {\n if (docSnapshot.id === productId) return false\n const candidate = docSnapshot.data() as any\n if (!CommerceModel.productListedOnline(candidate)) return false\n const sharedTag = (candidate.tags ?? []).some((tag: string) =>\n (product.tags ?? []).includes(tag),\n )\n const sharedCategory = (candidate.categoryIds ?? []).some(\n (id: string) => (product.categoryIds ?? []).includes(id),\n )\n return sharedTag || sharedCategory\n })\n .slice(0, 8)\n .map((docSnapshot) => docSnapshot.id)\n }\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=300, stale-while-revalidate=600',\n )\n return res.status(200).json({ productIds: ids })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Related unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","relatedHandler","req","res","hostId","String","query","productId","status","json","error","productSnapshot","product","firestore","app","hostRef","collection","doc","get","exists","productSellsOnline","data","productIds","liftLegacyProduct","ids","relatedProductIds","slice","length","orders","orderBy","FieldPath","documentId","limit","counts","Map","docSnapshot","docs","order","isUnpaidQuote","liftLegacyOrder","lineIds","lineItems","map","line","includes","id","set","entries","sort","a","b","tags","categoryIds","catalog","filter","candidate","productListedOnline","sharedTag","some","tag","sharedCategory","setHeader","console"],"mappings":"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"}
@@ -54,3 +54,5 @@ export declare function productReturnWindow(product: Pick<CommerceModel.HostProd
54
54
  export declare function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema>;
55
55
  /** The return form's node tree, root first, for the layout's slot. */
56
56
  export declare function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema>;
57
+ /** The quote page's node tree, root first, for the layout's slot. */
58
+ export declare function buildQuotePageNodes(): Record<string, Aglyn.AglynNodeSchema>;
@@ -21,6 +21,7 @@ import * as Aglyn from "@aglyn/aglyn/server";
21
21
  import { ORDER_STATUS_PATH } from "./order-status-token.js";
22
22
  import { ORDER_STATUS_COMPONENT_ID } from "../constants/order-status.js";
23
23
  import { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from "../constants/return-request.js";
24
+ import { QUOTE_PAGE_COMPONENT_ID, QUOTE_PAGE_PATH } from "../constants/quote-page.js";
24
25
  import getScreen from "@aglyn/tenant-runtime/get-screen";
25
26
  import { collectSocialImageFacts } from "@aglyn/tenant-runtime/social-image-facts";
26
27
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
@@ -59,6 +60,12 @@ import { readProductReviews } from "./reviews.js";
59
60
  if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {
60
61
  return composeReturnRequestPage(hostId, host);
61
62
  }
63
+ // The customer's quote page (AGL-4094), at /pay-quote, under the same
64
+ // terms: no page of the site's own there, the quote read after hydration
65
+ // from the pay link's `?q=&t=`, UNLISTED.
66
+ if (pdpSegments.length === 1 && pdpSegments[0] === QUOTE_PAGE_PATH) {
67
+ return composeQuotePage(hostId, host);
68
+ }
62
69
  if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {
63
70
  var _productSnapshot_docs_;
64
71
  const adminFirestore = firebaseAdmin.app().firestore();
@@ -595,6 +602,74 @@ async function composeReturnRequestPage(hostId, host) {
595
602
  return undefined;
596
603
  }
597
604
  }
605
+ const QUOTE_PAGE_NODE = 'quote-page__block';
606
+ /** The quote page's node tree, root first, for the layout's slot. */ export function buildQuotePageNodes() {
607
+ return {
608
+ [Aglyn.NODE_ROOT_ID]: {
609
+ $id: Aglyn.NODE_ROOT_ID,
610
+ componentId: 'div',
611
+ nodes: [
612
+ 'quote-page__container'
613
+ ]
614
+ },
615
+ 'quote-page__container': {
616
+ $id: 'quote-page__container',
617
+ parentId: Aglyn.NODE_ROOT_ID,
618
+ componentId: 'muiContainer',
619
+ pluginId: 'mui',
620
+ props: {
621
+ maxWidth: 'md',
622
+ sx: {
623
+ paddingTop: 6,
624
+ paddingBottom: 8
625
+ }
626
+ },
627
+ nodes: [
628
+ QUOTE_PAGE_NODE
629
+ ]
630
+ },
631
+ [QUOTE_PAGE_NODE]: {
632
+ $id: QUOTE_PAGE_NODE,
633
+ parentId: 'quote-page__container',
634
+ componentId: QUOTE_PAGE_COMPONENT_ID,
635
+ pluginId: 'commerce',
636
+ props: {}
637
+ }
638
+ };
639
+ }
640
+ async function composeQuotePage(hostId, host) {
641
+ try {
642
+ const layoutId = await resolveBuiltInPageLayoutId({
643
+ hostId,
644
+ host: host
645
+ });
646
+ const nodes = await composeNodesWithChrome({
647
+ hostId,
648
+ layoutId,
649
+ screenNodes: buildQuotePageNodes(),
650
+ host: host
651
+ });
652
+ if (!nodes) return undefined;
653
+ return {
654
+ props: JSON.parse(JSON.stringify({
655
+ data: {
656
+ host,
657
+ screen: {
658
+ data: {
659
+ displayName: 'Your quote',
660
+ visibility: Aglyn.HostScreenVisibility.UNLISTED
661
+ }
662
+ }
663
+ },
664
+ nodes
665
+ })),
666
+ revalidate: 3600
667
+ };
668
+ } catch (error) {
669
+ console.error('quote page composition failed', error);
670
+ return undefined;
671
+ }
672
+ }
598
673
  /**
599
674
  * The store's time zone for a product page whose product asks for a use day
600
675
  * (AGL-4043), so the seeded page offers the same days the API's does; null
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-page-resolver.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 { SitePageResolver } from '@aglyn/aglyn/server'\nimport { hostCollectionKind } from '@aglyn/aglyn/server'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from '@aglyn/tenant-runtime/built-in-page-layout'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { ORDER_STATUS_PATH } from './order-status-token'\nimport { ORDER_STATUS_COMPONENT_ID } from '../constants/order-status'\nimport { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from '../constants/return-request'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { productAsksUseDateAnywhere, toPublicProductDetail } from './product'\nimport { readStoreDay } from './store-day'\nimport { readProductReviews } from './reviews'\n\n/**\n * Commerce page resolver (AGL-292/298/418), relocated verbatim from the\n * tenant loader: /products/{slug} renders the host's designated PDP\n * template with product tokens, /collections/{slug} the collection\n * template — both compose server-side and return complete page props.\n * Runs only for paths that didn't match a published screen; returns\n * undefined for everything else so the loader falls through.\n */\nexport const commerceSitePageResolver: SitePageResolver = async ({\n hostId,\n host,\n path,\n}) => {\n const hostRes = { host }\n // Product detail routes (AGL-292): /products/{slug} renders the\n // host's designated PDP template screen (settings/store\n // `pdpScreenId`, AGL-295) with product tokens; the product-detail\n // block hydrates variants client-side from the same slug. Same\n // mechanism as entry-template screens below.\n const pdpSegments = path.split('/').filter(Boolean)\n // The guest order-status page (AGL-3610), at /order-status — reached only\n // when the site has no page of its own there. The site's header and footer\n // around the order-status block, which reads the signed `?o=&t=` link after\n // hydration, so this page holds no order and is safe to cache. UNLISTED, so\n // the head says noindex: it is a private page, not a landing page.\n if (pdpSegments.length === 1 && pdpSegments[0] === ORDER_STATUS_PATH) {\n return composeOrderStatusPage(hostId, host)\n }\n // The buyer's return form (AGL-3611), at /order-return, under the same\n // terms: no page of the site's own there, the order read after hydration\n // from the link's `?o=&t=` (or the member's session), UNLISTED.\n if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {\n return composeReturnRequestPage(hostId, host)\n }\n if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, productSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('products')\n .where('slug', '==', pdpSegments[1])\n .limit(1)\n .get(),\n ])\n const pdpScreenId = storeSettings.get('pdpScreenId')\n const productRaw = productSnapshot.docs[0]?.data() as any\n // Live, active and sold online (AGL-4060): a product sold in person only\n // has no page here, designed or built in, and falls through to the 404.\n const listedOnline = CommerceModel.productListedOnline(productRaw)\n if (pdpScreenId && listedOnline) {\n const product = CommerceModel.liftLegacyProduct(productRaw)\n const templateRes = await getScreen({\n hostId,\n screenId: pdpScreenId,\n // A PDP template renders against a routed PRODUCT, never at an address\n // of its own, so it is served here and 404s everywhere else (AGL-1400).\n allowTemplate: true,\n })\n if (templateRes.screen) {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n // Best-effort: a reviews read that fails costs the page its rating\n // snippet, not the page.\n const productReviews = await readProductReviews(\n hostId,\n productSnapshot.docs[0].id,\n ).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\n // The head shares this page as the template's image, then the site\n // default, so their documents are read in the template's own batch\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: pdpScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The template's host variables, and its layout's, fill in from\n // this site as they do on every other page (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'product.name': product.name,\n 'product.description': product.description ?? '',\n 'product.price': productPriceText(product, minPrice, maxPrice),\n 'product.image':\n product.mediaUrls?.[0] ?? product.imageUrl ?? '',\n 'product.slug': product.slug,\n },\n // Named as the product template it is if the page review holds it\n // (AGL-3374); without it the route has no page.\n page: {\n template: {\n role: 'entry',\n route: '/products/:slug',\n collectionName: 'Products',\n entryPath: `/products/${product.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n // The PDP block used to fetch this in an effect, so the\n // server rendered a skeleton and crawlers saw a product page\n // with no product in it (AGL-659). The product is already\n // loaded here — hand it down so it renders server-side.\n pageData: {\n commerce: {\n product: toPublicProductDetail(\n productSnapshot.docs[0].id,\n product,\n // The store's calendar, for a pass that asks for a day.\n { timeZone: await passDayTimeZone(hostId, hostRes.host, product) },\n ),\n // Rating aggregate (AGL-686): needed HERE because\n // `aggregateRating` belongs nested inside the Product\n // structured data, and that is emitted server-side.\n // The reviews block used to publish a free-standing\n // AggregateRating node, which schema.org ignores.\n ...(productReviews.aggregate.count\n ? { reviews: productReviews }\n : {}),\n },\n },\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // The product NAMES this page; the template's own name\n // (\"Product detail\") describes none of the products it\n // renders. Kept distinct from the SEO title because the\n // head treats an authored title as verbatim and a name\n // as the side the site title joins (AGL-1341) — so a\n // product with its own SEO title gets exactly that, and\n // one without gets \"Blue Widget – Acme Store\".\n displayName: product.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n // Only when the PRODUCT authored one: inheriting the\n // template screen's title would give every product\n // page in the store the same `<title>`.\n title: product.seo?.title ?? undefined,\n description:\n product.seo?.description ??\n product.description ??\n undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n // No product template designated, or none that renders: the store's\n // built-in product page (AGL-3676), in the site's own header and footer,\n // rather than a 404 behind every card the product grid links. A store a\n // guided start built has no template of its own, and its grid links each\n // product here.\n if (listedOnline) {\n return composeBuiltInProductPage(\n hostId,\n host,\n productSnapshot.docs[0].id,\n CommerceModel.liftLegacyProduct(productRaw),\n storeSettings.get('returns'),\n )\n }\n }\n // Commerce collection routes (AGL-298): /collections/{slug} renders\n // the designated collection template with collection tokens; the\n // product-grid block derives the same slug from the URL.\n if (pdpSegments.length === 2 && pdpSegments[0] === 'collections') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, collectionSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('collections')\n .where('slug', '==', pdpSegments[1])\n .limit(5)\n .get(),\n ])\n const collectionScreenId = storeSettings.get('collectionScreenId')\n // Content collections share this path and a slug is only unique within a\n // kind (AGL-954), so take the first CATALOG match rather than the first\n // doc — otherwise a blog with the same slug renders as a product page.\n const shopCollection = collectionSnapshot.docs\n .find((docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'catalog')\n ?.data() as CommerceModel.HostCollection | undefined\n if (collectionScreenId && shopCollection) {\n const templateRes = await getScreen({\n hostId,\n screenId: collectionScreenId,\n allowTemplate: true,\n })\n if (templateRes.screen) {\n // As on the PDP above: the template's image, then the site default\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: collectionScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // As on the PDP above (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'collection.name': shopCollection.name,\n 'collection.description': shopCollection.description ?? '',\n 'collection.image': shopCollection.imageUrl ?? '',\n 'collection.slug': shopCollection.slug,\n },\n // As on the PDP above (AGL-3374).\n page: {\n template: {\n role: 'entry',\n route: '/collections/:slug',\n collectionName: 'Store collections',\n entryPath: `/collections/${shopCollection.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // As on the PDP above: the catalog collection names the\n // page, and a name takes the site title after it while\n // an authored SEO title would not (AGL-1341). A shop\n // collection has no SEO title field of its own, so the\n // template's must not stand in for one.\n displayName: shopCollection.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n title: undefined,\n description:\n shopCollection.description ?? undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n return undefined\n}\n\n/**\n * What `{{product.price}}` says: the price, \"From\" the lowest where variants\n * differ, or \"Price coming soon\" for a product listed before any variant has\n * one (AGL-3676) — never \"$0\".\n */\nexport function productPriceText(\n product: Pick<CommerceModel.HostProduct, 'variants'>,\n minPrice: number,\n maxPrice: number,\n): string {\n if (!(product.variants ?? []).some((variant) => CommerceModel.variantHasPrice(variant))) {\n return 'Price coming soon'\n }\n return minPrice === maxPrice ? `$${minPrice}` : `From $${minPrice}`\n}\n\nconst PRODUCT_PAGE_NODE = 'pdp__detail'\nconst PRODUCT_PAGE_REVIEWS_NODE = 'pdp__reviews'\nconst PRODUCT_PAGE_RELATED_NODE = 'pdp__related'\n\n/**\n * The store's built-in product page (AGL-3676), root first, for the layout's\n * slot: the product block for the routed slug — with its Details and\n * Shipping & returns folds under the description, as a designer theme's\n * product page has them — then the product's reviews where the store's plan\n * collects them, then the related products as a grid of photo cards.\n */\nexport function buildProductPageNodes(\n slug: string,\n options: { reviews?: boolean } = {},\n): Record<string, Aglyn.AglynNodeSchema> {\n const reviews = Boolean(options.reviews)\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['pdp__container'],\n } as Aglyn.AglynNodeSchema,\n pdp__container: {\n $id: 'pdp__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'lg', sx: { paddingTop: 6, paddingBottom: 10 } },\n nodes: [\n PRODUCT_PAGE_NODE,\n ...(reviews ? [PRODUCT_PAGE_REVIEWS_NODE] : []),\n PRODUCT_PAGE_RELATED_NODE,\n ],\n } as Aglyn.AglynNodeSchema,\n [PRODUCT_PAGE_NODE]: {\n $id: PRODUCT_PAGE_NODE,\n parentId: 'pdp__container',\n componentId: 'product-detail',\n pluginId: 'commerce',\n props: { slug, showDetails: true, showShipping: true },\n } as Aglyn.AglynNodeSchema,\n ...(reviews\n ? {\n [PRODUCT_PAGE_REVIEWS_NODE]: {\n $id: PRODUCT_PAGE_REVIEWS_NODE,\n parentId: 'pdp__container',\n componentId: 'product-reviews',\n pluginId: 'commerce',\n props: { heading: 'Reviews', sx: { marginTop: 8 } },\n } as Aglyn.AglynNodeSchema,\n }\n : {}),\n [PRODUCT_PAGE_RELATED_NODE]: {\n $id: PRODUCT_PAGE_RELATED_NODE,\n parentId: 'pdp__container',\n componentId: 'related-products',\n pluginId: 'commerce',\n props: {\n heading: 'You may also like',\n maxItems: 4,\n layout: 'grid',\n sx: { marginTop: 8 },\n },\n } as Aglyn.AglynNodeSchema,\n }\n}\n\n/**\n * The store's return window for this product, when the store takes returns of\n * its kind: the settings the return form enforces (`readReturnSettings`), so\n * the page never states a policy the store does not keep.\n */\nexport function productReturnWindow(\n product: Pick<CommerceModel.HostProduct, 'type'>,\n rawReturns: unknown,\n): { windowDays: number } | undefined {\n const settings = CommerceModel.readReturnSettings(rawReturns)\n if (!settings.enabled || settings.windowDays <= 0) return undefined\n if (!settings.eligibleTypes.includes(product.type ?? 'physical')) return undefined\n return { windowDays: settings.windowDays }\n}\n\n/** Whether the store's plan collects reviews; a failed read leaves them off. */\nasync function storeTakesReviews(hostId: string): Promise<boolean> {\n try {\n const org = await getOrgForHost(hostId)\n return Aglyn.checkEntitlement(org?.org as never, 'productReviews')\n } catch (error) {\n console.error('reviews entitlement read failed', error)\n return false\n }\n}\n\nasync function composeBuiltInProductPage(\n hostId: string,\n host: unknown,\n productId: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n rawReturns?: unknown,\n) {\n try {\n const [layoutId, productReviews, reviewsOn] = await Promise.all([\n resolveBuiltInPageLayoutId({ hostId, host: host as never }),\n readProductReviews(hostId, productId).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n }),\n storeTakesReviews(hostId),\n ])\n const returns = productReturnWindow(product, rawReturns)\n const card = collectSocialImageFacts([\n product.mediaUrls?.[0] ?? product.imageUrl,\n (host as { seo?: { image?: string } } | null)?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildProductPageNodes(product.slug, { reviews: reviewsOn }),\n socialImages: card.socialImages,\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n // Seeded, so the product renders server-side and the head writes\n // its Product structured data, as on a template page (AGL-659).\n pageData: {\n commerce: {\n product: toPublicProductDetail(productId, product, {\n timeZone: await passDayTimeZone(hostId, host, product),\n }),\n ...(productReviews.aggregate.count ? { reviews: productReviews } : {}),\n // The Shipping & returns fold's return line, only when the\n // store takes returns of this product's kind.\n ...(returns ? { returns } : {}),\n },\n },\n data: {\n host,\n screen: {\n data: {\n displayName: product.name,\n seo: {\n title: product.seo?.title ?? undefined,\n description: product.seo?.description ?? product.description ?? undefined,\n },\n },\n },\n },\n nodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n } catch (error) {\n console.error('built-in product page composition failed', error)\n return undefined\n }\n}\n\nconst ORDER_STATUS_NODE = 'order-status__block'\n\n/** The order-status page's node tree, root first, for the layout's slot. */\nexport function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['order-status__container'],\n } as Aglyn.AglynNodeSchema,\n 'order-status__container': {\n $id: 'order-status__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [ORDER_STATUS_NODE],\n } as Aglyn.AglynNodeSchema,\n [ORDER_STATUS_NODE]: {\n $id: ORDER_STATUS_NODE,\n parentId: 'order-status__container',\n componentId: ORDER_STATUS_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeOrderStatusPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildOrderStatusNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Order status',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('order status page composition failed', error)\n return undefined\n }\n}\n\nconst RETURN_REQUEST_NODE = 'ret__block'\n\n/** The return form's node tree, root first, for the layout's slot. */\nexport function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['ret__container'],\n } as Aglyn.AglynNodeSchema,\n ret__container: {\n $id: 'ret__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [RETURN_REQUEST_NODE],\n } as Aglyn.AglynNodeSchema,\n [RETURN_REQUEST_NODE]: {\n $id: RETURN_REQUEST_NODE,\n parentId: 'ret__container',\n componentId: RETURN_REQUEST_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeReturnRequestPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildReturnRequestNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Request a return',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('return request page composition failed', error)\n return undefined\n }\n}\n\n/**\n * The store's time zone for a product page whose product asks for a use day\n * (AGL-4043), so the seeded page offers the same days the API's does; null\n * otherwise, without a read.\n */\nasync function passDayTimeZone(\n hostId: string,\n host: unknown,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n): Promise<string | null> {\n if (!productAsksUseDateAnywhere(product)) return null\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n return (await readStoreDay(hostRef, { host: (host ?? null) as Record<string, unknown> | null })).timeZone\n}\n"],"names":["hostCollectionKind","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","Aglyn","ORDER_STATUS_PATH","ORDER_STATUS_COMPONENT_ID","RETURN_REQUEST_COMPONENT_ID","RETURN_REQUEST_PATH","getScreen","collectSocialImageFacts","firebaseAdmin","getOrgForHost","CommerceModel","productAsksUseDateAnywhere","toPublicProductDetail","readStoreDay","readProductReviews","commerceSitePageResolver","hostId","host","path","hostRes","pdpSegments","split","filter","Boolean","length","composeOrderStatusPage","composeReturnRequestPage","productSnapshot","adminFirestore","app","firestore","hostDocRef","collection","doc","storeSettings","Promise","all","get","where","limit","pdpScreenId","productRaw","docs","data","listedOnline","productListedOnline","product","liftLegacyProduct","templateRes","screenId","allowTemplate","screen","minPrice","maxPrice","productPriceRange","productReviews","id","catch","error","console","reviews","aggregate","count","average","card","seo","image","templateNodes","socialImages","tokens","name","description","productPriceText","mediaUrls","imageUrl","slug","page","template","role","route","collectionName","entryPath","fallback","props","JSON","parse","stringify","pageData","commerce","timeZone","passDayTimeZone","displayName","title","undefined","nodes","collected","revalidate","composeBuiltInProductPage","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot","variants","some","variant","variantHasPrice","PRODUCT_PAGE_NODE","PRODUCT_PAGE_REVIEWS_NODE","PRODUCT_PAGE_RELATED_NODE","buildProductPageNodes","options","NODE_ROOT_ID","$id","componentId","pdp__container","parentId","pluginId","maxWidth","sx","paddingTop","paddingBottom","showDetails","showShipping","heading","marginTop","maxItems","layout","productReturnWindow","rawReturns","settings","readReturnSettings","enabled","windowDays","eligibleTypes","includes","type","storeTakesReviews","org","checkEntitlement","productId","layoutId","reviewsOn","returns","screenNodes","ORDER_STATUS_NODE","buildOrderStatusNodes","visibility","HostScreenVisibility","UNLISTED","RETURN_REQUEST_NODE","buildReturnRequestNodes","ret__container","hostRef"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-page-resolver.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 { SitePageResolver } from '@aglyn/aglyn/server'\nimport { hostCollectionKind } from '@aglyn/aglyn/server'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from '@aglyn/tenant-runtime/built-in-page-layout'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { ORDER_STATUS_PATH } from './order-status-token'\nimport { ORDER_STATUS_COMPONENT_ID } from '../constants/order-status'\nimport { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from '../constants/return-request'\nimport { QUOTE_PAGE_COMPONENT_ID, QUOTE_PAGE_PATH } from '../constants/quote-page'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { productAsksUseDateAnywhere, toPublicProductDetail } from './product'\nimport { readStoreDay } from './store-day'\nimport { readProductReviews } from './reviews'\n\n/**\n * Commerce page resolver (AGL-292/298/418), relocated verbatim from the\n * tenant loader: /products/{slug} renders the host's designated PDP\n * template with product tokens, /collections/{slug} the collection\n * template — both compose server-side and return complete page props.\n * Runs only for paths that didn't match a published screen; returns\n * undefined for everything else so the loader falls through.\n */\nexport const commerceSitePageResolver: SitePageResolver = async ({\n hostId,\n host,\n path,\n}) => {\n const hostRes = { host }\n // Product detail routes (AGL-292): /products/{slug} renders the\n // host's designated PDP template screen (settings/store\n // `pdpScreenId`, AGL-295) with product tokens; the product-detail\n // block hydrates variants client-side from the same slug. Same\n // mechanism as entry-template screens below.\n const pdpSegments = path.split('/').filter(Boolean)\n // The guest order-status page (AGL-3610), at /order-status — reached only\n // when the site has no page of its own there. The site's header and footer\n // around the order-status block, which reads the signed `?o=&t=` link after\n // hydration, so this page holds no order and is safe to cache. UNLISTED, so\n // the head says noindex: it is a private page, not a landing page.\n if (pdpSegments.length === 1 && pdpSegments[0] === ORDER_STATUS_PATH) {\n return composeOrderStatusPage(hostId, host)\n }\n // The buyer's return form (AGL-3611), at /order-return, under the same\n // terms: no page of the site's own there, the order read after hydration\n // from the link's `?o=&t=` (or the member's session), UNLISTED.\n if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {\n return composeReturnRequestPage(hostId, host)\n }\n // The customer's quote page (AGL-4094), at /pay-quote, under the same\n // terms: no page of the site's own there, the quote read after hydration\n // from the pay link's `?q=&t=`, UNLISTED.\n if (pdpSegments.length === 1 && pdpSegments[0] === QUOTE_PAGE_PATH) {\n return composeQuotePage(hostId, host)\n }\n if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, productSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('products')\n .where('slug', '==', pdpSegments[1])\n .limit(1)\n .get(),\n ])\n const pdpScreenId = storeSettings.get('pdpScreenId')\n const productRaw = productSnapshot.docs[0]?.data() as any\n // Live, active and sold online (AGL-4060): a product sold in person only\n // has no page here, designed or built in, and falls through to the 404.\n const listedOnline = CommerceModel.productListedOnline(productRaw)\n if (pdpScreenId && listedOnline) {\n const product = CommerceModel.liftLegacyProduct(productRaw)\n const templateRes = await getScreen({\n hostId,\n screenId: pdpScreenId,\n // A PDP template renders against a routed PRODUCT, never at an address\n // of its own, so it is served here and 404s everywhere else (AGL-1400).\n allowTemplate: true,\n })\n if (templateRes.screen) {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n // Best-effort: a reviews read that fails costs the page its rating\n // snippet, not the page.\n const productReviews = await readProductReviews(\n hostId,\n productSnapshot.docs[0].id,\n ).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\n // The head shares this page as the template's image, then the site\n // default, so their documents are read in the template's own batch\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: pdpScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The template's host variables, and its layout's, fill in from\n // this site as they do on every other page (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'product.name': product.name,\n 'product.description': product.description ?? '',\n 'product.price': productPriceText(product, minPrice, maxPrice),\n 'product.image':\n product.mediaUrls?.[0] ?? product.imageUrl ?? '',\n 'product.slug': product.slug,\n },\n // Named as the product template it is if the page review holds it\n // (AGL-3374); without it the route has no page.\n page: {\n template: {\n role: 'entry',\n route: '/products/:slug',\n collectionName: 'Products',\n entryPath: `/products/${product.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n // The PDP block used to fetch this in an effect, so the\n // server rendered a skeleton and crawlers saw a product page\n // with no product in it (AGL-659). The product is already\n // loaded here — hand it down so it renders server-side.\n pageData: {\n commerce: {\n product: toPublicProductDetail(\n productSnapshot.docs[0].id,\n product,\n // The store's calendar, for a pass that asks for a day.\n { timeZone: await passDayTimeZone(hostId, hostRes.host, product) },\n ),\n // Rating aggregate (AGL-686): needed HERE because\n // `aggregateRating` belongs nested inside the Product\n // structured data, and that is emitted server-side.\n // The reviews block used to publish a free-standing\n // AggregateRating node, which schema.org ignores.\n ...(productReviews.aggregate.count\n ? { reviews: productReviews }\n : {}),\n },\n },\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // The product NAMES this page; the template's own name\n // (\"Product detail\") describes none of the products it\n // renders. Kept distinct from the SEO title because the\n // head treats an authored title as verbatim and a name\n // as the side the site title joins (AGL-1341) — so a\n // product with its own SEO title gets exactly that, and\n // one without gets \"Blue Widget – Acme Store\".\n displayName: product.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n // Only when the PRODUCT authored one: inheriting the\n // template screen's title would give every product\n // page in the store the same `<title>`.\n title: product.seo?.title ?? undefined,\n description:\n product.seo?.description ??\n product.description ??\n undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n // No product template designated, or none that renders: the store's\n // built-in product page (AGL-3676), in the site's own header and footer,\n // rather than a 404 behind every card the product grid links. A store a\n // guided start built has no template of its own, and its grid links each\n // product here.\n if (listedOnline) {\n return composeBuiltInProductPage(\n hostId,\n host,\n productSnapshot.docs[0].id,\n CommerceModel.liftLegacyProduct(productRaw),\n storeSettings.get('returns'),\n )\n }\n }\n // Commerce collection routes (AGL-298): /collections/{slug} renders\n // the designated collection template with collection tokens; the\n // product-grid block derives the same slug from the URL.\n if (pdpSegments.length === 2 && pdpSegments[0] === 'collections') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, collectionSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('collections')\n .where('slug', '==', pdpSegments[1])\n .limit(5)\n .get(),\n ])\n const collectionScreenId = storeSettings.get('collectionScreenId')\n // Content collections share this path and a slug is only unique within a\n // kind (AGL-954), so take the first CATALOG match rather than the first\n // doc — otherwise a blog with the same slug renders as a product page.\n const shopCollection = collectionSnapshot.docs\n .find((docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'catalog')\n ?.data() as CommerceModel.HostCollection | undefined\n if (collectionScreenId && shopCollection) {\n const templateRes = await getScreen({\n hostId,\n screenId: collectionScreenId,\n allowTemplate: true,\n })\n if (templateRes.screen) {\n // As on the PDP above: the template's image, then the site default\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: collectionScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // As on the PDP above (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'collection.name': shopCollection.name,\n 'collection.description': shopCollection.description ?? '',\n 'collection.image': shopCollection.imageUrl ?? '',\n 'collection.slug': shopCollection.slug,\n },\n // As on the PDP above (AGL-3374).\n page: {\n template: {\n role: 'entry',\n route: '/collections/:slug',\n collectionName: 'Store collections',\n entryPath: `/collections/${shopCollection.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // As on the PDP above: the catalog collection names the\n // page, and a name takes the site title after it while\n // an authored SEO title would not (AGL-1341). A shop\n // collection has no SEO title field of its own, so the\n // template's must not stand in for one.\n displayName: shopCollection.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n title: undefined,\n description:\n shopCollection.description ?? undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n return undefined\n}\n\n/**\n * What `{{product.price}}` says: the price, \"From\" the lowest where variants\n * differ, or \"Price coming soon\" for a product listed before any variant has\n * one (AGL-3676) — never \"$0\".\n */\nexport function productPriceText(\n product: Pick<CommerceModel.HostProduct, 'variants'>,\n minPrice: number,\n maxPrice: number,\n): string {\n if (!(product.variants ?? []).some((variant) => CommerceModel.variantHasPrice(variant))) {\n return 'Price coming soon'\n }\n return minPrice === maxPrice ? `$${minPrice}` : `From $${minPrice}`\n}\n\nconst PRODUCT_PAGE_NODE = 'pdp__detail'\nconst PRODUCT_PAGE_REVIEWS_NODE = 'pdp__reviews'\nconst PRODUCT_PAGE_RELATED_NODE = 'pdp__related'\n\n/**\n * The store's built-in product page (AGL-3676), root first, for the layout's\n * slot: the product block for the routed slug — with its Details and\n * Shipping & returns folds under the description, as a designer theme's\n * product page has them — then the product's reviews where the store's plan\n * collects them, then the related products as a grid of photo cards.\n */\nexport function buildProductPageNodes(\n slug: string,\n options: { reviews?: boolean } = {},\n): Record<string, Aglyn.AglynNodeSchema> {\n const reviews = Boolean(options.reviews)\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['pdp__container'],\n } as Aglyn.AglynNodeSchema,\n pdp__container: {\n $id: 'pdp__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'lg', sx: { paddingTop: 6, paddingBottom: 10 } },\n nodes: [\n PRODUCT_PAGE_NODE,\n ...(reviews ? [PRODUCT_PAGE_REVIEWS_NODE] : []),\n PRODUCT_PAGE_RELATED_NODE,\n ],\n } as Aglyn.AglynNodeSchema,\n [PRODUCT_PAGE_NODE]: {\n $id: PRODUCT_PAGE_NODE,\n parentId: 'pdp__container',\n componentId: 'product-detail',\n pluginId: 'commerce',\n props: { slug, showDetails: true, showShipping: true },\n } as Aglyn.AglynNodeSchema,\n ...(reviews\n ? {\n [PRODUCT_PAGE_REVIEWS_NODE]: {\n $id: PRODUCT_PAGE_REVIEWS_NODE,\n parentId: 'pdp__container',\n componentId: 'product-reviews',\n pluginId: 'commerce',\n props: { heading: 'Reviews', sx: { marginTop: 8 } },\n } as Aglyn.AglynNodeSchema,\n }\n : {}),\n [PRODUCT_PAGE_RELATED_NODE]: {\n $id: PRODUCT_PAGE_RELATED_NODE,\n parentId: 'pdp__container',\n componentId: 'related-products',\n pluginId: 'commerce',\n props: {\n heading: 'You may also like',\n maxItems: 4,\n layout: 'grid',\n sx: { marginTop: 8 },\n },\n } as Aglyn.AglynNodeSchema,\n }\n}\n\n/**\n * The store's return window for this product, when the store takes returns of\n * its kind: the settings the return form enforces (`readReturnSettings`), so\n * the page never states a policy the store does not keep.\n */\nexport function productReturnWindow(\n product: Pick<CommerceModel.HostProduct, 'type'>,\n rawReturns: unknown,\n): { windowDays: number } | undefined {\n const settings = CommerceModel.readReturnSettings(rawReturns)\n if (!settings.enabled || settings.windowDays <= 0) return undefined\n if (!settings.eligibleTypes.includes(product.type ?? 'physical')) return undefined\n return { windowDays: settings.windowDays }\n}\n\n/** Whether the store's plan collects reviews; a failed read leaves them off. */\nasync function storeTakesReviews(hostId: string): Promise<boolean> {\n try {\n const org = await getOrgForHost(hostId)\n return Aglyn.checkEntitlement(org?.org as never, 'productReviews')\n } catch (error) {\n console.error('reviews entitlement read failed', error)\n return false\n }\n}\n\nasync function composeBuiltInProductPage(\n hostId: string,\n host: unknown,\n productId: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n rawReturns?: unknown,\n) {\n try {\n const [layoutId, productReviews, reviewsOn] = await Promise.all([\n resolveBuiltInPageLayoutId({ hostId, host: host as never }),\n readProductReviews(hostId, productId).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n }),\n storeTakesReviews(hostId),\n ])\n const returns = productReturnWindow(product, rawReturns)\n const card = collectSocialImageFacts([\n product.mediaUrls?.[0] ?? product.imageUrl,\n (host as { seo?: { image?: string } } | null)?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildProductPageNodes(product.slug, { reviews: reviewsOn }),\n socialImages: card.socialImages,\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n // Seeded, so the product renders server-side and the head writes\n // its Product structured data, as on a template page (AGL-659).\n pageData: {\n commerce: {\n product: toPublicProductDetail(productId, product, {\n timeZone: await passDayTimeZone(hostId, host, product),\n }),\n ...(productReviews.aggregate.count ? { reviews: productReviews } : {}),\n // The Shipping & returns fold's return line, only when the\n // store takes returns of this product's kind.\n ...(returns ? { returns } : {}),\n },\n },\n data: {\n host,\n screen: {\n data: {\n displayName: product.name,\n seo: {\n title: product.seo?.title ?? undefined,\n description: product.seo?.description ?? product.description ?? undefined,\n },\n },\n },\n },\n nodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n } catch (error) {\n console.error('built-in product page composition failed', error)\n return undefined\n }\n}\n\nconst ORDER_STATUS_NODE = 'order-status__block'\n\n/** The order-status page's node tree, root first, for the layout's slot. */\nexport function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['order-status__container'],\n } as Aglyn.AglynNodeSchema,\n 'order-status__container': {\n $id: 'order-status__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [ORDER_STATUS_NODE],\n } as Aglyn.AglynNodeSchema,\n [ORDER_STATUS_NODE]: {\n $id: ORDER_STATUS_NODE,\n parentId: 'order-status__container',\n componentId: ORDER_STATUS_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeOrderStatusPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildOrderStatusNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Order status',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('order status page composition failed', error)\n return undefined\n }\n}\n\nconst RETURN_REQUEST_NODE = 'ret__block'\n\n/** The return form's node tree, root first, for the layout's slot. */\nexport function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['ret__container'],\n } as Aglyn.AglynNodeSchema,\n ret__container: {\n $id: 'ret__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [RETURN_REQUEST_NODE],\n } as Aglyn.AglynNodeSchema,\n [RETURN_REQUEST_NODE]: {\n $id: RETURN_REQUEST_NODE,\n parentId: 'ret__container',\n componentId: RETURN_REQUEST_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeReturnRequestPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildReturnRequestNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Request a return',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('return request page composition failed', error)\n return undefined\n }\n}\n\nconst QUOTE_PAGE_NODE = 'quote-page__block'\n\n/** The quote page's node tree, root first, for the layout's slot. */\nexport function buildQuotePageNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['quote-page__container'],\n } as Aglyn.AglynNodeSchema,\n 'quote-page__container': {\n $id: 'quote-page__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [QUOTE_PAGE_NODE],\n } as Aglyn.AglynNodeSchema,\n [QUOTE_PAGE_NODE]: {\n $id: QUOTE_PAGE_NODE,\n parentId: 'quote-page__container',\n componentId: QUOTE_PAGE_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeQuotePage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildQuotePageNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Your quote',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('quote page composition failed', error)\n return undefined\n }\n}\n\n/**\n * The store's time zone for a product page whose product asks for a use day\n * (AGL-4043), so the seeded page offers the same days the API's does; null\n * otherwise, without a read.\n */\nasync function passDayTimeZone(\n hostId: string,\n host: unknown,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n): Promise<string | null> {\n if (!productAsksUseDateAnywhere(product)) return null\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n return (await readStoreDay(hostRef, { host: (host ?? null) as Record<string, unknown> | null })).timeZone\n}\n"],"names":["hostCollectionKind","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","Aglyn","ORDER_STATUS_PATH","ORDER_STATUS_COMPONENT_ID","RETURN_REQUEST_COMPONENT_ID","RETURN_REQUEST_PATH","QUOTE_PAGE_COMPONENT_ID","QUOTE_PAGE_PATH","getScreen","collectSocialImageFacts","firebaseAdmin","getOrgForHost","CommerceModel","productAsksUseDateAnywhere","toPublicProductDetail","readStoreDay","readProductReviews","commerceSitePageResolver","hostId","host","path","hostRes","pdpSegments","split","filter","Boolean","length","composeOrderStatusPage","composeReturnRequestPage","composeQuotePage","productSnapshot","adminFirestore","app","firestore","hostDocRef","collection","doc","storeSettings","Promise","all","get","where","limit","pdpScreenId","productRaw","docs","data","listedOnline","productListedOnline","product","liftLegacyProduct","templateRes","screenId","allowTemplate","screen","minPrice","maxPrice","productPriceRange","productReviews","id","catch","error","console","reviews","aggregate","count","average","card","seo","image","templateNodes","socialImages","tokens","name","description","productPriceText","mediaUrls","imageUrl","slug","page","template","role","route","collectionName","entryPath","fallback","props","JSON","parse","stringify","pageData","commerce","timeZone","passDayTimeZone","displayName","title","undefined","nodes","collected","revalidate","composeBuiltInProductPage","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot","variants","some","variant","variantHasPrice","PRODUCT_PAGE_NODE","PRODUCT_PAGE_REVIEWS_NODE","PRODUCT_PAGE_RELATED_NODE","buildProductPageNodes","options","NODE_ROOT_ID","$id","componentId","pdp__container","parentId","pluginId","maxWidth","sx","paddingTop","paddingBottom","showDetails","showShipping","heading","marginTop","maxItems","layout","productReturnWindow","rawReturns","settings","readReturnSettings","enabled","windowDays","eligibleTypes","includes","type","storeTakesReviews","org","checkEntitlement","productId","layoutId","reviewsOn","returns","screenNodes","ORDER_STATUS_NODE","buildOrderStatusNodes","visibility","HostScreenVisibility","UNLISTED","RETURN_REQUEST_NODE","buildReturnRequestNodes","ret__container","QUOTE_PAGE_NODE","buildQuotePageNodes","hostRef"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -351,6 +351,10 @@ registerPluginJob({
351
351
  */ const CARD_PAYMENT_DOOR = {
352
352
  cardPayment: true
353
353
  };
354
+ /**
355
+ * The quote page's Pay button (AGL-4094): a fresh Checkout Session for a
356
+ * quote, for whoever holds its signed pay link. Loaded on first call.
357
+ */ const quotePayDoor = async (req, res)=>(await import("./server/quote-pay.js")).quotePayHandler(req, res);
354
358
  /** Registers the commerce plugin's storefront API routes. */ export function registerCommerceApi() {
355
359
  registerProductCardReader();
356
360
  // The merchant's tax rule, for every plugin that prices a charge here.
@@ -393,6 +397,13 @@ registerPluginJob({
393
397
  // …and its "Request a return" button (AGL-3611), on an order that can
394
398
  // still send something back.
395
399
  registerOrderStatusActions(returnRequestStatusAction);
400
+ // The customer's quote page (AGL-4094), behind the signed pay link a
401
+ // business sends: the quote, and its Pay button. The page's data is a
402
+ // recipient link, so it outlives the site's gates like the order status.
403
+ registerPluginApiRoute('commerce/quote-view', async (req, res)=>(await import("./server/quote-pay.js")).quoteViewHandler(req, res), {
404
+ recipientLink: true
405
+ });
406
+ registerPluginApiRoute('commerce/quote-pay', quotePayDoor, CARD_PAYMENT_DOOR);
396
407
  // What became of a session the shopper was returned from (AGL-3606).
397
408
  registerPluginApiRoute('commerce/checkout-status', checkoutStatusHandler);
398
409
  registerPluginApiRoute('commerce/product', productHandler);
@@ -489,6 +500,9 @@ registerPluginJob({
489
500
  registerPluginApiRoute('commerce/shipping/carrier-rates', carrierRatesAvailabilityHandler);
490
501
  registerPluginApiRoute('commerce/connect', connectHandler);
491
502
  registerPluginApiRoute('commerce/draft-order', draftOrderHandler);
503
+ // Quotes (AGL-4093): create, edit and delete a quote, and find the customer
504
+ // it is for, under "Create and edit quotes". Loaded on first call.
505
+ registerPluginApiRoute('commerce/quote', async (req, res)=>(await import("./server/quote.js")).quoteHandler(req, res));
492
506
  // Fulfil + mark-delivered with the transition re-asked under the write
493
507
  // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the
494
508
  // stock release those two transitions never had. The customer REST API's