@aglyn/plugins-commerce 1.0.0-beta.292 → 1.0.0-beta.293

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.
@@ -1 +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), AND AN\n * INVOICE'S (AGL-4099).\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 * AN INVOICE'S LINK IS THIS ONE (AGL-4099). A draft order the business sent\n * an invoice for opens on the same page from the same kind of link (its\n * version on `payLink.version`, `commerce-invoices.ts`), as \"Invoice #N\" with\n * no proofs or approval, and Pay opens the same kind of fresh session from\n * its stored lines, discount, tax and the shipping rates it was made with.\n * Its webhook is the draft branch it always completed through, which flips\n * the draft to paid without restamping its list fields. The proof and answer\n * routes stay a quote's alone: `loadLinkedQuote` opens an invoice only for\n * the routes that ask (`{ invoices: true }`).\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\n/** The not-found answer of a route an invoice's link reaches too: it names neither. */\nconst NOT_FOUND_ANY: QuoteLinkOutcome = {\n status: 404,\n body: { error: 'We could not find that quote or invoice. Check the link you were sent.' },\n}\n\n/** `quote` or `invoice`, as the customer reads it. */\nconst nounOf = (kind: CommerceModel.OrderPayLinkKind) => (kind === 'invoice' ? 'invoice' : 'quote')\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 /** A quote, or an order's invoice (only for a route that opens invoices). */\n kind: CommerceModel.OrderPayLinkKind\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 * With `invoices`, a draft order's invoice too (AGL-4099).\n */\nexport async function loadLinkedQuote(\n deps: QuotePayDeps,\n hostId: string,\n quoteId: string,\n token: string,\n options: { invoices?: boolean } = {},\n): Promise<{ loaded: LoadedQuote } | { outcome: QuoteLinkOutcome }> {\n const notFound = options.invoices ? NOT_FOUND_ANY : NOT_FOUND\n const version = verifyQuoteLinkToken(hostId, quoteId, token)\n if (version === null) return { outcome: notFound }\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: notFound }\n const order = orderSnapshot.data() as CommerceModel.HostOrder\n const kind = CommerceModel.orderPayLinkKind(order)\n if (kind === null || (kind === 'invoice' && !options.invoices)) return { outcome: notFound }\n const host = (hostSnapshot.data() ?? {}) as Record<string, unknown>\n const storeName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '')\n const current = CommerceModel.orderPayLinkVersion(order)\n if (version > current) return { outcome: notFound }\n if (version < current) {\n return {\n outcome: {\n status: 410,\n body: {\n state: 'replaced',\n kind,\n storeName,\n replaced: true,\n error: `This ${nounOf(kind)} was updated, and ${storeName || 'the business'} sent you a newer link.`,\n },\n },\n }\n }\n return { loaded: { hostRef, host, order, storeName, store, kind } }\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, { invoices: true })\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, kind } = linked.outcome.body as { storeName: string; kind: CommerceModel.OrderPayLinkKind }\n return { status: 200, body: { state: 'replaced', kind, storeName } }\n }\n return linked.outcome\n }\n const { host, order, storeName, store, kind } = linked.loaded\n const currency = String(store.get('currency') ?? 'USD')\n const receiptUrl = order.status === 'pending' ? null : orderStatusUrl(host, hostId, quoteId)\n const view =\n kind === 'quote'\n ? CommerceModel.buildQuotePageView({ order, orderId: quoteId, storeName, currency, receiptUrl })\n : CommerceModel.buildInvoicePageView({ order, orderId: quoteId, storeName, currency, receiptUrl })\n return { status: 200, body: view as unknown as Record<string, unknown> }\n}\n\n/** The Checkout Session a quote's (or an invoice'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 & Partial<Pick<CommerceModel.InvoiceCheckoutCharge, 'shippingOptions' | 'shippingCountries'>>\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 invoice = CommerceModel.orderPayLinkKind(order) === 'invoice'\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 // An invoiced draft offers the rates it was made with, and the buyer picks\n // one, as on the draft's first link (AGL-4099).\n const rates = charge.shippingOptions ?? []\n if (rates.length > 0) {\n CommerceModel.appendShippingAddressCollectionParams(\n params,\n charge.shippingCountries?.length ? charge.shippingCountries : CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,\n )\n CommerceModel.appendCheckoutShippingParams(params, rates)\n } else 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 = rates.length > 0 ? CommerceModel.shippingFloorCents(rates) : 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((invoice ? order.customerEmail : 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 if (invoice) {\n // An invoice's link version, which the draft branch compares as a\n // quote's (AGL-4099), and the promotion the draft was priced with, which\n // it counts once the draft is paid (no hold: the draft's lapsed with its\n // first session).\n params.set('metadata[payLinkVersion]', String(CommerceModel.orderPayLinkVersion(order)))\n if (order.discountId) params.set('metadata[discountId]', String(order.discountId))\n } else {\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 }\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, { invoices: true })\n if ('outcome' in linked) return linked.outcome\n const { hostRef, host, order, storeName, store, kind } = linked.loaded\n const noun = nounOf(kind)\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 // An invoice is not edited, so it sends none.\n const seen = Number(bodyOf(req)['v'])\n if (kind === 'quote' && Number.isFinite(seen) && seen > 0 && seen !== (Number(order.quote?.updatedAtMs) || 0)) {\n return refusal(409, 'updated', QUOTE_UPDATED_MESSAGE)\n }\n const blocked = CommerceModel.invoicePayRefusal(order, quoteId)\n if (blocked) return refusal(409, blocked, CommerceModel.invoicePayRefusalMessage(blocked, storeName, kind))\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 ${noun}.`,\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: `${noun} ${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.invoicePayRefusalMessage('tax-undecided', storeName, kind))\n }\n\n const charge = CommerceModel.invoiceCheckoutCharge(order, quoteId)\n // The fee is sized on the most the buyer can pay, as a draft's first link\n // sized it on its dearest rate.\n const dearestRateCents = charge.shippingOptions.reduce((most, rate) => Math.max(most, rate.amountCents), 0)\n const feeCents = resolveTransactionFeeCents(\n owner?.org as never,\n charge.feeProductType,\n charge.goodsCents,\n charge.totalCents + dearestRateCents,\n )\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 // A draft's first session (the one its paymentLinkUrl opens) is closed\n // here too, so the draft has one way to pay from the first press on.\n const openedFor = order.checkoutSessionId ?? ''\n const openedAtEdit = Number(order.quote?.updatedAtMs) || 0\n const openedAtVersion = CommerceModel.orderPayLinkVersion(order)\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 ${noun} 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.orderPayLinkKind(current) !== kind) return { outcome: NOT_FOUND_ANY, displaced: '' }\n if (current.status !== 'pending') {\n const closed = current.status === 'cancelled' || current.status === 'refunded'\n return {\n outcome: refusal(\n 409,\n closed ? 'closed' : 'paid',\n CommerceModel.invoicePayRefusalMessage(closed ? 'closed' : 'paid', storeName, kind),\n ),\n displaced: '',\n }\n }\n if (CommerceModel.orderPayLinkVersion(current) !== openedAtVersion) {\n return {\n outcome: {\n status: 410,\n body: { replaced: true, error: `This ${noun} was updated, and ${storeName || 'the business'} sent you a newer link.` },\n },\n displaced: '',\n }\n }\n if (kind === 'quote' && (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","NOT_FOUND_ANY","nounOf","kind","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","notFound","invoices","version","outcome","hostRef","collection","doc","store","orderSnapshot","Promise","all","get","exists","order","data","orderPayLinkKind","host","storeName","current","orderPayLinkVersion","state","replaced","loaded","quoteLinkParams","source","slice","complete","test","Boolean","handleQuoteView","query","refused","linked","currency","receiptUrl","view","buildQuotePageView","orderId","buildInvoicePageView","buildQuoteSessionParams","charge","invoice","quote","URLSearchParams","mode","taxAtPayment","lines","forEach","line","index","at","set","amountCents","description","taxable","rates","shippingOptions","length","appendShippingAddressCollectionParams","shippingCountries","CHECKOUT_SHIPPING_COUNTRIES","appendCheckoutShippingParams","shippingCents","country","shipTo","toUpperCase","rateId","shippingFloorCents","Object","entries","destinationChargeParams","accountId","feeCents","taxOwner","merchantGoodsCents","goodsCents","email","customerEmail","customer","separator","returnUrl","includes","discountId","quoteLinkVersion","Number","updatedAtMs","taxCents","taxPct","platformLiableTransferCents","appendStorefrontPaymentMethodParams","paymentMethods","QUOTE_UPDATED_MESSAGE","refusal","reason","extra","handleQuotePay","owner","idempotencyKey","noun","seen","isFinite","blocked","invoicePayRefusal","invoicePayRefusalMessage","unavailable","ownerUid","profile","ready","chargesEnabled","accountLivemode","subject","taxSettings","storefrontTaxDecision","settings","invoiceCheckoutCharge","dearestRateCents","reduce","most","feeProductType","totalCents","site","origin","candidates","requestHost","openedFor","checkoutSessionId","openedAtEdit","openedAtVersion","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","closed","merge","record","sendQuoteLinkOutcome","res","setHeader","quoteViewHandler","quotePayHandler"],"mappings":"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"}
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), AND AN\n * INVOICE'S (AGL-4099).\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 * AN INVOICE'S LINK IS THIS ONE (AGL-4099). A draft order the business sent\n * an invoice for opens on the same page from the same kind of link (its\n * version on `payLink.version`, `commerce-invoices.ts`), as \"Invoice #N\" with\n * no proofs or approval, and Pay opens the same kind of fresh session from\n * its stored lines, discount, tax and the shipping rates it was made with.\n * Its webhook is the draft branch it always completed through, which flips\n * the draft to paid without restamping its list fields. The proof and answer\n * routes stay a quote's alone: `loadLinkedQuote` opens an invoice only for\n * the routes that ask (`{ invoices: true }`).\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\n/** The not-found answer of a route an invoice's link reaches too: it names neither. */\nconst NOT_FOUND_ANY: QuoteLinkOutcome = {\n status: 404,\n body: { error: 'We could not find that quote or invoice. Check the link you were sent.' },\n}\n\n/** `quote` or `invoice`, as the customer reads it. */\nconst nounOf = (kind: CommerceModel.OrderPayLinkKind) => (kind === 'invoice' ? 'invoice' : 'quote')\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 /** A quote, or an order's invoice (only for a route that opens invoices). */\n kind: CommerceModel.OrderPayLinkKind\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 * With `invoices`, a draft order's invoice too (AGL-4099). With `history`, a\n * link to an older version opens the quote as well: only for a route that\n * serves the quote's record and acts on nothing (its proofs, which an earlier\n * email draws, AGL-4097); pay, answers and uploads keep the current version.\n */\nexport async function loadLinkedQuote(\n deps: QuotePayDeps,\n hostId: string,\n quoteId: string,\n token: string,\n options: { invoices?: boolean; history?: boolean } = {},\n): Promise<{ loaded: LoadedQuote } | { outcome: QuoteLinkOutcome }> {\n const notFound = options.invoices ? NOT_FOUND_ANY : NOT_FOUND\n const version = verifyQuoteLinkToken(hostId, quoteId, token)\n if (version === null) return { outcome: notFound }\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: notFound }\n const order = orderSnapshot.data() as CommerceModel.HostOrder\n const kind = CommerceModel.orderPayLinkKind(order)\n if (kind === null || (kind === 'invoice' && !options.invoices)) return { outcome: notFound }\n const host = (hostSnapshot.data() ?? {}) as Record<string, unknown>\n const storeName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '')\n const current = CommerceModel.orderPayLinkVersion(order)\n if (version > current) return { outcome: notFound }\n if (version < current && !options.history) {\n return {\n outcome: {\n status: 410,\n body: {\n state: 'replaced',\n kind,\n storeName,\n replaced: true,\n error: `This ${nounOf(kind)} was updated, and ${storeName || 'the business'} sent you a newer link.`,\n },\n },\n }\n }\n return { loaded: { hostRef, host, order, storeName, store, kind } }\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, { invoices: true })\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, kind } = linked.outcome.body as { storeName: string; kind: CommerceModel.OrderPayLinkKind }\n return { status: 200, body: { state: 'replaced', kind, storeName } }\n }\n return linked.outcome\n }\n const { host, order, storeName, store, kind } = linked.loaded\n const currency = String(store.get('currency') ?? 'USD')\n const receiptUrl = order.status === 'pending' ? null : orderStatusUrl(host, hostId, quoteId)\n const view =\n kind === 'quote'\n ? CommerceModel.buildQuotePageView({ order, orderId: quoteId, storeName, currency, receiptUrl })\n : CommerceModel.buildInvoicePageView({ order, orderId: quoteId, storeName, currency, receiptUrl })\n return { status: 200, body: view as unknown as Record<string, unknown> }\n}\n\n/** The Checkout Session a quote's (or an invoice'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 & Partial<Pick<CommerceModel.InvoiceCheckoutCharge, 'shippingOptions' | 'shippingCountries'>>\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 invoice = CommerceModel.orderPayLinkKind(order) === 'invoice'\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 // An invoiced draft offers the rates it was made with, and the buyer picks\n // one, as on the draft's first link (AGL-4099).\n const rates = charge.shippingOptions ?? []\n if (rates.length > 0) {\n CommerceModel.appendShippingAddressCollectionParams(\n params,\n charge.shippingCountries?.length ? charge.shippingCountries : CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,\n )\n CommerceModel.appendCheckoutShippingParams(params, rates)\n } else 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 = rates.length > 0 ? CommerceModel.shippingFloorCents(rates) : 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((invoice ? order.customerEmail : 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 if (invoice) {\n // An invoice's link version, which the draft branch compares as a\n // quote's (AGL-4099), and the promotion the draft was priced with, which\n // it counts once the draft is paid (no hold: the draft's lapsed with its\n // first session).\n params.set('metadata[payLinkVersion]', String(CommerceModel.orderPayLinkVersion(order)))\n if (order.discountId) params.set('metadata[discountId]', String(order.discountId))\n } else {\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 }\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, { invoices: true })\n if ('outcome' in linked) return linked.outcome\n const { hostRef, host, order, storeName, store, kind } = linked.loaded\n const noun = nounOf(kind)\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 // An invoice is not edited, so it sends none.\n const seen = Number(bodyOf(req)['v'])\n if (kind === 'quote' && Number.isFinite(seen) && seen > 0 && seen !== (Number(order.quote?.updatedAtMs) || 0)) {\n return refusal(409, 'updated', QUOTE_UPDATED_MESSAGE)\n }\n const blocked = CommerceModel.invoicePayRefusal(order, quoteId)\n if (blocked) return refusal(409, blocked, CommerceModel.invoicePayRefusalMessage(blocked, storeName, kind))\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 ${noun}.`,\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: `${noun} ${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.invoicePayRefusalMessage('tax-undecided', storeName, kind))\n }\n\n const charge = CommerceModel.invoiceCheckoutCharge(order, quoteId)\n // The fee is sized on the most the buyer can pay, as a draft's first link\n // sized it on its dearest rate.\n const dearestRateCents = charge.shippingOptions.reduce((most, rate) => Math.max(most, rate.amountCents), 0)\n const feeCents = resolveTransactionFeeCents(\n owner?.org as never,\n charge.feeProductType,\n charge.goodsCents,\n charge.totalCents + dearestRateCents,\n )\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 // A draft's first session (the one its paymentLinkUrl opens) is closed\n // here too, so the draft has one way to pay from the first press on.\n const openedFor = order.checkoutSessionId ?? ''\n const openedAtEdit = Number(order.quote?.updatedAtMs) || 0\n const openedAtVersion = CommerceModel.orderPayLinkVersion(order)\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 ${noun} 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.orderPayLinkKind(current) !== kind) return { outcome: NOT_FOUND_ANY, displaced: '' }\n if (current.status !== 'pending') {\n const closed = current.status === 'cancelled' || current.status === 'refunded'\n return {\n outcome: refusal(\n 409,\n closed ? 'closed' : 'paid',\n CommerceModel.invoicePayRefusalMessage(closed ? 'closed' : 'paid', storeName, kind),\n ),\n displaced: '',\n }\n }\n if (CommerceModel.orderPayLinkVersion(current) !== openedAtVersion) {\n return {\n outcome: {\n status: 410,\n body: { replaced: true, error: `This ${noun} was updated, and ${storeName || 'the business'} sent you a newer link.` },\n },\n displaced: '',\n }\n }\n if (kind === 'quote' && (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","NOT_FOUND_ANY","nounOf","kind","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","notFound","invoices","version","outcome","hostRef","collection","doc","store","orderSnapshot","Promise","all","get","exists","order","data","orderPayLinkKind","host","storeName","current","orderPayLinkVersion","history","state","replaced","loaded","quoteLinkParams","source","slice","complete","test","Boolean","handleQuoteView","query","refused","linked","currency","receiptUrl","view","buildQuotePageView","orderId","buildInvoicePageView","buildQuoteSessionParams","charge","invoice","quote","URLSearchParams","mode","taxAtPayment","lines","forEach","line","index","at","set","amountCents","description","taxable","rates","shippingOptions","length","appendShippingAddressCollectionParams","shippingCountries","CHECKOUT_SHIPPING_COUNTRIES","appendCheckoutShippingParams","shippingCents","country","shipTo","toUpperCase","rateId","shippingFloorCents","Object","entries","destinationChargeParams","accountId","feeCents","taxOwner","merchantGoodsCents","goodsCents","email","customerEmail","customer","separator","returnUrl","includes","discountId","quoteLinkVersion","Number","updatedAtMs","taxCents","taxPct","platformLiableTransferCents","appendStorefrontPaymentMethodParams","paymentMethods","QUOTE_UPDATED_MESSAGE","refusal","reason","extra","handleQuotePay","owner","idempotencyKey","noun","seen","isFinite","blocked","invoicePayRefusal","invoicePayRefusalMessage","unavailable","ownerUid","profile","ready","chargesEnabled","accountLivemode","subject","taxSettings","storefrontTaxDecision","settings","invoiceCheckoutCharge","dearestRateCents","reduce","most","feeProductType","totalCents","site","origin","candidates","requestHost","openedFor","checkoutSessionId","openedAtEdit","openedAtVersion","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","closed","merge","record","sendQuoteLinkOutcome","res","setHeader","quoteViewHandler","quotePayHandler"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,YAAY,EACZC,qBAAqB,EACrBC,0BAA0B,QAGrB,sBAAqB;AAC5B,SAASC,gBAAgB,QAA8B,qCAAoC;AAC3F,SAASC,YAAY,QAAQ,oCAAmC;AAChE,SAA8BC,gBAAgB,QAAQ,yCAAwC;AAC9F,SAASC,gBAAgB,EAAEC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACzF,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SAASC,uCAAuC,QAAQ,6DAA4D;AACpH,SAASC,aAAa,QAAQ,6BAAyB;AACvD,YAAYC,mBAAmB,oBAAU;AACzC,SAASC,qBAAqB,QAAoC,+BAA2B;AAC7F,SAASC,sBAAsB,QAAQ,uBAAmB;AAC1D,SACEC,yBAAyB,EACzBC,2BAA2B,EAC3BC,0BAA0B,EAC1BC,yBAAyB,QAEpB,uBAAmB;AAC1B,SAASC,cAAc,QAAQ,0BAAsB;AACrD,SAASC,oBAAoB,QAAQ,8BAA0B;AAC/D,SAASC,oBAAoB,QAAQ,wBAAoB;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAmC2C,GAE3C,wEAAwE,GACxE,OAAO,MAAMC,gCAAgC,GAAE;AAC/C,OAAO,MAAMC,+BAA+B,GAAE;AAC9C,OAAO,MAAMC,kBAAkB,KAAK,MAAM;AAmB1C,OAAO,SAASC;IACd,OAAO;QACLC,WAAW,IAAMnB,cAAcoB,GAAG,GAAGD,SAAS;QAC9CpB,kBAAkB,CAACsB,KAAKC,UAAYvB,iBAAiBsB,KAAKC;QAC1DC,YAAY,OAAOC;YACjB,MAAMC,QAAQ,MAAMxB,cAAcuB;YAClC,OAAOC,QAAQ;gBAAEC,IAAID,MAAME,KAAK;gBAAEC,KAAKH,MAAMG,GAAG;YAAC,IAAI;QACvD;QACAC,YAAY,CAACF,QAAUhB,0BAA0BgB;QACjDG,WAAW,CAACC,QAAUxB,uBAAuBwB;QAC7CC,eAAe,OAAOC,QAAQC;YAC5B,MAAMC,WAAW,MAAMC,MAAM,+CAA+C;gBAC1EC,QAAQ;gBACRH,SAAS;oBACPI,eAAe,CAAC,OAAO,EAAEC,QAAQC,GAAG,CAACC,iBAAiB,EAAE;oBACxD,gBAAgB;mBACbP;gBAELQ,MAAMT,OAAOU,QAAQ;YACvB;YACA,OAAO;gBAAEC,IAAIT,SAASS,EAAE;gBAAEC,SAAU,MAAMV,SAASW,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;YAAc;QACxF;QACAC,eAAe,CAACC,YAAc3C,sBAAsB2C;QACpDC,cAAc,CAACnB,QAAUlB,qBAAqBkB,OAAOgB,KAAK,CAAC,IAAM;QACjEI,KAAK,IAAMC,KAAKD,GAAG;IACrB;AACF;AAKA,MAAME,SAAS;AAEf,MAAMC,YAA8B;IAClCC,QAAQ;IACRb,MAAM;QAAEc,OAAO;IAA8D;AAC/E;AAEA,qFAAqF,GACrF,MAAMC,gBAAkC;IACtCF,QAAQ;IACRb,MAAM;QAAEc,OAAO;IAAyE;AAC1F;AAEA,oDAAoD,GACpD,MAAME,SAAS,CAACC,OAA0CA,SAAS,YAAY,YAAY;AAE3F,SAASC,OAAOC,GAAqB;IACnC,MAAMC,MAAMD,IAAInB,IAAI;IACpB,IAAI,OAAOoB,QAAQ,UAAU;QAC3B,IAAI;YACF,MAAMC,SAASC,KAAKC,KAAK,CAACH;YAC1B,OAAOC,UAAU,OAAOA,WAAW,WAAWA,SAAS,CAAC;QAC1D,EAAE,eAAM;YACN,OAAO,CAAC;QACV;IACF;IACA,OAAOD,OAAO,OAAOA,QAAQ,WAAYA,MAAkC,CAAC;AAC9E;AAEA,SAASI,SAASL,GAAqB,EAAEM,IAAY;QAClCN,cACH3B,2BACwBkC;IAFtC,MAAMlC,WAAW2B,eAAAA,IAAI3B,OAAO,YAAX2B,eAAe,CAAC;IACjC,MAAMO,SAAQlC,4BAAAA,OAAO,CAACiC,KAAKE,WAAW,GAAG,YAA3BnC,4BAA+BA,OAAO,CAACiC,KAAK;IAC1D,OAAOG,OAAOC,MAAMC,OAAO,CAACJ,UAAUA,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,KAAOA,gBAAAA,QAAS,IAAKK,IAAI;AAC7E;AAEA,0FAA0F,GAC1F,OAAO,eAAeC,iBACpBC,IAAkB,EAClBd,GAAqB,EACrBxC,GAAW,EACXuD,KAAa;QAEF/E;QAAoDgE;IAA/D,MAAMgB,MAAKhF,gBAAAA,aAAagE,IAAI3B,OAAO,EAAW;QAAE4C,aAAa,GAAEjB,cAAAA,IAAIkB,MAAM,qBAAVlB,YAAYiB,aAAa;IAAC,cAA9EjF,gBAAoF;IAC/F,MAAMmF,OAAO,MAAML,KAAK5E,gBAAgB,CAAC,GAAGsB,IAAI,CAAC,EAAEwD,IAAI,EAAE;QAAED;QAAOK,UAAUhE;IAAgB;IAC5F,IAAI+D,KAAKE,OAAO,EAAE,OAAO;IACzB,OAAO;QACL3B,QAAQ;QACRb,MAAM;YAAEc,OAAO;QAA8C;QAC7DtB,SAAS;YAAE,eAAeoC,OAAOa,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAACL,CAAAA,KAAKM,OAAO,GAAGX,KAAKxB,GAAG,EAAC,IAAK;QAAQ;IAC/F;AACF;AAYA;;;;;;;CAOC,GACD,OAAO,eAAeoC,gBACpBZ,IAAkB,EAClBnD,MAAc,EACdgE,OAAe,EACfC,KAAa,EACbnE,UAAqD,CAAC,CAAC;QAezCoE,oBACW9F;IAdzB,MAAM+F,WAAWrE,QAAQsE,QAAQ,GAAGnC,gBAAgBH;IACpD,MAAMuC,UAAU/E,qBAAqBU,QAAQgE,SAASC;IACtD,IAAII,YAAY,MAAM,OAAO;QAAEC,SAASH;IAAS;IACjD,MAAMI,UAAUpB,KAAKxD,SAAS,GAAG6E,UAAU,CAAC,SAASC,GAAG,CAACzE;IACzD,MAAM,CAACkE,cAAcQ,OAAOC,cAAc,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC7DN,QAAQO,GAAG;QACXP,QAAQC,UAAU,CAAC,YAAYC,GAAG,CAAC,SAASK,GAAG;QAC/CP,QAAQC,UAAU,CAAC,UAAUC,GAAG,CAACT,SAASc,GAAG;KAC9C;IACD,IAAI,CAACZ,aAAaa,MAAM,IAAI,CAACJ,cAAcI,MAAM,EAAE,OAAO;QAAET,SAASH;IAAS;IAC9E,MAAMa,QAAQL,cAAcM,IAAI;IAChC,MAAM9C,OAAOtD,cAAcqG,gBAAgB,CAACF;IAC5C,IAAI7C,SAAS,QAASA,SAAS,aAAa,CAACrC,QAAQsE,QAAQ,EAAG,OAAO;QAAEE,SAASH;IAAS;IAC3F,MAAMgB,QAAQjB,qBAAAA,aAAae,IAAI,cAAjBf,qBAAuB,CAAC;IACtC,MAAMkB,YAAYtC,QAAO1E,oBAAAA,iBAAiB,gBAAgB+G,iBAAjC/G,oBAA6D;IACtF,MAAMiH,UAAUxG,cAAcyG,mBAAmB,CAACN;IAClD,IAAIX,UAAUgB,SAAS,OAAO;QAAEf,SAASH;IAAS;IAClD,IAAIE,UAAUgB,WAAW,CAACvF,QAAQyF,OAAO,EAAE;QACzC,OAAO;YACLjB,SAAS;gBACPvC,QAAQ;gBACRb,MAAM;oBACJsE,OAAO;oBACPrD;oBACAiD;oBACAK,UAAU;oBACVzD,OAAO,CAAC,KAAK,EAAEE,OAAOC,MAAM,kBAAkB,EAAEiD,aAAa,eAAe,uBAAuB,CAAC;gBACtG;YACF;QACF;IACF;IACA,OAAO;QAAEM,QAAQ;YAAEnB;YAASY;YAAMH;YAAOI;YAAWV;YAAOvC;QAAK;IAAE;AACpE;AAEA,8FAA8F,GAC9F,OAAO,SAASwD,gBAAgBC,MAA+B;QACvCA,gBACCA,WACFA;IAFrB,MAAM5F,SAAS8C,QAAO8C,iBAAAA,MAAM,CAAC,SAAS,YAAhBA,iBAAoB;IAC1C,MAAM5B,UAAUlB,QAAO8C,YAAAA,MAAM,CAAC,IAAI,YAAXA,YAAe,IAAIC,KAAK,CAAC,GAAG;IACnD,MAAM5B,QAAQnB,QAAO8C,YAAAA,MAAM,CAAC,IAAI,YAAXA,YAAe,IAAIC,KAAK,CAAC,GAAG;IACjD,MAAMC,WAAWjE,OAAOkE,IAAI,CAAC/F,WAAW6B,OAAOkE,IAAI,CAAC/B,YAAYgC,QAAQ/B;IACxE,OAAO;QAAEjE;QAAQgE;QAASC;QAAO6B;IAAS;AAC5C;AAEA,2DAA2D,GAC3D,OAAO,eAAeG,gBAAgB9C,IAAkB,EAAEd,GAAqB;QAEfA,YActCqC;IAfxB,IAAIrC,IAAIxB,MAAM,KAAK,OAAO,OAAO;QAAEkB,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAAqB;IAAE;IACtF,MAAM,EAAEhC,MAAM,EAAEgE,OAAO,EAAEC,KAAK,EAAE6B,QAAQ,EAAE,GAAGH,iBAAiBtD,aAAAA,IAAI6D,KAAK,YAAT7D,aAAa,CAAC;IAC5E,IAAI,CAACyD,UAAU,OAAO;QAAE/D,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAA2B;IAAE;IACjF,MAAMmE,UAAU,MAAMjD,iBAAiBC,MAAMd,KAAK,CAAC,oBAAoB,EAAErC,QAAQ,EAAET;IACnF,IAAI4G,SAAS,OAAOA;IACpB,MAAMC,SAAS,MAAMrC,gBAAgBZ,MAAMnD,QAAQgE,SAASC,OAAO;QAAEG,UAAU;IAAK;IACpF,IAAI,aAAagC,QAAQ;QACvB,gEAAgE;QAChE,IAAIA,OAAO9B,OAAO,CAACvC,MAAM,KAAK,KAAK;YACjC,MAAM,EAAEqD,SAAS,EAAEjD,IAAI,EAAE,GAAGiE,OAAO9B,OAAO,CAACpD,IAAI;YAC/C,OAAO;gBAAEa,QAAQ;gBAAKb,MAAM;oBAAEsE,OAAO;oBAAYrD;oBAAMiD;gBAAU;YAAE;QACrE;QACA,OAAOgB,OAAO9B,OAAO;IACvB;IACA,MAAM,EAAEa,IAAI,EAAEH,KAAK,EAAEI,SAAS,EAAEV,KAAK,EAAEvC,IAAI,EAAE,GAAGiE,OAAOV,MAAM;IAC7D,MAAMW,WAAWvD,QAAO4B,aAAAA,MAAMI,GAAG,CAAC,uBAAVJ,aAAyB;IACjD,MAAM4B,aAAatB,MAAMjD,MAAM,KAAK,YAAY,OAAO3C,eAAe+F,MAAMnF,QAAQgE;IACpF,MAAMuC,OACJpE,SAAS,UACLtD,cAAc2H,kBAAkB,CAAC;QAAExB;QAAOyB,SAASzC;QAASoB;QAAWiB;QAAUC;IAAW,KAC5FzH,cAAc6H,oBAAoB,CAAC;QAAE1B;QAAOyB,SAASzC;QAASoB;QAAWiB;QAAUC;IAAW;IACpG,OAAO;QAAEvE,QAAQ;QAAKb,MAAMqF;IAA2C;AACzE;AAEA,sGAAsG,GACtG,OAAO,SAASI,wBAAwBpG,KAUvC;QAGgByE,cAsBD4B,yBA6BQC;QAAgCC;IArDtD,MAAM,EAAEF,MAAM,EAAE5B,KAAK,EAAE,GAAGzE;IAC1B,MAAMsG,UAAUhI,cAAcqG,gBAAgB,CAACF,WAAW;IAC1D,MAAM8B,SAAS9B,eAAAA,MAAM8B,KAAK,YAAX9B,eAAe,CAAC;IAC/B,MAAMvE,SAAS,IAAIsG,gBAAgB;QACjCC,MAAM;OACHrI,wCAAwC,YACvCiI,OAAOK,YAAY,GAAG;QAAE,0BAA0B;IAAO,IAAI,CAAC;IAEpEL,OAAOM,KAAK,CAACC,OAAO,CAAC,CAACC,MAAMC;QAC1B,MAAMC,KAAK,CAAC,WAAW,EAAED,MAAM,CAAC,CAAC;QACjC5G,OAAO8G,GAAG,CAAC,GAAGD,GAAG,UAAU,CAAC,EAAE;QAC9B7G,OAAO8G,GAAG,CAAC,GAAGD,GAAG,sBAAsB,CAAC,EAAE;QAC1C7G,OAAO8G,GAAG,CAAC,GAAGD,GAAG,yBAAyB,CAAC,EAAExE,OAAOsE,KAAKI,WAAW;QACpE/G,OAAO8G,GAAG,CAAC,GAAGD,GAAG,gCAAgC,CAAC,EAAEF,KAAKzE,IAAI,CAACkD,KAAK,CAAC,GAAG;QACvE,IAAIuB,KAAKK,WAAW,EAAEhH,OAAO8G,GAAG,CAAC,GAAGD,GAAG,uCAAuC,CAAC,EAAEF,KAAKK,WAAW;QACjG,2FAA2F;QAC3F,IAAI,CAACL,KAAKM,OAAO,EAAEjH,OAAO8G,GAAG,CAAC,GAAGD,GAAG,oCAAoC,CAAC,EAAE;QAC3E,IAAIF,KAAKM,OAAO,IAAInH,MAAMD,SAAS,EAAEG,OAAO8G,GAAG,CAAC,GAAGD,GAAG,cAAc,CAAC,EAAE/G,MAAMD,SAAS;IACxF;IACA,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,gDAAgD;IAChD,MAAMqH,SAAQf,0BAAAA,OAAOgB,eAAe,YAAtBhB,0BAA0B,EAAE;IAC1C,IAAIe,MAAME,MAAM,GAAG,GAAG;YAGlBjB;QAFF/H,cAAciJ,qCAAqC,CACjDrH,QACAmG,EAAAA,4BAAAA,OAAOmB,iBAAiB,qBAAxBnB,0BAA0BiB,MAAM,IAAGjB,OAAOmB,iBAAiB,GAAGlJ,cAAcmJ,2BAA2B;QAEzGnJ,cAAcoJ,4BAA4B,CAACxH,QAAQkH;IACrD,OAAO,IAAIf,OAAOsB,aAAa,GAAG,GAAG;;YACZpB;QAAvB,MAAMqB,UAAUrF,iBAAOgE,gBAAAA,MAAMsB,MAAM,qBAAZtB,cAAcqB,OAAO,oBAAI,IAAIE,WAAW;QAC/DxJ,cAAciJ,qCAAqC,CACjDrH,QACA,aAAasF,IAAI,CAACoC,WAAW;YAACA;SAAQ,GAAGtJ,cAAcmJ,2BAA2B;QAEpFnJ,cAAcoJ,4BAA4B,CAACxH,QAAQ;YACjD;gBAAE6H,QAAQ;gBAAS3F,MAAM;gBAAY6E,aAAaZ,OAAOsB,aAAa;YAAC;SACxE;IACH;IACA,MAAMK,qBAAqBZ,MAAME,MAAM,GAAG,IAAIhJ,cAAc0J,kBAAkB,CAACZ,SAASf,OAAOsB,aAAa;IAC5G,KAAK,MAAM,CAACrI,KAAK+C,MAAM,IAAI4F,OAAOC,OAAO,CACvC5J,cAAc6J,uBAAuB,CAAC;QACpCC,WAAWpI,MAAMoI,SAAS;QAC1BC,UAAUrI,MAAMqI,QAAQ;QACxBC,UAAUjC,OAAOK,YAAY,GAAG,aAAa;QAC7C6B,oBAAoBlC,OAAOmC,UAAU;QACrCR;IACF,IACC;QACD9H,OAAO8G,GAAG,CAAC1H,KAAK+C;IAClB;IACA,MAAMoG,QAAQlG,QAAQ+D,OAAAA,UAAU7B,MAAMiE,aAAa,IAAGnC,kBAAAA,MAAMoC,QAAQ,qBAAdpC,gBAAgBkC,KAAK,YAArDnC,OAA0D,IAAI5D,IAAI;IACxF,IAAI+F,OAAOvI,OAAO8G,GAAG,CAAC,kBAAkByB;IACxC,MAAMG,YAAY5I,MAAM6I,SAAS,CAACC,QAAQ,CAAC,OAAO,MAAM;IACxD5I,OAAO8G,GAAG,CAAC,eAAe,GAAGhH,MAAM6I,SAAS,GAAGD,UAAU,gCAAgC,CAAC;IAC1F1I,OAAO8G,GAAG,CAAC,cAAchH,MAAM6I,SAAS;IACxC,yEAAyE;IACzE,2EAA2E;IAC3E,sCAAsC;IACtC3I,OAAO8G,GAAG,CAAC,kBAAkB;IAC7B9G,OAAO8G,GAAG,CAAC,oBAAoBhH,MAAMP,MAAM;IAC3CS,OAAO8G,GAAG,CAAC,qBAAqBhH,MAAMyD,OAAO;IAC7C,IAAI6C,SAAS;QACX,kEAAkE;QAClE,yEAAyE;QACzE,yEAAyE;QACzE,kBAAkB;QAClBpG,OAAO8G,GAAG,CAAC,4BAA4BzE,OAAOjE,cAAcyG,mBAAmB,CAACN;QAChF,IAAIA,MAAMsE,UAAU,EAAE7I,OAAO8G,GAAG,CAAC,wBAAwBzE,OAAOkC,MAAMsE,UAAU;IAClF,OAAO;QACL7I,OAAO8G,GAAG,CAAC,qBAAqBhH,MAAMyD,OAAO;QAC7CvD,OAAO8G,GAAG,CAAC,8BAA8BzE,OAAOjE,cAAc0K,gBAAgB,CAACzC;QAC/ErG,OAAO8G,GAAG,CAAC,8BAA8BzE,OAAO0G,OAAO1C,MAAM2C,WAAW,KAAK;IAC/E;IACAhJ,OAAO8G,GAAG,CAAC,sBAAsBzE,OAAOvC,MAAMqI,QAAQ;IACtD,yEAAyE;IACzE,oDAAoD;IACpD,IAAIhC,OAAO8C,QAAQ,GAAG,KAAK9C,OAAO+C,MAAM,KAAK,MAAM;QACjDlJ,OAAO8G,GAAG,CAAC,sBAAsBzE,OAAO8D,OAAO8C,QAAQ;IACzD;IACA,IAAI9C,OAAOK,YAAY,EAAE;QACvBxG,OAAO8G,GAAG,CACR,2BACAzE,OACEjE,cAAc+K,2BAA2B,CAAC;YACxChB,UAAUrI,MAAMqI,QAAQ;YACxBE,oBAAoBlC,OAAOmC,UAAU;YACrCR;QACF;QAGJ9H,OAAO8G,GAAG,CAAC,mCAAmCzE,OAAOyF;IACvD;IACA1J,cAAcgL,mCAAmC,CAACpJ,QAAQF,MAAMuJ,cAAc;IAC9E,OAAOrJ;AACT;AAEA,MAAMsJ,wBAAwB;AAE9B,MAAMC,UAAU,CAACjI,QAAgBkI,QAAgBjI,OAAekI,QAAiC,CAAC,CAAC,GAAwB,CAAA;QACzHnI;QACAb,MAAM;YAAEc;YAAOiI;WAAWC;IAC5B,CAAA;AAEA,4EAA4E,GAC5E,OAAO,eAAeC,eAAehH,IAAkB,EAAEd,GAAqB;QA2CvDqC,kBAyBHM;QAjD4DA,cAY7DoF,YAsCWpF;IApE5B,IAAI3C,IAAIxB,MAAM,KAAK,QAAQ,OAAO;QAAEkB,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAAqB;IAAE;IACvF,IAAI,CAACjB,QAAQC,GAAG,CAACC,iBAAiB,EAAE,OAAO;QAAEc,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAA+B;IAAE;IAC1G,MAAM,EAAEhC,MAAM,EAAEgE,OAAO,EAAEC,KAAK,EAAE6B,QAAQ,EAAE,GAAGH,gBAAgBvD,OAAOC;IACpE,IAAI,CAACyD,UAAU,OAAO;QAAE/D,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAA2B;IAAE;IACjF,MAAMqI,iBAAiB3H,SAASL,KAAK,mBAAmBwD,KAAK,CAAC,GAAG;IACjE,IAAI,CAACwE,gBAAgB,OAAO;QAAEtI,QAAQ;QAAKb,MAAM;YAAEc,OAAO;QAA0C;IAAE;IACtG,MAAMmE,UAAU,MAAMjD,iBAAiBC,MAAMd,KAAK,CAAC,mBAAmB,EAAErC,QAAQ,EAAER;IAClF,IAAI2G,SAAS,OAAOA;IACpB,MAAMC,SAAS,MAAMrC,gBAAgBZ,MAAMnD,QAAQgE,SAASC,OAAO;QAAEG,UAAU;IAAK;IACpF,IAAI,aAAagC,QAAQ,OAAOA,OAAO9B,OAAO;IAC9C,MAAM,EAAEC,OAAO,EAAEY,IAAI,EAAEH,KAAK,EAAEI,SAAS,EAAEV,KAAK,EAAEvC,IAAI,EAAE,GAAGiE,OAAOV,MAAM;IACtE,MAAM4E,OAAOpI,OAAOC;IAEpB,yEAAyE;IACzE,yEAAyE;IACzE,8EAA8E;IAC9E,8CAA8C;IAC9C,MAAMoI,OAAOf,OAAOpH,OAAOC,IAAI,CAAC,IAAI;IACpC,IAAIF,SAAS,WAAWqH,OAAOgB,QAAQ,CAACD,SAASA,OAAO,KAAKA,SAAUf,CAAAA,QAAOxE,eAAAA,MAAM8B,KAAK,qBAAX9B,aAAayE,WAAW,KAAK,CAAA,GAAI;QAC7G,OAAOO,QAAQ,KAAK,WAAWD;IACjC;IACA,MAAMU,UAAU5L,cAAc6L,iBAAiB,CAAC1F,OAAOhB;IACvD,IAAIyG,SAAS,OAAOT,QAAQ,KAAKS,SAAS5L,cAAc8L,wBAAwB,CAACF,SAASrF,WAAWjD;IACrG,MAAMiI,QAAQ,MAAMjH,KAAKpD,UAAU,CAACC;IACpC,MAAM4K,cAAcZ,QAClB,KACA,wBACA,GAAG5E,aAAa,gBAAgB,gEAAgE,EAAEkF,KAAK,CAAC,CAAC;IAE3G,IAAI,CAACtM,iBAAiBoM,yBAAAA,MAAOhK,GAAG,EAAW,aAAa,OAAOwK;IAC/D,MAAMC,WAAWT,0BAAAA,aAAAA,MAAOhK,GAAG,qBAAVgK,WAAYS,QAAQ;IACrC,MAAMC,UAAUD,WAAW,MAAM1H,KAAKxD,SAAS,GAAG6E,UAAU,CAAC,YAAYC,GAAG,CAAC3B,OAAO+H,WAAW/F,GAAG,KAAK;IACvG,MAAM6D,YAAYmC,2BAAAA,QAAShG,GAAG,CAAC;IAC/B,MAAMiG,QAAQrM,uBACZ;QACEiK;QACAqC,cAAc,EAAEF,2BAAAA,QAAShG,GAAG,CAAC;QAC7BmG,eAAe,EAAEH,2BAAAA,QAAShG,GAAG,CAAC;IAChC,GACA;QAAEoG,SAAS,GAAGZ,KAAK,CAAC,EAAEtG,QAAQ,SAAS,EAAEhE,QAAQ;IAAC;IAEpD,IAAI,CAAC+K,OAAO,OAAOH;IACnB,MAAMO,eAAezG,aAAAA,MAAMI,GAAG,CAAC,kBAAVJ,aAAoB,CAAC;IAC1C,IAAI7F,cAAcuM,qBAAqB,CAAC;QAAEC,UAAUF;IAAY,GAAGhJ,IAAI,KAAK,aAAa;QACvF,OAAO6H,QAAQ,KAAK,iBAAiBnL,cAAc8L,wBAAwB,CAAC,iBAAiBvF,WAAWjD;IAC1G;IAEA,MAAMyE,SAAS/H,cAAcyM,qBAAqB,CAACtG,OAAOhB;IAC1D,0EAA0E;IAC1E,gCAAgC;IAChC,MAAMuH,mBAAmB3E,OAAOgB,eAAe,CAAC4D,MAAM,CAAC,CAACC,MAAMjI,OAASG,KAAKC,GAAG,CAAC6H,MAAMjI,KAAKgE,WAAW,GAAG;IACzG,MAAMoB,WAAWzK,2BACfiM,yBAAAA,MAAOhK,GAAG,EACVwG,OAAO8E,cAAc,EACrB9E,OAAOmC,UAAU,EACjBnC,OAAO+E,UAAU,GAAGJ;IAEtB,MAAMlL,aAAa,MAAM8C,KAAK9C,UAAU,CAACyC,eAAOsH,yBAAAA,MAAOlK,EAAE,mBAAI;IAC7D,MAAM0L,OAAOzG;IACb,MAAM0G,SAASvN,iBAAiB;QAC9BwN,YAAY;YAACpJ,SAASL,KAAK;YAAY,CAAC,QAAQ,EAAEK,SAASL,KAAK,SAAS;SAAC;QAC1EuJ;QACAG,aAAarJ,SAASL,KAAK;IAC7B;IACA,MAAM+G,YAAY,GAAGyC,SAASjN,cAAcoF,SAASC,QAAQ;IAC7D,uEAAuE;IACvE,qEAAqE;IACrE,MAAM+H,aAAYhH,2BAAAA,MAAMiH,iBAAiB,YAAvBjH,2BAA2B;IAC7C,MAAMkH,eAAe1C,QAAOxE,gBAAAA,MAAM8B,KAAK,qBAAX9B,cAAayE,WAAW,KAAK;IACzD,MAAM0C,kBAAkBtN,cAAcyG,mBAAmB,CAACN;IAE1D,MAAMoH,UAAU,MAAMnO,aAAakF,KAAKxD,SAAS,IAAI;QACnDwC,MAAM;QACNkK,SAAS,GAAGrM,OAAO,CAAC,EAAEgE,SAAS;QAC/B7D,OAAO2C,gBAAOsH,yBAAAA,MAAOlK,EAAE,oBAAI;QAC3BL,KAAKwK;QACLiC,aAAa;IACf;IACA,IAAI,YAAYF,SAAS;QACvB,OAAO;YAAErK,QAAQqK,QAAQG,MAAM,CAACxK,MAAM;YAAEb,MAAMkL,QAAQG,MAAM,CAACrL,IAAI;QAA4B;IAC/F;IACA,MAAMsL,QAAQJ,QAAQI,KAAK;IAC3B,MAAMC,YAAY,CAACC;QACjB,MAAMC,UAAUzO,sBAAsBsO,MAAMC,SAAS,EAAEC;QACvD,OAAOC,UAAU;YAAE,mBAAmBA;QAAQ,IAAI,CAAC;IACrD;IACA,IAAIC,SAAS;IACb,IAAI;QACF,wEAAwE;QACxE,yEAAyE;QACzE,8DAA8D;QAC9D,IAAIZ,WAAW;YACb,MAAMa,WAAW,MAAM1J,KAAK3B,aAAa,CAACwK;YAC1C,IAAIa,aAAa,YAAY;gBAC3B,MAAML,MAAMM,OAAO;gBACnB,OAAO9C,QAAQ,KAAK,QAAQ,CAAC,KAAK,EAAEM,KAAK,gDAAgD,CAAC,EAAE;oBAAEyC,YAAY;gBAAK;YACjH;YACA,IAAIF,aAAa,QAAQ;gBACvB,MAAML,MAAMM,OAAO;gBACnB,OAAO;oBAAE/K,QAAQ;oBAAKb,MAAM;wBAAEc,OAAO;oBAA+C;gBAAE;YACxF;QACF;QAEA,IAAI1B,YAAY;QAChB,IAAIsG,OAAO+C,MAAM,KAAK,QAAQ/C,OAAOoG,YAAY,GAAG,GAAG;gBAGlD;gBAFWhI;YAAd,MAAMiI,SAAQjI,gBAAAA,MAAM8B,KAAK,qBAAX9B,cAAakI,GAAG;YAC9B5M,aACG,QAAA,MAAM6C,KAAK7C,SAAS,CAAC;gBACpBiE;gBACAoF,QAAQ/C,OAAO+C,MAAM;gBACrBwD,UACEF,CAAAA,yBAAAA,MAAO9K,IAAI,MAAK,SACZ8K,MAAMG,KAAK,IAAI,CAAC,KAAK,EAAExG,OAAO+C,MAAM,CAAC,EAAE,CAAC,GACxC;gBACNjJ,SAAS+L,UAAU,CAAC,SAAS,EAAE9I,KAAK0J,KAAK,CAACzG,OAAO+C,MAAM,GAAG,QAAS;YACrE,cARC,QAQM;YACT,IAAI,CAACrJ,WAAW;gBACd,8CAA8C;gBAC9C,MAAMkM,MAAMM,OAAO;gBACnB,OAAO;oBAAE/K,QAAQ;oBAAKb,MAAM;wBAAEc,OAAO;oBAA+C;gBAAE;YACxF;QACF;QAEA,MAAM8H,iBAAiBjL,cAAcyO,sCAAsC,CACzEzO,cAAc0O,wCAAwC,CAAC7I,MAAMI,GAAG,CAAC;QAEnE,MAAMrE,SAASkG,wBAAwB;YACrC3G;YACAgE;YACAgB;YACA4B;YACA+B,WAAW7F,OAAO6F;YAClBC;YACAtI;YACA8I;YACAU;QACF;QACA,IAAIzJ,WAAWmN,MAAM,EAAE;gBACa/M;YAAlCzB,0BAA0ByB,SAAQA,cAAAA,OAAOqE,GAAG,CAAC,0BAAXrE,cAA6B2I;QACjE;QACA,MAAMqE,SAASpN,WAAWmN,MAAM,GAC5BrK,KAAKzB,YAAY,CAAC;YAAEgM,SAAStE;YAAWwC;YAAM5L;QAAO,KACrD4E,QAAQ+I,OAAO,CAAC;QACpB,MAAMC,UAAU,MAAMzK,KAAK3C,aAAa,CAACC,QAAQ,aAC5CgM,UAAU,YACVxN,4BAA4BoB;QAEjC,MAAMoN;QACN,MAAMI,UAAUD,QAAQxM,EAAE,GACtBlC,2BAA2B0O,QAAQvM,OAAO,EAAEhB,YAAYyJ,eAAegE,OAAO,IAC9E;QACJ,IAAI,CAACD,WAAW,CAACD,QAAQvM,OAAO,CAACnB,EAAE,EAAE;gBACU0N;YAA7CG,QAAQ/L,KAAK,CAAC,gCAA+B4L,mBAAAA,QAAQvM,OAAO,qBAAfuM,iBAAiB5L,KAAK;YACnE,MAAMwK,MAAMM,OAAO;YACnB,OAAO;gBAAE/K,QAAQ;gBAAKb,MAAM;oBAAEc,OAAO;gBAA+C;YAAE;QACxF;QACA4K,SAAS9J,OAAO8K,QAAQvM,OAAO,CAACnB,EAAE;QAElC,sEAAsE;QACtE,sEAAsE;QACtE,sEAAsE;QACtE,oBAAoB;QACpB,MAAM8N,WAAWzJ,QAAQC,UAAU,CAAC,UAAUC,GAAG,CAACT;QAClD,MAAMiK,UAAU,MAAM9K,KAAKxD,SAAS,GAAGuO,cAAc,CAAC,OAAOC;gBAwB3B9I;YAvBhC,MAAM+I,WAAW,MAAMD,YAAYrJ,GAAG,CAACkJ;YACvC,MAAM3I,UAAU+I,SAASrJ,MAAM,GAAIqJ,SAASnJ,IAAI,KAAiC;YACjF,IAAI,CAACI,WAAWxG,cAAcqG,gBAAgB,CAACG,aAAalD,MAAM,OAAO;gBAAEmC,SAASrC;gBAAeoM,WAAW;YAAG;YACjH,IAAIhJ,QAAQtD,MAAM,KAAK,WAAW;gBAChC,MAAMuM,SAASjJ,QAAQtD,MAAM,KAAK,eAAesD,QAAQtD,MAAM,KAAK;gBACpE,OAAO;oBACLuC,SAAS0F,QACP,KACAsE,SAAS,WAAW,QACpBzP,cAAc8L,wBAAwB,CAAC2D,SAAS,WAAW,QAAQlJ,WAAWjD;oBAEhFkM,WAAW;gBACb;YACF;YACA,IAAIxP,cAAcyG,mBAAmB,CAACD,aAAa8G,iBAAiB;gBAClE,OAAO;oBACL7H,SAAS;wBACPvC,QAAQ;wBACRb,MAAM;4BAAEuE,UAAU;4BAAMzD,OAAO,CAAC,KAAK,EAAEsI,KAAK,kBAAkB,EAAElF,aAAa,eAAe,uBAAuB,CAAC;wBAAC;oBACvH;oBACAiJ,WAAW;gBACb;YACF;YACA,IAAIlM,SAAS,WAAW,AAACqH,CAAAA,QAAOnE,iBAAAA,QAAQyB,KAAK,qBAAbzB,eAAeoE,WAAW,KAAK,CAAA,MAAOyC,cAAc;gBAClF,OAAO;oBAAE5H,SAAS0F,QAAQ,KAAK,WAAWD;oBAAwBsE,WAAW;gBAAG;YAClF;YACAF,YAAY5G,GAAG,CAACyG,UAAU;gBAAE/B,mBAAmBW;YAAO,GAAG;gBAAE2B,OAAO;YAAK;YACvE,MAAMF,YAAYhJ,QAAQ4G,iBAAiB,IAAI5G,QAAQ4G,iBAAiB,KAAKD,YAAY3G,QAAQ4G,iBAAiB,GAAG;YACrH,OAAO;gBAAE3H,SAAS;gBAAM+J;YAAU;QACpC;QACA,IAAIJ,QAAQ3J,OAAO,EAAE;YACnB,MAAMnB,KAAK3B,aAAa,CAACoL;YACzB,MAAMJ,MAAMM,OAAO;YACnB,OAAOmB,QAAQ3J,OAAO;QACxB;QACA,IAAI2J,QAAQI,SAAS,IAAIJ,QAAQI,SAAS,KAAKzB,QAAQ,MAAMzJ,KAAK3B,aAAa,CAACyM,QAAQI,SAAS;QACjG,MAAM7B,MAAMgC,MAAM,CAAC,KAAKX;QACxB,OAAO;YAAE9L,QAAQ;YAAKb,MAAM2M;QAA8C;IAC5E,EAAE,OAAO7L,OAAO;QACd+L,QAAQ/L,KAAK,CAAC,oBAAoBA;QAClC,IAAI4K,QAAQ,MAAMzJ,KAAK3B,aAAa,CAACoL,QAAQrL,KAAK,CAAC,IAAM;QACzD,MAAMiL,MAAMM,OAAO;QACnB,OAAO;YAAE/K,QAAQ;YAAKb,MAAM;gBAAEc,OAAO;YAA+C;QAAE;IACxF;AACF;AAEA,0DAA0D,GAC1D,OAAO,SAASyM,qBAAqBC,GAAoC,EAAEpK,OAAyB;QAGvDA;IAF3CoK,IAAIC,SAAS,CAAC,iBAAiB;IAC/BD,IAAIC,SAAS,CAAC,gBAAgB;IAC9B,KAAK,MAAM,CAAChM,MAAMC,MAAM,IAAI4F,OAAOC,OAAO,EAACnE,mBAAAA,QAAQ5D,OAAO,YAAf4D,mBAAmB,CAAC,GAAIoK,IAAIC,SAAS,CAAChM,MAAMC;IACvF,OAAO8L,IAAI3M,MAAM,CAACuC,QAAQvC,MAAM,EAAET,IAAI,CAACgD,QAAQpD,IAAI;AACrD;AAEA,oCAAoC,GACpC,OAAO,MAAM0N,mBAAqC,OAAOvM,KAAKqM;IAC5D,IAAI;QACF,OAAOD,qBAAqBC,KAAK,MAAMzI,gBAAgBvG,uBAAuB2C;IAChF,EAAE,OAAOL,OAAO;QACd+L,QAAQ/L,KAAK,CAAC,qBAAqBA;QACnC,OAAOyM,qBAAqBC,KAAK;YAAE3M,QAAQ;YAAKb,MAAM;gBAAEc,OAAO;YAA6C;QAAE;IAChH;AACF,EAAC;AAED,oCAAoC,GACpC,OAAO,MAAM6M,kBAAoC,OAAOxM,KAAKqM,MAC3DD,qBAAqBC,KAAK,MAAMvE,eAAezK,uBAAuB2C,MAAK"}
@@ -37,7 +37,15 @@ export type QuoteProofOutcome = QuoteLinkOutcome & {
37
37
  contentType: string;
38
38
  };
