@aglyn/plugins-commerce 1.0.0-beta.289 → 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 (66) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  4. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  5. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  6. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  7. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  8. package/src/lib/components/console/order-invoice.component.d.ts +6 -4
  9. package/src/lib/components/console/order-invoice.component.js +67 -11
  10. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  11. package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
  12. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  13. package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
  14. package/src/lib/components/console/quote-proofs.component.js +626 -0
  15. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  16. package/src/lib/components/quote-page-body.js +29 -1
  17. package/src/lib/components/quote-page-body.js.map +1 -1
  18. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  19. package/src/lib/components/quote-proof-panel.js +436 -0
  20. package/src/lib/components/quote-proof-panel.js.map +1 -0
  21. package/src/lib/model/commerce-quote-pay.d.ts +27 -0
  22. package/src/lib/model/commerce-quote-pay.js +36 -3
  23. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  24. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  25. package/src/lib/model/commerce-quote-proofs.js +348 -0
  26. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  27. package/src/lib/model/commerce-quotes.d.ts +59 -3
  28. package/src/lib/model/commerce-quotes.js.map +1 -1
  29. package/src/lib/model/index.d.ts +1 -0
  30. package/src/lib/model/index.js +1 -0
  31. package/src/lib/model/index.js.map +1 -1
  32. package/src/lib/server/commerce-pdf.d.ts +100 -0
  33. package/src/lib/server/commerce-pdf.js +283 -0
  34. package/src/lib/server/commerce-pdf.js.map +1 -0
  35. package/src/lib/server/order-print.d.ts +6 -0
  36. package/src/lib/server/order-print.js +22 -5
  37. package/src/lib/server/order-print.js.map +1 -1
  38. package/src/lib/server/pdf-response.d.ts +31 -0
  39. package/src/lib/server/pdf-response.js +30 -0
  40. package/src/lib/server/pdf-response.js.map +1 -0
  41. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  42. package/src/lib/server/pos-reader-splash.js +8 -93
  43. package/src/lib/server/pos-reader-splash.js.map +1 -1
  44. package/src/lib/server/quote-pay.d.ts +31 -4
  45. package/src/lib/server/quote-pay.js +11 -11
  46. package/src/lib/server/quote-pay.js.map +1 -1
  47. package/src/lib/server/quote-proofs.d.ts +51 -0
  48. package/src/lib/server/quote-proofs.js +500 -0
  49. package/src/lib/server/quote-proofs.js.map +1 -0
  50. package/src/lib/server/quote-uploads.d.ts +33 -0
  51. package/src/lib/server/quote-uploads.js +29 -0
  52. package/src/lib/server/quote-uploads.js.map +1 -0
  53. package/src/lib/server/quote.d.ts +14 -0
  54. package/src/lib/server/quote.js +336 -3
  55. package/src/lib/server/quote.js.map +1 -1
  56. package/src/lib/server/site-image-source.d.ts +38 -0
  57. package/src/lib/server/site-image-source.js +130 -0
  58. package/src/lib/server/site-image-source.js.map +1 -0
  59. package/src/lib/server.js +19 -1
  60. package/src/lib/server.js.map +1 -1
  61. package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
  62. package/src/lib/utils/commerce-document-pdf.js +608 -0
  63. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  64. package/src/lib/utils/order-invoice.d.ts +52 -3
  65. package/src/lib/utils/order-invoice.js +116 -64
  66. package/src/lib/utils/order-invoice.js.map +1 -1
