@aglyn/plugins-commerce 1.0.0-beta.236 → 1.0.0-beta.238

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 (34) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart.d.ts +2 -1
  3. package/src/lib/components/cart.js +2 -1
  4. package/src/lib/components/cart.js.map +1 -1
  5. package/src/lib/components/product-detail.d.ts +76 -0
  6. package/src/lib/components/product-detail.js +258 -52
  7. package/src/lib/components/product-detail.js.map +1 -1
  8. package/src/lib/components/product-grid.d.ts +46 -0
  9. package/src/lib/components/product-grid.js +283 -79
  10. package/src/lib/components/product-grid.js.map +1 -1
  11. package/src/lib/components/product-image-placeholder.d.ts +31 -0
  12. package/src/lib/components/product-image-placeholder.js +72 -0
  13. package/src/lib/components/product-image-placeholder.js.map +1 -0
  14. package/src/lib/components/product-reviews.d.ts +13 -0
  15. package/src/lib/components/product-reviews.js +267 -13
  16. package/src/lib/components/product-reviews.js.map +1 -1
  17. package/src/lib/components/related-products.d.ts +6 -0
  18. package/src/lib/components/related-products.js +73 -20
  19. package/src/lib/components/related-products.js.map +1 -1
  20. package/src/lib/constants/cart-events.d.ts +23 -0
  21. package/src/lib/constants/cart-events.js +23 -0
  22. package/src/lib/constants/cart-events.js.map +1 -0
  23. package/src/lib/server/catalog.d.ts +14 -0
  24. package/src/lib/server/catalog.js +25 -5
  25. package/src/lib/server/catalog.js.map +1 -1
  26. package/src/lib/server/reviews.d.ts +35 -0
  27. package/src/lib/server/reviews.js +120 -1
  28. package/src/lib/server/reviews.js.map +1 -1
  29. package/src/lib/server/site-page-resolver.d.ts +15 -3
  30. package/src/lib/server/site-page-resolver.js +78 -24
  31. package/src/lib/server/site-page-resolver.js.map +1 -1
  32. package/src/lib/utils/add-to-cart.d.ts +35 -0
  33. package/src/lib/utils/add-to-cart.js +53 -0
  34. package/src/lib/utils/add-to-cart.js.map +1 -0
@@ -13,9 +13,12 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import * as Aglyn from "@aglyn/aglyn/server";
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
18
+ import * as Aglyn from "@aglyn/aglyn/server";
17
19
  import * as CommerceModel from "../model/index.js";