39
39
  };
40
- /** `GET /api/commerce/quote-proof`: one of the current revision's proofs. */
40
+ /**
41
+ * `GET /api/commerce/quote-proof`: one of the proofs in the quote's history
42
+ * (AGL-4097). An email is a record the customer keeps, so the proof it draws
43
+ * keeps showing after the business starts the next revision or replaces the
44
+ * link: any link signed for this quote, an older version's too, is served the
45
+ * current revision's proofs and every earlier revision's as its snapshot kept
46
+ * them. A proof taken off its revision, another quote's file and a wrong token
47
+ * are not found.
48
+ */
41
49
  export declare function handleQuoteProof(deps: QuoteProofDeps, req: PluginApiRequest): Promise<QuoteProofOutcome>;
42
50
  /** `POST /api/commerce/quote-upload`: a ticket for one of the customer's files, or its finalize. */
43
51
  export declare function handleQuoteUpload(deps: QuoteProofDeps, req: PluginApiRequest): Promise<QuoteLinkOutcome>;
@@ -16,19 +16,20 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { claimAttempt } from "@aglyn/aglyn/server";
18
18
  import { readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
19
- import { VISITOR_UPLOAD_HONEYPOT_FIELD, visitorUploadFileView } from "@aglyn/aglyn/app-utils/visitor-upload";
19
+ import { VISITOR_UPLOAD_HONEYPOT_FIELD, visitorUploadFileView, visitorUploadFolder } from "@aglyn/aglyn/app-utils/visitor-upload";
20
20
  import { notifyHostManagers } from "@aglyn/tenant-data-admin";
21
21
  import * as CommerceModel from "../model/index.js";
22
22
  import { orderStatusUrl } from "./order-status-token.js";
23
23
  import { defaultQuotePayDeps, limitedQuoteLink, loadLinkedQuote, quoteLinkParams, sendQuoteLinkOutcome } from "./quote-pay.js";
24
+ import { keepSentQuoteUploads, quoteUploadKeyRef, recordPendingQuoteUpload } from "./quote-upload-keys.js";
24
25
  import { quoteUploadCalls } from "./quote-uploads.js";
25
26
  /*==========================================
26
27
  * THE CUSTOMER'S SIDE OF A PROOF (AGL-4096), on the business's site, behind
27
28
  * the quote's signed pay link like the page itself:
28
29
  *
29
30
  * 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
31
+ * → a proof from any revision, for a link to any version of this quote
32
+ * (a picture inline, anything else as a download) | 404 | 429
32
33
  * POST /api/commerce/quote-upload { hostId, q, t, action: 'ticket', file: { name, contentType, size } }
33
34
  * → { ticket, upload: { url, fields }, file }
34
35
  * POST /api/commerce/quote-upload { hostId, q, t, action: 'finalize', ticket }
@@ -38,14 +39,19 @@ import { quoteUploadCalls } from "./quote-uploads.js";
38
39
  * + Idempotency-Key → 200 { view } | 409 { error, reason } | 410 { replaced } | 404 | 400 | 429
39
40
  *
40
41
  * 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
42
+ * checked, answers a wrong token exactly as a missing quote, and (but for a
43
+ * proof, which an earlier email still draws) answers a link to an older
44
+ * version as replaced. The uploads are the shared visitor
43
45
  * upload (`@aglyn/tenant-data-admin/server/visitor-upload`), scoped to this
44
46
  * quote's artwork; the answer names the files by id and the server reads
45
47
  * them back from that scope, so nothing else can be attached.
46
48
  *
47
49
  * `v` is the edit of the quote the page showed, as Pay sends it: an answer to
48
50
  * proofs the customer has not seen is refused and the page is shown again.
51
+ *
52
+ * A file goes up before the answer does, so each one is recorded as pending
53
+ * under its quote from its ticket on, and the answer that sends it takes it
54
+ * off; one never sent is deleted a day later (`quote-upload-keys.ts`).
49
55
  *=========================================*/ /** 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
56
  /** 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
57
  /** Answers one address may send per site, per ten minutes. */ export const QUOTE_RESPONSES_PER_WINDOW = 20;
@@ -86,7 +92,21 @@ const reasonRefusal = (status, reason, error)=>({
86
92
  reason
87
93
  }
88
94
  });
