From f303cc8731363242b6b27051dbbaa58cc8c5c6fa Mon Sep 17 00:00:00 2001 From: Nathan Clevenger <4130910+nathanclevenger@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:43:28 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20/checkout=20deal=20leg=20=E2=80=94=20va?= =?UTF-8?q?riable-amount=20vin=20deal=20settlement,=20priced=20from=20the?= =?UTF-8?q?=20deal=20door's=20OFFER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /checkout?sku=deal&deal=…&link=…&vin=…&door=…&return_to=… prices a Stripe Checkout Session from the deal door's posted OFFER (https://{door}/buy/deals/{deal}/checkout.json?link=…), fetched server-side — the query string never carries a price. The door table is CLOSED (apis.vin); ids are shape-validated; return_to must be https on the offering door (no open redirect). A superseded / expired / settled link refuses — a re-desk changes the numbers, so a stale link never collects them. PAID deal sessions (metadata estate=vin, kind=deal) forward from POST /webhooks to the deal door's own settle leg — POST https://{door}/buy/deals/{deal}/settle, bearer VIN_SETTLE_TOKEN, body { order_id, settlement_ref, amount_total, currency } — where the estate refuses any amount that disagrees with the desked cash-to-close and dedupes by order_id. A failed forward answers Stripe 500 so the event redelivers. The fixed-price extractor now ignores kind=deal sessions, so a deal never double-forwards to /_settle. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 14 + src/checkout.ts | 4 + src/deal-checkout.ts | 312 +++++++++++++++++++++ src/env.d.ts | 7 +- src/index.ts | 67 ++++- test/deal-checkout.test.ts | 549 +++++++++++++++++++++++++++++++++++++ 6 files changed, 950 insertions(+), 3 deletions(-) create mode 100644 src/deal-checkout.ts create mode 100644 test/deal-checkout.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index ddddabe..22d5796 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,20 @@ Session → 303. `POST /webhooks` forwards PAID vin sessions (metadata `settlement_ref`; a configured-but-failed forward answers 500 so Stripe redelivers. +## Vin deal checkout (variable amount) + +`GET /checkout?sku=deal&deal=…&link=…&vin=…&door=…&return_to=…` is the +variable-amount deal leg (`src/deal-checkout.ts`): the amount is fetched +server-side from the deal door's posted OFFER +(`https://{door}/buy/deals/{deal}/checkout.json?link=…` — door from the +CLOSED `VIN_DEAL_DOORS` table, currently `apis.vin`), verified open and +naming the asked payment link, then priced into a Stripe Checkout Session → +303. The query string never carries a price. PAID deal sessions (metadata +`kind: "deal"`) forward to the deal door's own settle leg — +`POST https://{door}/buy/deals/{deal}/settle`, bearer `VIN_SETTLE_TOKEN` — +where the estate refuses any amount that disagrees with the desked +cash-to-close and dedupes by `order_id`. + ## Secrets - `STRIPE_SECRET_KEY` — Stripe API key (required; founder act) diff --git a/src/checkout.ts b/src/checkout.ts index 3cec6f5..16f3761 100644 --- a/src/checkout.ts +++ b/src/checkout.ts @@ -181,6 +181,10 @@ export function vinSettlementFromSession( ): VinSettlement | null { const metadata = (session.metadata ?? null) as Record | null if (metadata?.estate !== 'vin') return null + // Deal sessions (kind=deal) settle on the deal door's own settle leg + // (src/deal-checkout.ts) — never on the fixed-price /_settle forward, whose + // closed SKU table would refuse them. + if (metadata.kind === 'deal') return null if (session.payment_status !== 'paid') return null const pi = session.payment_intent const settlementRef = diff --git a/src/deal-checkout.ts b/src/deal-checkout.ts new file mode 100644 index 0000000..a3ac2e2 --- /dev/null +++ b/src/deal-checkout.ts @@ -0,0 +1,312 @@ +/** + * /checkout?sku=deal — the vin estate's VARIABLE-AMOUNT deal-settlement front + * (the buy pillar's cash-to-close; vin apis.vin deal lifecycle). + * + * The fixed-price legs (src/checkout.ts) price from a closed table. A vehicle + * deal's cash-to-close is desked per deal, so its price CANNOT live in a + * table here — and it may NEVER ride the query string (client input prices + * nothing). Instead the amount comes from the deal's own posted OFFER: + * + * 1. The deal door (closed table: apis.vin) mints a payment link and posts + * the OFFER at GET https://{door}/buy/deals/{deal}/checkout.json — + * status, the standing paymentLink id, and settle.amount_total in cents. + * 2. This front fetches that OFFER server-side, verifies the asked link IS + * the standing OPEN link, and prices the Stripe Checkout Session from + * settle.amount_total. A superseded / expired / settled link refuses — + * a re-desk changes the numbers, so a stale link never collects them. + * 3. {deal, payment_link, vin, door} ride as metadata on the session AND + * its PaymentIntent; the PaymentIntent id is the settlement_ref. + * + * The settlement leg: `checkout.session.completed` events whose metadata + * carries `estate: "vin", kind: "deal"` forward to the deal's OWN settle + * door — POST https://{door}/buy/deals/{deal}/settle (bearer + * VIN_SETTLE_TOKEN) with { order_id, settlement_ref, amount_total, + * currency }. The estate refuses any amount that disagrees with the desked + * cash-to-close (refused, never repriced) and dedupes by order_id. A failed + * forward answers Stripe 500 so the event redelivers (at-least-once). + */ +import type Stripe from 'stripe' + +// --------------------------------------------------------------------------- +// The closed deal-door table — which hosts may post a deal OFFER this front +// will price from. Closed like the SKU table: an unknown door refuses, it +// never fetches. (This is the amount-integrity boundary: the OFFER source URL +// is constructed from this table + validated ids, never from client input.) +// --------------------------------------------------------------------------- + +export const DEAL_SKU = 'deal' + +export const VIN_DEAL_DOORS: ReadonlySet = new Set(['apis.vin']) + +/** + * Stripe Checkout's card ceiling ($999,999.99) — and a sanity ceiling for a + * cash-to-close. An OFFER above it refuses honestly rather than failing + * opaquely at Stripe. + */ +export const MAX_DEAL_AMOUNT_CENTS = 99_999_999 + +const VIN_TOKEN = /^[A-HJ-NPR-Z0-9]{17}$/ +const DEAL_ID = /^deal_[A-Za-z0-9]{1,32}$/ +const PAYMENT_LINK_ID = /^paymentlink_[A-Za-z0-9]{1,32}$/ + +// --------------------------------------------------------------------------- +// Request validation +// --------------------------------------------------------------------------- + +export interface VinDealCheckoutIntent { + /** The deal id (`deal_…`) — the correlation object the settlement lands on. */ + deal: string + /** The payment-link id (`paymentlink_…`) the OFFER minted — must be the standing OPEN link. */ + link: string + vin: string + /** The offering deal door, e.g. "apis.vin" — member of the closed table. */ + door: string + /** Where the buyer returns — https on the door's own host. */ + returnTo: string +} + +export type ParsedVinDealCheckout = + | { ok: true; intent: VinDealCheckoutIntent } + | { ok: false; error: string } + +/** Validate /checkout?sku=deal query params into a deal-checkout intent, or refuse. */ +export function parseVinDealCheckout(url: URL): ParsedVinDealCheckout { + const refuse = (error: string): ParsedVinDealCheckout => ({ ok: false, error }) + if (url.searchParams.get('sku') !== DEAL_SKU) return refuse('not a deal checkout') + const deal = url.searchParams.get('deal') ?? '' + const link = url.searchParams.get('link') ?? '' + const vin = (url.searchParams.get('vin') ?? '').toUpperCase() + const door = (url.searchParams.get('door') ?? '').toLowerCase() + const returnTo = url.searchParams.get('return_to') ?? '' + + if (!DEAL_ID.test(deal)) return refuse('deal must be a deal_… id') + if (!PAYMENT_LINK_ID.test(link)) return refuse('link must be a paymentlink_… id — mint the OFFER first (POST /buy/deals/{dealId}/checkout on the deal door)') + if (!VIN_TOKEN.test(vin)) return refuse('vin must be a 17-character VIN token') + if (!VIN_DEAL_DOORS.has(door)) { + return refuse(`unknown deal door "${door}" — the deal-door table is closed; this front prices only from a door it knows`) + } + let parsedReturn: URL + try { + parsedReturn = new URL(returnTo) + } catch { + return refuse('return_to must be an absolute URL') + } + if (parsedReturn.protocol !== 'https:' || parsedReturn.host !== door) { + return refuse('return_to must be https on the offering door — checkout never redirects off the estate') + } + return { ok: true, intent: { deal, link, vin, door, returnTo } } +} + +// --------------------------------------------------------------------------- +// The OFFER fetch — the ONLY price authority for a deal checkout +// --------------------------------------------------------------------------- + +/** The priced OFFER as read from the deal door. */ +export interface VinDealOffer { + /** The desked cash-to-close in integer cents, from settle.amount_total. */ + amountCents: number + currency: 'usd' +} + +export type FetchedVinDealOffer = + | { ok: true; offer: VinDealOffer } + | { ok: false; error: string; status: number } + +/** The deal door's OFFER address — built from the CLOSED door table + validated ids only. */ +export function vinDealOfferUrl(intent: VinDealCheckoutIntent): string { + return `https://${intent.door}/buy/deals/${intent.deal}/checkout.json?link=${intent.link}` +} + +/** + * Fetch the deal's posted OFFER from its door and verify it prices THIS link: + * the page must answer OK with status "open", name the asked paymentLink as + * the standing one, name the same VIN, and post an integer-cents USD + * amount within the ceiling. Anything else refuses — never guesses. + */ +export async function fetchVinDealOffer( + intent: VinDealCheckoutIntent, + fetcher: typeof fetch = fetch +): Promise { + const refuse = (error: string, status = 409): FetchedVinDealOffer => ({ ok: false, error, status }) + let res: Response + try { + res = await fetcher(vinDealOfferUrl(intent), { headers: { accept: 'application/json' } }) + } catch (err) { + return refuse(`the deal door did not answer: ${err instanceof Error ? err.message : String(err)}`, 502) + } + if (!res.ok) { + return refuse(`the deal door answered ${res.status} for this OFFER — nothing to price`, 502) + } + let page: Record + try { + page = (await res.json()) as Record + } catch { + return refuse('the deal door answered non-JSON — nothing to price', 502) + } + const status = typeof page.status === 'string' ? page.status : '' + if (status !== 'open') { + return refuse( + `this payment link is not open on the deal door (status: ${status || 'unknown'}) — ` + + 'a superseded, expired, or settled link never collects; mint a fresh OFFER (POST /buy/deals/{dealId}/checkout)' + ) + } + if (page.paymentLink !== intent.link) { + return refuse('the deal door names a different standing payment link — this link never collects') + } + if (typeof page.vin === 'string' && page.vin.toUpperCase() !== intent.vin) { + return refuse('the OFFER names a different VIN than this checkout') + } + const settle = (page.settle ?? null) as Record | null + const amount = settle?.amount_total + if (typeof amount !== 'number' || !Number.isInteger(amount) || amount <= 0) { + return refuse('the OFFER posts no integer-cents amount_total — nothing to price', 502) + } + if (amount > MAX_DEAL_AMOUNT_CENTS) { + return refuse(`the OFFER's cash-to-close (${amount} cents) exceeds this front's card ceiling (${MAX_DEAL_AMOUNT_CENTS}) — settle off-rail`) + } + const currency = typeof settle?.currency === 'string' ? settle.currency.toLowerCase() : '' + if (currency !== 'usd') { + return refuse(`the OFFER posts currency "${currency}" — this front settles USD only`) + } + return { ok: true, offer: { amountCents: amount, currency: 'usd' } } +} + +// --------------------------------------------------------------------------- +// Checkout Session build — priced from the fetched OFFER only +// --------------------------------------------------------------------------- + +export function vinDealCheckoutSessionParams( + intent: VinDealCheckoutIntent, + offer: VinDealOffer +): Stripe.Checkout.SessionCreateParams { + const metadata = { + estate: 'vin', + kind: 'deal', + sku: DEAL_SKU, + deal: intent.deal, + payment_link: intent.link, + vin: intent.vin, + door: intent.door, + } + // Literal {CHECKOUT_SESSION_ID} — Stripe substitutes it; URLSearchParams + // would percent-encode the braces, so the query string is concatenated. + const sep = intent.returnTo.includes('?') ? '&' : '?' + return { + mode: 'payment', + client_reference_id: intent.deal, + line_items: [ + { + quantity: 1, + price_data: { + currency: offer.currency, + unit_amount: offer.amountCents, + product_data: { + name: `Cash to close — VIN ${intent.vin}`, + description: `Vehicle deal ${intent.deal} settlement as desked on ${intent.door}. Amount as posted on the deal's OFFER.`, + }, + }, + }, + ], + metadata, + payment_intent_data: { metadata }, + success_url: `${intent.returnTo}${sep}settled={CHECKOUT_SESSION_ID}`, + cancel_url: intent.returnTo, + } +} + +// --------------------------------------------------------------------------- +// Settlement forward — the webhook's deal leg +// --------------------------------------------------------------------------- + +/** The compact settlement confirmation the deal door receives. */ +export interface VinDealSettlement { + deal: string + payment_link: string + vin: string + door: string + /** The Checkout Session id (`cs_…`) — the deal door's idempotency/order key. */ + order_id: string + /** The object that MOVED the money (`pi_…`) — the settlement_ref. */ + settlement_ref: string + amount_total: number | null + currency: string | null + livemode: boolean +} + +/** + * Read a `checkout.session.completed` payload; return the deal settlement + * when it is a PAID vin-estate DEAL session, else null. (Fixed-price vin + * sessions carry no `kind` and stay on the src/checkout.ts leg.) + */ +export function vinDealSettlementFromSession( + session: Record, + livemode: boolean +): VinDealSettlement | null { + const metadata = (session.metadata ?? null) as Record | null + if (metadata?.estate !== 'vin' || metadata?.kind !== 'deal') return null + if (session.payment_status !== 'paid') return null + const pi = session.payment_intent + const settlementRef = + typeof pi === 'string' ? pi : ((pi as { id?: string } | null)?.id ?? String(session.id)) + return { + deal: metadata.deal ?? '', + payment_link: metadata.payment_link ?? '', + vin: metadata.vin ?? '', + door: metadata.door ?? '', + order_id: String(session.id), + settlement_ref: settlementRef, + amount_total: typeof session.amount_total === 'number' ? session.amount_total : null, + currency: typeof session.currency === 'string' ? session.currency : null, + livemode, + } +} + +export interface VinDealForwardResult { + forwarded: boolean + /** Set when the endpoint answered non-2xx. */ + status?: number + /** Set when the forward was refused locally (bad metadata) or threw. */ + reason?: string +} + +/** + * POST the settlement confirmation to the deal door's own settle leg — + * https://{door}/buy/deals/{deal}/settle, bearer VIN_SETTLE_TOKEN. The door + * is re-validated against the closed table at forward time (metadata is + * ours, but money code re-checks). A failing forward → the caller answers + * Stripe 500 so the webhook redelivers; the door dedupes by order_id. + */ +export async function forwardVinDealSettlement( + settlement: VinDealSettlement, + cfg: { token?: string }, + fetcher: typeof fetch = fetch +): Promise { + if (!VIN_DEAL_DOORS.has(settlement.door)) { + return { forwarded: false, reason: `unknown deal door "${settlement.door}" — refused` } + } + if (!DEAL_ID.test(settlement.deal)) { + return { forwarded: false, reason: 'malformed deal id — refused' } + } + if (settlement.amount_total === null || !Number.isInteger(settlement.amount_total)) { + return { forwarded: false, reason: 'session carries no integer amount_total — refused' } + } + const headers: Record = { 'Content-Type': 'application/json' } + if (cfg.token) headers.Authorization = `Bearer ${cfg.token}` + const body = { + order_id: settlement.order_id, + settlement_ref: settlement.settlement_ref, + amount_total: settlement.amount_total, + currency: settlement.currency ?? 'usd', + } + try { + const res = await fetcher(`https://${settlement.door}/buy/deals/${settlement.deal}/settle`, { + method: 'POST', + headers, + body: JSON.stringify(body), + }) + if (!res.ok) return { forwarded: false, status: res.status } + return { forwarded: true, status: res.status } + } catch (err) { + return { forwarded: false, reason: err instanceof Error ? err.message : String(err) } + } +} diff --git a/src/env.d.ts b/src/env.d.ts index 7bab2ce..234e001 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -24,7 +24,12 @@ declare global { EVENTS?: unknown /** Vin estate settlement-confirm endpoint (var; unset → skip forward). */ VIN_SETTLE_URL?: string - /** Bearer token for the vin settlement forward (secret; founder act). */ + /** + * Bearer token for the vin settlement forwards (secret; founder act): + * the fixed-price /_settle leg AND the deal doors' own settle legs + * (deal-checkout.ts) — the vin estate configures the SAME operator + * token on its settle receivers. + */ VIN_SETTLE_TOKEN?: string } } diff --git a/src/index.ts b/src/index.ts index a3de4af..7d510c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,14 @@ import { vinCheckoutSessionParams, vinSettlementFromSession, } from './checkout' +import { + DEAL_SKU, + fetchVinDealOffer, + forwardVinDealSettlement, + parseVinDealCheckout, + vinDealCheckoutSessionParams, + vinDealSettlementFromSession, +} from './deal-checkout' // --------------------------------------------------------------------------- // Lazy Stripe + RPC init @@ -230,7 +238,9 @@ route('GET', '/', async () => { products: { create: 'POST /products', retrieve: 'GET /products/:id', update: 'PATCH /products/:id' }, prices: { create: 'POST /prices', retrieve: 'GET /prices/:id' }, refunds: { create: 'POST /refunds' }, - checkout: 'GET /checkout?sku=…&vin=…&door=…&return_to=… → 303 to Stripe Checkout (vin estate gatefold; PW-5)', + checkout: + 'GET /checkout?sku=…&vin=…&door=…&return_to=… → 303 to Stripe Checkout (vin estate gatefold; PW-5). ' + + 'Deal leg: sku=deal&deal=…&link=… prices from the deal door’s posted OFFER (variable amount; never the query string)', webhooks: 'POST /webhooks', }, }) @@ -465,7 +475,32 @@ route('POST', '/import', async (request) => { // Prices come from the closed table in src/checkout.ts — the query string // never carries a price. Refusals are 400 with the reason stated. route('GET', '/checkout', async (request) => { - const parsed = parseVinCheckout(new URL(request.url)) + const url = new URL(request.url) + + // The variable-amount deal leg (sku=deal): the amount comes from the deal + // door's posted OFFER, fetched server-side — never from the query string. + if (url.searchParams.get('sku') === DEAL_SKU) { + const parsed = parseVinDealCheckout(url) + if (!parsed.ok) { + return error(parsed.error, 400) + } + if (!env.STRIPE_SECRET_KEY) { + return error('payments.do is deployed but unconfigured. Founder act: wrangler secret put STRIPE_SECRET_KEY', 503) + } + const fetched = await fetchVinDealOffer(parsed.intent) + if (!fetched.ok) { + return error(fetched.error, fetched.status) + } + const session = await getStripe().checkout.sessions.create( + vinDealCheckoutSessionParams(parsed.intent, fetched.offer), + ) + if (!session.url) { + return error('Stripe created the session but returned no redirect URL', 502) + } + return new Response(null, { status: 303, headers: { Location: session.url } }) + } + + const parsed = parseVinCheckout(url) if (!parsed.ok) { return error(parsed.error, 400) } @@ -531,6 +566,34 @@ route('POST', '/webhooks', async (request) => { // carries the raw Stripe event); configured but failing → 500 so Stripe // redelivers (at-least-once; the estate dedupes by order_id). if (event.type === 'checkout.session.completed') { + // The deal leg first: a PAID vin DEAL session (metadata kind=deal) + // forwards to the deal's OWN settle door — POST + // https://{door}/buy/deals/{deal}/settle (closed door table; bearer + // VIN_SETTLE_TOKEN). The door refuses any amount that disagrees with the + // desked cash-to-close and dedupes by order_id; a failed forward answers + // Stripe 500 so the event redelivers. + const dealSettlement = vinDealSettlementFromSession(dataObj, event.livemode) + if (dealSettlement) { + const forward = await forwardVinDealSettlement(dealSettlement, { token: env.VIN_SETTLE_TOKEN }) + if (!forward.forwarded) { + console.log( + `[webhook] vin deal settlement forward failed (${forward.status ?? forward.reason}) — answering 500 so Stripe redelivers`, + ) + return error('vin deal settlement forward failed — Stripe will redeliver', 500) + } + return json({ + received: true, + type: event.type, + account, + vin_deal: { + deal: dealSettlement.deal, + order_id: dealSettlement.order_id, + settlement_ref: dealSettlement.settlement_ref, + forwarded: forward.forwarded, + }, + }) + } + const settlement = vinSettlementFromSession(dataObj, event.livemode) if (settlement) { const forward = await forwardVinSettlement(settlement, { diff --git a/test/deal-checkout.test.ts b/test/deal-checkout.test.ts new file mode 100644 index 0000000..705d261 --- /dev/null +++ b/test/deal-checkout.test.ts @@ -0,0 +1,549 @@ +/** + * /checkout?sku=deal — the vin estate variable-amount deal leg. + * + * Two layers under test: + * 1. The pure module (src/deal-checkout.ts): closed deal-door table, query + * validation, OFFER fetch-and-verify (the ONLY price authority), + * Checkout Session params, deal-settlement extraction, settle forward. + * 2. The routes (src/index.ts): GET /checkout?sku=deal → OFFER fetch → 303 + * to Stripe Checkout; POST /webhooks forwards PAID vin deal sessions to + * the deal door's own settle leg and answers Stripe 500 on failure. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { + MAX_DEAL_AMOUNT_CENTS, + VIN_DEAL_DOORS, + fetchVinDealOffer, + forwardVinDealSettlement, + parseVinDealCheckout, + vinDealCheckoutSessionParams, + vinDealOfferUrl, + vinDealSettlementFromSession, +} from '../src/deal-checkout' +import { vinSettlementFromSession } from '../src/checkout' + +// --------------------------------------------------------------------------- +// Worker mocks (used by the route layer only) — the checkout.test.ts pattern +// --------------------------------------------------------------------------- + +const mockSessionsCreate = vi.fn() +const mockWebhooksConstructEvent = vi.fn() + +vi.mock('stripe', () => { + class MockStripeError extends Error { + type: string + constructor(message: string, type: string) { + super(message) + this.type = type + } + } + + const MockStripe = vi.fn().mockImplementation(() => ({ + checkout: { sessions: { create: mockSessionsCreate } }, + webhooks: { constructEvent: mockWebhooksConstructEvent }, + })) as unknown as { errors: { StripeError: typeof MockStripeError } } + + MockStripe.errors = { StripeError: MockStripeError } + + return { default: MockStripe, Stripe: MockStripe } +}) + +vi.mock('rpc.do', () => ({ + RPC: vi.fn().mockReturnValue({ + fetch: vi.fn().mockResolvedValue(new Response(JSON.stringify({ rpc: true }), { status: 200 })), + }), +})) + +const mockEnv: Record = {} + +vi.mock('cloudflare:workers', () => ({ env: mockEnv })) + +type Worker = { default: { fetch: (request: Request, envArg?: unknown, ctx?: unknown) => Promise } } + +async function loadWorker(envOverrides: Record = {}): Promise { + vi.resetModules() + for (const key of Object.keys(mockEnv)) delete mockEnv[key] + Object.assign( + mockEnv, + { STRIPE_SECRET_KEY: 'sk_test_mock', STRIPE_WEBHOOK_SECRET: 'whsec_test_mock' }, + envOverrides, + ) + return (await import('../src/index.js')) as Worker +} + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const VIN = '1FTFW1E55PFA10001' +const DEAL = 'deal_Ab12Cd34' +const LINK = 'paymentlink_Ef56Gh78' +const RETURN_TO = `https://apis.vin/buy/deals/${DEAL}/checkout` + +const DEAL_QS = + `sku=deal&deal=${DEAL}&link=${LINK}&vin=${VIN}&door=apis.vin&return_to=${encodeURIComponent(RETURN_TO)}` + +function dealUrl(qs: string = DEAL_QS): URL { + return new URL(`https://payments.do/checkout?${qs}`) +} + +const intent = { deal: DEAL, link: LINK, vin: VIN, door: 'apis.vin', returnTo: RETURN_TO } + +/** The deal door's OFFER page (apis.vin GET /buy/deals/{id}/checkout.json). */ +function offerPage(overrides: Record = {}): Record { + return { + type: 'OK', + status: 'open', + deal: DEAL, + vin: VIN, + paymentLink: LINK, + settle: { path: `/buy/deals/${DEAL}/settle`, amount_total: 3_250_000, currency: 'usd' }, + ...overrides, + } +} + +function offerResponse(page: Record = offerPage()): Response { + return new Response(JSON.stringify(page), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) +} + +beforeEach(() => { + vi.clearAllMocks() +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +// --------------------------------------------------------------------------- +// The closed deal-door table +// --------------------------------------------------------------------------- + +describe('VIN_DEAL_DOORS — the closed table', () => { + it('posts exactly the deal doors this front prices from', () => { + expect([...VIN_DEAL_DOORS].sort()).toEqual(['apis.vin']) + }) +}) + +// --------------------------------------------------------------------------- +// parseVinDealCheckout +// --------------------------------------------------------------------------- + +describe('parseVinDealCheckout', () => { + it('accepts a well-formed deal checkout query', () => { + expect(parseVinDealCheckout(dealUrl())).toEqual({ ok: true, intent }) + }) + + it('refuses a door outside the closed deal-door table', () => { + const qs = DEAL_QS.replace(/apis\.vin/g, 'other.vin') + const parsed = parseVinDealCheckout(dealUrl(qs)) + expect(parsed.ok).toBe(false) + if (!parsed.ok) expect(parsed.error).toContain('closed') + }) + + it('refuses a malformed deal id', () => { + const parsed = parseVinDealCheckout(dealUrl(DEAL_QS.replace(`deal=${DEAL}`, 'deal=../../etc'))) + expect(parsed.ok).toBe(false) + }) + + it('refuses a malformed payment-link id — mint the OFFER first', () => { + const parsed = parseVinDealCheckout(dealUrl(DEAL_QS.replace(`link=${LINK}`, 'link=whatever'))) + expect(parsed.ok).toBe(false) + if (!parsed.ok) expect(parsed.error).toContain('paymentlink_') + }) + + it('refuses a malformed VIN token', () => { + const parsed = parseVinDealCheckout(dealUrl(DEAL_QS.replace(new RegExp(VIN, 'g'), 'NOT-A-VIN'))) + expect(parsed.ok).toBe(false) + }) + + it('refuses a return_to off the door host — no open redirect', () => { + const qs = DEAL_QS.replace(encodeURIComponent(RETURN_TO), encodeURIComponent('https://evil.example.com/x')) + expect(parseVinDealCheckout(dealUrl(qs)).ok).toBe(false) + }) + + it('refuses a non-https return_to', () => { + const qs = DEAL_QS.replace(encodeURIComponent('https://'), encodeURIComponent('http://')) + expect(parseVinDealCheckout(dealUrl(qs)).ok).toBe(false) + }) +}) + +// --------------------------------------------------------------------------- +// fetchVinDealOffer — the only price authority +// --------------------------------------------------------------------------- + +describe('fetchVinDealOffer', () => { + it('fetches the OFFER from the closed-table door address, never client input', () => { + expect(vinDealOfferUrl(intent)).toBe( + `https://apis.vin/buy/deals/${DEAL}/checkout.json?link=${LINK}`, + ) + }) + + it('accepts an open OFFER naming this link and prices in integer cents', async () => { + const fetcher = vi.fn().mockResolvedValue(offerResponse()) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched).toEqual({ ok: true, offer: { amountCents: 3_250_000, currency: 'usd' } }) + expect(fetcher).toHaveBeenCalledWith(vinDealOfferUrl(intent), { headers: { accept: 'application/json' } }) + }) + + it.each(['superseded', 'expired', 'settled'])('refuses a %s link — a stale link never collects', async (status) => { + const fetcher = vi.fn().mockResolvedValue(offerResponse(offerPage({ status }))) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + if (!fetched.ok) expect(fetched.error).toContain(status) + }) + + it('refuses when the door names a different standing link', async () => { + const fetcher = vi.fn().mockResolvedValue(offerResponse(offerPage({ paymentLink: 'paymentlink_Other1' }))) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + }) + + it('refuses when the OFFER names a different VIN', async () => { + const fetcher = vi.fn().mockResolvedValue(offerResponse(offerPage({ vin: '1HGCM82633A004352' }))) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + }) + + it('refuses a non-integer or missing amount — never guesses', async () => { + for (const amount_total of [12.5, '3250000', undefined, 0, -1]) { + const fetcher = vi.fn().mockResolvedValue( + offerResponse(offerPage({ settle: { amount_total, currency: 'usd' } })), + ) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + } + }) + + it('refuses an amount above the card ceiling', async () => { + const fetcher = vi.fn().mockResolvedValue( + offerResponse(offerPage({ settle: { amount_total: MAX_DEAL_AMOUNT_CENTS + 1, currency: 'usd' } })), + ) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + if (!fetched.ok) expect(fetched.error).toContain('ceiling') + }) + + it('refuses a non-USD OFFER', async () => { + const fetcher = vi.fn().mockResolvedValue( + offerResponse(offerPage({ settle: { amount_total: 3_250_000, currency: 'eur' } })), + ) + const fetched = await fetchVinDealOffer(intent, fetcher as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + }) + + it('refuses when the door answers non-2xx or non-JSON or throws', async () => { + const notFound = vi.fn().mockResolvedValue(new Response('gone', { status: 404 })) + expect((await fetchVinDealOffer(intent, notFound as unknown as typeof fetch)).ok).toBe(false) + + const notJson = vi.fn().mockResolvedValue(new Response('', { status: 200 })) + expect((await fetchVinDealOffer(intent, notJson as unknown as typeof fetch)).ok).toBe(false) + + const down = vi.fn().mockRejectedValue(new Error('network down')) + const fetched = await fetchVinDealOffer(intent, down as unknown as typeof fetch) + expect(fetched.ok).toBe(false) + if (!fetched.ok) expect(fetched.status).toBe(502) + }) +}) + +// --------------------------------------------------------------------------- +// vinDealCheckoutSessionParams +// --------------------------------------------------------------------------- + +describe('vinDealCheckoutSessionParams', () => { + const offer = { amountCents: 3_250_000, currency: 'usd' as const } + + it('prices from the fetched OFFER and carries the deal correlation on session AND PaymentIntent', () => { + const params = vinDealCheckoutSessionParams(intent, offer) + expect(params.mode).toBe('payment') + expect(params.client_reference_id).toBe(DEAL) + expect(params.line_items?.[0]?.price_data?.unit_amount).toBe(3_250_000) + const metadata = { + estate: 'vin', + kind: 'deal', + sku: 'deal', + deal: DEAL, + payment_link: LINK, + vin: VIN, + door: 'apis.vin', + } + expect(params.metadata).toEqual(metadata) + expect(params.payment_intent_data?.metadata).toEqual(metadata) + }) + + it('returns the buyer to the deal checkout page with the literal {CHECKOUT_SESSION_ID} placeholder', () => { + const params = vinDealCheckoutSessionParams(intent, offer) + expect(params.success_url).toBe(`${RETURN_TO}?settled={CHECKOUT_SESSION_ID}`) + expect(params.cancel_url).toBe(RETURN_TO) + }) +}) + +// --------------------------------------------------------------------------- +// vinDealSettlementFromSession +// --------------------------------------------------------------------------- + +const paidDealSession = { + id: 'cs_test_deal_1', + payment_status: 'paid', + payment_intent: 'pi_test_deal_2', + amount_total: 3_250_000, + currency: 'usd', + metadata: { + estate: 'vin', + kind: 'deal', + sku: 'deal', + deal: DEAL, + payment_link: LINK, + vin: VIN, + door: 'apis.vin', + }, +} + +describe('vinDealSettlementFromSession', () => { + it('extracts the deal settlement: session id as order_id, PaymentIntent as settlement_ref', () => { + expect(vinDealSettlementFromSession(paidDealSession, false)).toEqual({ + deal: DEAL, + payment_link: LINK, + vin: VIN, + door: 'apis.vin', + order_id: 'cs_test_deal_1', + settlement_ref: 'pi_test_deal_2', + amount_total: 3_250_000, + currency: 'usd', + livemode: false, + }) + }) + + it('ignores non-deal vin sessions (the fixed-price leg keeps them)', () => { + const sticker = { + ...paidDealSession, + metadata: { estate: 'vin', sku: 'sticker', vin: VIN, door: 'sticker.vin' }, + } + expect(vinDealSettlementFromSession(sticker, false)).toBeNull() + }) + + it('ignores unpaid deal sessions', () => { + expect(vinDealSettlementFromSession({ ...paidDealSession, payment_status: 'unpaid' }, false)).toBeNull() + }) + + it('and the fixed-price extractor ignores deal sessions — no double forward', () => { + expect(vinSettlementFromSession(paidDealSession, false)).toBeNull() + }) +}) + +// --------------------------------------------------------------------------- +// forwardVinDealSettlement +// --------------------------------------------------------------------------- + +describe('forwardVinDealSettlement', () => { + const settlement = { + deal: DEAL, + payment_link: LINK, + vin: VIN, + door: 'apis.vin', + order_id: 'cs_test_deal_1', + settlement_ref: 'pi_test_deal_2', + amount_total: 3_250_000, + currency: 'usd', + livemode: false, + } + + it('POSTs the compact settle body to the deal door with the bearer', async () => { + const fetcher = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })) + const result = await forwardVinDealSettlement(settlement, { token: 'tok_test' }, fetcher as unknown as typeof fetch) + expect(result).toEqual({ forwarded: true, status: 200 }) + expect(fetcher).toHaveBeenCalledWith(`https://apis.vin/buy/deals/${DEAL}/settle`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer tok_test' }, + body: JSON.stringify({ + order_id: 'cs_test_deal_1', + settlement_ref: 'pi_test_deal_2', + amount_total: 3_250_000, + currency: 'usd', + }), + }) + }) + + it('refuses a door outside the closed table — money code re-checks its own metadata', async () => { + const fetcher = vi.fn() + const result = await forwardVinDealSettlement( + { ...settlement, door: 'evil.example.com' }, + { token: 'tok_test' }, + fetcher as unknown as typeof fetch, + ) + expect(result.forwarded).toBe(false) + expect(fetcher).not.toHaveBeenCalled() + }) + + it('refuses a session with no integer amount_total', async () => { + const fetcher = vi.fn() + const result = await forwardVinDealSettlement( + { ...settlement, amount_total: null }, + {}, + fetcher as unknown as typeof fetch, + ) + expect(result.forwarded).toBe(false) + expect(fetcher).not.toHaveBeenCalled() + }) + + it('reports a non-2xx answer without throwing', async () => { + const fetcher = vi.fn().mockResolvedValue(new Response('nope', { status: 401 })) + const result = await forwardVinDealSettlement(settlement, {}, fetcher as unknown as typeof fetch) + expect(result).toEqual({ forwarded: false, status: 401 }) + }) + + it('reports a thrown fetch as not forwarded', async () => { + const fetcher = vi.fn().mockRejectedValue(new Error('network down')) + const result = await forwardVinDealSettlement(settlement, {}, fetcher as unknown as typeof fetch) + expect(result.forwarded).toBe(false) + expect(result.reason).toBe('network down') + }) +}) + +// --------------------------------------------------------------------------- +// GET /checkout?sku=deal — the route +// --------------------------------------------------------------------------- + +describe('GET /checkout?sku=deal', () => { + it('fetches the OFFER, prices the session from it, and answers 303 to Stripe', async () => { + const worker = await loadWorker() + const fetchMock = vi.fn().mockResolvedValue(offerResponse()) + vi.stubGlobal('fetch', fetchMock) + mockSessionsCreate.mockResolvedValue({ id: 'cs_test_deal_1', url: 'https://checkout.stripe.com/c/pay/cs_test_deal_1' }) + + const res = await worker.default.fetch(new Request(dealUrl().toString())) + + expect(res.status).toBe(303) + expect(res.headers.get('Location')).toBe('https://checkout.stripe.com/c/pay/cs_test_deal_1') + expect(fetchMock).toHaveBeenCalledWith( + `https://apis.vin/buy/deals/${DEAL}/checkout.json?link=${LINK}`, + { headers: { accept: 'application/json' } }, + ) + const params = mockSessionsCreate.mock.calls[0][0] + expect(params.line_items[0].price_data.unit_amount).toBe(3_250_000) + expect(params.metadata.deal).toBe(DEAL) + expect(params.metadata.payment_link).toBe(LINK) + }) + + it('refuses a stale link with the door-stated status — no session minted', async () => { + const worker = await loadWorker() + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(offerResponse(offerPage({ status: 'superseded' })))) + + const res = await worker.default.fetch(new Request(dealUrl().toString())) + + expect(res.status).toBe(409) + expect(mockSessionsCreate).not.toHaveBeenCalled() + }) + + it('refuses an unknown deal door with 400 before any fetch', async () => { + const worker = await loadWorker() + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + const res = await worker.default.fetch( + new Request(dealUrl(DEAL_QS.replace(/apis\.vin/g, 'other.vin')).toString()), + ) + + expect(res.status).toBe(400) + expect(fetchMock).not.toHaveBeenCalled() + expect(mockSessionsCreate).not.toHaveBeenCalled() + }) + + it('answers 503 naming the founder act while unconfigured — before any OFFER fetch', async () => { + const worker = await loadWorker({ STRIPE_SECRET_KEY: undefined }) + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + const res = await worker.default.fetch(new Request(dealUrl().toString())) + + expect(res.status).toBe(503) + expect(fetchMock).not.toHaveBeenCalled() + }) +}) + +// --------------------------------------------------------------------------- +// POST /webhooks — the deal settlement leg +// --------------------------------------------------------------------------- + +function webhookRequest(event: Record): Request { + return new Request('https://payments.do/webhooks', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Stripe-Signature': 't=1,v1=mock' }, + body: JSON.stringify(event), + }) +} + +const completedDealEvent = { + id: 'evt_test_deal_1', + type: 'checkout.session.completed', + livemode: false, + data: { object: paidDealSession }, +} + +describe('POST /webhooks — vin deal settlement forward', () => { + it('forwards a PAID deal session to the deal door settle leg, NOT the fixed-price /_settle', async () => { + const worker = await loadWorker({ VIN_SETTLE_URL: 'https://all.vin/_settle', VIN_SETTLE_TOKEN: 'tok_test' }) + mockWebhooksConstructEvent.mockReturnValue(completedDealEvent) + const fetchMock = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })) + vi.stubGlobal('fetch', fetchMock) + + const res = await worker.default.fetch(webhookRequest(completedDealEvent)) + + expect(res.status).toBe(200) + const body = (await res.json()) as { + vin_deal: { deal: string; order_id: string; settlement_ref: string; forwarded: boolean } + } + expect(body.vin_deal).toEqual({ + deal: DEAL, + order_id: 'cs_test_deal_1', + settlement_ref: 'pi_test_deal_2', + forwarded: true, + }) + expect(fetchMock).toHaveBeenCalledTimes(1) + const [url, init] = fetchMock.mock.calls[0] + expect(url).toBe(`https://apis.vin/buy/deals/${DEAL}/settle`) + expect(init.headers.Authorization).toBe('Bearer tok_test') + const forwarded = JSON.parse(init.body) + expect(forwarded).toEqual({ + order_id: 'cs_test_deal_1', + settlement_ref: 'pi_test_deal_2', + amount_total: 3_250_000, + currency: 'usd', + }) + }) + + it('answers 500 when the deal forward fails, so Stripe redelivers', async () => { + const worker = await loadWorker({ VIN_SETTLE_TOKEN: 'tok_test' }) + mockWebhooksConstructEvent.mockReturnValue(completedDealEvent) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('nope', { status: 401 }))) + + const res = await worker.default.fetch(webhookRequest(completedDealEvent)) + + expect(res.status).toBe(500) + }) + + it('leaves fixed-price vin sessions on the existing /_settle leg', async () => { + const worker = await loadWorker({ VIN_SETTLE_URL: 'https://all.vin/_settle', VIN_SETTLE_TOKEN: 'tok_test' }) + const stickerEvent = { + ...completedDealEvent, + data: { + object: { + ...paidDealSession, + metadata: { estate: 'vin', sku: 'sticker', vin: VIN, door: 'sticker.vin' }, + }, + }, + } + mockWebhooksConstructEvent.mockReturnValue(stickerEvent) + const fetchMock = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })) + vi.stubGlobal('fetch', fetchMock) + + const res = await worker.default.fetch(webhookRequest(stickerEvent)) + + expect(res.status).toBe(200) + const [url] = fetchMock.mock.calls[0] + expect(url).toBe('https://all.vin/_settle') + }) +})