@@ -1 +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 { quotePayLinkUrl } from './quote-link-token'\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 *\n * The first four ask \"Create and edit quotes\" (`commerce.quotes`); the pay\n * link's two ask \"Send quotes and invoices to customers\"\n * (`commerce.quotes.send`, AGL-4094). 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 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 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 = { status: number; body: Record<string, unknown> }\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 (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 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 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/** `POST /api/commerce/quote`. */\nexport const quoteHandler: PluginApiHandler = async (req, res) => {\n const outcome = await handleQuote(defaultQuoteDeps(), req)\n return res.status(outcome.status).json(outcome.body)\n}\n"],"names":["claimAttempt","createResourceUid","checkEntitlement","searchPluginPeople","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","commerceSitePermissionRefusal","holdsCommerceSiteKey","quoteSaleTaxWithEngine","expireCheckoutSession","quotePayLinkUrl","defaultQuoteDeps","verifyIdToken","token","app","auth","firestore","orgForHost","hostId","found","id","orgId","org","searchPeople","request","quoteTax","expireSession","sessionId","payLinkUrl","host","quoteId","version","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","input","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","text","available","customers","owner","people","limit","person","read","record","company","phone","deleted","stage","delete","idempotencyKey","parseQuoteInput","claimed","scopeId","busyMessage","replay","claim","orderRef","counterRef","number","counter","merge","revision","requireProofApproval","createdAtMs","updatedAtMs","createdBy","updatedBy","atMs","event","detail","baseUpdatedAtMs","existing","undefined","release","quoteHandler","res","json"],"mappings":"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"}
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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"}
@@ -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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"}
package/src/lib/server.js CHANGED
@@ -404,6 +404,22 @@ registerPluginJob({
404
404
  recipientLink: true
405
405
  });
406
406
  registerPluginApiRoute('commerce/quote-pay', quotePayDoor, CARD_PAYMENT_DOOR);
407
+ // The quote as a PDF (AGL-4095), by the same signed link and on the same
408
+ // terms as the page's data. Loaded on first call, with sharp and the PDF
409
+ // writer behind it.
410
+ registerPluginApiRoute('commerce/quote-pdf', async (req, res)=>(await import("./server/commerce-pdf.js")).quotePdfHandler(req, res), {
411
+ recipientLink: true
412
+ });
413
+ // The quote's proofs (AGL-4096), by the same signed link and on the same
414
+ // terms as the page's data; and the customer's answer to them, with any
415
+ // artwork they send through the shared visitor upload. The two writes are
416
+ // ordinary visitor writes: the dispatcher's cross-origin check, lockdown
417
+ // and visitor-write limit stand in front of their own. Loaded on first call.
418
+ registerPluginApiRoute('commerce/quote-proof', async (req, res)=>(await import("./server/quote-proofs.js")).quoteProofHandler(req, res), {
419
+ recipientLink: true
420
+ });
421
+ registerPluginApiRoute('commerce/quote-upload', async (req, res)=>(await import("./server/quote-proofs.js")).quoteUploadHandler(req, res));
422
+ registerPluginApiRoute('commerce/quote-respond', async (req, res)=>(await import("./server/quote-proofs.js")).quoteRespondHandler(req, res));
407
423
  // What became of a session the shopper was returned from (AGL-3606).
408
424
  registerPluginApiRoute('commerce/checkout-status', checkoutStatusHandler);
409
425
  registerPluginApiRoute('commerce/product', productHandler);
@@ -542,7 +558,9 @@ registerPluginJob({
542
558
  registerPluginApiRoute('commerce/order-restock-answer', async (req, res)=>(await import("./server/order-annotations.js")).orderRestockAnswerHandler(req, res));
543
559
  // The order dialog's Packing slip and Invoice for the native apps
544
560
  // (AGL-3841): the console's own two pages over the stored order, which the
545
- // apps print on the device's print sheet. Loaded on first call.
561
+ // apps print on the device's print sheet; and the invoice as a PDF file
562
+ // (`kind=invoice-pdf`, AGL-4095) for the console and the apps. Loaded on
563
+ // first call.
546
564
  registerPluginApiRoute('commerce/order-print', async (req, res)=>(await import("./server/order-print.js")).orderPrintHandler(req, res));
547
565
  // Issue / void store credit (AGL-2226). Server-side because the host
548
566
  // catch-all in the Firestore rules would otherwise let a client write