18
20
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
21
+ import { AggregateField } from "firebase-admin/firestore";
19
22
  /**
20
23
  * Approved reviews for a product, newest first, plus their aggregate.
21
24
  *
@@ -50,6 +53,90 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
50
53
  }
51
54
  };
52
55
  }
56
+ /** The most reviews the store-wide list returns. */ export const STORE_REVIEWS_MAX = 12;
57
+ /** What the store-wide list returns when the caller names no limit. */ export const STORE_REVIEWS_DEFAULT = 6;
58
+ /** "Jane Doe" → "Jane D."; one word stays as it is. */ export function reviewerShortName(name) {
59
+ const words = String(name != null ? name : '').trim().split(/\s+/).filter(Boolean);
60
+ if (!words.length) return 'Anonymous';
61
+ const [first, ...others] = words;
62
+ const last = others[others.length - 1];
63
+ return last ? `${first} ${last[0].toUpperCase()}.` : first;
64
+ }
65
+ /**
66
+ * The store's latest APPROVED reviews across every product, newest first, with
67
+ * the aggregate over all of them — the store-wide reviews band a storefront
68
+ * home carries. Approved only: a pending or rejected review never reaches a
69
+ * page, and nothing here is ever sample text (a published page shows real
70
+ * reviews or none).
71
+ *
72
+ * The list is `status == approved` ordered by `createdAtMs desc`, served by
73
+ * the COLLECTION composite `reviews (status ASC, createdAtMs DESC)` that the
74
+ * moderation queue already declares; the aggregate is a count and an average
75
+ * over the same equality, which the automatic single-field index serves.
76
+ */ export async function readStoreReviews(hostId, limit = STORE_REVIEWS_DEFAULT) {
77
+ var _data_count, _data_average;
78
+ const firestore = firebaseAdmin.app().firestore();
79
+ const hostRef = firestore.collection('hosts').doc(hostId);
80
+ const approved = hostRef.collection('reviews').where('status', '==', 'approved');
81
+ const max = Math.min(STORE_REVIEWS_MAX, Math.max(1, Math.floor(Number(limit)) || STORE_REVIEWS_DEFAULT));
82
+ const [listSnapshot, aggregateSnapshot] = await Promise.all([
83
+ approved.orderBy('createdAtMs', 'desc').limit(max).get(),
84
+ approved.aggregate({
85
+ count: AggregateField.count(),
86
+ average: AggregateField.average('rating')
87
+ }).get()
88
+ ]);
89
+ const rows = listSnapshot.docs.map((docSnapshot)=>{
90
+ var _docSnapshot_get, _docSnapshot_get1, _docSnapshot_get2, _docSnapshot_get3, _docSnapshot_get4;
91
+ return {
92
+ id: docSnapshot.id,
93
+ productId: String((_docSnapshot_get = docSnapshot.get('productId')) != null ? _docSnapshot_get : ''),
94
+ rating: Number((_docSnapshot_get1 = docSnapshot.get('rating')) != null ? _docSnapshot_get1 : 0),
95
+ body: String((_docSnapshot_get2 = docSnapshot.get('body')) != null ? _docSnapshot_get2 : ''),
96
+ authorName: reviewerShortName(String((_docSnapshot_get3 = docSnapshot.get('authorName')) != null ? _docSnapshot_get3 : '')),
97
+ verified: Boolean(docSnapshot.get('verified')),
98
+ createdAtMs: Number((_docSnapshot_get4 = docSnapshot.get('createdAtMs')) != null ? _docSnapshot_get4 : 0)
99
+ };
100
+ });
101
+ // The reviewed products' names, in one batched read of at most `max` docs.
102
+ const productIds = [
103
+ ...new Set(rows.map((row)=>row.productId).filter(Boolean))
104
+ ];
105
+ const products = new Map();
106
+ if (productIds.length) {
107
+ const snapshots = await firestore.getAll(...productIds.map((id)=>hostRef.collection('products').doc(id)));
108
+ for (const snapshot of snapshots){
109
+ var _data_name, _data_slug;
110
+ const data = snapshot.exists ? snapshot.data() : null;
111
+ if (!data || data['deletedAt'] || data['status'] !== 'active') continue;
112
+ products.set(snapshot.id, {
113
+ name: String((_data_name = data['name']) != null ? _data_name : ''),
114
+ slug: String((_data_slug = data['slug']) != null ? _data_slug : '')
115
+ });
116
+ }
117
+ }
118
+ const reviews = rows.map((_0)=>{
119
+ let { productId } = _0, row = _object_without_properties_loose(_0, [
120
+ "productId"
121
+ ]);
122
+ const product = products.get(productId);
123
+ return _extends({}, row, (product == null ? void 0 : product.name) ? {
124
+ productName: product.name
125
+ } : {}, (product == null ? void 0 : product.slug) ? {
126
+ productSlug: product.slug
127
+ } : {});
128
+ });
129
+ const data = aggregateSnapshot.data();
130
+ const count = Number((_data_count = data.count) != null ? _data_count : 0);
131
+ const average = count ? Number((_data_average = data.average) != null ? _data_average : 0) : 0;
132
+ return {
133
+ reviews,
134
+ aggregate: {
135
+ count,
136
+ average: Math.round(average * 10) / 10
137
+ }
138
+ };
139
+ }
53
140
  /**
54
141
  * Product reviews (AGL-324). GET returns approved reviews + aggregate;
55
142
  * POST submits into the moderation queue, marking `verified` when the
@@ -60,6 +147,38 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
60
147
  const isPost = req.method === 'POST';
61
148
  const hostId = String((_ref = isPost ? (_req_body = req.body) == null ? void 0 : _req_body.hostId : req.query.hostId) != null ? _ref : '');
62
149
  const productId = String((_ref1 = isPost ? (_req_body1 = req.body) == null ? void 0 : _req_body1.productId : req.query.productId) != null ? _ref1 : '');
150
+ // Store-wide (`scope=store`, no productId): the latest approved reviews
151
+ // across the store, for the storefront's reviews band. Read only, and only
152
+ // where the store's plan has reviews — a store without the feature shows
153
+ // no band rather than reviews it can no longer collect.
154
+ if (!isPost && !productId && req.query.scope === 'store') {
155
+ if (!hostId) return res.status(400).json({
156
+ error: 'Missing hostId'
157
+ });
158
+ try {
159
+ const org = await getOrgForHost(hostId);
160
+ if (!Aglyn.checkEntitlement(org == null ? void 0 : org.org, 'productReviews')) {
161
+ return res.status(200).json({
162
+ reviews: [],
163
+ aggregate: {
164
+ count: 0,
165
+ average: 0
166
+ }
167
+ });
168
+ }
169
+ const { reviews, aggregate } = await readStoreReviews(hostId, Number(req.query.limit));
170
+ res.setHeader('Cache-Control', 'public, s-maxage=120, stale-while-revalidate=300');
171
+ return res.status(200).json({
172
+ reviews,
173
+ aggregate
174
+ });
175
+ } catch (error) {
176
+ console.error(error);
177
+ return res.status(500).json({
178
+ error: 'Reviews unavailable'
179
+ });
180
+ }
181
+ }
63
182
  if (!hostId || !productId) {
64
183
  return res.status(400).json({
65
184
  error: 'Missing hostId or productId'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/reviews.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, getOrgForHost } from '@aglyn/tenant-data-admin'\n\nexport interface PublicProductReview {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply: string | null\n createdAtMs: number\n}\n\nexport interface ProductReviewAggregate {\n count: number\n /** Mean rating, one decimal. */\n average: number\n}\n\n/**\n * Approved reviews for a product, newest first, plus their aggregate.\n *\n * Extracted from the handler (AGL-686) so the site-page resolver can nest\n * `aggregateRating` inside the server-rendered `Product` node. Schema.org\n * wants it as a PROPERTY of the product; the reviews block was emitting a\n * free-standing `AggregateRating` node alongside it, which is orphaned and\n * ignored. One reader means the rating in the structured data and the rating\n * on the page cannot disagree.\n */\nexport async function readProductReviews(\n hostId: string,\n productId: string,\n): Promise<{\n reviews: PublicProductReview[]\n aggregate: ProductReviewAggregate\n}> {\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n const reviewsSnapshot = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('status', '==', 'approved')\n .limit(100)\n .get()\n const reviews = reviewsSnapshot.docs\n .map((docSnapshot) => ({\n id: docSnapshot.id,\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: String(docSnapshot.get('authorName') ?? 'Anonymous'),\n verified: Boolean(docSnapshot.get('verified')),\n reply: (docSnapshot.get('reply') as string | undefined) ?? null,\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n const count = reviews.length\n const average = count\n ? reviews.reduce((sum, review) => sum + review.rating, 0) / count\n : 0\n return {\n reviews,\n aggregate: { count, average: Math.round(average * 10) / 10 },\n }\n}\n\n/**\n * Product reviews (AGL-324). GET returns approved reviews + aggregate;\n * POST submits into the moderation queue, marking `verified` when the\n * email has a paid order containing the product. Pro-plan gated.\n */\nexport const reviewsHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n const hostId = String((isPost ? req.body?.hostId : req.query.hostId) ?? '')\n const productId = String(\n (isPost ? req.body?.productId : req.query.productId) ?? '',\n )\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\n if (isPost) {\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(403).json({ error: 'Reviews are not enabled' })\n }\n const rating = Math.min(5, Math.max(1, Math.round(Number(req.body?.rating ?? 0))))\n const body = String(req.body?.body ?? '').trim().slice(0, 2000)\n const authorName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n const authorEmail = String(req.body?.authorEmail ?? '')\n .trim()\n .toLowerCase()\n .slice(0, 120)\n if (!rating || !body || !authorEmail) {\n return res.status(400).json({ error: 'Rating, review, and email required' })\n }\n // Verified buyer: a paid order with the product under this email.\n let verified = false\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', authorEmail)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // Through the shared entitlement test (AGL-2454). \"Verified buyer\" on a\n // purchase that was refunded line by line is a badge for goods the\n // buyer no longer has — and the inline status literal this replaced\n // only moved on a FULL refund.\n if (CommerceModel.orderEntitlesProduct(order, productId)) {\n verified = true\n break\n }\n }\n await hostRef.collection('reviews').add({\n productId,\n rating,\n body,\n authorName: authorName || 'Anonymous',\n authorEmail,\n verified,\n status: 'pending',\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true })\n }\n\n const { reviews, aggregate } = await readProductReviews(hostId, productId)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=120, stale-while-revalidate=300',\n )\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","readProductReviews","hostId","productId","hostRef","app","firestore","collection","doc","reviewsSnapshot","where","limit","get","reviews","docs","map","docSnapshot","id","rating","Number","body","String","authorName","verified","Boolean","reply","createdAtMs","sort","a","b","count","length","average","reduce","sum","review","aggregate","Math","round","reviewsHandler","req","res","isPost","method","query","status","json","error","org","checkEntitlement","min","max","trim","slice","authorEmail","toLowerCase","orders","order","liftLegacyOrder","data","orderEntitlesProduct","add","Date","now","ok","pending","setHeader","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/reviews.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, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { AggregateField } from 'firebase-admin/firestore'\n\nexport interface PublicProductReview {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply: string | null\n createdAtMs: number\n}\n\nexport interface ProductReviewAggregate {\n count: number\n /** Mean rating, one decimal. */\n average: number\n}\n\n/**\n * Approved reviews for a product, newest first, plus their aggregate.\n *\n * Extracted from the handler (AGL-686) so the site-page resolver can nest\n * `aggregateRating` inside the server-rendered `Product` node. Schema.org\n * wants it as a PROPERTY of the product; the reviews block was emitting a\n * free-standing `AggregateRating` node alongside it, which is orphaned and\n * ignored. One reader means the rating in the structured data and the rating\n * on the page cannot disagree.\n */\nexport async function readProductReviews(\n hostId: string,\n productId: string,\n): Promise<{\n reviews: PublicProductReview[]\n aggregate: ProductReviewAggregate\n}> {\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n const reviewsSnapshot = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('status', '==', 'approved')\n .limit(100)\n .get()\n const reviews = reviewsSnapshot.docs\n .map((docSnapshot) => ({\n id: docSnapshot.id,\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: String(docSnapshot.get('authorName') ?? 'Anonymous'),\n verified: Boolean(docSnapshot.get('verified')),\n reply: (docSnapshot.get('reply') as string | undefined) ?? null,\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n const count = reviews.length\n const average = count\n ? reviews.reduce((sum, review) => sum + review.rating, 0) / count\n : 0\n return {\n reviews,\n aggregate: { count, average: Math.round(average * 10) / 10 },\n }\n}\n\n/** One approved review as the store-wide list shows it. */\nexport interface PublicStoreReview {\n id: string\n rating: number\n body: string\n /** First name and last initial only (\"Jane D.\"), never the full name. */\n authorName: string\n verified: boolean\n createdAtMs: number\n /** The reviewed product, when it is still on sale. */\n productName?: string\n productSlug?: string\n}\n\n/** The most reviews the store-wide list returns. */\nexport const STORE_REVIEWS_MAX = 12\n/** What the store-wide list returns when the caller names no limit. */\nexport const STORE_REVIEWS_DEFAULT = 6\n\n/** \"Jane Doe\" → \"Jane D.\"; one word stays as it is. */\nexport function reviewerShortName(name: string): string {\n const words = String(name ?? '').trim().split(/\\s+/).filter(Boolean)\n if (!words.length) return 'Anonymous'\n const [first, ...others] = words\n const last = others[others.length - 1]\n return last ? `${first} ${last[0].toUpperCase()}.` : first\n}\n\n/**\n * The store's latest APPROVED reviews across every product, newest first, with\n * the aggregate over all of them — the store-wide reviews band a storefront\n * home carries. Approved only: a pending or rejected review never reaches a\n * page, and nothing here is ever sample text (a published page shows real\n * reviews or none).\n *\n * The list is `status == approved` ordered by `createdAtMs desc`, served by\n * the COLLECTION composite `reviews (status ASC, createdAtMs DESC)` that the\n * moderation queue already declares; the aggregate is a count and an average\n * over the same equality, which the automatic single-field index serves.\n */\nexport async function readStoreReviews(\n hostId: string,\n limit: number = STORE_REVIEWS_DEFAULT,\n): Promise<{ reviews: PublicStoreReview[]; aggregate: ProductReviewAggregate }> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const approved = hostRef.collection('reviews').where('status', '==', 'approved')\n const max = Math.min(STORE_REVIEWS_MAX, Math.max(1, Math.floor(Number(limit)) || STORE_REVIEWS_DEFAULT))\n const [listSnapshot, aggregateSnapshot] = await Promise.all([\n approved.orderBy('createdAtMs', 'desc').limit(max).get(),\n approved\n .aggregate({ count: AggregateField.count(), average: AggregateField.average('rating') })\n .get(),\n ])\n const rows = listSnapshot.docs.map((docSnapshot) => ({\n id: docSnapshot.id,\n productId: String(docSnapshot.get('productId') ?? ''),\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: reviewerShortName(String(docSnapshot.get('authorName') ?? '')),\n verified: Boolean(docSnapshot.get('verified')),\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n // The reviewed products' names, in one batched read of at most `max` docs.\n const productIds = [...new Set(rows.map((row) => row.productId).filter(Boolean))]\n const products = new Map<string, { name: string; slug: string }>()\n if (productIds.length) {\n const snapshots = await firestore.getAll(\n ...productIds.map((id) => hostRef.collection('products').doc(id)),\n )\n for (const snapshot of snapshots) {\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n if (!data || data['deletedAt'] || data['status'] !== 'active') continue\n products.set(snapshot.id, { name: String(data['name'] ?? ''), slug: String(data['slug'] ?? '') })\n }\n }\n const reviews = rows.map(({ productId, ...row }) => {\n const product = products.get(productId)\n return {\n ...row,\n ...(product?.name ? { productName: product.name } : {}),\n ...(product?.slug ? { productSlug: product.slug } : {}),\n }\n })\n const data = aggregateSnapshot.data() as { count?: number; average?: number | null }\n const count = Number(data.count ?? 0)\n const average = count ? Number(data.average ?? 0) : 0\n return { reviews, aggregate: { count, average: Math.round(average * 10) / 10 } }\n}\n\n/**\n * Product reviews (AGL-324). GET returns approved reviews + aggregate;\n * POST submits into the moderation queue, marking `verified` when the\n * email has a paid order containing the product. Pro-plan gated.\n */\nexport const reviewsHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n const hostId = String((isPost ? req.body?.hostId : req.query.hostId) ?? '')\n const productId = String(\n (isPost ? req.body?.productId : req.query.productId) ?? '',\n )\n // Store-wide (`scope=store`, no productId): the latest approved reviews\n // across the store, for the storefront's reviews band. Read only, and only\n // where the store's plan has reviews — a store without the feature shows\n // no band rather than reviews it can no longer collect.\n if (!isPost && !productId && req.query.scope === 'store') {\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n try {\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(200).json({ reviews: [], aggregate: { count: 0, average: 0 } })\n }\n const { reviews, aggregate } = await readStoreReviews(hostId, Number(req.query.limit))\n res.setHeader('Cache-Control', 'public, s-maxage=120, stale-while-revalidate=300')\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n }\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\n if (isPost) {\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(403).json({ error: 'Reviews are not enabled' })\n }\n const rating = Math.min(5, Math.max(1, Math.round(Number(req.body?.rating ?? 0))))\n const body = String(req.body?.body ?? '').trim().slice(0, 2000)\n const authorName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n const authorEmail = String(req.body?.authorEmail ?? '')\n .trim()\n .toLowerCase()\n .slice(0, 120)\n if (!rating || !body || !authorEmail) {\n return res.status(400).json({ error: 'Rating, review, and email required' })\n }\n // Verified buyer: a paid order with the product under this email.\n let verified = false\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', authorEmail)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // Through the shared entitlement test (AGL-2454). \"Verified buyer\" on a\n // purchase that was refunded line by line is a badge for goods the\n // buyer no longer has — and the inline status literal this replaced\n // only moved on a FULL refund.\n if (CommerceModel.orderEntitlesProduct(order, productId)) {\n verified = true\n break\n }\n }\n await hostRef.collection('reviews').add({\n productId,\n rating,\n body,\n authorName: authorName || 'Anonymous',\n authorEmail,\n verified,\n status: 'pending',\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true })\n }\n\n const { reviews, aggregate } = await readProductReviews(hostId, productId)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=120, stale-while-revalidate=300',\n )\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","AggregateField","readProductReviews","hostId","productId","hostRef","app","firestore","collection","doc","reviewsSnapshot","where","limit","get","reviews","docs","map","docSnapshot","id","rating","Number","body","String","authorName","verified","Boolean","reply","createdAtMs","sort","a","b","count","length","average","reduce","sum","review","aggregate","Math","round","STORE_REVIEWS_MAX","STORE_REVIEWS_DEFAULT","reviewerShortName","name","words","trim","split","filter","first","others","last","toUpperCase","readStoreReviews","data","approved","max","min","floor","listSnapshot","aggregateSnapshot","Promise","all","orderBy","rows","productIds","Set","row","products","Map","snapshots","getAll","snapshot","exists","set","slug","product","productName","productSlug","reviewsHandler","req","res","isPost","method","query","scope","status","json","error","org","checkEntitlement","setHeader","console","slice","authorEmail","toLowerCase","orders","order","liftLegacyOrder","orderEntitlesProduct","add","Date","now","ok","pending"],"mappings":"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"}
@@ -34,10 +34,22 @@ export declare const commerceSitePageResolver: SitePageResolver;
34
34
  export declare function productPriceText(product: Pick<CommerceModel.HostProduct, 'variants'>, minPrice: number, maxPrice: number): string;
35
35
  /**
36
36
  * The store's built-in product page (AGL-3676), root first, for the layout's
37
- * slot: the product block for the routed slug and the related products under
38
- * it — the two blocks a product template is made of.
37
+ * slot: the product block for the routed slug — with its Details and
38
+ * Shipping & returns folds under the description, as a designer theme's
39
+ * product page has them — then the product's reviews where the store's plan
40
+ * collects them, then the related products as a grid of photo cards.
39
41
  */
40
- export declare function buildProductPageNodes(slug: string): Record<string, Aglyn.AglynNodeSchema>;
42
+ export declare function buildProductPageNodes(slug: string, options?: {
43
+ reviews?: boolean;
44
+ }): Record<string, Aglyn.AglynNodeSchema>;
45
+ /**
46
+ * The store's return window for this product, when the store takes returns of
47
+ * its kind: the settings the return form enforces (`readReturnSettings`), so
48
+ * the page never states a policy the store does not keep.
49
+ */
50
+ export declare function productReturnWindow(product: Pick<CommerceModel.HostProduct, 'type'>, rawReturns: unknown): {
51
+ windowDays: number;
52
+ } | undefined;
41
53
  /** The order-status page's node tree, root first, for the layout's slot. */
42
54
  export declare function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema>;
43
55
  /** The return form's node tree, root first, for the layout's slot. */
@@ -23,7 +23,7 @@ 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
24
  import getScreen from "@aglyn/tenant-runtime/get-screen";
25
25
  import { collectSocialImageFacts } from "@aglyn/tenant-runtime/social-image-facts";
26
- import { firebaseAdmin } from "@aglyn/tenant-data-admin";
26
+ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
27
27
  import * as CommerceModel from "../model/index.js";
28
28
  import { toPublicProductDetail } from "./product.js";
29
29
  import { readProductReviews } from "./reviews.js";
@@ -178,7 +178,7 @@ import { readProductReviews } from "./reviews.js";
178
178
  // guided start built has no template of its own, and its grid links each
179
179
  // product here.
180
180
  if (productRaw && !productRaw.deletedAt && productRaw.status === 'active') {
181
- return composeBuiltInProductPage(hostId, host, productSnapshot.docs[0].id, CommerceModel.liftLegacyProduct(productRaw));
181
+ return composeBuiltInProductPage(hostId, host, productSnapshot.docs[0].id, CommerceModel.liftLegacyProduct(productRaw), storeSettings.get('returns'));
182
182
  }
183
183
  }
184
184
  // Commerce collection routes (AGL-298): /collections/{slug} renders
@@ -279,13 +279,17 @@ import { readProductReviews } from "./reviews.js";
279
279
  return minPrice === maxPrice ? `$${minPrice}` : `From $${minPrice}`;
280
280
  }
281
281
  const PRODUCT_PAGE_NODE = 'pdp__detail';
282
+ const PRODUCT_PAGE_REVIEWS_NODE = 'pdp__reviews';
282
283
  const PRODUCT_PAGE_RELATED_NODE = 'pdp__related';
283
284
  /**
284
285
  * The store's built-in product page (AGL-3676), root first, for the layout's
285
- * slot: the product block for the routed slug and the related products under
286
- * it — the two blocks a product template is made of.
287
- */ export function buildProductPageNodes(slug) {
288
- return {
286
+ * slot: the product block for the routed slug — with its Details and
287
+ * Shipping & returns folds under the description, as a designer theme's
288
+ * product page has them — then the product's reviews where the store's plan
289
+ * collects them, then the related products as a grid of photo cards.
290
+ */ export function buildProductPageNodes(slug, options = {}) {
291
+ const reviews = Boolean(options.reviews);
292
+ return _extends({
289
293
  [Aglyn.NODE_ROOT_ID]: {
290
294
  $id: Aglyn.NODE_ROOT_ID,
291
295
  componentId: 'div',
@@ -307,6 +311,9 @@ const PRODUCT_PAGE_RELATED_NODE = 'pdp__related';
307
311
  },
308
312
  nodes: [
309
313
  PRODUCT_PAGE_NODE,
314
+ ...reviews ? [
315
+ PRODUCT_PAGE_REVIEWS_NODE
316
+ ] : [],
310
317
  PRODUCT_PAGE_RELATED_NODE
311
318
  ]
312
319
  },
@@ -316,9 +323,25 @@ const PRODUCT_PAGE_RELATED_NODE = 'pdp__related';
316
323
  componentId: 'product-detail',
317
324
  pluginId: 'commerce',
318
325
  props: {
319
- slug
326
+ slug,
327
+ showDetails: true,
328
+ showShipping: true
320
329
  }
321
- },
330
+ }
331
+ }, reviews ? {
332
+ [PRODUCT_PAGE_REVIEWS_NODE]: {
333
+ $id: PRODUCT_PAGE_REVIEWS_NODE,
334
+ parentId: 'pdp__container',
335
+ componentId: 'product-reviews',
336
+ pluginId: 'commerce',
337
+ props: {
338
+ heading: 'Reviews',
339
+ sx: {
340
+ marginTop: 8
341
+ }
342
+ }
343
+ }
344
+ } : {}, {
322
345
  [PRODUCT_PAGE_RELATED_NODE]: {
323
346
  $id: PRODUCT_PAGE_RELATED_NODE,
324
347
  parentId: 'pdp__container',
@@ -327,31 +350,58 @@ const PRODUCT_PAGE_RELATED_NODE = 'pdp__related';
327
350
  props: {
328
351
  heading: 'You may also like',
329
352
  maxItems: 4,
353
+ layout: 'grid',
330
354
  sx: {
331
355
  marginTop: 8
332
356
  }
333
357
  }
334
358
  }
359
+ });
360
+ }
361
+ /**
362
+ * The store's return window for this product, when the store takes returns of
363
+ * its kind: the settings the return form enforces (`readReturnSettings`), so
364
+ * the page never states a policy the store does not keep.
365
+ */ export function productReturnWindow(product, rawReturns) {
366
+ var _product_type;
367
+ const settings = CommerceModel.readReturnSettings(rawReturns);
368
+ if (!settings.enabled || settings.windowDays <= 0) return undefined;
369
+ if (!settings.eligibleTypes.includes((_product_type = product.type) != null ? _product_type : 'physical')) return undefined;
370
+ return {
371
+ windowDays: settings.windowDays
335
372
  };
336
373
  }
