@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-pay.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 {\n checkEntitlement,\n claimAttempt,\n deriveStripeObjectKey,\n resolveTransactionFeeCents,\n type PluginApiHandler,\n type PluginApiRequest,\n} from '@aglyn/aglyn/server'\nimport { resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { type SiteReturnHost, siteReturnOrigin } from '@aglyn/aglyn/app-utils/site-return-url'\nimport { consumeRateLimit, firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport { checkoutSessionCardAuthenticationParams } from '@aglyn/tenant-data-admin/server/stripe-card-authentication'\nimport { quotePageHref } from '../constants/quote-page'\nimport * as CommerceModel from '../model'\nimport { expireCheckoutSession, type CheckoutSessionExpiry } from './checkout-session-expiry'\nimport { resolveManualTaxRateId } from './manual-tax-rate'\nimport {\n applyNativeCheckoutParams,\n nativeCheckoutStripeHeaders,\n readCheckoutSessionPayload,\n resolveNativeCheckoutMode,\n type NativeCheckoutMode,\n} from './native-checkout'\nimport { orderStatusUrl } from './order-status-token'\nimport { ensureCheckoutDomain } from './payment-method-domains'\nimport { verifyQuoteLinkToken } from './quote-link-token'\n\n/*==========================================\n * THE CUSTOMER'S QUOTE PAGE AND ITS PAY BUTTON (AGL-4094).\n *\n * GET /api/commerce/quote-view?hostId=&q=<quoteId>&t=<token>\n * → 200 QuotePageView | 200 { state: 'replaced', storeName } | 404\n * POST /api/commerce/quote-pay { hostId, q, t, v } + Idempotency-Key\n * → 200 { url } (Stripe's page) | 200 { clientSecret, publishableKey, sessionId, wallets? }\n * (the in-page form) | 409 { error, reason } | 410 { error, replaced: true } | 404\n *\n * The signed link (`quote-link-token.ts`) is the whole credential. A wrong\n * token answers exactly as a missing quote does, so neither route tells\n * anyone which quote ids exist; both are rate-limited per address per site\n * BEFORE the token check, so guessing costs what looking does. A link signed\n * for an older version of the quote opens the replaced page and nothing else.\n *\n * Pay opens a FRESH Checkout Session from the stored quote on every press:\n * the prices are the stored lines', never anything the page sends, and the\n * session the previous press opened is expired first, so two sessions for one\n * quote can never both be open. The quote keeps the open one in\n * `checkoutSessionId`. A Stripe session lapses after a day; the quote and its\n * link do not, because the next press opens another.\n *\n * The payment completes in `billing-webhook.ts`, which turns this same\n * document into the order (`paidQuoteOrder`); nothing here marks it paid.\n *=========================================*/\n\n/** Lookups and presses one address may make per site, per ten minutes. */\nexport const QUOTE_VIEW_LOOKUPS_PER_WINDOW = 60\nexport const QUOTE_PAY_PRESSES_PER_WINDOW = 20\nexport const QUOTE_WINDOW_MS = 10 * 60_000\n\nexport interface QuotePayDeps {\n firestore(): FirebaseFirestore.Firestore\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean; resetMs: number }>\n orgForHost(hostId: string): Promise<{ id?: string; org?: any } | null>\n nativeMode(orgId: string): Promise<NativeCheckoutMode>\n /** The store's manual tax rate as a Stripe tax rate, or null when one cannot be had. */\n taxRateId(input: Parameters<typeof resolveManualTaxRateId>[0]): Promise<string | null>\n createSession(\n params: URLSearchParams,\n headers: Record<string, string>,\n ): Promise<{ ok: boolean; session: { id?: string; url?: string; client_secret?: string; error?: unknown } }>\n expireSession(sessionId: string): Promise<CheckoutSessionExpiry>\n /** Registers the page's domain for Apple Pay; never throws. */\n ensureDomain(input: Parameters<typeof ensureCheckoutDomain>[0]): Promise<unknown>\n now(): number\n}\n\nexport function defaultQuotePayDeps(): QuotePayDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n consumeRateLimit: (key, options) => consumeRateLimit(key, options),\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n return found ? { id: found.orgId, org: found.org } : null\n },\n nativeMode: (orgId) => resolveNativeCheckoutMode(orgId),\n taxRateId: (input) => resolveManualTaxRateId(input),\n createSession: async (params, headers) => {\n const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n ...headers,\n },\n body: params.toString(),\n })\n return { ok: response.ok, session: (await response.json().catch(() => ({}))) as never }\n },\n expireSession: (sessionId) => expireCheckoutSession(sessionId),\n ensureDomain: (input) => ensureCheckoutDomain(input).catch(() => null),\n now: () => Date.now(),\n }\n}\n\n/** A link route's answer: a status, a JSON body and any extra headers. */\nexport type QuoteLinkOutcome = { status: number; body: Record<string, unknown>; headers?: Record<string, string> }\n\nconst DOC_ID = /^(?!__.*__$)[^/]{1,200}$/\n\nconst NOT_FOUND: QuoteLinkOutcome = {\n status: 404,\n body: { error: 'We could not find that quote. Check the link you were sent.' },\n}\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\n/** The rate limit a link route spends per address per site, BEFORE the token is checked. */\nexport async function limitedQuoteLink(\n deps: QuotePayDeps,\n req: PluginApiRequest,\n key: string,\n limit: number,\n): Promise<QuoteLinkOutcome | null> {\n const ip = readClientIp(req.headers as never, { remoteAddress: req.socket?.remoteAddress }) ?? 'no-address'\n const rate = await deps.consumeRateLimit(`${key}:${ip}`, { limit, windowMs: QUOTE_WINDOW_MS })\n if (rate.allowed) return null\n return {\n status: 429,\n body: { error: 'Too many tries. Try again in a few minutes.' },\n headers: { 'Retry-After': String(Math.max(1, Math.ceil((rate.resetMs - deps.now()) / 1000))) },\n }\n}\n\nexport interface LoadedQuote {\n hostRef: FirebaseFirestore.DocumentReference\n host: Record<string, unknown>\n order: CommerceModel.HostOrder\n storeName: string\n store: FirebaseFirestore.DocumentSnapshot\n}\n\n/**\n * The quote a link names, after its token: the quote, the replaced answer for\n * a link to an older version, or the one not-found answer for everything else.\n */\nexport async function loadLinkedQuote(\n deps: QuotePayDeps,\n hostId: string,\n quoteId: string,\n token: string,\n): Promise<{ loaded: LoadedQuote } | { outcome: QuoteLinkOutcome }> {\n const version = verifyQuoteLinkToken(hostId, quoteId, token)\n if (version === null) return { outcome: NOT_FOUND }\n const hostRef = deps.firestore().collection('hosts').doc(hostId)\n const [hostSnapshot, store, orderSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('settings').doc('store').get(),\n hostRef.collection('orders').doc(quoteId).get(),\n ])\n if (!hostSnapshot.exists || !orderSnapshot.exists) return { outcome: NOT_FOUND }\n const order = orderSnapshot.data() as CommerceModel.HostOrder\n if (!CommerceModel.isQuote(order) || !order.quote) return { outcome: NOT_FOUND }\n const host = (hostSnapshot.data() ?? {}) as Record<string, unknown>\n const storeName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '')\n const current = CommerceModel.quoteLinkVersion(order.quote)\n if (version > current) return { outcome: NOT_FOUND }\n if (version < current) {\n return {\n outcome: {\n status: 410,\n body: {\n state: 'replaced',\n storeName,\n replaced: true,\n error: `This quote was updated, and ${storeName || 'the business'} sent you a newer link.`,\n },\n },\n }\n }\n return { loaded: { hostRef, host, order, storeName, store } }\n}\n\n/** The link's site, quote and token from a query or a body, and whether all three are there. */\nexport function quoteLinkParams(source: Record<string, unknown>) {\n const hostId = String(source['hostId'] ?? '')\n const quoteId = String(source['q'] ?? '').slice(0, 200)\n const token = String(source['t'] ?? '').slice(0, 200)\n const complete = DOC_ID.test(hostId) && DOC_ID.test(quoteId) && Boolean(token)\n return { hostId, quoteId, token, complete }\n}\n\n/** `GET /api/commerce/quote-view`: the quote page's data. */\nexport async function handleQuoteView(deps: QuotePayDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome> {\n if (req.method !== 'GET') return { status: 405, body: { error: 'Method not allowed' } }\n const { hostId, quoteId, token, complete } = quoteLinkParams((req.query ?? {}) as Record<string, unknown>)\n if (!complete) return { status: 400, body: { error: 'This link is incomplete.' } }\n const refused = await limitedQuoteLink(deps, req, `commerce-quote-view:${hostId}`, QUOTE_VIEW_LOOKUPS_PER_WINDOW)\n if (refused) return refused\n const linked = await loadLinkedQuote(deps, hostId, quoteId, token)\n if ('outcome' in linked) {\n // The replaced page is a page, not a failure: answered as data.\n if (linked.outcome.status === 410) {\n const { storeName } = linked.outcome.body as { storeName: string }\n return { status: 200, body: { state: 'replaced', storeName } }\n }\n return linked.outcome\n }\n const { host, order, storeName, store } = linked.loaded\n const view = CommerceModel.buildQuotePageView({\n order,\n orderId: quoteId,\n storeName,\n currency: String(store.get('currency') ?? 'USD'),\n receiptUrl: CommerceModel.isUnpaidQuote(order) ? null : orderStatusUrl(host, hostId, quoteId),\n })\n return { status: 200, body: view as unknown as Record<string, unknown> }\n}\n\n/** The Checkout Session a quote's Pay opens, as form params. Pure over its inputs. */\nexport function buildQuoteSessionParams(input: {\n hostId: string\n quoteId: string\n order: CommerceModel.HostOrder\n charge: CommerceModel.QuoteCheckoutCharge\n accountId: string\n feeCents: number\n taxRateId: string\n returnUrl: string\n paymentMethods: CommerceModel.StorefrontPaymentMethodControls\n}): URLSearchParams {\n const { charge, order } = input\n const quote = order.quote as CommerceModel.OrderQuote\n const params = new URLSearchParams({\n mode: 'payment',\n ...checkoutSessionCardAuthenticationParams('payment'),\n ...(charge.taxAtPayment ? { 'automatic_tax[enabled]': 'true' } : {}),\n })\n charge.lines.forEach((line, index) => {\n const at = `line_items[${index}]`\n params.set(`${at}[quantity]`, '1')\n params.set(`${at}[price_data][currency]`, 'usd')\n params.set(`${at}[price_data][unit_amount]`, String(line.amountCents))\n params.set(`${at}[price_data][product_data][name]`, line.name.slice(0, 120))\n if (line.description) params.set(`${at}[price_data][product_data][description]`, line.description)\n // Stripe Tax's nontaxable code, for tax-exempt products and a tax charged as its own line.\n if (!line.taxable) params.set(`${at}[price_data][product_data][tax_code]`, 'txcd_00000000')\n if (line.taxable && input.taxRateId) params.set(`${at}[tax_rates][0]`, input.taxRateId)\n })\n // Shipping the business priced for the quote's ship-to, charged as the one\n // option, with the address asked for where it ships (and where Stripe Tax\n // reads the tax from). The quote's own address stands when it asks nothing.\n if (charge.shippingCents > 0) {\n const country = String(quote.shipTo?.country ?? '').toUpperCase()\n CommerceModel.appendShippingAddressCollectionParams(\n params,\n /^[A-Z]{2}$/.test(country) ? [country] : CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,\n )\n CommerceModel.appendCheckoutShippingParams(params, [\n { rateId: 'quote', name: 'Shipping', amountCents: charge.shippingCents },\n ])\n }\n const shippingFloorCents = charge.shippingCents\n for (const [key, value] of Object.entries(\n CommerceModel.destinationChargeParams({\n accountId: input.accountId,\n feeCents: input.feeCents,\n taxOwner: charge.taxAtPayment ? 'platform' : 'merchant',\n merchantGoodsCents: charge.goodsCents,\n shippingFloorCents,\n }),\n )) {\n params.set(key, value)\n }\n const email = String(quote.customer?.email ?? '').trim()\n if (email) params.set('customer_email', email)\n const separator = input.returnUrl.includes('?') ? '&' : '?'\n params.set('success_url', `${input.returnUrl}${separator}session_id={CHECKOUT_SESSION_ID}`)\n params.set('cancel_url', input.returnUrl)\n // What the `commerce-draft` webhook branch reads to turn this quote into\n // its order, and to tell a payment for the quote as it stands from one for\n // a replaced link or an earlier edit.\n params.set('metadata[type]', 'commerce-draft')\n params.set('metadata[hostId]', input.hostId)\n params.set('metadata[orderId]', input.quoteId)\n params.set('metadata[quoteId]', input.quoteId)\n params.set('metadata[quoteLinkVersion]', String(CommerceModel.quoteLinkVersion(quote)))\n params.set('metadata[quoteUpdatedAtMs]', String(Number(quote.updatedAtMs) || 0))\n params.set('metadata[feeCents]', String(input.feeCents))\n // A tax charged as its own line is invisible to Stripe's tax total; this\n // witness is what records it as the business's tax.\n if (charge.taxCents > 0 && charge.taxPct === null) {\n params.set('metadata[taxCents]', String(charge.taxCents))\n }\n if (charge.taxAtPayment) {\n params.set(\n 'metadata[transferCents]',\n String(\n CommerceModel.platformLiableTransferCents({\n feeCents: input.feeCents,\n merchantGoodsCents: charge.goodsCents,\n shippingFloorCents,\n }),\n ),\n )\n params.set('metadata[transferShippingCents]', String(shippingFloorCents))\n }\n CommerceModel.appendStorefrontPaymentMethodParams(params, input.paymentMethods)\n return params\n}\n\nconst QUOTE_UPDATED_MESSAGE = 'This quote was updated since you opened it. Reload the page to see it before you pay.'\n\nconst refusal = (status: number, reason: string, error: string, extra: Record<string, unknown> = {}): QuoteLinkOutcome => ({\n status,\n body: { error, reason, ...extra },\n})\n\n/** `POST /api/commerce/quote-pay`: a fresh Checkout Session for the quote. */\nexport async function handleQuotePay(deps: QuotePayDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome> {\n if (req.method !== 'POST') return { status: 405, body: { error: 'Method not allowed' } }\n if (!process.env.STRIPE_SECRET_KEY) return { status: 501, body: { error: 'Payments are not configured.' } }\n const { hostId, quoteId, token, complete } = quoteLinkParams(bodyOf(req))\n if (!complete) return { status: 400, body: { error: 'This link is incomplete.' } }\n const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200)\n if (!idempotencyKey) return { status: 400, body: { error: 'Paying needs an Idempotency-Key header.' } }\n const refused = await limitedQuoteLink(deps, req, `commerce-quote-pay:${hostId}`, QUOTE_PAY_PRESSES_PER_WINDOW)\n if (refused) return refused\n const linked = await loadLinkedQuote(deps, hostId, quoteId, token)\n if ('outcome' in linked) return linked.outcome\n const { hostRef, host, order, storeName, store } = linked.loaded\n\n // Every refusal the customer can be given is above the claim, so none of\n // them spends the press. `v` is the edit of the quote the page showed: a\n // quote changed since is shown again before it is paid, never charged unseen.\n const seen = Number(bodyOf(req)['v'])\n if (Number.isFinite(seen) && seen > 0 && seen !== (Number(order.quote?.updatedAtMs) || 0)) {\n return refusal(409, 'updated', QUOTE_UPDATED_MESSAGE)\n }\n const blocked = CommerceModel.quotePayRefusal(order, quoteId)\n if (blocked) return refusal(409, blocked, CommerceModel.quotePayRefusalMessage(blocked, storeName))\n const owner = await deps.orgForHost(hostId)\n const unavailable = refusal(\n 409,\n 'payments-unavailable',\n `${storeName || 'This business'} can’t take payments online right now. Contact them to pay this quote.`,\n )\n if (!checkEntitlement(owner?.org as never, 'commerce')) return unavailable\n const ownerUid = owner?.org?.ownerUid\n const profile = ownerUid ? await deps.firestore().collection('profiles').doc(String(ownerUid)).get() : null\n const accountId = profile?.get('stripeAccountId')\n const ready = merchantAccountIsReady(\n {\n accountId,\n chargesEnabled: profile?.get('stripeChargesEnabled'),\n accountLivemode: profile?.get('stripeAccountLivemode'),\n },\n { subject: `quote ${quoteId} on host ${hostId}` },\n )\n if (!ready) return unavailable\n const taxSettings = (store.get('tax') ?? {}) as CommerceModel.TaxSettings\n if (CommerceModel.storefrontTaxDecision({ settings: taxSettings }).kind === 'undecided') {\n return refusal(409, 'tax-undecided', CommerceModel.quotePayRefusalMessage('tax-undecided', storeName))\n }\n\n const charge = CommerceModel.quoteCheckoutCharge(order, quoteId)\n const feeCents = resolveTransactionFeeCents(owner?.org as never, charge.feeProductType, charge.goodsCents, charge.totalCents)\n const nativeMode = await deps.nativeMode(String(owner?.id ?? ''))\n const site = host as SiteReturnHost\n const origin = siteReturnOrigin({\n candidates: [headerOf(req, 'referer'), `https://${headerOf(req, 'host')}`],\n site,\n requestHost: headerOf(req, 'host'),\n })\n const returnUrl = `${origin}${quotePageHref(quoteId, token)}`\n const openedFor = order.checkoutSessionId ?? ''\n const openedAtEdit = Number(order.quote?.updatedAtMs) || 0\n\n const claimed = await claimAttempt(deps.firestore(), {\n kind: 'commerce-quote-pay',\n scopeId: `${hostId}:${quoteId}`,\n orgId: String(owner?.id ?? ''),\n key: idempotencyKey,\n busyMessage: 'Your payment is already being set up',\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 stripeKey = (object: string): Record<string, string> => {\n const derived = deriveStripeObjectKey(claim.stripeKey, object)\n return derived ? { 'Idempotency-Key': derived } : {}\n }\n let minted = ''\n try {\n // The session the last press opened is closed before another opens. One\n // that was paid a moment ago means the quote is paid; one that cannot be\n // closed means a second session would be a second way to pay.\n if (openedFor) {\n const previous = await deps.expireSession(openedFor)\n if (previous === 'complete') {\n await claim.release()\n return refusal(409, 'paid', 'This quote has just been paid. Your receipt is on its way.', { confirming: true })\n }\n if (previous === 'open') {\n await claim.release()\n return { status: 502, body: { error: 'Your payment could not be set up. Try again.' } }\n }\n }\n\n let taxRateId = ''\n if (charge.taxPct !== null && charge.taxableCents > 0) {\n const basis = order.quote?.tax\n taxRateId =\n (await deps.taxRateId({\n hostRef,\n taxPct: charge.taxPct,\n taxLabel:\n basis?.kind === 'rate'\n ? basis.label || `Tax (${charge.taxPct}%)`\n : 'Sales tax',\n headers: stripeKey(`tax-rate-${Math.round(charge.taxPct * 10_000)}`),\n })) ?? ''\n if (!taxRateId) {\n // Never an untaxed session for a taxed quote.\n await claim.release()\n return { status: 502, body: { error: 'Your payment could not be set up. Try again.' } }\n }\n }\n\n const paymentMethods = CommerceModel.resolveStorefrontPaymentMethodControls(\n CommerceModel.normalizeStorefrontPaymentMethodSettings(store.get('paymentMethods')),\n )\n const params = buildQuoteSessionParams({\n hostId,\n quoteId,\n order,\n charge,\n accountId: String(accountId),\n feeCents,\n taxRateId,\n returnUrl,\n paymentMethods,\n })\n if (nativeMode.native) {\n applyNativeCheckoutParams(params, params.get('success_url') ?? returnUrl)\n }\n const domain = nativeMode.native\n ? deps.ensureDomain({ pageUrl: returnUrl, site, hostId })\n : Promise.resolve(null)\n const created = await deps.createSession(params, {\n ...stripeKey('session'),\n ...nativeCheckoutStripeHeaders(nativeMode),\n })\n await domain\n const payload = created.ok\n ? readCheckoutSessionPayload(created.session, nativeMode, paymentMethods.wallets)\n : null\n if (!payload || !created.session.id) {\n console.error('Stripe quote checkout error', created.session?.error)\n await claim.release()\n return { status: 502, body: { error: 'Your payment could not be set up. Try again.' } }\n }\n minted = String(created.session.id)\n\n // The quote keeps the session it now has open. Whatever another press\n // stored meanwhile is closed after, so the last press is the one open\n // session; a quote that was paid, replaced or edited meanwhile closes\n // this one instead.\n const orderRef = hostRef.collection('orders').doc(quoteId)\n const settled = await deps.firestore().runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n const current = snapshot.exists ? (snapshot.data() as CommerceModel.HostOrder) : null\n if (!current || !CommerceModel.isQuote(current)) return { outcome: NOT_FOUND, displaced: '' }\n if (!CommerceModel.isUnpaidQuote(current)) {\n return {\n outcome: refusal(409, 'paid', CommerceModel.quotePayRefusalMessage('paid', storeName)),\n displaced: '',\n }\n }\n if (CommerceModel.quoteLinkVersion(current.quote) !== CommerceModel.quoteLinkVersion(order.quote)) {\n return {\n outcome: {\n status: 410,\n body: { replaced: true, error: `This quote was updated, and ${storeName || 'the business'} sent you a newer link.` },\n },\n displaced: '',\n }\n }\n if ((Number(current.quote?.updatedAtMs) || 0) !== openedAtEdit) {\n return { outcome: refusal(409, 'updated', QUOTE_UPDATED_MESSAGE), displaced: '' }\n }\n transaction.set(orderRef, { checkoutSessionId: minted }, { merge: true })\n const displaced = current.checkoutSessionId && current.checkoutSessionId !== openedFor ? current.checkoutSessionId : ''\n return { outcome: null, displaced }\n })\n if (settled.outcome) {\n await deps.expireSession(minted)\n await claim.release()\n return settled.outcome\n }\n if (settled.displaced && settled.displaced !== minted) await deps.expireSession(settled.displaced)\n await claim.record(200, payload)\n return { status: 200, body: payload as unknown as Record<string, unknown> }\n } catch (error) {\n console.error('quote pay failed', error)\n if (minted) await deps.expireSession(minted).catch(() => 'open')\n await claim.release()\n return { status: 500, body: { error: 'Your payment could not be set up. Try again.' } }\n }\n}\n\n/** Sends a link route's answer, never cached or indexed. */\nexport function sendQuoteLinkOutcome(res: Parameters<PluginApiHandler>[1], outcome: QuoteLinkOutcome) {\n res.setHeader('Cache-Control', 'private, no-store')\n res.setHeader('X-Robots-Tag', 'noindex, nofollow')\n for (const [name, value] of Object.entries(outcome.headers ?? {})) res.setHeader(name, value)\n return res.status(outcome.status).json(outcome.body)\n}\n\n/** `GET /api/commerce/quote-view`. */\nexport const quoteViewHandler: PluginApiHandler = async (req, res) => {\n try {\n return sendQuoteLinkOutcome(res, await handleQuoteView(defaultQuotePayDeps(), req))\n } catch (error) {\n console.error('quote view failed', error)\n return sendQuoteLinkOutcome(res, { status: 500, body: { error: 'This quote could not be loaded. Try again.' } })\n }\n}\n\n/** `POST /api/commerce/quote-pay`. */\nexport const quotePayHandler: PluginApiHandler = async (req, res) =>\n sendQuoteLinkOutcome(res, await handleQuotePay(defaultQuotePayDeps(), req))\n"],"names":["checkEntitlement","claimAttempt","deriveStripeObjectKey","resolveTransactionFeeCents","resolveHostToken","readClientIp","siteReturnOrigin","consumeRateLimit","firebaseAdmin","getOrgForHost","merchantAccountIsReady","checkoutSessionCardAuthenticationParams","quotePageHref","CommerceModel","expireCheckoutSession","resolveManualTaxRateId","applyNativeCheckoutParams","nativeCheckoutStripeHeaders","readCheckoutSessionPayload","resolveNativeCheckoutMode","orderStatusUrl","ensureCheckoutDomain","verifyQuoteLinkToken","QUOTE_VIEW_LOOKUPS_PER_WINDOW","QUOTE_PAY_PRESSES_PER_WINDOW","QUOTE_WINDOW_MS","defaultQuotePayDeps","firestore","app","key","options","orgForHost","hostId","found","id","orgId","org","nativeMode","taxRateId","input","createSession","params","headers","response","fetch","method","Authorization","process","env","STRIPE_SECRET_KEY","body","toString","ok","session","json","catch","expireSession","sessionId","ensureDomain","now","Date","DOC_ID","NOT_FOUND","status","error","bodyOf","req","raw","parsed","JSON","parse","headerOf","name","value","toLowerCase","String","Array","isArray","trim","limitedQuoteLink","deps","limit","ip","remoteAddress","socket","rate","windowMs","allowed","Math","max","ceil","resetMs","loadLinkedQuote","quoteId","token","hostSnapshot","version","outcome","hostRef","collection","doc","store","orderSnapshot","Promise","all","get","exists","order","data","isQuote","quote","host","storeName","current","quoteLinkVersion","state","replaced","loaded","quoteLinkParams","source","slice","complete","test","Boolean","handleQuoteView","query","refused","linked","view","buildQuotePageView","orderId","currency","receiptUrl","isUnpaidQuote","buildQuoteSessionParams","charge","URLSearchParams","mode","taxAtPayment","lines","forEach","line","index","at","set","amountCents","description","taxable","shippingCents","country","shipTo","toUpperCase","appendShippingAddressCollectionParams","CHECKOUT_SHIPPING_COUNTRIES","appendCheckoutShippingParams","rateId","shippingFloorCents","Object","entries","destinationChargeParams","accountId","feeCents","taxOwner","merchantGoodsCents","goodsCents","email","customer","separator","returnUrl","includes","Number","updatedAtMs","taxCents","taxPct","platformLiableTransferCents","appendStorefrontPaymentMethodParams","paymentMethods","QUOTE_UPDATED_MESSAGE","refusal","reason","extra","handleQuotePay","owner","idempotencyKey","seen","isFinite","blocked","quotePayRefusal","quotePayRefusalMessage","unavailable","ownerUid","profile","ready","chargesEnabled","accountLivemode","subject","taxSettings","storefrontTaxDecision","settings","kind","quoteCheckoutCharge","feeProductType","totalCents","site","origin","candidates","requestHost","openedFor","checkoutSessionId","openedAtEdit","claimed","scopeId","busyMessage","replay","claim","stripeKey","object","derived","minted","previous","release","confirming","taxableCents","basis","tax","taxLabel","label","round","resolveStorefrontPaymentMethodControls","normalizeStorefrontPaymentMethodSettings","native","domain","pageUrl","resolve","created","payload","wallets","console","orderRef","settled","runTransaction","transaction","snapshot","displaced","merge","record","sendQuoteLinkOutcome","res","setHeader","quoteViewHandler","quotePayHandler"],"mappings":"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"}
@@ -0,0 +1,51 @@
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 PluginApiHandler, type PluginApiRequest } from '@aglyn/aglyn/server';
18
+ import { notifyHostManagers } from '@aglyn/tenant-data-admin';
19
+ import { type QuoteLinkOutcome, type QuotePayDeps } from './quote-pay';
20
+ import { type QuoteUploadCalls } from './quote-uploads';
21
+ /** Proofs one address may load per site, per ten minutes: every thumbnail of a few visits. */
22
+ export declare const QUOTE_PROOF_LOADS_PER_WINDOW = 120;
23
+ /** Upload calls (tickets and finalizes) one address may make per site, per ten minutes, before the upload's own limits. */
24
+ export declare const QUOTE_UPLOAD_CALLS_PER_WINDOW = 80;
25
+ /** Answers one address may send per site, per ten minutes. */
26
+ export declare const QUOTE_RESPONSES_PER_WINDOW = 20;
27
+ export interface QuoteProofDeps extends QuotePayDeps {
28
+ uploads(): Promise<QuoteUploadCalls>;
29
+ notify(...args: Parameters<typeof notifyHostManagers>): Promise<void>;
30
+ }
31
+ export declare function defaultQuoteProofDeps(): QuoteProofDeps;
32
+ /** What a proof route answers: a JSON outcome, or a file to stream. */
33
+ export type QuoteProofOutcome = QuoteLinkOutcome & {
34
+ file?: {
35
+ path: string;
36
+ name: string;
37
+ contentType: string;
38
+ };
39
+ };
40
+ /** `GET /api/commerce/quote-proof`: one of the current revision's proofs. */
41
+ export declare function handleQuoteProof(deps: QuoteProofDeps, req: PluginApiRequest): Promise<QuoteProofOutcome>;
42
+ /** `POST /api/commerce/quote-upload`: a ticket for one of the customer's files, or its finalize. */
43
+ export declare function handleQuoteUpload(deps: QuoteProofDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome>;
44
+ /** `POST /api/commerce/quote-respond`: the customer approves the proof or asks for changes. */
45
+ export declare function handleQuoteRespond(deps: QuoteProofDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome>;
46
+ /** `GET /api/commerce/quote-proof`. */
47
+ export declare const quoteProofHandler: PluginApiHandler;
48
+ /** `POST /api/commerce/quote-upload`. */
49
+ export declare const quoteUploadHandler: PluginApiHandler;
50
+ /** `POST /api/commerce/quote-respond`. */
51
+ export declare const quoteRespondHandler: PluginApiHandler;
@@ -0,0 +1,500 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { claimAttempt } from "@aglyn/aglyn/server";
18
+ import { readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
19
+ import { VISITOR_UPLOAD_HONEYPOT_FIELD, visitorUploadFileView } from "@aglyn/aglyn/app-utils/visitor-upload";
20
+ import { notifyHostManagers } from "@aglyn/tenant-data-admin";
21
+ import * as CommerceModel from "../model/index.js";
22
+ import { orderStatusUrl } from "./order-status-token.js";
23
+ import { defaultQuotePayDeps, limitedQuoteLink, loadLinkedQuote, quoteLinkParams, sendQuoteLinkOutcome } from "./quote-pay.js";
24
+ import { quoteUploadCalls } from "./quote-uploads.js";
25
+ /*==========================================
26
+ * THE CUSTOMER'S SIDE OF A PROOF (AGL-4096), on the business's site, behind
27
+ * the quote's signed pay link like the page itself:
28
+ *
29
+ * GET /api/commerce/quote-proof?hostId=&q=&t=&file=<proofId>
30
+ * → the current revision's proof (a picture inline, anything else as a
31
+ * download) | 404 | 410 replaced | 429
32
+ * POST /api/commerce/quote-upload { hostId, q, t, action: 'ticket', file: { name, contentType, size } }
33
+ * → { ticket, upload: { url, fields }, file }
34
+ * POST /api/commerce/quote-upload { hostId, q, t, action: 'finalize', ticket }
35
+ * → { file: { id, name, contentType, size, uploadedAtMs } }
36
+ * POST /api/commerce/quote-respond { hostId, q, t, v, action: 'approve', name }
37
+ * { hostId, q, t, v, action: 'changes', comment, fileIds? }
38
+ * + Idempotency-Key → 200 { view } | 409 { error, reason } | 410 { replaced } | 404 | 400 | 429
39
+ *
40
+ * Every route spends a per-address rate limit on the site BEFORE the token is
41
+ * checked, answers a wrong token exactly as a missing quote, and answers a
42
+ * link to an older version as replaced. The uploads are the shared visitor
43
+ * upload (`@aglyn/tenant-data-admin/server/visitor-upload`), scoped to this
44
+ * quote's artwork; the answer names the files by id and the server reads
45
+ * them back from that scope, so nothing else can be attached.
46
+ *
47
+ * `v` is the edit of the quote the page showed, as Pay sends it: an answer to
48
+ * proofs the customer has not seen is refused and the page is shown again.
49
+ *=========================================*/ /** Proofs one address may load per site, per ten minutes: every thumbnail of a few visits. */ export const QUOTE_PROOF_LOADS_PER_WINDOW = 120;
50
+ /** Upload calls (tickets and finalizes) one address may make per site, per ten minutes, before the upload's own limits. */ export const QUOTE_UPLOAD_CALLS_PER_WINDOW = 80;
51
+ /** Answers one address may send per site, per ten minutes. */ export const QUOTE_RESPONSES_PER_WINDOW = 20;
52
+ export function defaultQuoteProofDeps() {
53
+ return _extends({}, defaultQuotePayDeps(), {
54
+ uploads: async ()=>quoteUploadCalls(),
55
+ notify: (...args)=>notifyHostManagers(...args)
56
+ });
57
+ }
58
+ function bodyOf(req) {
59
+ const raw = req.body;
60
+ if (typeof raw === 'string') {
61
+ try {
62
+ const parsed = JSON.parse(raw);
63
+ return parsed && typeof parsed === 'object' ? parsed : {};
64
+ } catch (unused) {
65
+ return {};
66
+ }
67
+ }
68
+ return raw && typeof raw === 'object' ? raw : {};
69
+ }
70
+ function headerOf(req, name) {
71
+ var _req_headers, _headers_name_toLowerCase, _value_;
72
+ const headers = (_req_headers = req.headers) != null ? _req_headers : {};
73
+ const value = (_headers_name_toLowerCase = headers[name.toLowerCase()]) != null ? _headers_name_toLowerCase : headers[name];
74
+ return String(Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : '' : value != null ? value : '').trim();
75
+ }
76
+ const clientIp = (req)=>{
77
+ var _req_socket;
78
+ return readClientIp(req.headers, {
79
+ remoteAddress: (_req_socket = req.socket) == null ? void 0 : _req_socket.remoteAddress
80
+ });
81
+ };
82
+ const reasonRefusal = (status, reason, error)=>({
83
+ status,
84
+ body: {
85
+ error,
86
+ reason
87
+ }
88
+ });
89
+ /** `GET /api/commerce/quote-proof`: one of the current revision's proofs. */ export async function handleQuoteProof(deps, req) {
90
+ var _req_query, _query_file;
91
+ if (req.method !== 'GET') return {
92
+ status: 405,
93
+ body: {
94
+ error: 'Method not allowed'
95
+ }
96
+ };
97
+ const query = (_req_query = req.query) != null ? _req_query : {};
98
+ const { hostId, quoteId, token, complete } = quoteLinkParams(query);
99
+ const fileId = String((_query_file = query['file']) != null ? _query_file : '').slice(0, 200);
100
+ if (!complete || !fileId) return {
101
+ status: 400,
102
+ body: {
103
+ error: 'This link is incomplete.'
104
+ }
105
+ };
106
+ const limited = await limitedQuoteLink(deps, req, `commerce-quote-proof:${hostId}`, QUOTE_PROOF_LOADS_PER_WINDOW);
107
+ if (limited) return limited;
108
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
109
+ if ('outcome' in linked) return linked.outcome;
110
+ const proof = CommerceModel.currentQuoteProofs(linked.loaded.order.quote).find((entry)=>entry.id === fileId);
111
+ if (!(proof == null ? void 0 : proof.path)) return {
112
+ status: 404,
113
+ body: {
114
+ error: 'That proof is no longer on this quote.'
115
+ }
116
+ };
117
+ return {
118
+ status: 200,
119
+ body: {},
120
+ file: {
121
+ path: proof.path,
122
+ name: proof.name,
123
+ contentType: proof.contentType
124
+ }
125
+ };
126
+ }
127
+ /** `POST /api/commerce/quote-upload`: a ticket for one of the customer's files, or its finalize. */ export async function handleQuoteUpload(deps, req) {
128
+ var _clientIp;
129
+ if (req.method !== 'POST') return {
130
+ status: 405,
131
+ body: {
132
+ error: 'Method not allowed'
133
+ }
134
+ };
135
+ const body = bodyOf(req);
136
+ const { hostId, quoteId, token, complete } = quoteLinkParams(body);
137
+ if (!complete) return {
138
+ status: 400,
139
+ body: {
140
+ error: 'This link is incomplete.'
141
+ }
142
+ };
143
+ // A form a person fills never fills this; a bot that did learns nothing.
144
+ const honeypot = body[VISITOR_UPLOAD_HONEYPOT_FIELD];
145
+ if (typeof honeypot === 'string' && honeypot.trim()) return {
146
+ status: 200,
147
+ body: {
148
+ received: true
149
+ }
150
+ };
151
+ const limited = await limitedQuoteLink(deps, req, `commerce-quote-upload:${hostId}`, QUOTE_UPLOAD_CALLS_PER_WINDOW);
152
+ if (limited) return limited;
153
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
154
+ if ('outcome' in linked) return linked.outcome;
155
+ const { order, storeName } = linked.loaded;
156
+ // Artwork rides an answer; a quote that cannot be answered takes none.
157
+ const refused = CommerceModel.quoteRespondRefusal(order);
158
+ if (refused) return reasonRefusal(409, refused, CommerceModel.quoteRespondRefusalMessage(refused, storeName));
159
+ const scope = {
160
+ hostId,
161
+ purpose: CommerceModel.QUOTE_ARTWORK_PURPOSE,
162
+ purposeId: quoteId
163
+ };
164
+ const caller = {
165
+ ip: (_clientIp = clientIp(req)) != null ? _clientIp : 'no-address'
166
+ };
167
+ const uploads = await deps.uploads();
168
+ if (body['action'] === 'ticket') {
169
+ const minted = await uploads.mint({
170
+ scope,
171
+ request: body['file'],
172
+ caller
173
+ });
174
+ if ('error' in minted) {
175
+ return _extends({
176
+ status: minted.status,
177
+ body: {
178
+ error: minted.error,
179
+ code: minted.code
180
+ }
181
+ }, minted.retryAfterSeconds ? {
182
+ headers: {
183
+ 'Retry-After': String(minted.retryAfterSeconds)
184
+ }
185
+ } : {});
186
+ }
187
+ return {
188
+ status: 200,
189
+ body: {
190
+ ticket: minted.ticket,
191
+ upload: minted.upload,
192
+ file: minted.file
193
+ }
194
+ };
195
+ }
196
+ if (body['action'] === 'finalize') {
197
+ const kept = await uploads.finalize({
198
+ scope,
199
+ ticket: body['ticket'],
200
+ caller
201
+ });
202
+ if ('error' in kept) return {
203
+ status: kept.status,
204
+ body: {
205
+ error: kept.error,
206
+ code: kept.code
207
+ }
208
+ };
209
+ return {
210
+ status: 200,
211
+ body: {
212
+ file: visitorUploadFileView(kept.file)
213
+ }
214
+ };
215
+ }
216
+ return {
217
+ status: 400,
218
+ body: {
219
+ error: 'Unknown action'
220
+ }
221
+ };
222
+ }
223
+ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it over again before you answer.';
224
+ /** The managers' notice of an answer: who, which quote and revision, what they said, and what to do next. */ function noticeOf(input) {
225
+ var _response_files, _response_name, _response_comment, _ref;
226
+ var _quote_customer, _quote_customer1, _quote_customer2, _quote_customer3, _quote_customer4, _quote_customer5, _quote_customer6;
227
+ const { hostId, quoteId, order, response } = input;
228
+ const quote = order.quote;
229
+ const number = CommerceModel.formatQuoteNumber(order, quoteId);
230
+ const customer = ((_quote_customer = quote.customer) == null ? void 0 : _quote_customer.name) || ((_quote_customer1 = quote.customer) == null ? void 0 : _quote_customer1.company) || ((_quote_customer2 = quote.customer) == null ? void 0 : _quote_customer2.email) || 'The customer';
231
+ const revision = response.revision;
232
+ const files = (_response_files = response.files) != null ? _response_files : [];
233
+ const approving = response.action === 'approve';
234
+ const payNote = quote.requireProofApproval === true ? ' They can pay the quote now.' : '';
235
+ const details = [
236
+ {
237
+ label: 'Quote',
238
+ value: number
239
+ },
240
+ {
241
+ label: 'Customer',
242
+ value: [
243
+ (_quote_customer3 = quote.customer) == null ? void 0 : _quote_customer3.name,
244
+ (_quote_customer4 = quote.customer) == null ? void 0 : _quote_customer4.company,
245
+ (_quote_customer5 = quote.customer) == null ? void 0 : _quote_customer5.email
246
+ ].filter(Boolean).join(', ')
247
+ },
248
+ {
249
+ label: 'Revision',
250
+ value: String(revision)
251
+ },
252
+ ...approving ? [
253
+ {
254
+ label: 'Approved as',
255
+ value: (_response_name = response.name) != null ? _response_name : ''
256
+ }
257
+ ] : [
258
+ {
259
+ label: 'What to change',
260
+ value: (_response_comment = response.comment) != null ? _response_comment : ''
261
+ },
262
+ ...files.length ? [
263
+ {
264
+ label: 'Files sent',
265
+ value: files.map((file)=>file.name).join(', ')
266
+ }
267
+ ] : []
268
+ ]
269
+ ];
270
+ const email = String((_ref = (_quote_customer6 = quote.customer) == null ? void 0 : _quote_customer6.email) != null ? _ref : '').trim();
271
+ return {
272
+ payload: {
273
+ type: 'content.order',
274
+ title: approving ? `Proof approved — ${number} on {site}` : `Changes requested — ${number} on {site}`,
275
+ body: approving ? `${customer} approved the proof for revision ${revision} of ${number}.${payNote} The approval, with the name, time and address it was given from, is on the quote.` : `${customer} asked for changes to revision ${revision} of ${number}` + (files.length ? ` and sent ${files.length} file${files.length === 1 ? '' : 's'}` : '') + `. Read what they asked for on the quote, then start revision ${revision + 1} with new proofs.`,
276
+ link: `/${hostId}/products/orders?quote=${encodeURIComponent(quoteId)}`
277
+ },
278
+ options: _extends({
279
+ emailDetails: details
280
+ }, /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? {
281
+ replyTo: email
282
+ } : {})
283
+ };
284
+ }
285
+ /** `POST /api/commerce/quote-respond`: the customer approves the proof or asks for changes. */ export async function handleQuoteRespond(deps, req) {
286
+ var _order_quote;
287
+ if (req.method !== 'POST') return {
288
+ status: 405,
289
+ body: {
290
+ error: 'Method not allowed'
291
+ }
292
+ };
293
+ const body = bodyOf(req);
294
+ const { hostId, quoteId, token, complete } = quoteLinkParams(body);
295
+ if (!complete) return {
296
+ status: 400,
297
+ body: {
298
+ error: 'This link is incomplete.'
299
+ }
300
+ };
301
+ const honeypot = body[VISITOR_UPLOAD_HONEYPOT_FIELD];
302
+ if (typeof honeypot === 'string' && honeypot.trim()) return {
303
+ status: 200,
304
+ body: {
305
+ received: true
306
+ }
307
+ };
308
+ const idempotencyKey = headerOf(req, 'idempotency-key').slice(0, 200);
309
+ if (!idempotencyKey) return {
310
+ status: 400,
311
+ body: {
312
+ error: 'An answer needs an Idempotency-Key header.'
313
+ }
314
+ };
315
+ const limited = await limitedQuoteLink(deps, req, `commerce-quote-respond:${hostId}`, QUOTE_RESPONSES_PER_WINDOW);
316
+ if (limited) return limited;
317
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
318
+ if ('outcome' in linked) return linked.outcome;
319
+ const { hostRef, host, order, storeName, store } = linked.loaded;
320
+ // Everything the customer can be refused is decided before the claim, so
321
+ // none of it spends the attempt.
322
+ const seen = Number(body['v']);
323
+ const version = Number((_order_quote = order.quote) == null ? void 0 : _order_quote.updatedAtMs) || 0;
324
+ if (!Number.isFinite(seen) || seen !== version) return reasonRefusal(409, 'updated', UPDATED_MESSAGE);
325
+ const parsed = CommerceModel.parseQuoteResponseInput(body);
326
+ if ('error' in parsed) return {
327
+ status: 400,
328
+ body: {
329
+ error: parsed.error
330
+ }
331
+ };
332
+ const refused = CommerceModel.quoteRespondRefusal(order);
333
+ if (refused) return reasonRefusal(409, refused, CommerceModel.quoteRespondRefusalMessage(refused, storeName));
334
+ // The files are read back from this quote's artwork, by id: nothing the
335
+ // browser says about a file is believed, and no other record's file can
336
+ // be named.
337
+ let files = [];
338
+ if (parsed.input.action === 'changes' && parsed.input.fileIds.length) {
339
+ const uploads = await deps.uploads();
340
+ const scope = {
341
+ hostId,
342
+ purpose: CommerceModel.QUOTE_ARTWORK_PURPOSE,
343
+ purposeId: quoteId
344
+ };
345
+ const read = await Promise.all(parsed.input.fileIds.map((id)=>uploads.read(scope, id)));
346
+ if (read.some((file)=>!file)) {
347
+ return {
348
+ status: 400,
349
+ body: {
350
+ error: 'One of your files did not finish sending. Send it again.'
351
+ }
352
+ };
353
+ }
354
+ files = read.map((file)=>({
355
+ id: file.id,
356
+ name: file.name,
357
+ contentType: file.contentType,
358
+ size: file.size,
359
+ path: file.path
360
+ }));
361
+ }
362
+ const claimed = await claimAttempt(deps.firestore(), {
363
+ kind: 'commerce-quote-respond',
364
+ scopeId: `${hostId}:${quoteId}`,
365
+ orgId: '',
366
+ key: idempotencyKey,
367
+ busyMessage: 'Your answer is already being sent'
368
+ });
369
+ if ('replay' in claimed) {
370
+ return {
371
+ status: claimed.replay.status,
372
+ body: claimed.replay.body
373
+ };
374
+ }
375
+ const claim = claimed.claim;
376
+ const now = deps.now();
377
+ try {
378
+ var _store_get;
379
+ const ref = hostRef.collection('orders').doc(quoteId);
380
+ const settled = await deps.firestore().runTransaction(async (transaction)=>{
381
+ const snapshot = await transaction.get(ref);
382
+ const current = snapshot.exists ? snapshot.data() : null;
383
+ if (!current || !CommerceModel.isQuote(current) || !current.quote) {
384
+ return {
385
+ outcome: {
386
+ status: 404,
387
+ body: {
388
+ error: 'We could not find that quote. Check the link you were sent.'
389
+ }
390
+ }
391
+ };
392
+ }
393
+ if (CommerceModel.quoteLinkVersion(current.quote) !== CommerceModel.quoteLinkVersion(order.quote)) {
394
+ return {
395
+ outcome: {
396
+ status: 410,
397
+ body: {
398
+ replaced: true,
399
+ error: `This quote was updated, and ${storeName || 'the business'} sent you a newer link.`
400
+ }
401
+ }
402
+ };
403
+ }
404
+ if ((Number(current.quote.updatedAtMs) || 0) !== version) {
405
+ return {
406
+ outcome: reasonRefusal(409, 'updated', UPDATED_MESSAGE)
407
+ };
408
+ }
409
+ const answered = CommerceModel.applyQuoteResponse(current, _extends({}, parsed.input, files.length ? {
410
+ files
411
+ } : {}), {
412
+ atMs: now,
413
+ ip: clientIp(req),
414
+ userAgent: headerOf(req, 'user-agent') || null
415
+ });
416
+ if ('reason' in answered) {
417
+ return {
418
+ outcome: reasonRefusal(409, answered.reason, CommerceModel.quoteRespondRefusalMessage(answered.reason, storeName))
419
+ };
420
+ }
421
+ transaction.set(ref, answered.order);
422
+ return {
423
+ outcome: {
424
+ status: 200,
425
+ body: {}
426
+ },
427
+ order: answered.order,
428
+ response: answered.response
429
+ };
430
+ });
431
+ if (!settled.order || !settled.response) {
432
+ await claim.release();
433
+ return settled.outcome;
434
+ }
435
+ const view = CommerceModel.buildQuotePageView({
436
+ order: settled.order,
437
+ orderId: quoteId,
438
+ storeName,
439
+ currency: String((_store_get = store.get('currency')) != null ? _store_get : 'USD'),
440
+ receiptUrl: CommerceModel.isUnpaidQuote(settled.order) ? null : orderStatusUrl(host, hostId, quoteId)
441
+ });
442
+ const outcome = {
443
+ status: 200,
444
+ body: {
445
+ view
446
+ }
447
+ };
448
+ await claim.record(200, outcome.body);
449
+ const notice = noticeOf({
450
+ hostId,
451
+ quoteId,
452
+ order: settled.order,
453
+ response: settled.response
454
+ });
455
+ void deps.notify(hostId, notice.payload, notice.options);
456
+ return outcome;
457
+ } catch (error) {
458
+ console.error('quote respond failed', error);
459
+ await claim.release();
460
+ return {
461
+ status: 500,
462
+ body: {
463
+ error: 'Your answer could not be sent. Try again.'
464
+ }
465
+ };
466
+ }
467
+ }
468
+ /** `GET /api/commerce/quote-proof`. */ export const quoteProofHandler = async (req, res)=>{
469
+ const deps = defaultQuoteProofDeps();
470
+ try {
471
+ const outcome = await handleQuoteProof(deps, req);
472
+ if (!outcome.file) return sendQuoteLinkOutcome(res, outcome);
473
+ const uploads = await deps.uploads();
474
+ return await uploads.send(res, outcome.file);
475
+ } catch (error) {
476
+ console.error('quote proof failed', error);
477
+ return sendQuoteLinkOutcome(res, {
478
+ status: 500,
479
+ body: {
480
+ error: 'This proof could not be loaded. Try again.'
481
+ }
482
+ });
483
+ }
484
+ };
485
+ /** `POST /api/commerce/quote-upload`. */ export const quoteUploadHandler = async (req, res)=>{
486
+ try {
487
+ return sendQuoteLinkOutcome(res, await handleQuoteUpload(defaultQuoteProofDeps(), req));
488
+ } catch (error) {
489
+ console.error('quote upload failed', error);
490
+ return sendQuoteLinkOutcome(res, {
491
+ status: 500,
492
+ body: {
493
+ error: 'The file could not be sent. Try again.'
494
+ }
495
+ });
496
+ }
497
+ };
498
+ /** `POST /api/commerce/quote-respond`. */ export const quoteRespondHandler = async (req, res)=>sendQuoteLinkOutcome(res, await handleQuoteRespond(defaultQuoteProofDeps(), req));
499
+
500
+ //# sourceMappingURL=quote-proofs.js.map