89
- /** `GET /api/commerce/quote-proof`: one of the current revision's proofs. */ export async function handleQuoteProof(deps, req) {
95
+ const PROOF_GONE = {
96
+ status: 404,
97
+ body: {
98
+ error: 'That proof is no longer on this quote.'
99
+ }
100
+ };
101
+ /**
102
+ * `GET /api/commerce/quote-proof`: one of the proofs in the quote's history
103
+ * (AGL-4097). An email is a record the customer keeps, so the proof it draws
104
+ * keeps showing after the business starts the next revision or replaces the
105
+ * link: any link signed for this quote, an older version's too, is served the
106
+ * current revision's proofs and every earlier revision's as its snapshot kept
107
+ * them. A proof taken off its revision, another quote's file and a wrong token
108
+ * are not found.
109
+ */ export async function handleQuoteProof(deps, req) {
90
110
  var _req_query, _query_file;
91
111
  if (req.method !== 'GET') return {
92
112
  status: 405,
@@ -105,15 +125,25 @@ const reasonRefusal = (status, reason, error)=>({
105
125
  };
106
126
  const limited = await limitedQuoteLink(deps, req, `commerce-quote-proof:${hostId}`, QUOTE_PROOF_LOADS_PER_WINDOW);
107
127
  if (limited) return limited;
108
- const linked = await loadLinkedQuote(deps, hostId, quoteId, token);
128
+ const linked = await loadLinkedQuote(deps, hostId, quoteId, token, {
129
+ history: true
130
+ });
109
131
  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
- };
132
+ const quote = linked.loaded.order.quote;
133
+ // The earlier revisions are read only when the current one lacks the proof.
134
+ let proof = CommerceModel.quoteProofInHistory(quote, [], fileId);
135
+ if (!proof) {
136
+ const listed = await linked.loaded.hostRef.collection('orders').doc(quoteId).collection('revisions').get();
137
+ const snapshots = listed.docs.map((entry)=>entry.data());
138
+ proof = CommerceModel.quoteProofInHistory(quote, snapshots, fileId);
139
+ }
140
+ // A proof is kept under this quote's own folder; a path anywhere else is never served.
141
+ const folder = visitorUploadFolder({
142
+ hostId,
143
+ purpose: CommerceModel.QUOTE_PROOF_PURPOSE,
144
+ purposeId: quoteId
145
+ });
146
+ if (!(proof == null ? void 0 : proof.path) || !proof.path.startsWith(folder)) return PROOF_GONE;
117
147
  return {
118
148
  status: 200,
119
149
  body: {},
@@ -184,6 +214,13 @@ const reasonRefusal = (status, reason, error)=>({
184
214
  }
185
215
  } : {});
186
216
  }
217
+ // Pending before a byte can arrive, so a file kept and never sent is always swept.
218
+ await recordPendingQuoteUpload(deps.firestore(), {
219
+ hostId,
220
+ quoteId,
221
+ fileId: minted.file.id,
222
+ nowMs: deps.now()
223
+ });
187
224
  return {
188
225
  status: 200,
189
226
  body: {
@@ -206,6 +243,13 @@ const reasonRefusal = (status, reason, error)=>({
206
243
  code: kept.code
207
244
  }
208
245
  };
246
+ // Its day starts again from the latest file.
247
+ await recordPendingQuoteUpload(deps.firestore(), {
248
+ hostId,
249
+ quoteId,
250
+ fileId: kept.file.id,
251
+ nowMs: deps.now()
252
+ });
209
253
  return {
210
254
  status: 200,
211
255
  body: {
@@ -221,9 +265,19 @@ const reasonRefusal = (status, reason, error)=>({
221
265
  };
222
266
  }
223
267
  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;
268
+ /**
269
+ * The managers' notice of an answer (AGL-4096): who, which quote and
270
+ * revision, what they said, and what to do next.
271
+ *
272
+ * It rides `content.order`, so whoever is told about orders (in the feed, by
273
+ * email, by push) is told about the quotes that become them, and it names
274
+ * itself as what it is rather than as that type: "Proof approved" or
275
+ * "Changes requested" where the feed and the email's eyebrow would say "New
276
+ * order", the customer and the quote as its heading, and "View quote" on the
277
+ * email's button, which opens the quote's dialog on the Orders page.
278
+ */ function noticeOf(input) {
279
+ var _response_files, _ref, _response_name, _response_comment, _ref1;
280
+ var _quote_customer, _quote_customer1, _quote_customer2, _order_totals, _quote_tax, _quote_customer3, _quote_customer4, _quote_customer5, _quote_customer6;
227
281
  const { hostId, quoteId, order, response } = input;
228
282
  const quote = order.quote;
229
283
  const number = CommerceModel.formatQuoteNumber(order, quoteId);
@@ -231,7 +285,9 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
231
285
  const revision = response.revision;
232
286
  const files = (_response_files = response.files) != null ? _response_files : [];
233
287
  const approving = response.action === 'approve';
234
- const payNote = quote.requireProofApproval === true ? ' They can pay the quote now.' : '';
288
+ const money = CommerceModel.formatOrderMoney(Number((_ref = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref : 0), input.currency);
289
+ const total = ((_quote_tax = quote.tax) == null ? void 0 : _quote_tax.kind) === 'at-payment' ? `${money} before tax` : money;
290
+ const filesSent = `${files.length} file${files.length === 1 ? '' : 's'}`;
235
291
  const details = [
236
292
  {
237
293
  label: 'Quote',
@@ -249,6 +305,10 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
249
305
  label: 'Revision',
250
306
  value: String(revision)
251
307
  },
308
+ {
309
+ label: 'Total',
310
+ value: total
311
+ },
252
312
  ...approving ? [
253
313
  {
254
314
  label: 'Approved as',
@@ -267,12 +327,16 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
267
327
  ] : []
268
328
  ]
269
329
  ];
270
- const email = String((_ref = (_quote_customer6 = quote.customer) == null ? void 0 : _quote_customer6.email) != null ? _ref : '').trim();
330
+ const email = String((_ref1 = (_quote_customer6 = quote.customer) == null ? void 0 : _quote_customer6.email) != null ? _ref1 : '').trim();
271
331
  return {
272
332
  payload: {
273
333
  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.`,
334
+ // Good news for an approval; a change request is work to read.
335
+ level: approving ? 'success' : 'info',
336
+ label: approving ? 'Proof approved' : 'Changes requested',
337
+ actionLabel: 'View quote',
338
+ title: approving ? `${customer} approved the proof for ${number} on {site}` : `${customer} asked for changes to ${number} on {site}`,
339
+ body: approving ? (quote.requireProofApproval === true ? `Revision ${revision} is approved, so they can pay the quote now. It becomes an order when they do.` : `Revision ${revision} is approved. The quote becomes an order when they pay.`) + ' The approval, with the name, time and address it was given from, is on the quote.' : files.length ? `What they want changed on revision ${revision}, and the ${filesSent} they sent, are on the quote. ` + `Read them, then start revision ${revision + 1} with new proofs and send it.` : `What they want changed on revision ${revision} is on the quote. ` + `Read it, then start revision ${revision + 1} with new proofs and send it.`,
276
340
  link: `/${hostId}/products/orders?quote=${encodeURIComponent(quoteId)}`
277
341
  },
278
342
  options: _extends({
@@ -375,10 +439,13 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
375
439
  const claim = claimed.claim;
376
440
  const now = deps.now();
377
441
  try {
378
- var _store_get;
442
+ var _store_get, _store_get1;
379
443
  const ref = hostRef.collection('orders').doc(quoteId);
444
+ const pendingRef = quoteUploadKeyRef(deps.firestore(), hostId, quoteId);
380
445
  const settled = await deps.firestore().runTransaction(async (transaction)=>{
381
446
  const snapshot = await transaction.get(ref);
447
+ // The files this answer sends leave the pending record with it.
448
+ const pending = files.length ? await transaction.get(pendingRef) : null;
382
449
  const current = snapshot.exists ? snapshot.data() : null;
383
450
  if (!current || !CommerceModel.isQuote(current) || !current.quote) {
384
451
  return {
@@ -419,6 +486,7 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
419
486
  };
420
487
  }
421
488
  transaction.set(ref, answered.order);
489
+ if (pending) keepSentQuoteUploads(transaction, pending, pendingRef, files.map((file)=>file.id));
422
490
  return {
423
491
  outcome: {
424
492
  status: 200,
@@ -450,7 +518,8 @@ const UPDATED_MESSAGE = 'This quote was updated since you opened it. Look it ove
450
518
  hostId,
451
519
  quoteId,
452
520
  order: settled.order,
453
- response: settled.response
521
+ response: settled.response,
522
+ currency: String((_store_get1 = store.get('currency')) != null ? _store_get1 : 'USD')
454
523
  });
455
524
  void deps.notify(hostId, notice.payload, notice.options);
456
525
  return outcome;