337
- async function composeBuiltInProductPage(hostId, host, productId, product) {
374
+ /** Whether the store's plan collects reviews; a failed read leaves them off. */ async function storeTakesReviews(hostId) {
375
+ try {
376
+ const org = await getOrgForHost(hostId);
377
+ return Aglyn.checkEntitlement(org == null ? void 0 : org.org, 'productReviews');
378
+ } catch (error) {
379
+ console.error('reviews entitlement read failed', error);
380
+ return false;
381
+ }
382
+ }
383
+ async function composeBuiltInProductPage(hostId, host, productId, product, rawReturns) {
338
384
  try {
339
385
  var _ref, _ref1, _ref2, _ref3;
340
386
  var _product_mediaUrls, _host_seo, _product_seo, _product_seo1;
341
- const layoutId = await resolveBuiltInPageLayoutId({
342
- hostId,
343
- host: host
344
- });
345
- const productReviews = await readProductReviews(hostId, productId).catch((error)=>{
346
- console.error('product review aggregate failed', error);
347
- return {
348
- reviews: [],
349
- aggregate: {
350
- count: 0,
351
- average: 0
352
- }
353
- };
354
- });
387
+ const [layoutId, productReviews, reviewsOn] = await Promise.all([
388
+ resolveBuiltInPageLayoutId({
389
+ hostId,
390
+ host: host
391
+ }),
392
+ readProductReviews(hostId, productId).catch((error)=>{
393
+ console.error('product review aggregate failed', error);
394
+ return {
395
+ reviews: [],
396
+ aggregate: {
397
+ count: 0,
398
+ average: 0
399
+ }
400
+ };
401
+ }),
402
+ storeTakesReviews(hostId)
403
+ ]);
404
+ const returns = productReturnWindow(product, rawReturns);
355
405
  const card = collectSocialImageFacts([
356
406
  (_ref = (_product_mediaUrls = product.mediaUrls) == null ? void 0 : _product_mediaUrls[0]) != null ? _ref : product.imageUrl,
357
407
  host == null ? void 0 : (_host_seo = host.seo) == null ? void 0 : _host_seo.image
@@ -359,7 +409,9 @@ async function composeBuiltInProductPage(hostId, host, productId, product) {
359
409
  const nodes = await composeNodesWithChrome({
360
410
  hostId,
361
411
  layoutId,
362
- screenNodes: buildProductPageNodes(product.slug),
412
+ screenNodes: buildProductPageNodes(product.slug, {
413
+ reviews: reviewsOn
414
+ }),
363
415
  socialImages: card.socialImages,
364
416
  host: host
365
417
  });
@@ -373,6 +425,8 @@ async function composeBuiltInProductPage(hostId, host, productId, product) {
373
425
  product: toPublicProductDetail(productId, product)
374
426
  }, productReviews.aggregate.count ? {
375
427
  reviews: productReviews
428
+ } : {}, returns ? {
429
+ returns
376
430
  } : {})
377
431
  },
378
432
  data: {
@@ -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 } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { toPublicProductDetail } from './product'\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 if (\n pdpScreenId &&\n productRaw &&\n !productRaw.deletedAt &&\n productRaw.status === 'active'\n ) {\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 ),\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 (productRaw && !productRaw.deletedAt && productRaw.status === 'active') {\n return composeBuiltInProductPage(\n hostId,\n host,\n productSnapshot.docs[0].id,\n CommerceModel.liftLegacyProduct(productRaw),\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_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 and the related products under\n * it — the two blocks a product template is made of.\n */\nexport function buildProductPageNodes(slug: string): Record<string, Aglyn.AglynNodeSchema> {\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: [PRODUCT_PAGE_NODE, PRODUCT_PAGE_RELATED_NODE],\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 },\n } as Aglyn.AglynNodeSchema,\n [PRODUCT_PAGE_RELATED_NODE]: {\n $id: PRODUCT_PAGE_RELATED_NODE,\n parentId: 'pdp__container',\n componentId: 'related-products',\n pluginId: 'commerce',\n props: { heading: 'You may also like', maxItems: 4, sx: { marginTop: 8 } },\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeBuiltInProductPage(\n hostId: string,\n host: unknown,\n productId: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const productReviews = await readProductReviews(hostId, productId).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\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),\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 ...(productReviews.aggregate.count ? { reviews: productReviews } : {}),\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"],"names":["hostCollectionKind","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","Aglyn","ORDER_STATUS_PATH","ORDER_STATUS_COMPONENT_ID","RETURN_REQUEST_COMPONENT_ID","RETURN_REQUEST_PATH","getScreen","collectSocialImageFacts","firebaseAdmin","CommerceModel","toPublicProductDetail","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","deletedAt","status","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","displayName","title","undefined","nodes","collected","revalidate","composeBuiltInProductPage","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot","variants","some","variant","variantHasPrice","PRODUCT_PAGE_NODE","PRODUCT_PAGE_RELATED_NODE","buildProductPageNodes","NODE_ROOT_ID","$id","componentId","pdp__container","parentId","pluginId","maxWidth","sx","paddingTop","paddingBottom","heading","maxItems","marginTop","productId","layoutId","screenNodes","ORDER_STATUS_NODE","buildOrderStatusNodes","visibility","HostScreenVisibility","UNLISTED","RETURN_REQUEST_NODE","buildReturnRequestNodes","ret__container"],"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 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 { toPublicProductDetail } from './product'\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 if (\n pdpScreenId &&\n productRaw &&\n !productRaw.deletedAt &&\n productRaw.status === 'active'\n ) {\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 ),\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 (productRaw && !productRaw.deletedAt && productRaw.status === 'active') {\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 ...(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"],"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","toPublicProductDetail","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","deletedAt","status","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","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"],"mappings":"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"}