@aglyn/plugins-commerce 1.0.0-beta.278 → 1.0.0-beta.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- package/src/lib/testing/fake-firestore.js.map +1 -1
|
@@ -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 { consumeRateLimit, firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { AggregateField } from 'firebase-admin/firestore'\nimport {\n MEMBER_SUSPENDED_ERROR,\n readActiveMemberSession,\n type MemberSessionCheck,\n} from './membership'\nimport { inspectReviewToken, redeemReviewTokenIn } from './review-tokens'\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/** Reviews one address may submit per site, per hour (AGL-3736). */\nexport const REVIEW_POSTS_PER_IP_PER_HOUR = 10\n/** Reviews one signed-in member may submit per site, per day. */\nexport const REVIEW_POSTS_PER_MEMBER_PER_DAY = 10\nconst HOUR_MS = 60 * 60 * 1000\n\n/**\n * The address a signed-in member has PROVEN they own, or `''`.\n *\n * A site member signs up by typing an email nobody checks, so the address on\n * the member document alone would let anyone register with a buyer's address\n * and collect a \"Verified buyer\" badge on their purchases. Only a member\n * whose inbox has answered — `emailVerifiedAtMs`, stamped when they complete\n * a password reset from the emailed link or redeem a review link while signed\n * in — counts.\n */\nexport function memberProvenEmail(\n member: FirebaseFirestore.DocumentSnapshot | null | undefined,\n): string {\n if (!member?.exists) return ''\n if (!Number(member.get('emailVerifiedAtMs') ?? 0)) return ''\n return String(member.get('email') ?? '').trim().toLowerCase()\n}\n\n/** The first order under `email` that still entitles `productId`, or null. */\nexport async function entitlingOrderId(\n hostRef: FirebaseFirestore.DocumentReference,\n email: string,\n productId: string,\n): Promise<string | null> {\n if (!email) return null\n // By the member's email, as the account page matches order history.\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', email)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // The shared entitlement test (AGL-2454): a line refunded by name is\n // goods the buyer no longer has.\n if (CommerceModel.orderEntitlesProduct(order, productId)) return docSnapshot.id\n }\n return null\n}\n\n/** A store's review settings, from `settings/store`. */\nasync function readReviewSettings(\n hostRef: FirebaseFirestore.DocumentReference,\n): Promise<CommerceModel.ReviewSettings> {\n const snapshot = await hostRef\n .collection('settings')\n .doc('store')\n .get()\n .catch(() => null)\n return CommerceModel.reviewSettingsFrom(snapshot?.data?.() ?? null)\n}\n\ntype ReviewsRequest = Parameters<PluginApiHandler>[0]\ntype ReviewsResponse = Parameters<PluginApiHandler>[1]\n\nfunction clientIp(req: ReviewsRequest): string {\n return (\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n )\n}\n\n/** What the server established about who is reviewing. */\ninterface ReviewerIdentity {\n signedIn: boolean\n suspended: boolean\n memberId: string | null\n memberEmail: string\n memberName: string\n /** The member's proven email holds an order that entitles the product. */\n memberOrderId: string | null\n}\n\nconst ANONYMOUS_REVIEWER: ReviewerIdentity = {\n signedIn: false,\n suspended: false,\n memberId: null,\n memberEmail: '',\n memberName: '',\n memberOrderId: null,\n}\n\nasync function identifyReviewer(\n req: ReviewsRequest,\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n productId: string,\n): Promise<ReviewerIdentity> {\n const session: MemberSessionCheck = await readActiveMemberSession(req, hostId).catch(\n () => ({ status: 'anonymous' }) as MemberSessionCheck,\n )\n if (session.status === 'suspended') {\n return { ...ANONYMOUS_REVIEWER, signedIn: true, suspended: true, memberId: session.memberId }\n }\n if (session.status !== 'active') return ANONYMOUS_REVIEWER\n const proven = memberProvenEmail(session.member)\n return {\n signedIn: true,\n suspended: false,\n memberId: session.memberId,\n memberEmail: String(session.member.get('email') ?? '').trim().toLowerCase(),\n memberName: String(session.member.get('displayName') ?? '').trim(),\n memberOrderId: proven ? await entitlingOrderId(hostRef, proven, productId) : null,\n }\n}\n\n/**\n * The storefront form's question (AGL-3736): may this visitor review this\n * product, and would the review be verified? `GET ?mode=eligibility`, never\n * cached — the answer depends on the visitor's cookie and link.\n */\nasync function eligibilityHandler(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n res.setHeader('Cache-Control', 'private, no-store')\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n const enabled = Aglyn.checkEntitlement(org?.org as any, 'productReviews')\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n const presented = req.query.review\n let tokenVerified = false\n let tokenProblem: CommerceModel.ReviewErrorCode | undefined\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (check.ok) tokenVerified = true\n else tokenProblem = check.code ?? CommerceModel.REVIEW_ERROR_CODES.tokenInvalid\n }\n const eligibility = CommerceModel.reviewEligibility({\n policy: settings.policy,\n signedIn: reviewer.signedIn && !reviewer.suspended,\n memberVerified: Boolean(reviewer.memberOrderId),\n tokenVerified,\n tokenProblem,\n })\n return res.status(200).json({\n ...eligibility,\n enabled,\n canReview: enabled && !reviewer.suspended && eligibility.canReview,\n })\n}\n\n/**\n * Product reviews (AGL-324, AGL-3736).\n *\n * GET returns approved reviews + aggregate (`scope=store`: store-wide;\n * `mode=eligibility`: what the form may do). POST submits one review:\n *\n * - VERIFIED when the reviewer presents an unspent, unexpired review link\n * for this product, or is a signed-in member whose proven email has an\n * order that still entitles it. Published at once (`approved`): the FTC's\n * 2024 rule bars suppressing honest negative reviews; the merchant may\n * still hide or reject one for a content reason.\n * - Otherwise, under the store's `verified` policy (the default), 403\n * `verified_purchase_required`; under `anyone`, saved `pending` with the\n * name and email given — the email is never shown and verifies nothing.\n *\n * Rate-limited per address and per member, durably. Plan-gated\n * (`productReviews`).\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 if (isPost) return await submitReview(req, res, hostId, productId)\n if (req.query.mode === 'eligibility') {\n return await eligibilityHandler(req, res, hostId, productId)\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\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function submitReview(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n const codes = CommerceModel.REVIEW_ERROR_CODES\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\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', code: codes.notEnabled })\n }\n const refuseRate = (resetMs: number) => {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({\n error: 'Too many reviews from here. Try again later.',\n code: codes.rateLimited,\n })\n }\n // Per address, before anything else is read.\n const perIp = await consumeRateLimit(`commerce-review:ip:${hostId}:${clientIp(req)}`, {\n limit: REVIEW_POSTS_PER_IP_PER_HOUR,\n windowMs: HOUR_MS,\n })\n if (!perIp.allowed) return refuseRate(perIp.resetMs)\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 typedName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n if (!rating || !body) {\n return res.status(400).json({ error: 'A rating and a review are required' })\n }\n\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n if (reviewer.suspended) {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR, code: codes.memberSuspended })\n }\n if (reviewer.memberId) {\n const perMember = await consumeRateLimit(\n `commerce-review:member:${hostId}:${reviewer.memberId}`,\n { limit: REVIEW_POSTS_PER_MEMBER_PER_DAY, windowMs: 24 * HOUR_MS },\n )\n if (!perMember.allowed) return refuseRate(perMember.resetMs)\n }\n\n // 1. A review link from the email. A link that does not work is an error,\n // never a silent fall back to an unverified review.\n const presented = req.body?.reviewToken\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (!check.ok || !check.record || !check.ref) {\n const code = check.code ?? codes.tokenInvalid\n return res.status(403).json({ error: tokenProblemMessage(code), code })\n }\n const reviewRef = hostRef.collection('reviews').doc(createResourceUid())\n const nowMs = Date.now()\n const problem = await firestore.runTransaction(async (transaction) => {\n const spent = await redeemReviewTokenIn(transaction, check.ref, productId, reviewRef.id, nowMs)\n if (spent) return spent\n transaction.create(reviewRef, {\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: check.record.email,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: true,\n verifiedVia: 'token',\n orderId: check.record.orderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: nowMs,\n })\n return null\n })\n if (problem) {\n return res.status(403).json({ error: tokenProblemMessage(problem), code: problem })\n }\n // The link came through the inbox, so a member signed in with the same\n // address has now proven it.\n if (reviewer.memberId && reviewer.memberEmail === check.record.email) {\n await hostRef\n .collection('siteMembers')\n .doc(reviewer.memberId)\n .set({ emailVerifiedAtMs: nowMs }, { merge: true })\n .catch(() => undefined)\n }\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 2. A signed-in member whose proven email bought it. One verified review\n // per member per product.\n if (reviewer.memberOrderId) {\n const already = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('memberId', '==', reviewer.memberId)\n .limit(1)\n .get()\n if (!already.empty) {\n return res.status(409).json({\n error: 'You have already reviewed this product.',\n code: codes.alreadyReviewed,\n })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: reviewer.memberEmail,\n memberId: reviewer.memberId,\n verified: true,\n verifiedVia: 'member',\n orderId: reviewer.memberOrderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 3. Nobody the store can verify.\n if (settings.policy !== 'anyone') {\n return res.status(403).json({\n error: 'Only customers who bought this can review it.',\n code: codes.verifiedPurchaseRequired,\n })\n }\n const authorEmail = reviewer.signedIn\n ? reviewer.memberEmail\n : String(req.body?.authorEmail ?? '').trim().toLowerCase().slice(0, 120)\n const authorName = typedName || reviewer.memberName\n if (!authorName || !EMAIL_PATTERN.test(authorEmail)) {\n return res.status(400).json({ error: 'Your name and email are required' })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName,\n // Kept for the merchant; never shown, and it verifies nothing.\n authorEmail,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: false,\n status: CommerceModel.initialReviewStatus(false),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true, verified: false })\n}\n\n/** The words for a review link that does not work. */\nexport function tokenProblemMessage(code: CommerceModel.ReviewErrorCode): string {\n switch (code) {\n case CommerceModel.REVIEW_ERROR_CODES.tokenUsed:\n return 'This review link has already been used.'\n case CommerceModel.REVIEW_ERROR_CODES.tokenExpired:\n return 'This review link has expired.'\n default:\n return 'This review link is not valid for this product.'\n }\n}\n"],"names":["Aglyn","CommerceModel","consumeRateLimit","firebaseAdmin","getOrgForHost","readClientIp","createResourceUid","AggregateField","MEMBER_SUSPENDED_ERROR","readActiveMemberSession","inspectReviewToken","redeemReviewTokenIn","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","REVIEW_POSTS_PER_IP_PER_HOUR","REVIEW_POSTS_PER_MEMBER_PER_DAY","HOUR_MS","memberProvenEmail","member","toLowerCase","entitlingOrderId","email","orders","order","liftLegacyOrder","orderEntitlesProduct","readReviewSettings","catch","reviewSettingsFrom","clientIp","req","headers","remoteAddress","socket","ANONYMOUS_REVIEWER","signedIn","suspended","memberId","memberEmail","memberName","memberOrderId","identifyReviewer","session","status","proven","eligibilityHandler","res","setHeader","org","enabled","checkEntitlement","settings","reviewer","presented","query","tokenVerified","tokenProblem","check","ok","code","REVIEW_ERROR_CODES","tokenInvalid","eligibility","reviewEligibility","policy","memberVerified","json","canReview","reviewsHandler","isPost","method","scope","error","console","submitReview","mode","EMAIL_PATTERN","codes","notEnabled","refuseRate","resetMs","ceil","Date","now","rateLimited","perIp","windowMs","allowed","slice","typedName","memberSuspended","perMember","reviewToken","record","ref","tokenProblemMessage","reviewRef","nowMs","problem","runTransaction","transaction","spent","create","authorEmail","verifiedVia","orderId","initialReviewStatus","emailVerifiedAtMs","merge","undefined","pending","already","empty","alreadyReviewed","verifiedPurchaseRequired","test","tokenUsed","tokenExpired"],"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 { consumeRateLimit, firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { AggregateField } from 'firebase-admin/firestore'\nimport {\n MEMBER_SUSPENDED_ERROR,\n readActiveMemberSession,\n type MemberSessionCheck,\n} from './membership'\nimport { inspectReviewToken, redeemReviewTokenIn } from './review-tokens'\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 // A product sold in person only (AGL-4060) is named and linked nowhere\n // online, as a draft is not.\n if (!CommerceModel.productListedOnline(data)) 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/** Reviews one address may submit per site, per hour (AGL-3736). */\nexport const REVIEW_POSTS_PER_IP_PER_HOUR = 10\n/** Reviews one signed-in member may submit per site, per day. */\nexport const REVIEW_POSTS_PER_MEMBER_PER_DAY = 10\nconst HOUR_MS = 60 * 60 * 1000\n\n/**\n * The address a signed-in member has PROVEN they own, or `''`.\n *\n * A site member signs up by typing an email nobody checks, so the address on\n * the member document alone would let anyone register with a buyer's address\n * and collect a \"Verified buyer\" badge on their purchases. Only a member\n * whose inbox has answered — `emailVerifiedAtMs`, stamped when they complete\n * a password reset from the emailed link or redeem a review link while signed\n * in — counts.\n */\nexport function memberProvenEmail(\n member: FirebaseFirestore.DocumentSnapshot | null | undefined,\n): string {\n if (!member?.exists) return ''\n if (!Number(member.get('emailVerifiedAtMs') ?? 0)) return ''\n return String(member.get('email') ?? '').trim().toLowerCase()\n}\n\n/** The first order under `email` that still entitles `productId`, or null. */\nexport async function entitlingOrderId(\n hostRef: FirebaseFirestore.DocumentReference,\n email: string,\n productId: string,\n): Promise<string | null> {\n if (!email) return null\n // By the member's email, as the account page matches order history.\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', email)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // The shared entitlement test (AGL-2454): a line refunded by name is\n // goods the buyer no longer has.\n if (CommerceModel.orderEntitlesProduct(order, productId)) return docSnapshot.id\n }\n return null\n}\n\n/** A store's review settings, from `settings/store`. */\nasync function readReviewSettings(\n hostRef: FirebaseFirestore.DocumentReference,\n): Promise<CommerceModel.ReviewSettings> {\n const snapshot = await hostRef\n .collection('settings')\n .doc('store')\n .get()\n .catch(() => null)\n return CommerceModel.reviewSettingsFrom(snapshot?.data?.() ?? null)\n}\n\ntype ReviewsRequest = Parameters<PluginApiHandler>[0]\ntype ReviewsResponse = Parameters<PluginApiHandler>[1]\n\nfunction clientIp(req: ReviewsRequest): string {\n return (\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n )\n}\n\n/** What the server established about who is reviewing. */\ninterface ReviewerIdentity {\n signedIn: boolean\n suspended: boolean\n memberId: string | null\n memberEmail: string\n memberName: string\n /** The member's proven email holds an order that entitles the product. */\n memberOrderId: string | null\n}\n\nconst ANONYMOUS_REVIEWER: ReviewerIdentity = {\n signedIn: false,\n suspended: false,\n memberId: null,\n memberEmail: '',\n memberName: '',\n memberOrderId: null,\n}\n\nasync function identifyReviewer(\n req: ReviewsRequest,\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n productId: string,\n): Promise<ReviewerIdentity> {\n const session: MemberSessionCheck = await readActiveMemberSession(req, hostId).catch(\n () => ({ status: 'anonymous' }) as MemberSessionCheck,\n )\n if (session.status === 'suspended') {\n return { ...ANONYMOUS_REVIEWER, signedIn: true, suspended: true, memberId: session.memberId }\n }\n if (session.status !== 'active') return ANONYMOUS_REVIEWER\n const proven = memberProvenEmail(session.member)\n return {\n signedIn: true,\n suspended: false,\n memberId: session.memberId,\n memberEmail: String(session.member.get('email') ?? '').trim().toLowerCase(),\n memberName: String(session.member.get('displayName') ?? '').trim(),\n memberOrderId: proven ? await entitlingOrderId(hostRef, proven, productId) : null,\n }\n}\n\n/**\n * The storefront form's question (AGL-3736): may this visitor review this\n * product, and would the review be verified? `GET ?mode=eligibility`, never\n * cached — the answer depends on the visitor's cookie and link.\n */\nasync function eligibilityHandler(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n res.setHeader('Cache-Control', 'private, no-store')\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n const enabled = Aglyn.checkEntitlement(org?.org as any, 'productReviews')\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n const presented = req.query.review\n let tokenVerified = false\n let tokenProblem: CommerceModel.ReviewErrorCode | undefined\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (check.ok) tokenVerified = true\n else tokenProblem = check.code ?? CommerceModel.REVIEW_ERROR_CODES.tokenInvalid\n }\n const eligibility = CommerceModel.reviewEligibility({\n policy: settings.policy,\n signedIn: reviewer.signedIn && !reviewer.suspended,\n memberVerified: Boolean(reviewer.memberOrderId),\n tokenVerified,\n tokenProblem,\n })\n return res.status(200).json({\n ...eligibility,\n enabled,\n canReview: enabled && !reviewer.suspended && eligibility.canReview,\n })\n}\n\n/**\n * Product reviews (AGL-324, AGL-3736).\n *\n * GET returns approved reviews + aggregate (`scope=store`: store-wide;\n * `mode=eligibility`: what the form may do). POST submits one review:\n *\n * - VERIFIED when the reviewer presents an unspent, unexpired review link\n * for this product, or is a signed-in member whose proven email has an\n * order that still entitles it. Published at once (`approved`): the FTC's\n * 2024 rule bars suppressing honest negative reviews; the merchant may\n * still hide or reject one for a content reason.\n * - Otherwise, under the store's `verified` policy (the default), 403\n * `verified_purchase_required`; under `anyone`, saved `pending` with the\n * name and email given — the email is never shown and verifies nothing.\n *\n * Rate-limited per address and per member, durably. Plan-gated\n * (`productReviews`).\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 if (isPost) return await submitReview(req, res, hostId, productId)\n if (req.query.mode === 'eligibility') {\n return await eligibilityHandler(req, res, hostId, productId)\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\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function submitReview(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n const codes = CommerceModel.REVIEW_ERROR_CODES\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\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', code: codes.notEnabled })\n }\n const refuseRate = (resetMs: number) => {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({\n error: 'Too many reviews from here. Try again later.',\n code: codes.rateLimited,\n })\n }\n // Per address, before anything else is read.\n const perIp = await consumeRateLimit(`commerce-review:ip:${hostId}:${clientIp(req)}`, {\n limit: REVIEW_POSTS_PER_IP_PER_HOUR,\n windowMs: HOUR_MS,\n })\n if (!perIp.allowed) return refuseRate(perIp.resetMs)\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 typedName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n if (!rating || !body) {\n return res.status(400).json({ error: 'A rating and a review are required' })\n }\n\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n if (reviewer.suspended) {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR, code: codes.memberSuspended })\n }\n if (reviewer.memberId) {\n const perMember = await consumeRateLimit(\n `commerce-review:member:${hostId}:${reviewer.memberId}`,\n { limit: REVIEW_POSTS_PER_MEMBER_PER_DAY, windowMs: 24 * HOUR_MS },\n )\n if (!perMember.allowed) return refuseRate(perMember.resetMs)\n }\n\n // 1. A review link from the email. A link that does not work is an error,\n // never a silent fall back to an unverified review.\n const presented = req.body?.reviewToken\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (!check.ok || !check.record || !check.ref) {\n const code = check.code ?? codes.tokenInvalid\n return res.status(403).json({ error: tokenProblemMessage(code), code })\n }\n const reviewRef = hostRef.collection('reviews').doc(createResourceUid())\n const nowMs = Date.now()\n const problem = await firestore.runTransaction(async (transaction) => {\n const spent = await redeemReviewTokenIn(transaction, check.ref, productId, reviewRef.id, nowMs)\n if (spent) return spent\n transaction.create(reviewRef, {\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: check.record.email,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: true,\n verifiedVia: 'token',\n orderId: check.record.orderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: nowMs,\n })\n return null\n })\n if (problem) {\n return res.status(403).json({ error: tokenProblemMessage(problem), code: problem })\n }\n // The link came through the inbox, so a member signed in with the same\n // address has now proven it.\n if (reviewer.memberId && reviewer.memberEmail === check.record.email) {\n await hostRef\n .collection('siteMembers')\n .doc(reviewer.memberId)\n .set({ emailVerifiedAtMs: nowMs }, { merge: true })\n .catch(() => undefined)\n }\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 2. A signed-in member whose proven email bought it. One verified review\n // per member per product.\n if (reviewer.memberOrderId) {\n const already = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('memberId', '==', reviewer.memberId)\n .limit(1)\n .get()\n if (!already.empty) {\n return res.status(409).json({\n error: 'You have already reviewed this product.',\n code: codes.alreadyReviewed,\n })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: reviewer.memberEmail,\n memberId: reviewer.memberId,\n verified: true,\n verifiedVia: 'member',\n orderId: reviewer.memberOrderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 3. Nobody the store can verify.\n if (settings.policy !== 'anyone') {\n return res.status(403).json({\n error: 'Only customers who bought this can review it.',\n code: codes.verifiedPurchaseRequired,\n })\n }\n const authorEmail = reviewer.signedIn\n ? reviewer.memberEmail\n : String(req.body?.authorEmail ?? '').trim().toLowerCase().slice(0, 120)\n const authorName = typedName || reviewer.memberName\n if (!authorName || !EMAIL_PATTERN.test(authorEmail)) {\n return res.status(400).json({ error: 'Your name and email are required' })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName,\n // Kept for the merchant; never shown, and it verifies nothing.\n authorEmail,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: false,\n status: CommerceModel.initialReviewStatus(false),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true, verified: false })\n}\n\n/** The words for a review link that does not work. */\nexport function tokenProblemMessage(code: CommerceModel.ReviewErrorCode): string {\n switch (code) {\n case CommerceModel.REVIEW_ERROR_CODES.tokenUsed:\n return 'This review link has already been used.'\n case CommerceModel.REVIEW_ERROR_CODES.tokenExpired:\n return 'This review link has expired.'\n default:\n return 'This review link is not valid for this product.'\n }\n}\n"],"names":["Aglyn","CommerceModel","consumeRateLimit","firebaseAdmin","getOrgForHost","readClientIp","createResourceUid","AggregateField","MEMBER_SUSPENDED_ERROR","readActiveMemberSession","inspectReviewToken","redeemReviewTokenIn","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","productListedOnline","set","slug","product","productName","productSlug","REVIEW_POSTS_PER_IP_PER_HOUR","REVIEW_POSTS_PER_MEMBER_PER_DAY","HOUR_MS","memberProvenEmail","member","toLowerCase","entitlingOrderId","email","orders","order","liftLegacyOrder","orderEntitlesProduct","readReviewSettings","catch","reviewSettingsFrom","clientIp","req","headers","remoteAddress","socket","ANONYMOUS_REVIEWER","signedIn","suspended","memberId","memberEmail","memberName","memberOrderId","identifyReviewer","session","status","proven","eligibilityHandler","res","setHeader","org","enabled","checkEntitlement","settings","reviewer","presented","query","tokenVerified","tokenProblem","check","ok","code","REVIEW_ERROR_CODES","tokenInvalid","eligibility","reviewEligibility","policy","memberVerified","json","canReview","reviewsHandler","isPost","method","scope","error","console","submitReview","mode","EMAIL_PATTERN","codes","notEnabled","refuseRate","resetMs","ceil","Date","now","rateLimited","perIp","windowMs","allowed","slice","typedName","memberSuspended","perMember","reviewToken","record","ref","tokenProblemMessage","reviewRef","nowMs","problem","runTransaction","transaction","spent","create","authorEmail","verifiedVia","orderId","initialReviewStatus","emailVerifiedAtMs","merge","undefined","pending","already","empty","alreadyReviewed","verifiedPurchaseRequired","test","tokenUsed","tokenExpired"],"mappings":"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"}
|
|
@@ -68,7 +68,10 @@ import { readProductReviews } from "./reviews.js";
|
|
|
68
68
|
]);
|
|
69
69
|
const pdpScreenId = storeSettings.get('pdpScreenId');
|
|
70
70
|
const productRaw = (_productSnapshot_docs_ = productSnapshot.docs[0]) == null ? void 0 : _productSnapshot_docs_.data();
|
|
71
|
-
|
|
71
|
+
// Live, active and sold online (AGL-4060): a product sold in person only
|
|
72
|
+
// has no page here, designed or built in, and falls through to the 404.
|
|
73
|
+
const listedOnline = CommerceModel.productListedOnline(productRaw);
|
|
74
|
+
if (pdpScreenId && listedOnline) {
|
|
72
75
|
const product = CommerceModel.liftLegacyProduct(productRaw);
|
|
73
76
|
const templateRes = await getScreen({
|
|
74
77
|
hostId,
|
|
@@ -177,7 +180,7 @@ import { readProductReviews } from "./reviews.js";
|
|
|
177
180
|
// rather than a 404 behind every card the product grid links. A store a
|
|
178
181
|
// guided start built has no template of its own, and its grid links each
|
|
179
182
|
// product here.
|
|
180
|
-
if (
|
|
183
|
+
if (listedOnline) {
|
|
181
184
|
return composeBuiltInProductPage(hostId, host, productSnapshot.docs[0].id, CommerceModel.liftLegacyProduct(productRaw), storeSettings.get('returns'));
|
|
182
185
|
}
|
|
183
186
|
}
|
|
@@ -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 { 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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"}
|
|
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 // 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 ),\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 ...(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","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","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":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,kBAAkB,QAAQ,sBAAqB;AACxD,OAAOC,sBACLC,sBAAsB,QACjB,6CAA4C;AACnD,SAASC,0BAA0B,QAAQ,6CAA4C;AACvF,YAAYC,WAAW,sBAAqB;AAC5C,SAASC,iBAAiB,QAAQ,0BAAsB;AACxD,SAASC,yBAAyB,QAAQ,+BAA2B;AACrE,SAASC,2BAA2B,EAAEC,mBAAmB,QAAQ,iCAA6B;AAC9F,OAAOC,eAAe,mCAAkC;AACxD,SAASC,uBAAuB,QAAQ,2CAA0C;AAClF,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,YAAYC,mBAAmB,oBAAU;AACzC,SAASC,qBAAqB,QAAQ,eAAW;AACjD,SAASC,kBAAkB,QAAQ,eAAW;AAE9C;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA6C,OAAO,EAC/DC,MAAM,EACNC,IAAI,EACJC,IAAI,EACL;IACC,MAAMC,UAAU;QAAEF;IAAK;IACvB,gEAAgE;IAChE,wDAAwD;IACxD,kEAAkE;IAClE,+DAA+D;IAC/D,6CAA6C;IAC7C,MAAMG,cAAcF,KAAKG,KAAK,CAAC,KAAKC,MAAM,CAACC;IAC3C,0EAA0E;IAC1E,2EAA2E;IAC3E,4EAA4E;IAC5E,4EAA4E;IAC5E,mEAAmE;IACnE,IAAIH,YAAYI,MAAM,KAAK,KAAKJ,WAAW,CAAC,EAAE,KAAKhB,mBAAmB;QACpE,OAAOqB,uBAAuBT,QAAQC;IACxC;IACA,uEAAuE;IACvE,yEAAyE;IACzE,gEAAgE;IAChE,IAAIG,YAAYI,MAAM,KAAK,KAAKJ,WAAW,CAAC,EAAE,KAAKb,qBAAqB;QACtE,OAAOmB,yBAAyBV,QAAQC;IAC1C;IACA,IAAIG,YAAYI,MAAM,KAAK,KAAKJ,WAAW,CAAC,EAAE,KAAK,YAAY;YAY1CO;QAXnB,MAAMC,iBAAiBlB,cAAcmB,GAAG,GAAGC,SAAS;QACpD,MAAMC,aAAaH,eAAeI,UAAU,CAAC,SAASC,GAAG,CAACjB;QAC1D,MAAM,CAACkB,eAAeP,gBAAgB,GAAG,MAAMQ,QAAQC,GAAG,CAAC;YACzDL,WAAWC,UAAU,CAAC,YAAYC,GAAG,CAAC,SAASI,GAAG;YAClDN,WACGC,UAAU,CAAC,YACXM,KAAK,CAAC,QAAQ,MAAMlB,WAAW,CAAC,EAAE,EAClCmB,KAAK,CAAC,GACNF,GAAG;SACP;QACD,MAAMG,cAAcN,cAAcG,GAAG,CAAC;QACtC,MAAMI,cAAad,yBAAAA,gBAAgBe,IAAI,CAAC,EAAE,qBAAvBf,uBAAyBgB,IAAI;QAChD,yEAAyE;QACzE,wEAAwE;QACxE,MAAMC,eAAehC,cAAciC,mBAAmB,CAACJ;QACvD,IAAID,eAAeI,cAAc;YAC/B,MAAME,UAAUlC,cAAcmC,iBAAiB,CAACN;YAChD,MAAMO,cAAc,MAAMxC,UAAU;gBAClCQ;gBACAiC,UAAUT;gBACV,uEAAuE;gBACvE,wEAAwE;gBACxEU,eAAe;YACjB;YACA,IAAIF,YAAYG,MAAM,EAAE;oBA4BKL,sBAGrBA;oBAhBJ,yBACA3B,mBAAAA,eAeI2B;gBA9BN,MAAM,CAACM,UAAUC,SAAS,GAAGzC,cAAc0C,iBAAiB,CAACR;gBAC7D,mEAAmE;gBACnE,yBAAyB;gBACzB,MAAMS,iBAAiB,MAAMzC,mBAC3BE,QACAW,gBAAgBe,IAAI,CAAC,EAAE,CAACc,EAAE,EAC1BC,KAAK,CAAC,CAACC;oBACPC,QAAQD,KAAK,CAAC,mCAAmCA;oBACjD,OAAO;wBAAEE,SAAS,EAAE;wBAAEC,WAAW;4BAAEC,OAAO;4BAAGC,SAAS;wBAAE;oBAAE;gBAC5D;gBACA,mEAAmE;gBACnE,mEAAmE;gBACnE,cAAc;gBACd,MAAMC,OAAOvD,wBAAwB;qBACnC,0BAAA,AAACuC,YAAYG,MAAM,CAASc,GAAG,qBAA/B,wBAAiCC,KAAK;qBACtC/C,gBAAAA,QAAQF,IAAI,sBAAZE,oBAAAA,cAAc8C,GAAG,qBAAjB9C,kBAAmB+C,KAAK;iBACzB;gBACD,MAAMC,gBAAgB,MAAMnE,mBAAmB;oBAC7CgB;oBACAiC,UAAUT;oBACVW,QAAQH,YAAYG,MAAM;oBAC1BiB,cAAcJ,KAAKI,YAAY;oBAC/B,gEAAgE;oBAChE,uDAAuD;oBACvDnD,MAAME,QAAQF,IAAI;oBAClBoD,QAAQ;wBACN,gBAAgBvB,QAAQwB,IAAI;wBAC5B,qBAAqB,GAAExB,uBAAAA,QAAQyB,WAAW,YAAnBzB,uBAAuB;wBAC9C,iBAAiB0B,iBAAiB1B,SAASM,UAAUC;wBACrD,eAAe,GACbP,iBAAAA,qBAAAA,QAAQ2B,SAAS,qBAAjB3B,kBAAmB,CAAC,EAAE,oBAAIA,QAAQ4B,QAAQ,YAA1C5B,OAA8C;wBAChD,gBAAgBA,QAAQ6B,IAAI;oBAC9B;oBACA,kEAAkE;oBAClE,gDAAgD;oBAChDC,MAAM;wBACJC,UAAU;4BACRC,MAAM;4BACNC,OAAO;4BACPC,gBAAgB;4BAChBC,WAAW,CAAC,UAAU,EAAEnC,QAAQ6B,IAAI,EAAE;4BACtCO,UAAU;wBACZ;oBACF;gBACF;gBACA,IAAIf,eAAe;wBAsCC,iCAMFrB;wBAFKA,cAELA;oBA3ChB,OAAO;wBACLqC,OAAOC,KAAKC,KAAK,CACfD,KAAKE,SAAS,CAAC;4BACb,wDAAwD;4BACxD,6DAA6D;4BAC7D,0DAA0D;4BAC1D,wDAAwD;4BACxDC,UAAU;gCACRC,UAAU;oCACR1C,SAASjC,sBACPc,gBAAgBe,IAAI,CAAC,EAAE,CAACc,EAAE,EAC1BV;mCAOES,eAAeM,SAAS,CAACC,KAAK,GAC9B;oCAAEF,SAASL;gCAAe,IAC1B,CAAC;4BAET;4BACAZ,MAAM;gCACJ1B,MAAME,QAAQF,IAAI;gCAClBkC,QAAQ;oCACNR,MAAM,aACDK,YAAYG,MAAM;wCACrB,uDAAuD;wCACvD,uDAAuD;wCACvD,wDAAwD;wCACxD,uDAAuD;wCACvD,qDAAqD;wCACrD,wDAAwD;wCACxD,+CAA+C;wCAC/CsC,aAAa3C,QAAQwB,IAAI;wCACzBL,KAAK,cACC,2BAAA,AAACjB,YAAYG,MAAM,CAASc,GAAG,YAA/B,2BAAmC,CAAC;4CACxC,qDAAqD;4CACrD,mDAAmD;4CACnD,wCAAwC;4CACxCyB,KAAK,YAAE5C,eAAAA,QAAQmB,GAAG,qBAAXnB,aAAa4C,KAAK,oBAAIC;4CAC7BpB,WAAW,GACTzB,kBAAAA,gBAAAA,QAAQmB,GAAG,qBAAXnB,cAAayB,WAAW,oBACxBzB,QAAQyB,WAAW,YADnBzB,QAEA6C;;;gCAGR;4BACF;4BACAC,OAAOzB;2BACJH,KAAK6B,SAAS;wBAGrBC,YAAY;oBACd;gBACF;YACF;QACF;QACA,oEAAoE;QACpE,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,gBAAgB;QAChB,IAAIlD,cAAc;YAChB,OAAOmD,0BACL/E,QACAC,MACAU,gBAAgBe,IAAI,CAAC,EAAE,CAACc,EAAE,EAC1B5C,cAAcmC,iBAAiB,CAACN,aAChCP,cAAcG,GAAG,CAAC;QAEtB;IACF;IACA,oEAAoE;IACpE,iEAAiE;IACjE,yDAAyD;IACzD,IAAIjB,YAAYI,MAAM,KAAK,KAAKJ,WAAW,CAAC,EAAE,KAAK,eAAe;YAezC4E;QAdvB,MAAMpE,iBAAiBlB,cAAcmB,GAAG,GAAGC,SAAS;QACpD,MAAMC,aAAaH,eAAeI,UAAU,CAAC,SAASC,GAAG,CAACjB;QAC1D,MAAM,CAACkB,eAAe8D,mBAAmB,GAAG,MAAM7D,QAAQC,GAAG,CAAC;YAC5DL,WAAWC,UAAU,CAAC,YAAYC,GAAG,CAAC,SAASI,GAAG;YAClDN,WACGC,UAAU,CAAC,eACXM,KAAK,CAAC,QAAQ,MAAMlB,WAAW,CAAC,EAAE,EAClCmB,KAAK,CAAC,GACNF,GAAG;SACP;QACD,MAAM4D,qBAAqB/D,cAAcG,GAAG,CAAC;QAC7C,yEAAyE;QACzE,wEAAwE;QACxE,uEAAuE;QACvE,MAAM6D,kBAAiBF,gCAAAA,mBAAmBtD,IAAI,CAC3CyD,IAAI,CAAC,CAACC,cAAgBrG,mBAAmBqG,YAAYzD,IAAI,QAAQ,+BAD7CqD,8BAEnBrD,IAAI;QACR,IAAIsD,sBAAsBC,gBAAgB;YACxC,MAAMlD,cAAc,MAAMxC,UAAU;gBAClCQ;gBACAiC,UAAUgD;gBACV/C,eAAe;YACjB;YACA,IAAIF,YAAYG,MAAM,EAAE;oBAgBQ+C,6BACNA;oBAbtB,0BACA/E,oBAAAA;gBAJF,mEAAmE;gBACnE,cAAc;gBACd,MAAM6C,OAAOvD,wBAAwB;qBACnC,2BAAA,AAACuC,YAAYG,MAAM,CAASc,GAAG,qBAA/B,yBAAiCC,KAAK;qBACtC/C,iBAAAA,QAAQF,IAAI,sBAAZE,qBAAAA,eAAc8C,GAAG,qBAAjB9C,mBAAmB+C,KAAK;iBACzB;gBACD,MAAMC,gBAAgB,MAAMnE,mBAAmB;oBAC7CgB;oBACAiC,UAAUgD;oBACV9C,QAAQH,YAAYG,MAAM;oBAC1BiB,cAAcJ,KAAKI,YAAY;oBAC/B,kCAAkC;oBAClCnD,MAAME,QAAQF,IAAI;oBAClBoD,QAAQ;wBACN,mBAAmB6B,eAAe5B,IAAI;wBACtC,wBAAwB,GAAE4B,8BAAAA,eAAe3B,WAAW,YAA1B2B,8BAA8B;wBACxD,kBAAkB,GAAEA,2BAAAA,eAAexB,QAAQ,YAAvBwB,2BAA2B;wBAC/C,mBAAmBA,eAAevB,IAAI;oBACxC;oBACA,kCAAkC;oBAClCC,MAAM;wBACJC,UAAU;4BACRC,MAAM;4BACNC,OAAO;4BACPC,gBAAgB;4BAChBC,WAAW,CAAC,aAAa,EAAEiB,eAAevB,IAAI,EAAE;4BAChDO,UAAU;wBACZ;oBACF;gBACF;gBACA,IAAIf,eAAe;wBAgBC,0BAGF+B;oBAlBhB,OAAO;wBACLf,OAAOC,KAAKC,KAAK,CACfD,KAAKE,SAAS,CAAC;4BACb3C,MAAM;gCACJ1B,MAAME,QAAQF,IAAI;gCAClBkC,QAAQ;oCACNR,MAAM,aACDK,YAAYG,MAAM;wCACrB,wDAAwD;wCACxD,uDAAuD;wCACvD,qDAAqD;wCACrD,uDAAuD;wCACvD,wCAAwC;wCACxCsC,aAAaS,eAAe5B,IAAI;wCAChCL,KAAK,cACC,2BAAA,AAACjB,YAAYG,MAAM,CAASc,GAAG,YAA/B,2BAAmC,CAAC;4CACxCyB,OAAOC;4CACPpB,WAAW,GACT2B,+BAAAA,eAAe3B,WAAW,YAA1B2B,+BAA8BP;;;gCAGtC;4BACF;4BACAC,OAAOzB;2BACJH,KAAK6B,SAAS;wBAGrBC,YAAY;oBACd;gBACF;YACF;QACF;IACF;IACA,OAAOH;AACT,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASnB,iBACd1B,OAAoD,EACpDM,QAAgB,EAChBC,QAAgB;QAEVP;IAAN,IAAI,CAAC,EAACA,oBAAAA,QAAQuD,QAAQ,YAAhBvD,oBAAoB,EAAE,EAAEwD,IAAI,CAAC,CAACC,UAAY3F,cAAc4F,eAAe,CAACD,WAAW;QACvF,OAAO;IACT;IACA,OAAOnD,aAAaC,WAAW,CAAC,CAAC,EAAED,UAAU,GAAG,CAAC,MAAM,EAAEA,UAAU;AACrE;AAEA,MAAMqD,oBAAoB;AAC1B,MAAMC,4BAA4B;AAClC,MAAMC,4BAA4B;AAElC;;;;;;CAMC,GACD,OAAO,SAASC,sBACdjC,IAAY,EACZkC,UAAiC,CAAC,CAAC;IAEnC,MAAMjD,UAAUrC,QAAQsF,QAAQjD,OAAO;IACvC,OAAO;QACL,CAACzD,MAAM2G,YAAY,CAAC,EAAE;YACpBC,KAAK5G,MAAM2G,YAAY;YACvBE,aAAa;YACbpB,OAAO;gBAAC;aAAiB;QAC3B;QACAqB,gBAAgB;YACdF,KAAK;YACLG,UAAU/G,MAAM2G,YAAY;YAC5BE,aAAa;YACbG,UAAU;YACVhC,OAAO;gBAAEiC,UAAU;gBAAMC,IAAI;oBAAEC,YAAY;oBAAGC,eAAe;gBAAG;YAAE;YAClE3B,OAAO;gBACLa;mBACI7C,UAAU;oBAAC8C;iBAA0B,GAAG,EAAE;gBAC9CC;aACD;QACH;QACA,CAACF,kBAAkB,EAAE;YACnBM,KAAKN;YACLS,UAAU;YACVF,aAAa;YACbG,UAAU;YACVhC,OAAO;gBAAER;gBAAM6C,aAAa;gBAAMC,cAAc;YAAK;QACvD;OACI7D,UACA;QACE,CAAC8C,0BAA0B,EAAE;YAC3BK,KAAKL;YACLQ,UAAU;YACVF,aAAa;YACbG,UAAU;YACVhC,OAAO;gBAAEuC,SAAS;gBAAWL,IAAI;oBAAEM,WAAW;gBAAE;YAAE;QACpD;IACF,IACA,CAAC;QACL,CAAChB,0BAA0B,EAAE;YAC3BI,KAAKJ;YACLO,UAAU;YACVF,aAAa;YACbG,UAAU;YACVhC,OAAO;gBACLuC,SAAS;gBACTE,UAAU;gBACVC,QAAQ;gBACRR,IAAI;oBAAEM,WAAW;gBAAE;YACrB;QACF;;AAEJ;AAEA;;;;CAIC,GACD,OAAO,SAASG,oBACdhF,OAAgD,EAChDiF,UAAmB;QAIkBjF;IAFrC,MAAMkF,WAAWpH,cAAcqH,kBAAkB,CAACF;IAClD,IAAI,CAACC,SAASE,OAAO,IAAIF,SAASG,UAAU,IAAI,GAAG,OAAOxC;IAC1D,IAAI,CAACqC,SAASI,aAAa,CAACC,QAAQ,EAACvF,gBAAAA,QAAQwF,IAAI,YAAZxF,gBAAgB,aAAa,OAAO6C;IACzE,OAAO;QAAEwC,YAAYH,SAASG,UAAU;IAAC;AAC3C;AAEA,8EAA8E,GAC9E,eAAeI,kBAAkBvH,MAAc;IAC7C,IAAI;QACF,MAAMwH,MAAM,MAAM7H,cAAcK;QAChC,OAAOb,MAAMsI,gBAAgB,CAACD,uBAAAA,IAAKA,GAAG,EAAW;IACnD,EAAE,OAAO9E,OAAO;QACdC,QAAQD,KAAK,CAAC,mCAAmCA;QACjD,OAAO;IACT;AACF;AAEA,eAAeqC,0BACb/E,MAAc,EACdC,IAAa,EACbyH,SAAiB,EACjB5F,OAA2D,EAC3DiF,UAAoB;IAEpB,IAAI;yBA2CyBjF;YAhCzBA,oBACA,WA8BmBA,cACMA;QA1C3B,MAAM,CAAC6F,UAAUpF,gBAAgBqF,UAAU,GAAG,MAAMzG,QAAQC,GAAG,CAAC;YAC9DlC,2BAA2B;gBAAEc;gBAAQC,MAAMA;YAAc;YACzDH,mBAAmBE,QAAQ0H,WAAWjF,KAAK,CAAC,CAACC;gBAC3CC,QAAQD,KAAK,CAAC,mCAAmCA;gBACjD,OAAO;oBAAEE,SAAS,EAAE;oBAAEC,WAAW;wBAAEC,OAAO;wBAAGC,SAAS;oBAAE;gBAAE;YAC5D;YACAwE,kBAAkBvH;SACnB;QACD,MAAM6H,UAAUf,oBAAoBhF,SAASiF;QAC7C,MAAM/D,OAAOvD,wBAAwB;qBACnCqC,qBAAAA,QAAQ2B,SAAS,qBAAjB3B,kBAAmB,CAAC,EAAE,mBAAIA,QAAQ4B,QAAQ;YACzCzD,yBAAD,YAAA,AAACA,KAA8CgD,GAAG,qBAAlD,UAAoDC,KAAK;SAC1D;QACD,MAAM0B,QAAQ,MAAM3F,uBAAuB;YACzCe;YACA2H;YACAG,aAAalC,sBAAsB9D,QAAQ6B,IAAI,EAAE;gBAAEf,SAASgF;YAAU;YACtExE,cAAcJ,KAAKI,YAAY;YAC/BnD,MAAMA;QACR;QACA,IAAI,CAAC2E,OAAO,OAAOD;QACnB,OAAO;YACLR,OAAOC,KAAKC,KAAK,CACfD,KAAKE,SAAS,CAAC;gBACb,iEAAiE;gBACjE,gEAAgE;gBAChEC,UAAU;oBACRC,UAAU;wBACR1C,SAASjC,sBAAsB6H,WAAW5F;uBACtCS,eAAeM,SAAS,CAACC,KAAK,GAAG;wBAAEF,SAASL;oBAAe,IAAI,CAAC,GAGhEsF,UAAU;wBAAEA;oBAAQ,IAAI,CAAC;gBAEjC;gBACAlG,MAAM;oBACJ1B;oBACAkC,QAAQ;wBACNR,MAAM;4BACJ8C,aAAa3C,QAAQwB,IAAI;4BACzBL,KAAK;gCACHyB,KAAK,YAAE5C,eAAAA,QAAQmB,GAAG,qBAAXnB,aAAa4C,KAAK,oBAAIC;gCAC7BpB,WAAW,GAAEzB,kBAAAA,gBAAAA,QAAQmB,GAAG,qBAAXnB,cAAayB,WAAW,oBAAIzB,QAAQyB,WAAW,YAA/CzB,QAAmD6C;4BAClE;wBACF;oBACF;gBACF;gBACAC;eACG5B,KAAK6B,SAAS;YAGrBC,YAAY;QACd;IACF,EAAE,OAAOpC,OAAO;QACdC,QAAQD,KAAK,CAAC,4CAA4CA;QAC1D,OAAOiC;IACT;AACF;AAEA,MAAMoD,oBAAoB;AAE1B,0EAA0E,GAC1E,OAAO,SAASC;IACd,OAAO;QACL,CAAC7I,MAAM2G,YAAY,CAAC,EAAE;YACpBC,KAAK5G,MAAM2G,YAAY;YACvBE,aAAa;YACbpB,OAAO;gBAAC;aAA0B;QACpC;QACA,2BAA2B;YACzBmB,KAAK;YACLG,UAAU/G,MAAM2G,YAAY;YAC5BE,aAAa;YACbG,UAAU;YACVhC,OAAO;gBAAEiC,UAAU;gBAAMC,IAAI;oBAAEC,YAAY;oBAAGC,eAAe;gBAAE;YAAE;YACjE3B,OAAO;gBAACmD;aAAkB;QAC5B;QACA,CAACA,kBAAkB,EAAE;YACnBhC,KAAKgC;YACL7B,UAAU;YACVF,aAAa3G;YACb8G,UAAU;YACVhC,OAAO,CAAC;QACV;IACF;AACF;AAEA,eAAe1D,uBAAuBT,MAAc,EAAEC,IAAa;IACjE,IAAI;QACF,MAAM0H,WAAW,MAAMzI,2BAA2B;YAAEc;YAAQC,MAAMA;QAAc;QAChF,MAAM2E,QAAQ,MAAM3F,uBAAuB;YACzCe;YACA2H;YACAG,aAAaE;YACb/H,MAAMA;QACR;QACA,IAAI,CAAC2E,OAAO,OAAOD;QACnB,OAAO;YACLR,OAAOC,KAAKC,KAAK,CACfD,KAAKE,SAAS,CAAC;gBACb3C,MAAM;oBACJ1B;oBACAkC,QAAQ;wBACNR,MAAM;4BACJ8C,aAAa;4BACbwD,YAAY9I,MAAM+I,oBAAoB,CAACC,QAAQ;wBACjD;oBACF;gBACF;gBACAvD;YACF;YAEFE,YAAY;QACd;IACF,EAAE,OAAOpC,OAAO;QACdC,QAAQD,KAAK,CAAC,wCAAwCA;QACtD,OAAOiC;IACT;AACF;AAEA,MAAMyD,sBAAsB;AAE5B,oEAAoE,GACpE,OAAO,SAASC;IACd,OAAO;QACL,CAAClJ,MAAM2G,YAAY,CAAC,EAAE;YACpBC,KAAK5G,MAAM2G,YAAY;YACvBE,aAAa;YACbpB,OAAO;gBAAC;aAAiB;QAC3B;QACA0D,gBAAgB;YACdvC,KAAK;YACLG,UAAU/G,MAAM2G,YAAY;YAC5BE,aAAa;YACbG,UAAU;YACVhC,OAAO;gBAAEiC,UAAU;gBAAMC,IAAI;oBAAEC,YAAY;oBAAGC,eAAe;gBAAE;YAAE;YACjE3B,OAAO;gBAACwD;aAAoB;QAC9B;QACA,CAACA,oBAAoB,EAAE;YACrBrC,KAAKqC;YACLlC,UAAU;YACVF,aAAa1G;YACb6G,UAAU;YACVhC,OAAO,CAAC;QACV;IACF;AACF;AAEA,eAAezD,yBAAyBV,MAAc,EAAEC,IAAa;IACnE,IAAI;QACF,MAAM0H,WAAW,MAAMzI,2BAA2B;YAAEc;YAAQC,MAAMA;QAAc;QAChF,MAAM2E,QAAQ,MAAM3F,uBAAuB;YACzCe;YACA2H;YACAG,aAAaO;YACbpI,MAAMA;QACR;QACA,IAAI,CAAC2E,OAAO,OAAOD;QACnB,OAAO;YACLR,OAAOC,KAAKC,KAAK,CACfD,KAAKE,SAAS,CAAC;gBACb3C,MAAM;oBACJ1B;oBACAkC,QAAQ;wBACNR,MAAM;4BACJ8C,aAAa;4BACbwD,YAAY9I,MAAM+I,oBAAoB,CAACC,QAAQ;wBACjD;oBACF;gBACF;gBACAvD;YACF;YAEFE,YAAY;QACd;IACF,EAAE,OAAOpC,OAAO;QACdC,QAAQD,KAAK,CAAC,0CAA0CA;QACxD,OAAOiC;IACT;AACF"}
|
package/src/lib/server.js
CHANGED
|
@@ -396,6 +396,14 @@ registerPluginJob({
|
|
|
396
396
|
// Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/
|
|
397
397
|
// subscriptions ride the platform webhook via the hook registry.
|
|
398
398
|
registerBillingWebhookHandler(commerceBillingWebhookHandler);
|
|
399
|
+
// A paid card-reader order from the POS hardware shop (AGL-4038), on the
|
|
400
|
+
// same platform webhook: its own handler, self-selecting on the session's
|
|
401
|
+
// `metadata.type`, so the storefront sections above never see it.
|
|
402
|
+
registerBillingWebhookHandler(async (event)=>(await import("./server/pos-hardware-webhook.js")).posHardwareBillingWebhookHandler(event));
|
|
403
|
+
// A card the Aglyn POS app took offline (AGL-4039): its PaymentIntent can
|
|
404
|
+
// reach Stripe before the sale reaches the server, so every event on one is
|
|
405
|
+
// kept by the sale's key. Self-selecting on `metadata.kind`.
|
|
406
|
+
registerBillingWebhookHandler(async (event)=>(await import("./server/pos-offline-card-reconcile.js")).posOfflineCardBillingWebhookHandler(event));
|
|
399
407
|
// Cancel + stock release in one transaction (AGL-1808). Server-side because
|
|
400
408
|
// the release depends on the transition rule, and a client write could not
|
|
401
409
|
// re-ask it under the same lock that flips the status.
|
|
@@ -423,6 +431,10 @@ registerPluginJob({
|
|
|
423
431
|
// on first call.
|
|
424
432
|
registerPluginApiRoute('commerce/products/save', async (req, res)=>(await import("./server/products-write.js")).productSaveHandler(req, res));
|
|
425
433
|
registerPluginApiRoute('commerce/products/stock', async (req, res)=>(await import("./server/products-write.js")).productStockHandler(req, res));
|
|
434
|
+
// A staff walk-in from the Reservations card and the native apps
|
|
435
|
+
// (AGL-4046): availability re-asked inside the transaction that writes, so
|
|
436
|
+
// two staff cannot hand out the last unit at once. Loaded on first call.
|
|
437
|
+
registerPluginApiRoute('commerce/reservations/walk-in', async (req, res)=>(await import("./server/reservation-walk-in.js")).reservationWalkInHandler(req, res));
|
|
426
438
|
// The products list's Duplicate and Delete for the native apps (AGL-3841):
|
|
427
439
|
// the console's own copy and soft delete, with the site-wide cache drop.
|
|
428
440
|
registerPluginApiRoute('commerce/products/duplicate', async (req, res)=>(await import("./server/products-write.js")).productDuplicateHandler(req, res));
|
|
@@ -486,6 +498,20 @@ registerPluginJob({
|
|
|
486
498
|
registerPluginApiRoute('commerce/pos-payment', posPaymentHandler);
|
|
487
499
|
registerPluginApiRoute('commerce/pos-readers', posReadersHandler);
|
|
488
500
|
registerPluginApiRoute('commerce/pos-display', posDisplayHandler);
|
|
501
|
+
// The POS hardware shop (AGL-4038): the Hardware card's catalog, Checkout
|
|
502
|
+
// and order list, gated like a sale and hidden until the deployment prices
|
|
503
|
+
// it; and the staff page's orders, which name no site. Loaded on first call.
|
|
504
|
+
registerPluginApiRoute('commerce/pos-hardware', async (req, res)=>(await import("./server/pos-hardware.js")).posHardwareHandler(req, res));
|
|
505
|
+
// A store's card reader start-up screen (AGL-4048): the POS devices card's
|
|
506
|
+
// setting, gated like a sale, and the staff page's platform default. Both
|
|
507
|
+
// draw images, so each loads with its first call.
|
|
508
|
+
registerPluginApiRoute('commerce/pos-reader-splash', async (req, res)=>(await import("./server/pos-reader-splash.js")).posReaderSplashHandler(req, res));
|
|
509
|
+
registerPluginApiRoute('commerce/admin/reader-splash', {
|
|
510
|
+
web: async (request)=>(await import("./server/pos-reader-splash-admin.js")).posReaderSplashAdmin(request)
|
|
511
|
+
});
|
|
512
|
+
registerPluginApiRoute('commerce/admin/hardware-orders', {
|
|
513
|
+
web: async (request)=>(await import("./server/pos-hardware-admin.js")).posHardwareAdminOrders(request)
|
|
514
|
+
});
|
|
489
515
|
// The self-service kiosk (AGL-3623): a register's paired screen where the
|
|
490
516
|
// customer orders and pays. Its device token, never a staff session; the
|
|
491
517
|
// register's queue of "pay at counter" orders is gated like a sale.
|