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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +29 -2
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
  6. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  7. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  8. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  9. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  10. package/src/lib/components/console/host-orders-card.component.js +5 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
  13. package/src/lib/components/console/host-quotes-card.component.js +263 -0
  14. package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
  15. package/src/lib/components/console/order-invoice.component.d.ts +6 -4
  16. package/src/lib/components/console/order-invoice.component.js +67 -11
  17. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  18. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
  19. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  20. package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
  21. package/src/lib/components/console/quote-editor-dialog.component.js +1317 -0
  22. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
  23. package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
  24. package/src/lib/components/console/quote-pay-link.component.js +213 -0
  25. package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
  26. package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
  27. package/src/lib/components/console/quote-proofs.component.js +626 -0
  28. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  29. package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
  30. package/src/lib/components/console/quote-settings-card.component.js +121 -0
  31. package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
  32. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  33. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  34. package/src/lib/components/quote-page-body.d.ts +33 -0
  35. package/src/lib/components/quote-page-body.js +724 -0
  36. package/src/lib/components/quote-page-body.js.map +1 -0
  37. package/src/lib/components/quote-page.d.ts +29 -0
  38. package/src/lib/components/quote-page.js +96 -0
  39. package/src/lib/components/quote-page.js.map +1 -0
  40. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  41. package/src/lib/components/quote-proof-panel.js +436 -0
  42. package/src/lib/components/quote-proof-panel.js.map +1 -0
  43. package/src/lib/components/storefront-payment-element.d.ts +7 -1
  44. package/src/lib/components/storefront-payment-element.js +8 -3
  45. package/src/lib/components/storefront-payment-element.js.map +1 -1
  46. package/src/lib/constants/orders-list-query.js +6 -1
  47. package/src/lib/constants/orders-list-query.js.map +1 -1
  48. package/src/lib/constants/quote-page.d.ts +30 -0
  49. package/src/lib/constants/quote-page.js +35 -0
  50. package/src/lib/constants/quote-page.js.map +1 -0
  51. package/src/lib/model/commerce-orders.d.ts +25 -2
  52. package/src/lib/model/commerce-orders.js +10 -1
  53. package/src/lib/model/commerce-orders.js.map +1 -1
  54. package/src/lib/model/commerce-quote-pay.d.ts +256 -0
  55. package/src/lib/model/commerce-quote-pay.js +426 -0
  56. package/src/lib/model/commerce-quote-pay.js.map +1 -0
  57. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  58. package/src/lib/model/commerce-quote-proofs.js +348 -0
  59. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  60. package/src/lib/model/commerce-quotes.d.ts +335 -0
  61. package/src/lib/model/commerce-quotes.js +522 -0
  62. package/src/lib/model/commerce-quotes.js.map +1 -0
  63. package/src/lib/model/index.d.ts +3 -0
  64. package/src/lib/model/index.js +3 -0
  65. package/src/lib/model/index.js.map +1 -1
  66. package/src/lib/model/plugin-permissions.d.ts +1 -1
  67. package/src/lib/model/plugin-permissions.js +20 -0
  68. package/src/lib/model/plugin-permissions.js.map +1 -1
  69. package/src/lib/server/api-v1/orders-and-products.js +6 -2
  70. package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
  71. package/src/lib/server/billing-webhook.js +127 -52
  72. package/src/lib/server/billing-webhook.js.map +1 -1
  73. package/src/lib/server/checkout-session-expiry.d.ts +36 -0
  74. package/src/lib/server/checkout-session-expiry.js +63 -0
  75. package/src/lib/server/checkout-session-expiry.js.map +1 -0
  76. package/src/lib/server/commerce-pdf.d.ts +100 -0
  77. package/src/lib/server/commerce-pdf.js +283 -0
  78. package/src/lib/server/commerce-pdf.js.map +1 -0
  79. package/src/lib/server/draft-order.js +16 -8
  80. package/src/lib/server/draft-order.js.map +1 -1
  81. package/src/lib/server/order-print.d.ts +6 -0
  82. package/src/lib/server/order-print.js +22 -5
  83. package/src/lib/server/order-print.js.map +1 -1
  84. package/src/lib/server/pdf-response.d.ts +31 -0
  85. package/src/lib/server/pdf-response.js +30 -0
  86. package/src/lib/server/pdf-response.js.map +1 -0
  87. package/src/lib/server/person-eraser.d.ts +7 -0
  88. package/src/lib/server/person-eraser.js +24 -2
  89. package/src/lib/server/person-eraser.js.map +1 -1
  90. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  91. package/src/lib/server/pos-reader-splash.js +8 -93
  92. package/src/lib/server/pos-reader-splash.js.map +1 -1
  93. package/src/lib/server/pos-return.js +3 -1
  94. package/src/lib/server/pos-return.js.map +1 -1
  95. package/src/lib/server/quote-link-token.d.ts +39 -0
  96. package/src/lib/server/quote-link-token.js +97 -0
  97. package/src/lib/server/quote-link-token.js.map +1 -0
  98. package/src/lib/server/quote-pay.d.ts +110 -0
  99. package/src/lib/server/quote-pay.js +606 -0
  100. package/src/lib/server/quote-pay.js.map +1 -0
  101. package/src/lib/server/quote-proofs.d.ts +51 -0
  102. package/src/lib/server/quote-proofs.js +500 -0
  103. package/src/lib/server/quote-proofs.js.map +1 -0
  104. package/src/lib/server/quote-uploads.d.ts +33 -0
  105. package/src/lib/server/quote-uploads.js +29 -0
  106. package/src/lib/server/quote-uploads.js.map +1 -0
  107. package/src/lib/server/quote.d.ts +63 -0
  108. package/src/lib/server/quote.js +846 -0
  109. package/src/lib/server/quote.js.map +1 -0
  110. package/src/lib/server/related.js +2 -0
  111. package/src/lib/server/related.js.map +1 -1
  112. package/src/lib/server/site-image-source.d.ts +38 -0
  113. package/src/lib/server/site-image-source.js +130 -0
  114. package/src/lib/server/site-image-source.js.map +1 -0
  115. package/src/lib/server/site-page-resolver.d.ts +2 -0
  116. package/src/lib/server/site-page-resolver.js +75 -0
  117. package/src/lib/server/site-page-resolver.js.map +1 -1
  118. package/src/lib/server.js +33 -1
  119. package/src/lib/server.js.map +1 -1
  120. package/src/lib/site.js +6 -0
  121. package/src/lib/site.js.map +1 -1
  122. package/src/lib/transfer/tracking.server.js +6 -0
  123. package/src/lib/transfer/tracking.server.js.map +1 -1
  124. package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
  125. package/src/lib/utils/commerce-document-pdf.js +608 -0
  126. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  127. package/src/lib/utils/order-invoice.d.ts +52 -3
  128. package/src/lib/utils/order-invoice.js +116 -64
  129. package/src/lib/utils/order-invoice.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/quote.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 { claimAttempt, type PluginApiHandler, type PluginApiRequest } from '@aglyn/aglyn/server'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport type { PluginTaxAddress } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport {\n searchPluginPeople,\n type PluginPersonRecord,\n type PluginPersonSearchRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport * as CommerceModel from '../model'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport {\n quoteSaleTaxWithEngine,\n type SaleTaxEngineRequest,\n type SaleTaxEngineResult,\n} from './tax-engine-quote'\nimport { expireCheckoutSession, type CheckoutSessionExpiry } from './checkout-session-expiry'\nimport type { quotePdfFile } from './commerce-pdf'\nimport { sendPdf, type CommercePdfFile } from './pdf-response'\nimport { quotePayLinkUrl } from './quote-link-token'\nimport type { QuoteUploadCalls } from './quote-uploads'\n\n/*==========================================\n * QUOTES IN THE CONSOLE (AGL-4093): `POST /api/commerce/quote`.\n *\n * { action: 'create', hostId, quote } → { quoteId, number, updatedAtMs }\n * { action: 'update', hostId, quoteId, baseUpdatedAtMs, quote } → { quoteId, number, updatedAtMs }\n * { action: 'delete', hostId, quoteId } → { deleted: true }\n * { action: 'search-customers', hostId, text } → { available, customers }\n * { action: 'pay-link', hostId, quoteId } → { url, linkVersion }\n * { action: 'replace-link', hostId, quoteId } → { url, linkVersion }\n * { action: 'pdf', hostId, quoteId } → application/pdf\n *\n * Proofs and revisions (AGL-4096), all under \"Create and edit quotes\":\n *\n * { action: 'proof-ticket', hostId, quoteId, file: { name, contentType, size } }\n * → { ticket, upload: { url, fields }, file }\n * { action: 'proof-finalize', hostId, quoteId, ticket } → { proof, updatedAtMs }\n * { action: 'proof-remove', hostId, quoteId, proofId } → { removed: true, updatedAtMs }\n * { action: 'start-revision', hostId, quoteId } → { revision, updatedAtMs }\n * { action: 'revisions', hostId, quoteId } → { revisions: QuoteRevisionView[] }\n * { action: 'file', hostId, quoteId, fileId, revision? } → the file (a proof, or the customer's)\n *\n * A proof goes up the way a visitor's file does (`visitor-upload`): a ticket\n * for a signed upload straight to Storage, then finalize, which inspects the\n * bytes and keeps them under the quote. Adding or removing a proof, and\n * starting a revision, change what the customer is shown, so each is an edit\n * of the quote: it moves `quote.updatedAtMs` (the page's version) and closes\n * the pay page's open session, as an edit does.\n *\n * Every action but the pay link's two asks \"Create and edit quotes\"\n * (`commerce.quotes`); the pay link's two ask \"Send quotes and invoices to\n * customers\" (`commerce.quotes.send`, AGL-4094). The PDF (AGL-4095) is the\n * document the customer would download from the quote page, and carries the\n * pay link only for someone who also holds `commerce.quotes.send`. Create and update take an\n * `Idempotency-Key` header, one per attempt, so a retried save replays its\n * answer instead of writing a second quote or failing its own change check;\n * delete is idempotent by nature, and so are the link's two (a replace bumps\n * the version once per press, and a second press is another replacement).\n *\n * A quote's open Checkout Session (`checkoutSessionId`, opened by the pay\n * page) was priced from the quote as it stood. Editing the quote, deleting it\n * or replacing its link expires that session, so nobody pays a price or a\n * link the business has withdrawn.\n *\n * The server prices every product line from the product and works out the\n * tax; the console sends what was chosen, never what it costs. A quote is\n * written to `hosts/{hostId}/orders` as an unpaid order on the `quote`\n * channel, numbered from the store's order counter, and WITHOUT the fields\n * order lists read (see `commerce-quotes.ts`). No payment is created here.\n *=========================================*/\n\nexport interface QuoteDeps {\n verifyIdToken(token: string): Promise<{ uid: string; email?: string | null }>\n firestore(): FirebaseFirestore.Firestore\n /** The workspace that owns the site, for its plan. */\n orgForHost(hostId: string): Promise<{ id?: string; org?: unknown } | null>\n searchPeople(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[] | null>\n /** The site's tax service, where one is connected; `{ quote: null }` otherwise. */\n quoteTax(request: SaleTaxEngineRequest): Promise<SaleTaxEngineResult>\n /** Closes a Checkout Session the pay page opened for the quote. */\n expireSession(sessionId: string): Promise<CheckoutSessionExpiry>\n /** The quote's pay link at a link version, or null when the site has no address or no signing secret. */\n payLinkUrl(host: Record<string, unknown>, hostId: string, quoteId: string, version: number): string | null\n /** The quote's PDF; loads the PDF writer and sharp only when asked. */\n quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>\n /** The upload pipeline, loaded only by the proof actions. */\n uploads(): Promise<QuoteUploadCalls>\n now(): number\n}\n\nexport function defaultQuoteDeps(): QuoteDeps {\n return {\n verifyIdToken: (token) => firebaseAdmin.app().auth().verifyIdToken(token),\n firestore: () => firebaseAdmin.app().firestore(),\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n searchPeople: (request) => searchPluginPeople(request),\n quoteTax: (request) => quoteSaleTaxWithEngine(request),\n expireSession: (sessionId) => expireCheckoutSession(sessionId),\n payLinkUrl: (host, hostId, quoteId, version) => quotePayLinkUrl(host, hostId, quoteId, version),\n quotePdf: async (input) => (await import('./commerce-pdf')).quotePdfFile(input),\n uploads: async () => (await import('./quote-uploads')).quoteUploadCalls(),\n now: () => Date.now(),\n }\n}\n\n/** Expires the session the pay page opened, when there is one; a failure is logged, never thrown. */\nasync function closeOpenSession(deps: QuoteDeps, sessionId: string | undefined, context: string): Promise<void> {\n if (!sessionId) return\n const result = await deps.expireSession(sessionId).catch((): CheckoutSessionExpiry => 'open')\n if (result === 'open') {\n console.error(`${context}: the quote's Stripe session was not expired and may still be paid`, { sessionId })\n }\n}\n\ntype Outcome = {\n status: number\n body: Record<string, unknown>\n pdf?: CommercePdfFile\n /** A proof or a customer's file, streamed by the handler. */\n file?: { path: string; name: string; contentType: string }\n}\n\nconst refuse = (status: number, error: string): Outcome => ({ status, body: { error } })\n\nfunction bodyOf(req: PluginApiRequest): Record<string, unknown> {\n const raw = req.body\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n}\n\nfunction headerOf(req: PluginApiRequest, name: string): string {\n const headers = (req.headers ?? {}) as Record<string, unknown>\n const value = headers[name.toLowerCase()] ?? headers[name]\n return String(Array.isArray(value) ? value[0] ?? '' : value ?? '').trim()\n}\n\nconst DOC_ID = /^(?!__.*__$)[^/]{1,200}$/\n\n/** The ship-to as a tax service reads it, or `null` to tax at the store's address. */\nfunction serviceAddress(shipTo: CommerceModel.OrderAddress | null): PluginTaxAddress | null {\n if (!shipTo?.country) return null\n return {\n country: shipTo.country,\n ...(shipTo.line1 ? { line1: shipTo.line1 } : {}),\n ...(shipTo.line2 ? { line2: shipTo.line2 } : {}),\n ...(shipTo.city ? { city: shipTo.city } : {}),\n ...(shipTo.state ? { region: shipTo.state } : {}),\n ...(shipTo.postalCode ? { postalCode: shipTo.postalCode } : {}),\n }\n}\n\ninterface PricedQuote {\n lineItems: CommerceModel.PricedQuoteLine[]\n totals: CommerceModel.OrderTotals\n tax: CommerceModel.QuoteTaxBasis\n taxMode: CommerceModel.StorefrontTaxMode | undefined\n requireProofApprovalDefault: boolean\n}\n\n/**\n * Prices a quote: each product line from its product, the tax from the\n * store's settings and, where the site connected one, its tax service.\n */\nasync function priceQuote(\n deps: QuoteDeps,\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n input: CommerceModel.QuoteInput,\n): Promise<PricedQuote | Outcome> {\n const productIds = [\n ...new Set(\n input.lines.flatMap((line) => (line.kind === 'catalog' ? [line.productId] : [])),\n ),\n ]\n const productDocs = await Promise.all(\n productIds.map((id) => hostRef.collection('products').doc(id).get()),\n )\n const products = new Map(\n productDocs.map((snapshot, index) => [\n productIds[index],\n snapshot.exists ? (snapshot.data() as Partial<CommerceModel.HostProduct>) : null,\n ]),\n )\n const lineItems: CommerceModel.PricedQuoteLine[] = []\n for (const line of input.lines) {\n if (line.kind === 'custom') {\n lineItems.push(CommerceModel.priceQuoteCustomLine(line))\n continue\n }\n const priced = CommerceModel.priceQuoteCatalogLine(products.get(line.productId), line)\n if ('error' in priced) return refuse(400, priced.error)\n lineItems.push(priced.line)\n }\n\n const store = await hostRef.collection('settings').doc('store').get()\n const taxSettings = (store.get('tax') ?? {}) as CommerceModel.TaxSettings\n let tax = CommerceModel.quoteTaxBasisFromSettings(taxSettings, input.shipTo)\n const shippingCents = input.shipping?.amountCents ?? 0\n const amounts = CommerceModel.quoteLineAmounts(lineItems, input.discount)\n // A tax service answers only for a store on its own rates; it is asked about\n // what each line is charged after its share of the discount.\n if (taxSettings.mode === 'manual' && amounts.itemsCents > 0) {\n const serviceLines = lineItems\n .map((line, index) => ({ line, index, amountCents: amounts.netCents[index] }))\n .filter((entry) => entry.amountCents > 0)\n const answer = await deps.quoteTax({\n hostId,\n settings: taxSettings,\n channel: 'invoice',\n lines: serviceLines.map(({ line, index, amountCents }) => ({\n id: String(index),\n ...(line.productId ? { productId: line.productId } : {}),\n ...(line.variantId ? { variantId: line.variantId } : {}),\n ...(line.sku ? { sku: line.sku } : {}),\n description: [line.name, line.variantLabel].filter(Boolean).join(' — '),\n quantity: line.quantity,\n amountCents,\n ...(line.taxExempt ? { exempt: true } : {}),\n })),\n ...(shippingCents ? { shippingCents } : {}),\n shipTo: serviceAddress(input.shipTo),\n customerEmail: input.customer.email ?? null,\n })\n if (answer.quote) {\n tax = {\n kind: 'service',\n taxCents: answer.quote.taxCents,\n providerLabel: answer.quote.providerLabel || answer.quote.provider,\n }\n }\n }\n const totals = CommerceModel.computeQuoteTotals({\n lines: lineItems,\n discount: input.discount,\n shippingCents,\n tax,\n })\n return {\n lineItems,\n totals: CommerceModel.quoteOrderTotals(totals),\n tax,\n taxMode: CommerceModel.quoteTaxMode(tax, totals.taxCents),\n requireProofApprovalDefault: CommerceModel.quoteRequiresProofApprovalByDefault(\n store.get('quotes') as CommerceModel.QuoteStoreSettings | undefined,\n ),\n }\n}\n\n/** The editable half of the `quote` section, every key written so a save clears what was removed. */\nfunction editedQuoteFields(input: CommerceModel.QuoteInput) {\n return {\n customer: input.customer,\n billTo: input.billTo,\n shipTo: input.shipTo,\n notes: input.notes,\n terms: input.terms,\n discount: input.discount,\n shipping: input.shipping,\n }\n}\n\nexport async function handleQuote(deps: QuoteDeps, req: PluginApiRequest): Promise<Outcome> {\n if (req.method !== 'POST') return refuse(405, 'Method not allowed')\n const authorization = headerOf(req, 'authorization')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return refuse(401, 'Unauthenticated')\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const action = String(body['action'] ?? '')\n if (!DOC_ID.test(hostId)) return refuse(400, 'Missing hostId')\n\n let decoded: { uid: string; email?: string | null }\n try {\n decoded = await deps.verifyIdToken(idToken)\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return refuse(401, 'Unauthenticated')\n }\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return refuse(404, 'Unknown site')\n const key = action === 'pay-link' || action === 'replace-link' ? 'commerce.quotes.send' : 'commerce.quotes'\n if (!holdsCommerceSiteKey(host, decoded.uid, key)) {\n return refuse(403, commerceSitePermissionRefusal(key))\n }\n const actor = String(decoded.email || decoded.uid)\n\n if (action === 'pay-link' || action === 'replace-link') {\n const quoteId = String(body['quoteId'] ?? '')\n if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId')\n const ref = hostRef.collection('orders').doc(quoteId)\n const now = deps.now()\n const answer = await firestore.runTransaction(\n async (transaction): Promise<{ outcome: Outcome; closeSession?: string }> => {\n const snapshot = await transaction.get(ref)\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order || !CommerceModel.isQuote(order) || !order.quote) return { outcome: refuse(404, 'Unknown quote') }\n const version = CommerceModel.quoteLinkVersion(order.quote)\n if (action === 'pay-link') return { outcome: { status: 200, body: { linkVersion: version } } }\n if (!CommerceModel.isUnpaidQuote(order)) {\n return { outcome: refuse(409, 'This quote has been paid, so its link can no longer be replaced.') }\n }\n const next = version + 1\n // The open session goes with the link it was opened from.\n const rest: Partial<CommerceModel.HostOrder> = { ...order }\n delete rest.checkoutSessionId\n transaction.set(ref, {\n ...rest,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'quote-link-replaced',\n `Pay link replaced by ${actor}; the earlier link no longer opens the quote`,\n now,\n ),\n quote: { ...order.quote, linkVersion: next },\n })\n return { outcome: { status: 200, body: { linkVersion: next } }, closeSession: order.checkoutSessionId }\n },\n )\n if (answer.closeSession) await closeOpenSession(deps, answer.closeSession, 'quote link replaced')\n if (answer.outcome.status !== 200) return answer.outcome\n const linkVersion = Number(answer.outcome.body['linkVersion'])\n const url = deps.payLinkUrl((host.data() ?? {}) as Record<string, unknown>, hostId, quoteId, linkVersion)\n if (!url) return refuse(409, 'This site has no web address yet, so its quotes have no pay link. Publish the site first.')\n return { status: 200, body: { url, linkVersion } }\n }\n\n if (PROOF_ACTIONS.has(action)) {\n const quoteId = String(body['quoteId'] ?? '')\n if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId')\n return handleProofAction(deps, { action, body, hostId, quoteId, hostRef, firestore, uid: decoded.uid, actor })\n }\n\n if (action === 'pdf') {\n const quoteId = String(body['quoteId'] ?? '')\n if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId')\n const [snapshot, store] = await Promise.all([\n hostRef.collection('orders').doc(quoteId).get(),\n hostRef.collection('settings').doc('store').get(),\n ])\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote')\n const hostData = (host.data() ?? {}) as Record<string, unknown>\n // The pay link is \"Send quotes and invoices to customers\"; without it the PDF leaves the link off.\n const payUrl =\n CommerceModel.isUnpaidQuote(order) && holdsCommerceSiteKey(host, decoded.uid, 'commerce.quotes.send')\n ? deps.payLinkUrl(hostData, hostId, quoteId, CommerceModel.quoteLinkVersion(order.quote))\n : null\n try {\n const pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host: hostData,\n store: (store.data() ?? null) as Record<string, unknown> | null,\n payUrl,\n })\n return { status: 200, body: {}, pdf }\n } catch (error) {\n console.error('commerce quote pdf failed', error)\n return refuse(500, 'The PDF could not be made. Try again.')\n }\n }\n\n if (action === 'search-customers') {\n const text = String(body['text'] ?? '').trim().slice(0, 120)\n if (text.length < 2) return { status: 200, body: { available: true, customers: [] } }\n const owner = await deps.orgForHost(hostId)\n const people = await deps.searchPeople({ hostId, orgId: owner?.id || null, text, limit: 10 })\n if (people === null) return { status: 200, body: { available: false, customers: [] } }\n return {\n status: 200,\n body: {\n available: true,\n customers: people.map((person) => {\n const data = person.data ?? {}\n const read = (key: string) => (typeof data[key] === 'string' ? String(data[key]).trim() : '')\n return {\n record: { kind: person.kind, id: person.id },\n name: read('name'),\n company: read('company'),\n email: person.email,\n phone: read('phone') || null,\n }\n }),\n },\n }\n }\n\n const quoteId = String(body['quoteId'] ?? '')\n if (action === 'delete') {\n if (!DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId')\n const ref = hostRef.collection('orders').doc(quoteId)\n const deleted = await firestore.runTransaction(async (transaction): Promise<Outcome & { closeSession?: string }> => {\n const snapshot = await transaction.get(ref)\n // Already gone is what a retried delete finds.\n if (!snapshot.exists) return { status: 200, body: { deleted: true } }\n const order = snapshot.data() as CommerceModel.HostOrder\n if (!CommerceModel.isQuote(order)) return refuse(404, 'Unknown quote')\n if (!CommerceModel.isUnpaidQuote(order)) {\n return refuse(409, 'This quote has been paid, so it is an order now and stays on record.')\n }\n if ((order.quote?.stage ?? 'draft') !== 'draft') {\n return refuse(409, 'Only a draft quote can be deleted.')\n }\n // Earlier revisions are what the customer was shown and answered; a\n // quote that has them stays on record.\n if (Number(order.quote?.revision ?? 1) > 1) {\n return refuse(409, 'This quote has earlier revisions the customer saw, so it stays on record.')\n }\n transaction.delete(ref)\n return { status: 200, body: { deleted: true }, closeSession: order.checkoutSessionId }\n })\n const { closeSession, ...outcome } = deleted as Outcome & { closeSession?: string }\n await closeOpenSession(deps, closeSession, 'quote deleted')\n // Its proofs and anything the customer sent go with it.\n if (outcome.status === 200) await removeQuoteFiles(deps, hostId, quoteId)\n return outcome\n }\n\n if (action !== 'create' && action !== 'update') return refuse(400, 'Unknown action')\n if (action === 'update' && !DOC_ID.test(quoteId)) return refuse(400, 'Missing quoteId')\n const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200)\n if (!idempotencyKey) return refuse(400, 'A quote save needs an Idempotency-Key header.')\n\n const owner = await deps.orgForHost(hostId)\n if (!checkEntitlement(owner?.org as never, 'commerce')) return refuse(403, 'Selling is not enabled')\n\n const parsed = CommerceModel.parseQuoteInput(body['quote'])\n if ('error' in parsed) return refuse(400, parsed.error)\n const input = parsed.input\n const priced = await priceQuote(deps, hostRef, hostId, input)\n if ('status' in priced) return priced\n\n const claimed = await claimAttempt(firestore, {\n kind: action === 'create' ? 'commerce-quote' : 'commerce-quote-update',\n scopeId: action === 'create' ? hostId : `${hostId}:${quoteId}`,\n orgId: owner?.id ?? '',\n key: idempotencyKey,\n busyMessage: 'This quote is already being saved',\n })\n if ('replay' in claimed) {\n return { status: claimed.replay.status, body: claimed.replay.body as Record<string, unknown> }\n }\n const claim = claimed.claim\n const now = deps.now()\n\n try {\n let outcome: Outcome\n if (action === 'create') {\n const orderRef = hostRef.collection('orders').doc(createResourceUid())\n const counterRef = hostRef.collection('counters').doc('orders')\n let number = 0\n await firestore.runTransaction(async (transaction) => {\n const counter = await transaction.get(counterRef)\n number = Number(counter.get('next') ?? 1)\n transaction.set(counterRef, { next: number + 1 }, { merge: true })\n const quote: CommerceModel.OrderQuote = {\n stage: 'draft',\n revision: 1,\n ...editedQuoteFields(input),\n tax: priced.tax,\n requireProofApproval: input.requireProofApproval ?? priced.requireProofApprovalDefault,\n createdAtMs: now,\n updatedAtMs: now,\n createdBy: actor,\n updatedBy: actor,\n }\n // No `createdAtMs`, list fields or customer fields at the top: an\n // unpaid quote is not an order any list counts (`commerce-quotes.ts`).\n transaction.set(orderRef, {\n number,\n status: 'pending',\n channel: 'quote',\n lineItems: priced.lineItems,\n totals: priced.totals,\n ...(priced.taxMode ? { taxMode: priced.taxMode } : {}),\n timeline: [{ atMs: now, event: 'quote', detail: `Quote created by ${actor}` }],\n quote,\n })\n })\n outcome = { status: 200, body: { quoteId: orderRef.id, number, updatedAtMs: now } }\n } else {\n const baseUpdatedAtMs = Number(body['baseUpdatedAtMs'])\n const orderRef = hostRef.collection('orders').doc(quoteId)\n let closeSession: string | undefined\n outcome = await firestore.runTransaction(async (transaction): Promise<Outcome> => {\n closeSession = undefined\n const snapshot = await transaction.get(orderRef)\n const existing = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!existing || !CommerceModel.isQuote(existing) || !existing.quote) {\n return refuse(404, 'Unknown quote')\n }\n if (!CommerceModel.isUnpaidQuote(existing)) {\n return refuse(409, 'This quote has been paid, so it can no longer be changed.')\n }\n if (Number(existing.quote.updatedAtMs) !== baseUpdatedAtMs) {\n return refuse(409, 'Someone else changed this quote. Close it and open it again to see their changes.')\n }\n // The tax mode is restated from this pricing, and dropped when it has\n // none. The pay page's open session was priced from the quote before\n // this edit, so it is closed and forgotten.\n const rest: Partial<CommerceModel.HostOrder> = { ...existing }\n delete rest.taxMode\n delete rest.checkoutSessionId\n closeSession = existing.checkoutSessionId\n transaction.set(orderRef, {\n ...rest,\n lineItems: priced.lineItems,\n totals: priced.totals,\n ...(priced.taxMode ? { taxMode: priced.taxMode } : {}),\n timeline: CommerceModel.appendOrderEvent(existing, 'quote-updated', `Quote edited by ${actor}`, now),\n quote: {\n ...existing.quote,\n ...editedQuoteFields(input),\n tax: priced.tax,\n requireProofApproval:\n input.requireProofApproval ?? existing.quote.requireProofApproval === true,\n updatedAtMs: now,\n updatedBy: actor,\n },\n })\n return { status: 200, body: { quoteId, number: existing.number ?? null, updatedAtMs: now } }\n })\n if (outcome.status === 200) await closeOpenSession(deps, closeSession, 'quote edited')\n }\n if (outcome.status === 200) await claim.record(outcome.status, outcome.body)\n else await claim.release()\n return outcome\n } catch (error) {\n console.error('commerce quote save failed', error)\n await claim.release()\n return refuse(500, 'The quote could not be saved')\n }\n}\n\n/*==========================================\n * PROOFS AND REVISIONS (AGL-4096)\n *=========================================*/\n\nconst PROOF_ACTIONS = new Set(['proof-ticket', 'proof-finalize', 'proof-remove', 'start-revision', 'revisions', 'file'])\n\n/** Deletes a quote's kept files, its proofs and the customer's artwork; never throws. */\nasync function removeQuoteFiles(deps: QuoteDeps, hostId: string, quoteId: string): Promise<void> {\n try {\n const uploads = await deps.uploads()\n await Promise.all(\n [CommerceModel.QUOTE_PROOF_PURPOSE, CommerceModel.QUOTE_ARTWORK_PURPOSE].map((purpose) =>\n uploads.removeAll({ hostId, purpose, purposeId: quoteId }),\n ),\n )\n } catch (error) {\n console.error('quote files were not removed', { hostId, quoteId, error })\n }\n}\n\n/** A file as the console is told about it: everything but where it is kept. */\nfunction fileView<T extends { path?: string }>(file: T): Omit<T, 'path'> {\n const rest: T = { ...file }\n delete rest.path\n return rest\n}\n\n/** A revision as the console's history lists it. */\nfunction revisionView(snapshot: CommerceModel.QuoteRevisionSnapshot) {\n return {\n ...snapshot,\n proofs: (snapshot.proofs ?? []).map(fileView),\n responses: (snapshot.responses ?? []).map((response) => ({\n ...response,\n ...(response.files ? { files: response.files.map(fileView) } : {}),\n })),\n }\n}\n\n/**\n * Writes `next` over the quote in `transaction`, without the pay page's open\n * session, which was priced from the quote as it stood; the caller closes it.\n */\nfunction writeEditedQuote(\n transaction: FirebaseFirestore.Transaction,\n ref: FirebaseFirestore.DocumentReference,\n next: CommerceModel.HostOrder,\n): string | undefined {\n const rest: Partial<CommerceModel.HostOrder> = { ...next }\n const session = rest.checkoutSessionId\n delete rest.checkoutSessionId\n transaction.set(ref, rest)\n return session\n}\n\nasync function handleProofAction(\n deps: QuoteDeps,\n context: {\n action: string\n body: Record<string, unknown>\n hostId: string\n quoteId: string\n hostRef: FirebaseFirestore.DocumentReference\n firestore: FirebaseFirestore.Firestore\n uid: string\n actor: string\n },\n): Promise<Outcome> {\n const { action, body, hostId, quoteId, hostRef, firestore, uid, actor } = context\n const ref = hostRef.collection('orders').doc(quoteId)\n const proofScope = { hostId, purpose: CommerceModel.QUOTE_PROOF_PURPOSE, purposeId: quoteId }\n\n if (action === 'revisions' || action === 'file') {\n const snapshot = await ref.get()\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order || !CommerceModel.isQuote(order) || !order.quote) return refuse(404, 'Unknown quote')\n const current = Number(order.quote.revision ?? 1)\n const wanted = body['revision'] === undefined || body['revision'] === null ? current : Number(body['revision'])\n let snapshots: CommerceModel.QuoteRevisionSnapshot[] = []\n if (action === 'revisions' || wanted !== current) {\n const listed = await ref.collection('revisions').get()\n snapshots = listed.docs\n .map((entry) => entry.data() as CommerceModel.QuoteRevisionSnapshot)\n .filter((entry) => Number.isInteger(Number(entry?.revision)))\n .sort((a, b) => Number(a.revision) - Number(b.revision))\n }\n if (action === 'revisions') return { status: 200, body: { revisions: snapshots.map(revisionView) } }\n\n const fileId = String(body['fileId'] ?? '')\n const source =\n wanted === current\n ? { proofs: CommerceModel.currentQuoteProofs(order.quote), responses: order.quote.responses ?? [] }\n : snapshots.find((entry) => Number(entry.revision) === wanted)\n const files = [\n ...(source?.proofs ?? []),\n ...(source?.responses ?? []).flatMap((response) => response.files ?? []),\n ]\n const file = files.find((entry) => entry.id === fileId)\n if (!file?.path) return refuse(404, 'That file is no longer on this quote.')\n return { status: 200, body: {}, file: { path: file.path, name: file.name, contentType: file.contentType } }\n }\n\n if (action === 'proof-ticket') {\n const snapshot = await ref.get()\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n const refused = CommerceModel.quoteProofChangeRefusal(order, 'add')\n if (refused) return refuse(refused === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(refused))\n // Storage is spent here, so the plan must sell, as a save's must.\n const owner = await deps.orgForHost(hostId)\n if (!checkEntitlement(owner?.org as never, 'commerce')) return refuse(403, 'Selling is not enabled')\n const uploads = await deps.uploads()\n const minted = await uploads.mint({ scope: proofScope, request: body['file'], caller: { uid } })\n if ('error' in minted) return refuse(minted.status, minted.error)\n return { status: 200, body: { ticket: minted.ticket, upload: minted.upload, file: minted.file } }\n }\n\n if (action === 'proof-finalize') {\n const uploads = await deps.uploads()\n const kept = await uploads.finalize({ scope: proofScope, ticket: body['ticket'], caller: { uid } })\n if ('error' in kept) return refuse(kept.status, kept.error)\n const now = deps.now()\n const answer = await firestore.runTransaction(\n async (transaction): Promise<{ outcome: Outcome; closeSession?: string }> => {\n const snapshot = await transaction.get(ref)\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order) return { outcome: refuse(404, 'Unknown quote') }\n // A retried finalize finds its proof already on the quote.\n const already = CommerceModel.currentQuoteProofs(order.quote).find((proof) => proof.id === kept.file.id)\n if (already) {\n return { outcome: { status: 200, body: { proof: fileView(already), updatedAtMs: order.quote?.updatedAtMs ?? null } } }\n }\n const added = CommerceModel.withQuoteProofAdded(\n order,\n {\n id: kept.file.id,\n name: kept.file.name,\n contentType: kept.file.contentType,\n size: kept.file.size,\n path: kept.file.path,\n },\n { atMs: now, by: actor },\n )\n if ('reason' in added) {\n return {\n outcome: refuse(added.reason === 'not-a-quote' ? 404 : 409, CommerceModel.quoteProofRefusalMessage(added.reason)),\n }\n }\n const closeSession = writeEditedQuote(transaction, ref, added.order)\n return { outcome: { status: 200, body: { proof: fileView(added.proof), updatedAtMs: now } }, closeSession }\n },\n )\n if (answer.outcome.status !== 200) await uploads.remove(kept.file.path).catch(() => undefined)\n await closeOpenSession(deps, answer.closeSession, 'quote proof added')\n return answer.outcome\n }\n\n if (action === 'proof-remove') {\n const proofId = String(body['proofId'] ?? '')\n const now = deps.now()\n const answer = await firestore.runTransaction(\n async (transaction): Promise<{ outcome: Outcome; closeSession?: string; path?: string }> => {\n const snapshot = await transaction.get(ref)\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order) return { outcome: refuse(404, 'Unknown quote') }\n const removed = CommerceModel.withQuoteProofRemoved(order, proofId, { atMs: now, by: actor })\n if ('reason' in removed) {\n return {\n outcome: refuse(\n removed.reason === 'not-a-quote' || removed.reason === 'unknown-proof' ? 404 : 409,\n CommerceModel.quoteProofRefusalMessage(removed.reason),\n ),\n }\n }\n const closeSession = writeEditedQuote(transaction, ref, removed.order)\n return { outcome: { status: 200, body: { removed: true, updatedAtMs: now } }, closeSession, path: removed.proof.path }\n },\n )\n if (answer.path) {\n const uploads = await deps.uploads()\n await uploads.remove(answer.path).catch((error) => console.error('quote proof file was not deleted', error))\n }\n await closeOpenSession(deps, answer.closeSession, 'quote proof removed')\n return answer.outcome\n }\n\n // start-revision\n const now = deps.now()\n const answer = await firestore.runTransaction(\n async (transaction): Promise<{ outcome: Outcome; closeSession?: string }> => {\n const snapshot = await transaction.get(ref)\n const order = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!order) return { outcome: refuse(404, 'Unknown quote') }\n const started = CommerceModel.startQuoteRevision(order, { atMs: now, by: actor })\n if ('reason' in started) return { outcome: refuse(started.reason === 'paid' ? 409 : 404, started.error) }\n const revisionRef = ref.collection('revisions').doc(String(started.revision))\n // A revision on record is never written over.\n if ((await transaction.get(revisionRef)).exists) {\n return { outcome: refuse(409, `Revision ${started.revision} is already on record. Close the quote and open it again.`) }\n }\n transaction.set(revisionRef, started.snapshot)\n const closeSession = writeEditedQuote(transaction, ref, started.order)\n return {\n outcome: { status: 200, body: { revision: started.revision + 1, updatedAtMs: now } },\n closeSession,\n }\n },\n )\n await closeOpenSession(deps, answer.closeSession, 'quote revision started')\n return answer.outcome\n}\n\n/** `POST /api/commerce/quote`. */\nexport const quoteHandler: PluginApiHandler = async (req, res) => {\n const deps = defaultQuoteDeps()\n const outcome = await handleQuote(deps, req)\n if (outcome.pdf) return sendPdf(res, outcome.pdf)\n if (outcome.file) {\n const uploads = await deps.uploads()\n return uploads.send(res as never, outcome.file)\n }\n return res.status(outcome.status).json(outcome.body)\n}\n"],"names":["claimAttempt","createResourceUid","checkEntitlement","searchPluginPeople","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","commerceSitePermissionRefusal","holdsCommerceSiteKey","quoteSaleTaxWithEngine","expireCheckoutSession","sendPdf","quotePayLinkUrl","defaultQuoteDeps","verifyIdToken","token","app","auth","firestore","orgForHost","hostId","found","id","orgId","org","searchPeople","request","quoteTax","expireSession","sessionId","payLinkUrl","host","quoteId","version","quotePdf","input","quotePdfFile","uploads","quoteUploadCalls","now","Date","closeOpenSession","deps","context","result","catch","console","error","refuse","status","body","bodyOf","req","raw","parsed","JSON","parse","headerOf","name","headers","value","toLowerCase","String","Array","isArray","trim","DOC_ID","serviceAddress","shipTo","country","line1","line2","city","state","region","postalCode","priceQuote","hostRef","store","productIds","Set","lines","flatMap","line","kind","productId","productDocs","Promise","all","map","collection","doc","get","products","Map","snapshot","index","exists","data","lineItems","push","priceQuoteCustomLine","priced","priceQuoteCatalogLine","taxSettings","tax","quoteTaxBasisFromSettings","shippingCents","shipping","amountCents","amounts","quoteLineAmounts","discount","mode","itemsCents","serviceLines","netCents","filter","entry","answer","settings","channel","variantId","sku","description","variantLabel","Boolean","join","quantity","taxExempt","exempt","customerEmail","customer","email","quote","taxCents","providerLabel","provider","totals","computeQuoteTotals","quoteOrderTotals","taxMode","quoteTaxMode","requireProofApprovalDefault","quoteRequiresProofApprovalByDefault","editedQuoteFields","billTo","notes","terms","handleQuote","method","authorization","idToken","startsWith","slice","length","action","test","decoded","key","uid","actor","ref","runTransaction","transaction","order","isQuote","outcome","quoteLinkVersion","linkVersion","isUnpaidQuote","next","rest","checkoutSessionId","set","timeline","appendOrderEvent","closeSession","Number","url","PROOF_ACTIONS","has","handleProofAction","hostData","payUrl","pdf","text","available","customers","owner","people","limit","person","read","record","company","phone","deleted","stage","revision","delete","removeQuoteFiles","idempotencyKey","parseQuoteInput","claimed","scopeId","busyMessage","replay","claim","orderRef","counterRef","number","counter","merge","requireProofApproval","createdAtMs","updatedAtMs","createdBy","updatedBy","atMs","event","detail","baseUpdatedAtMs","existing","undefined","release","QUOTE_PROOF_PURPOSE","QUOTE_ARTWORK_PURPOSE","purpose","removeAll","purposeId","fileView","file","path","revisionView","proofs","responses","response","files","writeEditedQuote","session","proofScope","current","wanted","snapshots","listed","docs","isInteger","sort","a","b","revisions","fileId","source","currentQuoteProofs","find","contentType","refused","quoteProofChangeRefusal","quoteProofRefusalMessage","minted","mint","scope","caller","ticket","upload","kept","finalize","already","proof","added","withQuoteProofAdded","size","by","reason","remove","proofId","removed","withQuoteProofRemoved","started","startQuoteRevision","revisionRef","quoteHandler","res","send","json"],"mappings":"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"}
@@ -61,6 +61,8 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
61
61
  const counts = new Map();
62
62
  for (const docSnapshot of orders.docs){
63
63
  var _order_lineItems;
64
+ // A quote nobody has paid is not something bought together (AGL-4093).
65
+ if (CommerceModel.isUnpaidQuote(docSnapshot.data())) continue;
64
66
  const order = CommerceModel.liftLegacyOrder(docSnapshot.data());
65
67
  const lineIds = ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).map((line)=>line.productId);
66
68
  if (!lineIds.includes(productId)) continue;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/related.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/**\n * Related products (AGL-325), best source first: the product's manual\n * list, then co-purchase pairs mined from recent orders (\"frequently\n * bought together\"), then tag/category neighbors. Returns catalog ids —\n * the block renders them through the ids-filtered catalog API.\n */\nexport const relatedHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const productId = String(req.query.productId ?? '')\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n // A product sold in person only (AGL-4060) has no page to show neighbors\n // on, and naming them would say what it is bought with.\n if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {\n return res.status(200).json({ productIds: [] })\n }\n const product = CommerceModel.liftLegacyProduct(\n (productSnapshot.data() as any) ?? {},\n )\n\n let ids = (product.relatedProductIds ?? []).slice(0, 8)\n\n if (ids.length === 0) {\n // Co-purchase pairs mined from a bounded sample of multi-line orders.\n //\n // Deliberately NOT \"recent\", which is what this said before: there is\n // no orderable recency field to ask for. Orders carry their timestamp\n // as `createdAtMs` on some documents and `createdAt: {seconds}` on\n // others — `orderCreatedAtMs` exists precisely to paper over that — and\n // an `orderBy` on either one DROPS every order written in the other\n // shape. Ordering by document id instead makes the sample the SAME\n // hundred orders on every call rather than an arbitrary hundred the\n // backend chose, so a product's \"bought together\" list stops changing\n // between requests for no reason. Genuine recency needs one timestamp\n // field across the collection first.\n const orders = await hostRef\n .collection('orders')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(100)\n .get()\n const counts = new Map<string, number>()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n const lineIds = (order.lineItems ?? []).map((line) => line.productId)\n if (!lineIds.includes(productId)) continue\n for (const id of lineIds) {\n if (id === productId) continue\n counts.set(id, (counts.get(id) ?? 0) + 1)\n }\n }\n ids = [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8)\n .map(([id]) => id)\n }\n\n if (ids.length === 0 && (product.tags?.length || product.categoryIds?.length)) {\n // Tag/category neighbors, from the same bounded deterministic sample:\n // an unordered `limit` let a catalog larger than 300 answer one request\n // from one arbitrary slice and the next from another, so a product's\n // neighbours appeared and disappeared without anything changing.\n const catalog = await hostRef\n .collection('products')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(300)\n .get()\n ids = catalog.docs\n .filter((docSnapshot) => {\n if (docSnapshot.id === productId) return false\n const candidate = docSnapshot.data() as any\n if (!CommerceModel.productListedOnline(candidate)) return false\n const sharedTag = (candidate.tags ?? []).some((tag: string) =>\n (product.tags ?? []).includes(tag),\n )\n const sharedCategory = (candidate.categoryIds ?? []).some(\n (id: string) => (product.categoryIds ?? []).includes(id),\n )\n return sharedTag || sharedCategory\n })\n .slice(0, 8)\n .map((docSnapshot) => docSnapshot.id)\n }\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=300, stale-while-revalidate=600',\n )\n return res.status(200).json({ productIds: ids })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Related unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","relatedHandler","req","res","hostId","String","query","productId","status","json","error","productSnapshot","product","firestore","app","hostRef","collection","doc","get","exists","productSellsOnline","data","productIds","liftLegacyProduct","ids","relatedProductIds","slice","length","orders","orderBy","FieldPath","documentId","limit","counts","Map","docSnapshot","docs","order","liftLegacyOrder","lineIds","lineItems","map","line","includes","id","set","entries","sort","a","b","tags","categoryIds","catalog","filter","candidate","productListedOnline","sharedTag","some","tag","sharedCategory","setHeader","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/related.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/**\n * Related products (AGL-325), best source first: the product's manual\n * list, then co-purchase pairs mined from recent orders (\"frequently\n * bought together\"), then tag/category neighbors. Returns catalog ids —\n * the block renders them through the ids-filtered catalog API.\n */\nexport const relatedHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const productId = String(req.query.productId ?? '')\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n // A product sold in person only (AGL-4060) has no page to show neighbors\n // on, and naming them would say what it is bought with.\n if (productSnapshot.exists && !CommerceModel.productSellsOnline(productSnapshot.data())) {\n return res.status(200).json({ productIds: [] })\n }\n const product = CommerceModel.liftLegacyProduct(\n (productSnapshot.data() as any) ?? {},\n )\n\n let ids = (product.relatedProductIds ?? []).slice(0, 8)\n\n if (ids.length === 0) {\n // Co-purchase pairs mined from a bounded sample of multi-line orders.\n //\n // Deliberately NOT \"recent\", which is what this said before: there is\n // no orderable recency field to ask for. Orders carry their timestamp\n // as `createdAtMs` on some documents and `createdAt: {seconds}` on\n // others — `orderCreatedAtMs` exists precisely to paper over that — and\n // an `orderBy` on either one DROPS every order written in the other\n // shape. Ordering by document id instead makes the sample the SAME\n // hundred orders on every call rather than an arbitrary hundred the\n // backend chose, so a product's \"bought together\" list stops changing\n // between requests for no reason. Genuine recency needs one timestamp\n // field across the collection first.\n const orders = await hostRef\n .collection('orders')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(100)\n .get()\n const counts = new Map<string, number>()\n for (const docSnapshot of orders.docs) {\n // A quote nobody has paid is not something bought together (AGL-4093).\n if (CommerceModel.isUnpaidQuote(docSnapshot.data())) continue\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n const lineIds = (order.lineItems ?? []).map((line) => line.productId)\n if (!lineIds.includes(productId)) continue\n for (const id of lineIds) {\n if (id === productId) continue\n counts.set(id, (counts.get(id) ?? 0) + 1)\n }\n }\n ids = [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8)\n .map(([id]) => id)\n }\n\n if (ids.length === 0 && (product.tags?.length || product.categoryIds?.length)) {\n // Tag/category neighbors, from the same bounded deterministic sample:\n // an unordered `limit` let a catalog larger than 300 answer one request\n // from one arbitrary slice and the next from another, so a product's\n // neighbours appeared and disappeared without anything changing.\n const catalog = await hostRef\n .collection('products')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(300)\n .get()\n ids = catalog.docs\n .filter((docSnapshot) => {\n if (docSnapshot.id === productId) return false\n const candidate = docSnapshot.data() as any\n if (!CommerceModel.productListedOnline(candidate)) return false\n const sharedTag = (candidate.tags ?? []).some((tag: string) =>\n (product.tags ?? []).includes(tag),\n )\n const sharedCategory = (candidate.categoryIds ?? []).some(\n (id: string) => (product.categoryIds ?? []).includes(id),\n )\n return sharedTag || sharedCategory\n })\n .slice(0, 8)\n .map((docSnapshot) => docSnapshot.id)\n }\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=300, stale-while-revalidate=600',\n )\n return res.status(200).json({ productIds: ids })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Related unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","relatedHandler","req","res","hostId","String","query","productId","status","json","error","productSnapshot","product","firestore","app","hostRef","collection","doc","get","exists","productSellsOnline","data","productIds","liftLegacyProduct","ids","relatedProductIds","slice","length","orders","orderBy","FieldPath","documentId","limit","counts","Map","docSnapshot","docs","order","isUnpaidQuote","liftLegacyOrder","lineIds","lineItems","map","line","includes","id","set","entries","sort","a","b","tags","categoryIds","catalog","filter","candidate","productListedOnline","sharedTag","some","tag","sharedCategory","setHeader","console"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type MediaCdnScope } from '@aglyn/aglyn/app-utils/media-cdn-scope';
18
+ export interface LocatedImageAsset {
19
+ kind: 'asset';
20
+ scope: MediaCdnScope;
21
+ scopeSegment: string;
22
+ mediaId: string;
23
+ doc: Record<string, unknown>;
24
+ /** What changes when the asset's bytes do. */
25
+ version: string;
26
+ }
27
+ export interface LocatedImageUrl {
28
+ kind: 'url';
29
+ url: string;
30
+ version: string;
31
+ }
32
+ export type LocatedImageSource = LocatedImageAsset | LocatedImageUrl;
33
+ /** A media library image this site may show, or null; one larger than `maxBytes` is refused. */
34
+ export declare function locateMediaImage(scopeSegment: string, mediaId: string, hostId: string, orgId: string, maxBytes: number): Promise<LocatedImageAsset | null>;
35
+ /** Where a stored logo or image value lives, or null when it cannot be read. */
36
+ export declare function locateImageSource(value: string, hostId: string, orgId: string, maxBytes: number): Promise<LocatedImageSource | null>;
37
+ /** The bytes of a located image, or null when it is blocked, gone, unreachable or over `maxBytes`. */
38
+ export declare function readImageSourceBytes(source: LocatedImageSource, maxBytes: number): Promise<Buffer | null>;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { parseMediaCdnScope } from "@aglyn/aglyn/app-utils/media-cdn-scope";
17
+ import { isMediaCdnPath, MEDIA_CDN_ROUTE, mediaRefFromStorageUrl, parseMediaRef } from "@aglyn/aglyn/app-utils/media-ref";
18
+ import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
19
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
20
+ /*==========================================
21
+ * READING A SITE'S IMAGE ON THE SERVER: a logo or a picked picture, for the
22
+ * card reader's start-up screen and a quote's or invoice's PDF.
23
+ *
24
+ * A media value is read from STORAGE, never fetched, when it names this
25
+ * site's library or its org's: the document must be live, not private,
26
+ * visible to this site, its scope not locked and the asset not quarantined,
27
+ * and the object path inside the scope's media prefix (the rules the media
28
+ * CDN serves by). An https address outside the libraries is fetched only from
29
+ * a public address, pinned against DNS rebinding, with no redirects, a
30
+ * timeout and a size cap. Each caller names its own cap.
31
+ *
32
+ * The media CDN's block, the storage path and the pinned fetch load only when
33
+ * an image is read, by relative path (`pos-reader-splash-server.ts` says why).
34
+ *=========================================*/ const firestore = ()=>firebaseAdmin.app().firestore();
35
+ function mediaBucket() {
36
+ return firebaseAdmin.app().storage().bucket(process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || undefined);
37
+ }
38
+ const cleanString = (value)=>typeof value === 'string' ? value.trim() : '';
39
+ /** A media library image this site may show, or null; one larger than `maxBytes` is refused. */ export async function locateMediaImage(scopeSegment, mediaId, hostId, orgId, maxBytes) {
40
+ var _doc_contentType, _ref, _doc_updatedAtMs;
41
+ const scope = parseMediaCdnScope(scopeSegment);
42
+ if (!scope || !/^[A-Za-z0-9_-]{1,64}$/.test(mediaId)) return null;
43
+ const owned = scope.isOrg ? scope.scopeId === orgId : scope.scopeId === hostId;
44
+ if (!owned) return null;
45
+ const snapshot = await firestore().collection(scope.isOrg ? 'orgs' : 'hosts').doc(scope.scopeId).collection('media').doc(mediaId).get();
46
+ const doc = snapshot.exists ? snapshot.data() : null;
47
+ if (!doc || doc['deletedAt'] || doc['private']) return null;
48
+ if (scope.isOrg && !visibleToHost(doc['visibleTo'], hostId)) return null;
49
+ if (!/^image\//.test(String((_doc_contentType = doc['contentType']) != null ? _doc_contentType : ''))) return null;
50
+ if (typeof doc['sizeBytes'] === 'number' && doc['sizeBytes'] > maxBytes) return null;
51
+ const version = cleanString(doc['contentSha256']) || cleanString(doc['contentHash']) || String((_ref = (_doc_updatedAtMs = doc['updatedAtMs']) != null ? _doc_updatedAtMs : doc['sizeBytes']) != null ? _ref : '');
52
+ return {
53
+ kind: 'asset',
54
+ scope,
55
+ scopeSegment,
56
+ mediaId,
57
+ doc,
58
+ version
59
+ };
60
+ }
61
+ /** Where a stored logo or image value lives, or null when it cannot be read. */ export async function locateImageSource(value, hostId, orgId, maxBytes) {
62
+ const stored = parseMediaRef(value) ? value : mediaRefFromStorageUrl(value);
63
+ const ref = stored ? parseMediaRef(stored) : null;
64
+ if (ref) return locateMediaImage(ref.scope, ref.mediaId, hostId, orgId, maxBytes);
65
+ if (isMediaCdnPath(value)) {
66
+ const rest = value.slice(`${MEDIA_CDN_ROUTE}/`.length).split('?')[0];
67
+ const slash = rest.indexOf('/');
68
+ if (slash <= 0) return null;
69
+ return locateMediaImage(rest.slice(0, slash), rest.slice(slash + 1), hostId, orgId, maxBytes);
70
+ }
71
+ // `isMediaCdnPath` narrows a string it refuses to `never`; the value is still the string.
72
+ const address = value;
73
+ if (/^https:\/\//.test(address) && address.length <= 2000) return {
74
+ kind: 'url',
75
+ url: address,
76
+ version: address
77
+ };
78
+ return null;
79
+ }
80
+ async function readAssetBytes(asset, maxBytes) {
81
+ const { mediaCdnServeBlock, mediaStoragePathInScope } = await import("./pos-reader-splash-server.js");
82
+ const blocked = await mediaCdnServeBlock(asset.scope, {
83
+ contentSha256: typeof asset.doc['contentSha256'] === 'string' ? asset.doc['contentSha256'] : undefined,
84
+ contentHash: typeof asset.doc['contentHash'] === 'string' ? asset.doc['contentHash'] : undefined,
85
+ scopeSegment: asset.scopeSegment,
86
+ mediaId: asset.mediaId
87
+ });
88
+ if (blocked) return null;
89
+ const objectPath = mediaStoragePathInScope({
90
+ storagePath: asset.doc['storagePath'],
91
+ base: `${asset.scope.isOrg ? 'orgs' : 'hosts'}/${asset.scope.scopeId}`,
92
+ mediaId: asset.mediaId
93
+ });
94
+ const [buffer] = await mediaBucket().file(objectPath).download();
95
+ return buffer.length > maxBytes ? null : buffer;
96
+ }
97
+ const FETCH_TIMEOUT_MS = 8000;
98
+ async function fetchPublicImage(url, maxBytes) {
99
+ const { createPinnedDispatcher, resolvePublicIp } = await import("./pos-reader-splash-server.js");
100
+ const pinnedIp = await resolvePublicIp(new URL(url).hostname);
101
+ if (!pinnedIp) return null;
102
+ const dispatcher = createPinnedDispatcher(pinnedIp);
103
+ const controller = new AbortController();
104
+ const timeout = setTimeout(()=>controller.abort(), FETCH_TIMEOUT_MS);
105
+ try {
106
+ var _response_headers_get, _response_headers_get1;
107
+ const init = {
108
+ redirect: 'error',
109
+ signal: controller.signal,
110
+ dispatcher
111
+ };
112
+ const response = await fetch(url, init);
113
+ if (!response.ok) return null;
114
+ if (!/^image\//.test((_response_headers_get = response.headers.get('content-type')) != null ? _response_headers_get : '')) return null;
115
+ const declared = Number((_response_headers_get1 = response.headers.get('content-length')) != null ? _response_headers_get1 : 0);
116
+ if (declared > maxBytes) return null;
117
+ const buffer = Buffer.from(await response.arrayBuffer());
118
+ return buffer.length > maxBytes ? null : buffer;
119
+ } catch (unused) {
120
+ return null;
121
+ } finally{
122
+ clearTimeout(timeout);
123
+ dispatcher.close == null ? void 0 : dispatcher.close.call(dispatcher).catch(()=>undefined);
124
+ }
125
+ }
126
+ /** The bytes of a located image, or null when it is blocked, gone, unreachable or over `maxBytes`. */ export async function readImageSourceBytes(source, maxBytes) {
127
+ return source.kind === 'asset' ? readAssetBytes(source, maxBytes) : fetchPublicImage(source.url, maxBytes);
128
+ }
129
+
130
+ //# sourceMappingURL=site-image-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-image-source.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 { parseMediaCdnScope, type MediaCdnScope } from '@aglyn/aglyn/app-utils/media-cdn-scope'\nimport {\n isMediaCdnPath,\n MEDIA_CDN_ROUTE,\n mediaRefFromStorageUrl,\n parseMediaRef,\n} from '@aglyn/aglyn/app-utils/media-ref'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/*==========================================\n * READING A SITE'S IMAGE ON THE SERVER: a logo or a picked picture, for the\n * card reader's start-up screen and a quote's or invoice's PDF.\n *\n * A media value is read from STORAGE, never fetched, when it names this\n * site's library or its org's: the document must be live, not private,\n * visible to this site, its scope not locked and the asset not quarantined,\n * and the object path inside the scope's media prefix (the rules the media\n * CDN serves by). An https address outside the libraries is fetched only from\n * a public address, pinned against DNS rebinding, with no redirects, a\n * timeout and a size cap. Each caller names its own cap.\n *\n * The media CDN's block, the storage path and the pinned fetch load only when\n * an image is read, by relative path (`pos-reader-splash-server.ts` says why).\n *=========================================*/\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nfunction mediaBucket() {\n return firebaseAdmin\n .app()\n .storage()\n .bucket(process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || undefined)\n}\n\nconst cleanString = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nexport interface LocatedImageAsset {\n kind: 'asset'\n scope: MediaCdnScope\n scopeSegment: string\n mediaId: string\n doc: Record<string, unknown>\n /** What changes when the asset's bytes do. */\n version: string\n}\n\nexport interface LocatedImageUrl {\n kind: 'url'\n url: string\n version: string\n}\n\nexport type LocatedImageSource = LocatedImageAsset | LocatedImageUrl\n\n/** A media library image this site may show, or null; one larger than `maxBytes` is refused. */\nexport async function locateMediaImage(\n scopeSegment: string,\n mediaId: string,\n hostId: string,\n orgId: string,\n maxBytes: number,\n): Promise<LocatedImageAsset | null> {\n const scope = parseMediaCdnScope(scopeSegment)\n if (!scope || !/^[A-Za-z0-9_-]{1,64}$/.test(mediaId)) return null\n const owned = scope.isOrg ? scope.scopeId === orgId : scope.scopeId === hostId\n if (!owned) return null\n const snapshot = await firestore()\n .collection(scope.isOrg ? 'orgs' : 'hosts')\n .doc(scope.scopeId)\n .collection('media')\n .doc(mediaId)\n .get()\n const doc = (snapshot.exists ? snapshot.data() : null) as Record<string, unknown> | null\n if (!doc || doc['deletedAt'] || doc['private']) return null\n if (scope.isOrg && !visibleToHost(doc['visibleTo'] as string[] | undefined, hostId)) return null\n if (!/^image\\//.test(String(doc['contentType'] ?? ''))) return null\n if (typeof doc['sizeBytes'] === 'number' && doc['sizeBytes'] > maxBytes) return null\n const version =\n cleanString(doc['contentSha256']) ||\n cleanString(doc['contentHash']) ||\n String(doc['updatedAtMs'] ?? doc['sizeBytes'] ?? '')\n return { kind: 'asset', scope, scopeSegment, mediaId, doc, version }\n}\n\n/** Where a stored logo or image value lives, or null when it cannot be read. */\nexport async function locateImageSource(\n value: string,\n hostId: string,\n orgId: string,\n maxBytes: number,\n): Promise<LocatedImageSource | null> {\n const stored = parseMediaRef(value) ? value : mediaRefFromStorageUrl(value)\n const ref = stored ? parseMediaRef(stored) : null\n if (ref) return locateMediaImage(ref.scope, ref.mediaId, hostId, orgId, maxBytes)\n if (isMediaCdnPath(value)) {\n const rest = value.slice(`${MEDIA_CDN_ROUTE}/`.length).split('?')[0]\n const slash = rest.indexOf('/')\n if (slash <= 0) return null\n return locateMediaImage(rest.slice(0, slash), rest.slice(slash + 1), hostId, orgId, maxBytes)\n }\n // `isMediaCdnPath` narrows a string it refuses to `never`; the value is still the string.\n const address: string = value as string\n if (/^https:\\/\\//.test(address) && address.length <= 2000) return { kind: 'url', url: address, version: address }\n return null\n}\n\nasync function readAssetBytes(asset: LocatedImageAsset, maxBytes: number): Promise<Buffer | null> {\n const { mediaCdnServeBlock, mediaStoragePathInScope } = await import('./pos-reader-splash-server')\n const blocked = await mediaCdnServeBlock(asset.scope, {\n contentSha256: typeof asset.doc['contentSha256'] === 'string' ? (asset.doc['contentSha256'] as string) : undefined,\n contentHash: typeof asset.doc['contentHash'] === 'string' ? (asset.doc['contentHash'] as string) : undefined,\n scopeSegment: asset.scopeSegment,\n mediaId: asset.mediaId,\n })\n if (blocked) return null\n const objectPath = mediaStoragePathInScope({\n storagePath: asset.doc['storagePath'],\n base: `${asset.scope.isOrg ? 'orgs' : 'hosts'}/${asset.scope.scopeId}`,\n mediaId: asset.mediaId,\n })\n const [buffer] = (await mediaBucket().file(objectPath).download()) as [Buffer]\n return buffer.length > maxBytes ? null : buffer\n}\n\nconst FETCH_TIMEOUT_MS = 8000\n\nasync function fetchPublicImage(url: string, maxBytes: number): Promise<Buffer | null> {\n const { createPinnedDispatcher, resolvePublicIp } = await import('./pos-reader-splash-server')\n const pinnedIp = await resolvePublicIp(new URL(url).hostname)\n if (!pinnedIp) return null\n const dispatcher = createPinnedDispatcher(pinnedIp)\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)\n try {\n const init = { redirect: 'error' as const, signal: controller.signal, dispatcher }\n const response = await fetch(url, init as RequestInit)\n if (!response.ok) return null\n if (!/^image\\//.test(response.headers.get('content-type') ?? '')) return null\n const declared = Number(response.headers.get('content-length') ?? 0)\n if (declared > maxBytes) return null\n const buffer = Buffer.from(await response.arrayBuffer())\n return buffer.length > maxBytes ? null : buffer\n } catch {\n return null\n } finally {\n clearTimeout(timeout)\n ;(dispatcher as { close?: () => Promise<void> }).close?.().catch(() => undefined)\n }\n}\n\n/** The bytes of a located image, or null when it is blocked, gone, unreachable or over `maxBytes`. */\nexport async function readImageSourceBytes(source: LocatedImageSource, maxBytes: number): Promise<Buffer | null> {\n return source.kind === 'asset' ? readAssetBytes(source, maxBytes) : fetchPublicImage(source.url, maxBytes)\n}\n"],"names":["parseMediaCdnScope","isMediaCdnPath","MEDIA_CDN_ROUTE","mediaRefFromStorageUrl","parseMediaRef","visibleToHost","firebaseAdmin","firestore","app","mediaBucket","storage","bucket","process","env","NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET","undefined","cleanString","value","trim","locateMediaImage","scopeSegment","mediaId","hostId","orgId","maxBytes","doc","scope","test","owned","isOrg","scopeId","snapshot","collection","get","exists","data","String","version","kind","locateImageSource","stored","ref","rest","slice","length","split","slash","indexOf","address","url","readAssetBytes","asset","mediaCdnServeBlock","mediaStoragePathInScope","blocked","contentSha256","contentHash","objectPath","storagePath","base","buffer","file","download","FETCH_TIMEOUT_MS","fetchPublicImage","createPinnedDispatcher","resolvePublicIp","pinnedIp","URL","hostname","dispatcher","controller","AbortController","timeout","setTimeout","abort","response","init","redirect","signal","fetch","ok","headers","declared","Number","Buffer","from","arrayBuffer","clearTimeout","close","catch","readImageSourceBytes","source"],"mappings":"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"}
@@ -54,3 +54,5 @@ export declare function productReturnWindow(product: Pick<CommerceModel.HostProd
54
54
  export declare function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema>;
55
55
  /** The return form's node tree, root first, for the layout's slot. */
56
56
  export declare function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema>;
57
+ /** The quote page's node tree, root first, for the layout's slot. */
58
+ export declare function buildQuotePageNodes(): Record<string, Aglyn.AglynNodeSchema>;
@@ -21,6 +21,7 @@ import * as Aglyn from "@aglyn/aglyn/server";
21
21
  import { ORDER_STATUS_PATH } from "./order-status-token.js";
22
22
  import { ORDER_STATUS_COMPONENT_ID } from "../constants/order-status.js";
23
23
  import { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from "../constants/return-request.js";
24
+ import { QUOTE_PAGE_COMPONENT_ID, QUOTE_PAGE_PATH } from "../constants/quote-page.js";
24
25
  import getScreen from "@aglyn/tenant-runtime/get-screen";
25
26
  import { collectSocialImageFacts } from "@aglyn/tenant-runtime/social-image-facts";
26
27
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
@@ -59,6 +60,12 @@ import { readProductReviews } from "./reviews.js";
59
60
  if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {
60
61
  return composeReturnRequestPage(hostId, host);
61
62
  }
63
+ // The customer's quote page (AGL-4094), at /pay-quote, under the same
64
+ // terms: no page of the site's own there, the quote read after hydration
65
+ // from the pay link's `?q=&t=`, UNLISTED.
66
+ if (pdpSegments.length === 1 && pdpSegments[0] === QUOTE_PAGE_PATH) {
67
+ return composeQuotePage(hostId, host);
68
+ }
62
69
  if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {
63
70
  var _productSnapshot_docs_;
64
71
  const adminFirestore = firebaseAdmin.app().firestore();
@@ -595,6 +602,74 @@ async function composeReturnRequestPage(hostId, host) {
595
602
  return undefined;
596
603
  }
597
604
  }
605
+ const QUOTE_PAGE_NODE = 'quote-page__block';
606
+ /** The quote page's node tree, root first, for the layout's slot. */ export function buildQuotePageNodes() {
607
+ return {
608
+ [Aglyn.NODE_ROOT_ID]: {
609
+ $id: Aglyn.NODE_ROOT_ID,
610
+ componentId: 'div',
611
+ nodes: [
612
+ 'quote-page__container'
613
+ ]
614
+ },
615
+ 'quote-page__container': {
616
+ $id: 'quote-page__container',
617
+ parentId: Aglyn.NODE_ROOT_ID,
618
+ componentId: 'muiContainer',
619
+ pluginId: 'mui',
620
+ props: {
621
+ maxWidth: 'md',
622
+ sx: {
623
+ paddingTop: 6,
624
+ paddingBottom: 8
625
+ }
626
+ },
627
+ nodes: [
628
+ QUOTE_PAGE_NODE
629
+ ]
630
+ },
631
+ [QUOTE_PAGE_NODE]: {
632
+ $id: QUOTE_PAGE_NODE,
633
+ parentId: 'quote-page__container',
634
+ componentId: QUOTE_PAGE_COMPONENT_ID,
635
+ pluginId: 'commerce',
636
+ props: {}
637
+ }
638
+ };
639
+ }
640
+ async function composeQuotePage(hostId, host) {
641
+ try {
642
+ const layoutId = await resolveBuiltInPageLayoutId({
643
+ hostId,
644
+ host: host
645
+ });
646
+ const nodes = await composeNodesWithChrome({
647
+ hostId,
648
+ layoutId,
649
+ screenNodes: buildQuotePageNodes(),
650
+ host: host
651
+ });
652
+ if (!nodes) return undefined;
653
+ return {
654
+ props: JSON.parse(JSON.stringify({
655
+ data: {
656
+ host,
657
+ screen: {
658
+ data: {
659
+ displayName: 'Your quote',
660
+ visibility: Aglyn.HostScreenVisibility.UNLISTED
661
+ }
662
+ }
663
+ },
664
+ nodes
665
+ })),
666
+ revalidate: 3600
667
+ };
668
+ } catch (error) {
669
+ console.error('quote page composition failed', error);
670
+ return undefined;
671
+ }
672
+ }
598
673
  /**
599
674
  * The store's time zone for a product page whose product asks for a use day
600
675
  * (AGL-4043), so the seeded page offers the same days the API's does; null