sanity-plugin-stripe 0.1.0 → 0.2.0

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,"file":"server.js","names":[],"sources":["../src/server/email.ts","../src/server/events.ts","../src/core/queries.ts","../src/core/regions.ts","../src/server/checkout.ts","../src/server/once.ts","../src/server/order.ts","../src/server/sanity.ts","../src/server/stock.ts","../src/server/routes.ts","../src/version.ts","../src/server/stripe.ts"],"sourcesContent":["import type Stripe from 'stripe'\n\nimport {formatMoney} from '../core/money'\nimport type {Order} from './order'\nimport type {StockChange} from './stock'\n\nconst escapeHtml = (value: string) =>\n value.replace(\n /[&<>\"']/g,\n (char) => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;'})[char] ?? char,\n )\n\nfunction addressLines(name: string | null, address: Stripe.Address | null): string[] {\n if (!address) return name ? [name] : []\n const city = [address.postal_code, address.city].filter(Boolean).join(' ')\n return [name, address.line1, address.line2, city, address.state, address.country].filter(\n (line): line is string => Boolean(line),\n )\n}\n\n/** Where the payment is in the Stripe Dashboard. */\nexport function dashboardUrl(\n order: Pick<Order, 'livemode' | 'paymentIntentId' | 'sessionId'>,\n): string {\n const base = `https://dashboard.stripe.com/${order.livemode ? '' : 'test/'}`\n return order.paymentIntentId\n ? `${base}payments/${order.paymentIntentId}`\n : `${base}checkout/sessions/${order.sessionId}`\n}\n\nexport interface OrderEmailOptions {\n /** Starts the subject line. */\n shopName: string\n /** For amounts: a BCP 47 tag such as `en`, `nl-NL`. Default `en`. */\n locale?: string\n /** What the order did to stock counts. Anything sold below 0 is called out. */\n stock?: readonly StockChange[]\n /** Why stock could not be lowered, from the webhook's context. The email says to check it. */\n stockError?: string\n}\n\nexport interface OrderEmail {\n subject: string\n text: string\n html: string\n}\n\n/** A plain order email for the shop: what to pack, where to send it and who paid. */\nexport function orderEmail(order: Order, options: OrderEmailOptions): OrderEmail {\n const {shopName, locale = 'en', stock = [], stockError} = options\n const money = (minor: number) => formatMoney(minor, order.currency, locale)\n const count = order.items.reduce((sum, item) => sum + item.quantity, 0)\n const who = order.shipping.name ?? order.customer.name ?? order.customer.email ?? 'a customer'\n const subject = `${order.livemode ? '' : '[Test] '}${shopName} order from ${who}: ${count} ${count === 1 ? 'item' : 'items'}, ${money(order.amountTotal)}`\n const address = addressLines(order.shipping.name, order.shipping.address)\n const contact = [order.customer.email, order.customer.phone].filter((line): line is string =>\n Boolean(line),\n )\n const shippingLabel = order.regionTitle ? `Shipping (${order.regionTitle})` : 'Shipping'\n const link = dashboardUrl(order)\n const date = order.createdAt.toISOString().slice(0, 10)\n\n const oversold = stock\n .filter((change) => change.to < 0)\n .map((change) => {\n const item = order.items.find(\n (candidate) =>\n candidate.productId === change.productId &&\n (candidate.variantKey ?? '') === (change.variantKey ?? ''),\n )\n return item?.name ?? change.productId\n })\n const warnings = [\n ...(oversold.length\n ? [`Check your stock: more was sold than was counted in stock for ${oversold.join(', ')}.`]\n : []),\n ...(stockError\n ? [`Stock counts were not lowered for this order. Change them by hand. (${stockError})`]\n : []),\n ]\n\n const text = [\n `New order on ${date}${order.livemode ? '' : ' (test mode, no money moved)'}`,\n '',\n ...warnings.flatMap((warning) => [warning, '']),\n ...order.items.map((item) => `${item.quantity} × ${item.name} ${money(item.amountTotal)}`),\n `${shippingLabel} ${money(order.amountShipping)}`,\n `Total ${money(order.amountTotal)}`,\n '',\n 'Ship to:',\n ...address,\n '',\n 'Contact:',\n ...contact,\n '',\n `In Stripe: ${link}`,\n ].join('\\n')\n\n const rows = order.items\n .map(\n (item) =>\n `<tr><td style=\"padding:4px 12px 4px 0\">${item.quantity} ×</td><td style=\"padding:4px 12px 4px 0\">${escapeHtml(item.name)}</td><td style=\"padding:4px 0;text-align:right\">${money(item.amountTotal)}</td></tr>`,\n )\n .join('')\n const html = `<div style=\"font-family:Helvetica,Arial,sans-serif;font-size:15px;line-height:1.45;color:#191919\">\n<p>New order on ${date}${order.livemode ? '' : ' <strong>(test mode, no money moved)</strong>'}</p>\n${warnings.map((warning) => `<p><strong>${escapeHtml(warning)}</strong></p>\\n`).join('')}<table style=\"border-collapse:collapse\">${rows}\n<tr><td></td><td style=\"padding:4px 12px 4px 0\">${escapeHtml(shippingLabel)}</td><td style=\"padding:4px 0;text-align:right\">${money(order.amountShipping)}</td></tr>\n<tr><td></td><td style=\"padding:4px 12px 4px 0\"><strong>Total</strong></td><td style=\"padding:4px 0;text-align:right\"><strong>${money(order.amountTotal)}</strong></td></tr>\n</table>\n<p><strong>Ship to</strong><br>${address.map(escapeHtml).join('<br>')}</p>\n<p><strong>Contact</strong><br>${contact.map(escapeHtml).join('<br>')}</p>\n<p><a href=\"${link}\">Open the payment in Stripe</a></p>\n</div>`\n\n return {subject, text, html}\n}\n","import type Stripe from 'stripe'\n\n/** Checks the signature and parses the event. Throws when the signature doesn't match. */\nexport function verifyEvent(\n stripe: Stripe,\n payload: string,\n signature: string | null,\n secret: string,\n): Stripe.Event {\n if (!signature) throw new Error('Missing Stripe-Signature header')\n return stripe.webhooks.constructEvent(payload, signature, secret)\n}\n\nexport type CheckoutEvent =\n /** Paid: fulfil the order. */\n | {type: 'paid'; session: Stripe.Checkout.Session}\n /** Completed, but a delayed payment method hasn't settled yet. Wait for async_payment_succeeded. */\n | {type: 'pending'; session: Stripe.Checkout.Session}\n /** A delayed payment failed. Nothing to fulfil. */\n | {type: 'failed'; session: Stripe.Checkout.Session}\n | {type: 'ignored'}\n\n/** The events a webhook endpoint for the shop listens to. */\nexport const WEBHOOK_EVENTS = [\n 'checkout.session.completed',\n 'checkout.session.async_payment_succeeded',\n 'checkout.session.async_payment_failed',\n] as const\n\n/** What a webhook event means for the shop. Only `paid` is fulfilled. */\nexport function classifyEvent(event: Stripe.Event): CheckoutEvent {\n switch (event.type) {\n case 'checkout.session.completed':\n case 'checkout.session.async_payment_succeeded': {\n const session = event.data.object\n return session.payment_status === 'unpaid'\n ? {type: 'pending', session}\n : {type: 'paid', session}\n }\n case 'checkout.session.async_payment_failed':\n return {type: 'failed', session: event.data.object}\n default:\n return {type: 'ignored'}\n }\n}\n","import {SHOP_SETTINGS_ID, SHOP_SETTINGS_TYPE} from './ids'\n\n/** Where a site keeps its products, for the queries the plugin writes. */\nexport interface ProductQueryOptions {\n /** Document types that can be bought. Default `['product']`. */\n types?: readonly string[]\n /** The name of the `stripeCommerce` field on those types. Default `commerce`. */\n field?: string\n /** GROQ for the product's name. Default `title`. */\n title?: string\n /** GROQ for the product's slug. Default `slug.current`. */\n slug?: string\n /** GROQ for the URL of the image shown in the cart and on Stripe's page. */\n image?: string\n}\n\nconst NAME = /^[A-Za-z_]\\w*$/\nconst TYPE_NAME = /^[A-Za-z_][\\w.-]*$/\n\n/** The options with their defaults, checked: names end up inside GROQ. */\nexport function resolveProductQuery(options: ProductQueryOptions = {}) {\n const types = options.types?.length ? [...options.types] : ['product']\n const field = options.field ?? 'commerce'\n for (const type of types) {\n if (!TYPE_NAME.test(type))\n throw new TypeError(`sanity-plugin-stripe: \"${type}\" is not a document type name`)\n }\n if (!NAME.test(field)) throw new TypeError(`sanity-plugin-stripe: \"${field}\" is not a field name`)\n return {\n types,\n field,\n title: options.title ?? 'title',\n slug: options.slug ?? 'slug.current',\n image:\n options.image ?? 'coalesce(images[0].asset->url, image.asset->url, mainImage.asset->url)',\n }\n}\n\nconst typeFilter = (types: readonly string[]) =>\n `_type in [${types.map((type) => JSON.stringify(type)).join(', ')}]`\n\n/** The price-and-availability field as the cart and the checkout read it. No stock counts. */\nexport const COMMERCE_PROJECTION = `{availability, price, taxClass, variants[]{_key, label, availability}}`\n\n/**\n * What the cart and the checkout need of a product, for the site's own queries:\n *\n * ```ts\n * defineQuery(`*[_type == \"product\"]{${cartProductProjection()}}`)\n * ```\n */\nexport function cartProductProjection(options?: ProductQueryOptions): string {\n const {field, title, slug, image} = resolveProductQuery(options)\n return `_id, \"title\": ${title}, \"slug\": ${slug}, \"imageUrl\": ${image}, \"commerce\": ${field}${COMMERCE_PROJECTION}`\n}\n\n/** Every product that has the commerce field filled in, for the cart page. */\nexport function cartProductsQuery(options?: ProductQueryOptions): string {\n const {types, field} = resolveProductQuery(options)\n return `*[${typeFilter(types)} && defined(${field})]{${cartProductProjection(options)}}`\n}\n\n/** The products with the ids in `$ids`, for the checkout. */\nexport function checkoutProductsQuery(options?: ProductQueryOptions): string {\n const {types} = resolveProductQuery(options)\n return `*[${typeFilter(types)} && _id in $ids]{${cartProductProjection(options)}}`\n}\n\n/**\n * Every version of the products in `productIds` (published, draft, and copies inside a Content\n * Release), with the keys of its variants: what the webhook marks sold out. It needs the raw\n * perspective, a token, and API version 2025-02-19 or later.\n */\nexport function productVersionsQuery(\n productIds: readonly string[],\n options?: ProductQueryOptions,\n): {query: string; params: Record<string, string>} {\n const {types, field} = resolveProductQuery(options)\n const params: Record<string, string> = {}\n const anyVersion = productIds.map((id, index) => {\n params[`product${index}`] = id\n return `sanity::versionOf($product${index})`\n })\n return {\n query: `*[${typeFilter(types)} && defined(${field}) && (${anyVersion.join(' || ') || 'false'})]{_id, \"variants\": ${field}.variants[]._key}`,\n params,\n }\n}\n\n/** The shop settings: shipping regions and whether stock sells out by itself. */\nexport const SHOP_SETTINGS_QUERY = `*[_type == \"${SHOP_SETTINGS_TYPE}\" && _id == \"${SHOP_SETTINGS_ID}\"][0]{shippingRegions[]{_key, title, everywhereElse, countries, rate}, autoSoldOut}`\n","import {STRIPE_SHIPPING_COUNTRIES} from './countries'\nimport {asArray, asNumber, asString, isObject} from './guards'\n\n/** A shipping region with one flat rate per order. */\nexport interface ShippingRegion {\n id: string\n title: string\n /** ISO 3166-1 alpha-2 codes. Empty for the \"everywhere else\" region until resolved. */\n countries: string[]\n everywhereElse: boolean\n /** In currency units. */\n rate: number\n}\n\n/** The shop settings document, as `SHOP_SETTINGS_QUERY` returns it. */\nexport interface ShopSettings {\n shippingRegions?: Array<{\n _key: string\n title?: string | null\n everywhereElse?: boolean | null\n countries?: string[] | null\n rate?: number | null\n }> | null\n autoSoldOut?: boolean | null\n}\n\n/** The shop settings as Sanity returned them, or null when they aren't published yet. */\nexport function readShopSettings(value: unknown): ShopSettings | null {\n if (!isObject(value)) return null\n return {\n shippingRegions: asArray(value.shippingRegions).flatMap((region) =>\n isObject(region) && typeof region._key === 'string'\n ? [\n {\n _key: region._key,\n title: asString(region.title),\n everywhereElse: region.everywhereElse === true,\n countries: asArray(region.countries).filter(\n (country): country is string => typeof country === 'string',\n ),\n rate: asNumber(region.rate),\n },\n ]\n : [],\n ),\n autoSoldOut: typeof value.autoSoldOut === 'boolean' ? value.autoSoldOut : null,\n }\n}\n\n/** Shipping regions from the shop settings, leaving out any that aren't filled in. */\nexport function regionsFromSettings(settings: ShopSettings | null | undefined): ShippingRegion[] {\n return (settings?.shippingRegions ?? []).flatMap((region) =>\n region.title && typeof region.rate === 'number'\n ? [\n {\n id: region._key,\n title: region.title,\n countries: region.countries ?? [],\n everywhereElse: Boolean(region.everywhereElse),\n rate: region.rate,\n },\n ]\n : [],\n )\n}\n\n/**\n * Fills in the \"everywhere else\" region with every country Stripe ships to that no other region\n * lists, and drops country codes Stripe doesn't accept. Regions left without countries are removed.\n */\nexport function resolveRegions(regions: readonly ShippingRegion[]): ShippingRegion[] {\n const supported = new Set<string>(STRIPE_SHIPPING_COUNTRIES)\n const listed = new Set(\n regions.filter((region) => !region.everywhereElse).flatMap((region) => region.countries),\n )\n return regions\n .map((region) => ({\n ...region,\n countries: region.everywhereElse\n ? STRIPE_SHIPPING_COUNTRIES.filter((country) => !listed.has(country))\n : region.countries.filter((country) => supported.has(country)),\n }))\n .filter((region) => region.countries.length > 0)\n}\n\n/** What a cart page needs of each region: no country lists. */\nexport type RegionOption = Pick<ShippingRegion, 'id' | 'title' | 'rate'>\n\n/** The regions a buyer can choose from, the same ones the checkout accepts. */\nexport function regionOptions(settings: ShopSettings | null | undefined): RegionOption[] {\n return resolveRegions(regionsFromSettings(settings)).map(({id, title, rate}) => ({\n id,\n title,\n rate,\n }))\n}\n","import type Stripe from 'stripe'\n\nimport {\n lineName,\n lineStatus,\n MAX_QUANTITY,\n sanitizeLines,\n type CartLine,\n type CartProduct,\n type CheckoutProblem,\n} from '../core/cart'\nimport {toMinorUnits} from '../core/money'\nimport {resolveRegions, type ShippingRegion} from '../core/regions'\nimport {stockLeft, type ProductStock} from '../core/stock'\n\n/**\n * Builds a Checkout Session from the cart and fresh product data. Nothing here trusts a price\n * sent by the browser: the cart holds references, and prices come from `products`.\n */\n\ntype SessionParams = Stripe.Checkout.SessionCreateParams\ntype LineItemParams = Stripe.Checkout.SessionCreateParams.LineItem\n\nexport interface CheckoutInput {\n /** The cart as the browser sent it. Checked here, so pass the raw value. */\n lines: unknown\n /** The shipping region the buyer chose, as the browser sent it. */\n regionId: unknown\n /** Fresh product data (not from a CDN cache) for every product in the cart. */\n products: readonly CartProduct[]\n /** Stock by product id, from `readStock`. Without it nothing is counted. */\n stock?: ReadonlyMap<string, ProductStock>\n /** Shipping regions from the shop settings, before `resolveRegions`. */\n regions: readonly ShippingRegion[]\n /** ISO 4217 code, such as `eur`. */\n currency: string\n successUrl: string\n cancelUrl: string\n /** Marks the session as this shop's, for the webhook and the Stripe Dashboard. */\n integrationIdentifier: string\n /** Stripe tax rate ids (`txr_…`) by tax class, so receipts show the tax. */\n taxRates?: Partial<Record<string, string>>\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n /** Don't say how many are left when a cart asks for too many. */\n hideRemaining?: boolean\n /** When the session stops taking payment, as a Unix time. Stripe's default is 24 hours on. */\n expiresAt?: number\n}\n\nexport type CheckoutResult =\n | {ok: true; params: SessionParams; lines: CartLine[]; region: ShippingRegion}\n | {ok: false; error: 'empty' | 'region' | 'unavailable'; problems: CheckoutProblem[]}\n\nexport function buildCheckoutSession(input: CheckoutInput): CheckoutResult {\n const max = input.maxQuantity ?? MAX_QUANTITY\n const currency = input.currency.toLowerCase()\n const lines = sanitizeLines(input.lines, max)\n if (!lines.length) return {ok: false, error: 'empty', problems: []}\n\n const region = resolveRegions(input.regions).find((candidate) => candidate.id === input.regionId)\n if (!region) return {ok: false, error: 'region', problems: []}\n\n const products = new Map(input.products.map((product) => [product._id, product]))\n const problems: CheckoutProblem[] = []\n const lineItems: LineItemParams[] = []\n\n for (const line of lines) {\n const product = products.get(line.productId)\n const status = lineStatus(product, line.variantKey, max)\n const ref = {\n productId: line.productId,\n ...(line.variantKey ? {variantKey: line.variantKey} : {}),\n }\n if (!product || !status.available) {\n problems.push({\n ...ref,\n ...(product?.title ? {name: product.title} : {}),\n reason: status.available ? 'missing' : status.reason,\n })\n continue\n }\n const name = lineName(product, status.variant)\n\n // Counts are private, so this is the one place a buyer meets them.\n const left = stockLeft(input.stock?.get(line.productId), line.variantKey)\n if (left !== null && left <= 0) {\n problems.push({...ref, name, reason: 'soldOut'})\n continue\n }\n const most = left === null ? status.maxQuantity : Math.min(status.maxQuantity, left)\n if (line.quantity > most) {\n problems.push({\n ...ref,\n name,\n reason: 'tooMany',\n ...(input.hideRemaining ? {} : {maxQuantity: most}),\n })\n continue\n }\n\n const taxClass = product.commerce?.taxClass\n const taxRate = taxClass ? input.taxRates?.[taxClass] : undefined\n lineItems.push({\n quantity: line.quantity,\n price_data: {\n currency,\n unit_amount: toMinorUnits(status.price, currency),\n product_data: {\n name,\n ...(product.imageUrl ? {images: [product.imageUrl]} : {}),\n // Read back by the webhook, to know which product and variant was bought.\n metadata: ref,\n },\n },\n ...(taxRate ? {tax_rates: [taxRate]} : {}),\n })\n }\n\n if (problems.length) return {ok: false, error: 'unavailable', problems}\n\n return {\n ok: true,\n lines,\n region,\n params: {\n mode: 'payment',\n line_items: lineItems,\n // No payment method list: Stripe shows what the Dashboard has switched on.\n shipping_address_collection: {allowed_countries: region.countries},\n shipping_options: [\n {\n shipping_rate_data: {\n type: 'fixed_amount',\n display_name: region.title,\n fixed_amount: {amount: toMinorUnits(region.rate, currency), currency},\n },\n },\n ],\n success_url: input.successUrl,\n cancel_url: input.cancelUrl,\n metadata: {region: region.id, regionTitle: region.title},\n integration_identifier: input.integrationIdentifier,\n ...(input.expiresAt ? {expires_at: input.expiresAt} : {}),\n },\n }\n}\n","import type Stripe from 'stripe'\n\n/**\n * Runs a fulfilment step once per Checkout Session, even when Stripe delivers the event again.\n * The step's completion is recorded in the session's own metadata, so a failed step is retried on\n * the next delivery while finished steps are skipped. It works for free orders too, which have no\n * PaymentIntent.\n *\n * `session` must be read fresh from Stripe's API. Two deliveries arriving in the same moment can\n * both run the step, so it is for steps where a rare repeat is harmless, such as an email sent\n * with an idempotency key. Stock is guarded on its own, in `decrementStock`.\n */\nexport async function once(\n stripe: Stripe,\n session: Stripe.Checkout.Session,\n step: string,\n fn: () => Promise<void>,\n): Promise<'done' | 'skipped'> {\n const key = `fulfilled_${step}`\n if (session.metadata?.[key]) return 'skipped'\n await fn()\n await stripe.checkout.sessions.update(session.id, {\n metadata: {[key]: new Date().toISOString()},\n })\n return 'done'\n}\n","import type Stripe from 'stripe'\n\nexport interface OrderItem {\n /** Sanity document id of the product. */\n productId?: string\n variantKey?: string\n name: string\n quantity: number\n /** For the whole line, in the currency's minor unit (cents). */\n amountTotal: number\n}\n\n/** A paid Checkout Session, read into what a shop needs to send it. It is never saved in Sanity. */\nexport interface Order {\n sessionId: string\n paymentIntentId: string | null\n /** False for a payment made in a sandbox or in test mode. */\n livemode: boolean\n currency: string\n /** Including shipping, in the currency's minor unit. */\n amountTotal: number\n amountShipping: number\n regionId?: string\n regionTitle?: string\n customer: {name: string | null; email: string | null; phone: string | null}\n shipping: {name: string | null; address: Stripe.Address | null}\n items: OrderItem[]\n createdAt: Date\n}\n\n/** Reads a completed session and its line items into an order. */\nexport async function getOrder(stripe: Stripe, session: Stripe.Checkout.Session): Promise<Order> {\n const lineItems = await stripe.checkout.sessions.listLineItems(session.id, {\n limit: 100,\n expand: ['data.price.product'],\n })\n const items = lineItems.data.map((item): OrderItem => {\n const product = item.price?.product\n const metadata =\n product && typeof product === 'object' && !product.deleted ? product.metadata : {}\n const ordered: OrderItem = {\n name: item.description ?? 'Item',\n quantity: item.quantity ?? 1,\n amountTotal: item.amount_total,\n }\n if (metadata.productId) ordered.productId = metadata.productId\n if (metadata.variantKey) ordered.variantKey = metadata.variantKey\n return ordered\n })\n const shippingDetails = session.collected_information?.shipping_details\n const paymentIntent = session.payment_intent\n return {\n sessionId: session.id,\n paymentIntentId:\n typeof paymentIntent === 'string' ? paymentIntent : (paymentIntent?.id ?? null),\n livemode: session.livemode,\n currency: session.currency ?? '',\n amountTotal: session.amount_total ?? 0,\n amountShipping: session.shipping_cost?.amount_total ?? 0,\n ...(session.metadata?.region ? {regionId: session.metadata.region} : {}),\n ...(session.metadata?.regionTitle ? {regionTitle: session.metadata.regionTitle} : {}),\n customer: {\n name: session.customer_details?.name ?? null,\n email: session.customer_details?.email ?? null,\n phone: session.customer_details?.phone ?? null,\n },\n shipping: {name: shippingDetails?.name ?? null, address: shippingDetails?.address ?? null},\n items,\n createdAt: new Date(session.created * 1000),\n }\n}\n","/**\n * As much of a Sanity client as the plugin uses, so `/server` needs no Sanity package of its own.\n * A client from `@sanity/client` or `next-sanity` fits all three.\n */\n\n/** What the plugin asks of a client that can see private documents: everything, uncached. */\nexport interface RawConfig {\n perspective: 'raw'\n useCdn: false\n apiVersion: string\n}\n\n/** Reads from Sanity. For products and the shop settings, use one that skips the CDN. */\nexport interface SanityReader {\n fetch(query: string, params?: Record<string, unknown>): Promise<unknown>\n /** When the client has it, the plugin asks for the raw perspective to read stock. */\n withConfig?(config: RawConfig): SanityReader\n}\n\nexport interface SanityTransaction {\n create(document: {_id: string; _type: string; [field: string]: unknown}): SanityTransaction\n patch(\n documentId: string,\n operations: {dec?: Record<string, number>; set?: Record<string, unknown>},\n ): SanityTransaction\n delete(documentId: string): SanityTransaction\n commit(): Promise<unknown>\n}\n\n/** Reads and lowers stock counts. It needs a token that can write: Editor rights are enough. */\nexport interface SanityWriter extends SanityReader {\n transaction(): SanityTransaction\n withConfig?(config: RawConfig): SanityWriter\n}\n\n/**\n * Stock documents and every version of a product are only visible with a token, the raw\n * perspective and an API version that knows Content Releases.\n */\nconst RAW: RawConfig = {perspective: 'raw', useCdn: false, apiVersion: '2025-02-19'}\n\nexport function rawReader(client: SanityReader): SanityReader {\n return client.withConfig?.(RAW) ?? client\n}\n\nexport function rawWriter(client: SanityWriter): SanityWriter {\n return client.withConfig?.(RAW) ?? client\n}\n","import {asArray, isObject} from '../core/guards'\nimport {publishedId, STOCK_CHANGE_TYPE, stockChangeId, stockId} from '../core/ids'\nimport {productVersionsQuery, resolveProductQuery, type ProductQueryOptions} from '../core/queries'\nimport {readStockDocuments, STOCK_QUERY, stockLeft, type ProductStock} from '../core/stock'\nimport type {OrderItem} from './order'\nimport {rawReader, rawWriter, type SanityReader, type SanityWriter} from './sanity'\n\nconst SAFE_KEY = /^[\\w-]+$/\n\n/** Stripe stops redelivering an event after three days; a month leaves a wide margin. */\nconst KEEP_CHANGES_DAYS = 30\n\n/**\n * The stock of the given products, by product id, read from their private stock documents. The\n * client needs a token. A product without a stock document isn't counted and has no entry.\n *\n * A site can use it on the server to say \"only 2 left\"; it should never send the counts of\n * everything to the browser.\n */\nexport async function readStock(\n client: SanityReader,\n productIds: readonly string[],\n): Promise<Map<string, ProductStock>> {\n const ids = productIds.filter((id) => id && !id.includes('.')).map((id) => stockId(id))\n if (!ids.length) return new Map()\n return readStockDocuments(await rawReader(client).fetch(STOCK_QUERY, {ids}))\n}\n\n/** What an order did to one stock count. */\nexport interface StockChange {\n productId: string\n variantKey?: string\n from: number\n /** Below 0 when more was sold than was in stock. */\n to: number\n}\n\nexport interface StockResult {\n /**\n * `lowered`: counts were changed now. `already`: this session's stock was lowered before, and\n * `changes` is what happened then. `untracked`: nothing in the order has a stock count.\n */\n status: 'lowered' | 'already' | 'untracked'\n changes: StockChange[]\n}\n\nexport interface StockOptions {\n /** Mark a product or variant sold out when its count reaches 0. Default true. */\n autoSoldOut?: boolean\n /** Where the site keeps its products. */\n products?: ProductQueryOptions\n /** The current time, for tests. */\n now?: Date\n}\n\nfunction readChanges(value: unknown): StockChange[] {\n return asArray(value).flatMap((item) => {\n if (!isObject(item)) return []\n const {productId, variantKey, from, to} = item\n if (typeof productId !== 'string') return []\n if (typeof from !== 'number' || typeof to !== 'number') return []\n const change: StockChange = {productId, from, to}\n if (typeof variantKey === 'string') change.variantKey = variantKey\n return [change]\n })\n}\n\ninterface ProductVersion {\n _id: string\n variants: string[]\n}\n\nfunction readVersions(value: unknown): ProductVersion[] {\n return asArray(value).flatMap((item) =>\n isObject(item) && typeof item._id === 'string'\n ? [\n {\n _id: item._id,\n variants: asArray(item.variants).filter(\n (key): key is string => typeof key === 'string',\n ),\n },\n ]\n : [],\n )\n}\n\nconst isConflict = (error: unknown) => isObject(error) && error.statusCode === 409\n\n/**\n * Lowers the stock count of every ordered product or variant that has one. Products and variants\n * without a count are left alone. When a count reaches 0 and `autoSoldOut` is on, the product or\n * variant is marked sold out in every version of the product document, so publishing a draft or a\n * release can't put it back on sale.\n *\n * It happens once per Checkout Session, however often it is called: the counts change in one\n * transaction with a record of the session, and Sanity refuses a second record with the same id.\n * The record holds product ids and counts only.\n */\nexport async function decrementStock(\n client: SanityWriter,\n order: {sessionId: string; items: readonly OrderItem[]},\n options: StockOptions = {},\n): Promise<StockResult> {\n const {field} = resolveProductQuery(options.products)\n const autoSoldOut = options.autoSoldOut ?? true\n const now = options.now ?? new Date()\n\n const ordered: Array<{productId: string; variantKey?: string; quantity: number}> = []\n for (const item of order.items) {\n const {productId, variantKey, quantity} = item\n if (!productId || productId.includes('.')) continue\n if (variantKey && !SAFE_KEY.test(variantKey)) continue\n const same = ordered.find(\n (line) => line.productId === productId && line.variantKey === variantKey,\n )\n if (same) same.quantity += quantity\n else ordered.push(variantKey ? {productId, variantKey, quantity} : {productId, quantity})\n }\n if (!ordered.length) return {status: 'untracked', changes: []}\n\n const raw = rawWriter(client)\n const recordId = stockChangeId(order.sessionId)\n const readRecord = async (): Promise<StockChange[] | null> => {\n const record = await raw.fetch(`*[_id == $id][0]{changes}`, {id: recordId})\n return isObject(record) ? readChanges(record.changes) : null\n }\n\n // Three reads at once: Stripe waits for the webhook's answer before it sends the buyer on.\n const productIds = [...new Set(ordered.map((line) => line.productId))]\n const cutoff = new Date(now.getTime() - KEEP_CHANGES_DAYS * 24 * 60 * 60 * 1000)\n const [before, stock, oldRecords] = await Promise.all([\n readRecord(),\n readStock(raw, productIds),\n raw.fetch(`*[_type == $type && loweredAt < $before][0...25]._id`, {\n type: STOCK_CHANGE_TYPE,\n before: cutoff.toISOString(),\n }),\n ])\n if (before) return {status: 'already', changes: before}\n const old = asArray(oldRecords).filter((id): id is string => typeof id === 'string')\n\n const changes: StockChange[] = []\n const lower = new Map<string, Record<string, number>>()\n for (const {productId, variantKey, quantity} of ordered) {\n const from = stockLeft(stock.get(productId), variantKey)\n if (from === null) continue\n const dec = lower.get(productId) ?? {}\n dec[variantKey ? `variants[_key==\"${variantKey}\"].stockCount` : 'stockCount'] = quantity\n lower.set(productId, dec)\n const change: StockChange = {productId, from, to: from - quantity}\n if (variantKey) change.variantKey = variantKey\n changes.push(change)\n }\n if (!changes.length) return {status: 'untracked', changes: []}\n\n // The public sees \"sold out\", never the count: set it on every version of the product.\n const soldOut = autoSoldOut ? changes.filter((change) => change.to <= 0) : []\n const mark = new Map<string, Record<string, string>>()\n if (soldOut.length) {\n const {query, params} = productVersionsQuery(\n [...new Set(soldOut.map((change) => change.productId))],\n options.products,\n )\n for (const version of readVersions(await raw.fetch(query, params))) {\n const productId = publishedId(version._id)\n const set: Record<string, string> = {}\n for (const change of soldOut) {\n if (change.productId !== productId) continue\n if (!change.variantKey) set[`${field}.availability`] = 'soldOut'\n else if (version.variants.includes(change.variantKey))\n set[`${field}.variants[_key==\"${change.variantKey}\"].availability`] = 'soldOut'\n }\n if (Object.keys(set).length) mark.set(version._id, set)\n }\n }\n\n const transaction = client.transaction().create({\n _id: recordId,\n _type: STOCK_CHANGE_TYPE,\n sessionId: order.sessionId,\n loweredAt: now.toISOString(),\n changes: changes.map((change, index) => Object.assign({_key: `change-${index}`}, change)),\n })\n for (const [productId, dec] of lower) transaction.patch(stockId(productId), {dec})\n for (const [documentId, set] of mark) transaction.patch(documentId, {set})\n for (const documentId of old) transaction.delete(documentId)\n\n try {\n await transaction.commit()\n } catch (error) {\n // The record exists: another delivery of the same event got here first, and nothing changed now.\n if (isConflict(error)) {\n const winner = await readRecord()\n if (winner) return {status: 'already', changes: winner}\n }\n throw error\n }\n return {status: 'lowered', changes}\n}\n","import type Stripe from 'stripe'\n\nimport {readCartProducts, sanitizeLines, type CartLine, type CartProduct} from '../core/cart'\nimport {isObject} from '../core/guards'\nimport {\n checkoutProductsQuery,\n resolveProductQuery,\n SHOP_SETTINGS_QUERY,\n type ProductQueryOptions,\n} from '../core/queries'\nimport {\n readShopSettings,\n regionsFromSettings,\n type ShippingRegion,\n type ShopSettings,\n} from '../core/regions'\nimport type {ProductStock} from '../core/stock'\nimport {buildCheckoutSession} from './checkout'\nimport {classifyEvent, verifyEvent} from './events'\nimport {once} from './once'\nimport {getOrder, type Order} from './order'\nimport type {SanityReader, SanityWriter} from './sanity'\nimport {decrementStock, readStock, type StockChange} from './stock'\n\ntype Handler = (request: Request) => Promise<Response>\ntype Log = (level: 'info' | 'warn' | 'error', message: string) => void\n\n// Info lines go to the server's log too: they say what each payment did to stock and orders.\nconst consoleLog: Log = (level, message) => console[level](message)\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), {\n status,\n headers: {'Content-Type': 'application/json', 'Cache-Control': 'no-store'},\n })\n\nconst reason = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\n/** Stripe wants 30 minutes at least, counted on its own clock: leave a minute's margin. */\nconst SHORTEST_EXPIRY = 31 * 60\n\n// Checkout\n\nexport interface CheckoutMessages {\n /** The checkout can't be started: no key, no shipping regions, or Stripe refused. */\n unavailable: string\n /** The request couldn't be read. */\n invalid: string\n empty: string\n /** No shipping region, or one that no longer exists. */\n region: string\n /** A product in the cart sold out, changed or went away. */\n changed: string\n}\n\nconst CHECKOUT_MESSAGES: CheckoutMessages = {\n unavailable: \"Checkout isn't available right now. Please try again later.\",\n invalid: 'The cart could not be read.',\n empty: 'Your cart is empty.',\n region: 'Choose where to ship the order.',\n changed: 'Some things in your cart have changed. Check them and try again.',\n}\n\nexport interface CheckoutRouteOptions {\n /** From `createStripe`. Without one the route answers 503, so a site runs before it has keys. */\n stripe: Stripe | null | undefined\n /**\n * Reads products and the shop settings, without a token, so it sees published documents only.\n * It must skip the CDN, so prices are fresh.\n */\n client: SanityReader\n /**\n * A Sanity client with a token, to read stock counts, which the public can't. Without it nothing\n * is counted and the shop sells by the on-sale switch alone.\n */\n stockClient?: SanityReader | null\n /** ISO 4217 code, the same one the Studio plugin has. */\n currency: string\n /**\n * Where Stripe sends the buyer after paying: a path on the site or a full address. Include\n * `{CHECKOUT_SESSION_ID}`, such as `/shop/thanks?session_id={CHECKOUT_SESSION_ID}`.\n */\n successUrl: string\n /** Where Stripe's back link goes: the cart. A path or a full address. */\n cancelUrl: string\n /**\n * Marks this shop's sessions: the webhook fulfils only sessions that carry it, and the Stripe\n * Dashboard groups them by it. Give the webhook route the same value. End it with 8 random\n * letters, such as `myshop_qhzvmkpt`.\n */\n integrationIdentifier: string\n /** Where the site keeps its products. Default: `product` documents with a `commerce` field. */\n products?: ProductQueryOptions\n /** Stripe tax rate ids (`txr_…`) by tax class, so receipts show the tax. */\n taxRates?: Partial<Record<string, string>>\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n /** Answer \"not that many left\" without saying how many, when a cart asks for too many. */\n hideRemaining?: boolean\n /** Minutes until an unpaid session expires, about 30 at least. Stripe's default is 24 hours. */\n expiresInMinutes?: number\n /** Changes the session before it is created: promotion codes, phone numbers, consent. */\n extend?: (\n params: Stripe.Checkout.SessionCreateParams,\n context: {lines: CartLine[]; region: ShippingRegion; request: Request},\n ) => Stripe.Checkout.SessionCreateParams | Promise<Stripe.Checkout.SessionCreateParams>\n /** What buyers read when the checkout can't start. */\n messages?: Partial<CheckoutMessages>\n /** Adds the reason to error replies. For development only. */\n debug?: boolean\n log?: Log\n}\n\n/**\n * The site's checkout route. The browser posts the cart's product references and the chosen\n * shipping region; prices, stock and shipping are read fresh from Sanity here.\n *\n * ```ts\n * // app/api/checkout/route.ts\n * export const POST = checkoutRoute({stripe, client, stockClient, currency: 'eur', …})\n * ```\n *\n * It answers `{url}` to go to, or `{error, message, problems}`.\n */\nexport function checkoutRoute(options: CheckoutRouteOptions): Handler {\n const {client, currency, log = consoleLog} = options\n const messages = {...CHECKOUT_MESSAGES, ...options.messages}\n const productsQuery = checkoutProductsQuery(options.products)\n let saidUncounted = false\n\n const unavailable = (detail: string, status = 503) => {\n log('error', `[checkout] ${detail}`)\n return json(\n {error: 'unavailable', message: messages.unavailable, ...(options.debug ? {detail} : {})},\n status,\n )\n }\n\n return async (request) => {\n const {stripe, stockClient} = options\n if (!stripe) return unavailable('There is no Stripe key. Set one in the environment.')\n\n const body: unknown = await request.json().catch(() => null)\n if (!isObject(body)) return json({error: 'invalid', message: messages.invalid}, 400)\n\n const lines = sanitizeLines(body.lines, options.maxQuantity)\n const ids = [...new Set(lines.map((line) => line.productId))]\n let products: CartProduct[]\n let settings: ShopSettings | null\n let stock: Map<string, ProductStock> | undefined\n try {\n const [foundProducts, foundSettings, foundStock] = await Promise.all([\n ids.length ? client.fetch(productsQuery, {ids}) : [],\n client.fetch(SHOP_SETTINGS_QUERY),\n stockClient ? readStock(stockClient, ids) : undefined,\n ])\n products = readCartProducts(foundProducts)\n settings = readShopSettings(foundSettings)\n stock = foundStock\n } catch (error) {\n return unavailable(`Sanity could not be read: ${reason(error)}`, 502)\n }\n if (!stockClient && !saidUncounted) {\n saidUncounted = true\n log('info', '[checkout] No stockClient: stock counts are not checked at checkout.')\n }\n const regions = regionsFromSettings(settings)\n if (!regions.length)\n return unavailable('No shipping regions. Publish the shop settings in the Studio.')\n\n const origin = new URL(request.url).origin\n const result = buildCheckoutSession({\n lines,\n regionId: body.regionId,\n products,\n regions,\n currency,\n successUrl: absoluteUrl(options.successUrl, origin),\n cancelUrl: absoluteUrl(options.cancelUrl, origin),\n integrationIdentifier: options.integrationIdentifier,\n ...(stock ? {stock} : {}),\n ...(options.hideRemaining ? {hideRemaining: true} : {}),\n ...(options.taxRates ? {taxRates: options.taxRates} : {}),\n ...(options.maxQuantity ? {maxQuantity: options.maxQuantity} : {}),\n ...(options.expiresInMinutes\n ? {\n expiresAt:\n Math.floor(Date.now() / 1000) +\n Math.max(SHORTEST_EXPIRY, options.expiresInMinutes * 60),\n }\n : {}),\n })\n\n if (!result.ok) {\n const message = {\n empty: messages.empty,\n region: messages.region,\n unavailable: messages.changed,\n }[result.error]\n return json({error: result.error, message, problems: result.problems}, 409)\n }\n\n try {\n const params = options.extend\n ? await options.extend(result.params, {lines: result.lines, region: result.region, request})\n : result.params\n const session = await stripe.checkout.sessions.create(params)\n if (!session.url)\n return unavailable(`Checkout Session ${session.id} came back without a URL.`, 502)\n return json({url: session.url})\n } catch (error) {\n return unavailable(`Stripe refused the Checkout Session: ${reason(error)}`, 502)\n }\n }\n}\n\n/** Stripe needs full addresses. `{CHECKOUT_SESSION_ID}` must reach it as written, not encoded. */\nfunction absoluteUrl(url: string, origin: string): string {\n return /^https?:\\/\\//.test(url) ? url : `${origin}${url.startsWith('/') ? '' : '/'}${url}`\n}\n\n// Webhook\n\nexport interface OrderContext {\n /** The shop settings as published. Null before they are published. */\n settings: ShopSettings | null\n /** What the order did to stock counts. Empty when nothing ordered is counted. */\n stock: StockChange[]\n /**\n * Set when stock could not be lowered, with the reason: say so to whoever packs the order.\n * Stripe is asked to deliver the event again, and only the stock is tried again.\n */\n stockError?: string\n session: Stripe.Checkout.Session\n stripe: Stripe\n}\n\nexport interface WebhookRouteOptions {\n /** From `createStripe`. */\n stripe: Stripe | null | undefined\n /** The endpoint's signing secret (`whsec_…`), from the environment. */\n webhookSecret: string | undefined\n /** The same value the checkout route has. Sessions without it are not this shop's. */\n integrationIdentifier: string\n /** Reads the shop settings, without a token. It must skip the CDN. */\n client: SanityReader\n /**\n * A Sanity client with a token that can write, to lower stock counts when an order is paid.\n * Without one, stock is left as it is.\n */\n stockClient?: SanityWriter | null\n /** Where the site keeps its products. The same as the checkout route's. */\n products?: ProductQueryOptions\n /**\n * Called once for each paid order, after stock is lowered: send the order email here. Throw\n * and Stripe delivers the event again later; stock isn't lowered twice.\n */\n onOrder?: (order: Order, context: OrderContext) => void | Promise<void>\n /** Called when a delayed payment (a bank transfer, for one) fails. Nothing was fulfilled. */\n onPaymentFailed?: (session: Stripe.Checkout.Session) => void | Promise<void>\n log?: Log\n}\n\n/**\n * The site's Stripe webhook. A paid order of this shop lowers stock and calls `onOrder`, each\n * once per payment however often Stripe delivers the event. Payments made any other way on the\n * same Stripe account (a Payment Link, another site) are left alone. Nothing is fulfilled on the\n * thank-you page.\n *\n * ```ts\n * // app/api/stripe/webhook/route.ts\n * export const POST = webhookRoute({stripe, webhookSecret, integrationIdentifier, client, …})\n * ```\n *\n * Point a Stripe webhook endpoint at it with the events in `WEBHOOK_EVENTS`.\n */\nexport function webhookRoute(options: WebhookRouteOptions): Handler {\n const {client, log = consoleLog} = options\n // Fail when the site starts, not on the first order.\n resolveProductQuery(options.products)\n let saidUncounted = false\n\n /**\n * The session as this client's API version shapes it, or null when it isn't this shop's. The\n * event carries the session as the endpoint's version shapes it, so it is read again.\n */\n const ownSession = async (stripe: Stripe, sessionId: string) => {\n const session = await stripe.checkout.sessions.retrieve(sessionId)\n const own =\n session.mode === 'payment' && session.integration_identifier === options.integrationIdentifier\n if (!own) log('info', `[stripe webhook] ${sessionId}: not one of this shop's, left alone.`)\n return own ? session : null\n }\n\n const fulfil = async (stripe: Stripe, sessionId: string) => {\n const session = await ownSession(stripe, sessionId)\n if (!session) return\n // Trust the payment status Stripe's API gives now over the one in the event.\n if (session.payment_status === 'unpaid') {\n log('info', `[stripe webhook] ${sessionId}: not paid yet.`)\n return\n }\n const [order, foundSettings] = await Promise.all([\n getOrder(stripe, session),\n client.fetch(SHOP_SETTINGS_QUERY),\n ])\n const settings = readShopSettings(foundSettings)\n\n let stock: StockChange[] = []\n let stockError: string | undefined\n if (options.stockClient) {\n try {\n const result = await decrementStock(options.stockClient, order, {\n autoSoldOut: settings?.autoSoldOut ?? true,\n ...(options.products ? {products: options.products} : {}),\n })\n stock = result.changes\n if (result.status === 'lowered') {\n const summary = stock\n .map(\n ({productId, variantKey, from, to}) =>\n `${productId}${variantKey ? ` ${variantKey}` : ''} ${from} → ${to}`,\n )\n .join(', ')\n log('info', `[stripe webhook] ${sessionId}: stock ${summary}`)\n }\n } catch (error) {\n // The order still has to reach the shop. It is told, and Stripe is asked to come back.\n stockError = reason(error)\n log('error', `[stripe webhook] ${sessionId}: stock was not lowered: ${stockError}`)\n }\n } else if (!saidUncounted) {\n saidUncounted = true\n log('info', '[stripe webhook] No stockClient: stock counts are left as they are.')\n }\n\n const {onOrder} = options\n if (onOrder) {\n const outcome = await once(stripe, session, 'order', async () => {\n await onOrder(order, {\n settings,\n stock,\n ...(stockError ? {stockError} : {}),\n session,\n stripe,\n })\n })\n log('info', `[stripe webhook] ${sessionId}: order ${outcome}.`)\n }\n\n if (stockError) throw new Error(`Stock was not lowered: ${stockError}`)\n }\n\n return async (request) => {\n const {stripe, webhookSecret} = options\n if (!stripe || !webhookSecret) {\n log('error', '[stripe webhook] The Stripe key or the webhook secret is not set.')\n return json({error: 'Webhook not configured'}, 503)\n }\n\n // The signature covers the exact bytes Stripe sent, so read the body as text before parsing.\n const payload = await request.text()\n let event: Stripe.Event\n try {\n event = verifyEvent(stripe, payload, request.headers.get('stripe-signature'), webhookSecret)\n } catch (error) {\n log('warn', `[stripe webhook] Signature check failed: ${reason(error)}`)\n return json({error: 'Invalid signature'}, 400)\n }\n\n const result = classifyEvent(event)\n try {\n switch (result.type) {\n case 'paid':\n await fulfil(stripe, result.session.id)\n break\n case 'pending':\n log('info', `[stripe webhook] ${result.session.id}: waiting for a delayed payment.`)\n break\n case 'failed': {\n const session = await ownSession(stripe, result.session.id)\n if (session) {\n log('warn', `[stripe webhook] ${session.id}: the delayed payment failed.`)\n await options.onPaymentFailed?.(session)\n }\n break\n }\n case 'ignored':\n break\n }\n } catch (error) {\n // A 500 makes Stripe deliver the event again. Steps that already finished are skipped then.\n log('error', `[stripe webhook] ${event.type} ${event.id} failed: ${reason(error)}`)\n return json({error: 'Fulfilment failed'}, 500)\n }\n\n return json({received: true})\n }\n}\n","/** The package's version, sent to Stripe with each call. A test keeps it in step with package.json. */\nexport const VERSION = '0.1.0'\n","import {Stripe} from 'stripe'\n\nimport {VERSION} from '../version'\n\n/** The Stripe API version the plugin is written and tested against (stripe-node 23). */\nexport const STRIPE_API_VERSION = '2026-10-28.endive'\n\n/**\n * A Stripe client pinned to the API version the plugin is tested against, so a Stripe upgrade\n * never changes the API under the shop. Pass the key from the site's environment, never from code:\n *\n * ```ts\n * const stripe = process.env.STRIPE_SECRET_KEY ? createStripe(process.env.STRIPE_SECRET_KEY) : null\n * ```\n *\n * A restricted key (`rk_…`) is enough, and safer than a secret key. `config` takes the rest of\n * stripe-node's options, such as `maxNetworkRetries` or `httpClient`.\n */\nexport function createStripe(\n secretKey: string,\n config: Omit<Stripe.StripeConfig, 'apiVersion' | 'appInfo'> = {},\n): Stripe {\n return new Stripe(secretKey, {\n ...config,\n apiVersion: STRIPE_API_VERSION,\n appInfo: {\n name: 'sanity-plugin-stripe',\n version: VERSION,\n url: 'https://github.com/ConfederationStudio/sanity-plugin-stripe',\n },\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":["DELIVERY_KEY","\"delivery\"","\"Location\""],"sources":["../src/server/email.ts","../src/server/events.ts","../src/core/queries.ts","../src/server/checkout.ts","../src/server/once.ts","../src/server/order.ts","../src/server/sanity.ts","../src/server/stock.ts","../src/server/routes.ts","../src/version.ts","../src/server/stripe.ts"],"sourcesContent":["import type Stripe from 'stripe'\n\nimport {describeInterval} from '../core/cart'\nimport {formatMoney} from '../core/money'\nimport type {Order} from './order'\nimport type {StockChange} from './stock'\n\nconst escapeHtml = (value: string) =>\n value.replace(\n /[&<>\"']/g,\n (char) => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;'})[char] ?? char,\n )\n\nfunction addressLines(name: string | null, address: Stripe.Address | null): string[] {\n if (!address) return name ? [name] : []\n const city = [address.postal_code, address.city].filter(Boolean).join(' ')\n return [name, address.line1, address.line2, city, address.state, address.country].filter(\n (line): line is string => Boolean(line),\n )\n}\n\n/** Where the payment is in the Stripe Dashboard: a renewal's invoice, a subscription, a payment. */\nexport function dashboardUrl(\n order: Pick<Order, 'livemode' | 'paymentIntentId' | 'sessionId' | 'invoiceId' | 'subscription'>,\n): string {\n const base = `https://dashboard.stripe.com/${order.livemode ? '' : 'test/'}`\n if (order.invoiceId) return `${base}invoices/${order.invoiceId}`\n if (order.subscription) return `${base}subscriptions/${order.subscription.id}`\n return order.paymentIntentId\n ? `${base}payments/${order.paymentIntentId}`\n : `${base}checkout/sessions/${order.sessionId}`\n}\n\nexport interface OrderEmailOptions {\n /** Starts the subject line. */\n shopName: string\n /** For amounts: a BCP 47 tag such as `en`, `nl-NL`. Default `en`. */\n locale?: string\n /** What the order did to stock counts. Anything sold below 0 is called out. */\n stock?: readonly StockChange[]\n /** Why stock could not be lowered, from the webhook's context. The email says to check it. */\n stockError?: string\n}\n\nexport interface OrderEmail {\n subject: string\n text: string\n html: string\n}\n\n/**\n * A plain order email for the shop: what to pack, where to send it and who paid. It serves a\n * subscription's renewal too, from `onRenewal`.\n */\nexport function orderEmail(order: Order, options: OrderEmailOptions): OrderEmail {\n const {shopName, locale = 'en', stock = [], stockError} = options\n const money = (minor: number) => formatMoney(minor, order.currency, locale)\n const count = order.items.reduce((sum, item) => sum + item.quantity, 0)\n const who = order.shipping.name ?? order.customer.name ?? order.customer.email ?? 'a customer'\n const renewal = Boolean(order.invoiceId)\n const subject = `${order.livemode ? '' : '[Test] '}${shopName} ${renewal ? 'renewal for' : 'order from'} ${who}: ${count} ${count === 1 ? 'item' : 'items'}, ${money(order.amountTotal)}`\n const heading = renewal ? 'Subscription renewal' : 'New order'\n const repeats = order.subscription?.interval\n ? `subscription, ${describeInterval(order.subscription.interval)}`\n : 'subscription'\n // On a renewal every line repeats, so only a first order says which lines do.\n const nameOf = (item: Order['items'][number]) =>\n item.subscription && !renewal ? `${item.name} (${repeats})` : item.name\n const address = addressLines(order.shipping.name, order.shipping.address)\n const contact = [order.customer.email, order.customer.phone].filter((line): line is string =>\n Boolean(line),\n )\n const shippingLabel = order.regionTitle ? `Shipping (${order.regionTitle})` : 'Shipping'\n const link = dashboardUrl(order)\n const date = order.createdAt.toISOString().slice(0, 10)\n\n const oversold = stock\n .filter((change) => change.to < 0)\n .map((change) => {\n const item = order.items.find(\n (candidate) =>\n candidate.productId === change.productId &&\n (candidate.variantKey ?? '') === (change.variantKey ?? ''),\n )\n return item?.name ?? change.productId\n })\n const warnings = [\n ...(oversold.length\n ? [`Check your stock: more was sold than was counted in stock for ${oversold.join(', ')}.`]\n : []),\n ...(stockError\n ? [`Stock counts were not lowered for this order. Change them by hand. (${stockError})`]\n : []),\n ]\n\n const text = [\n `${heading} on ${date}${order.livemode ? '' : ' (test mode, no money moved)'}`,\n '',\n ...warnings.flatMap((warning) => [warning, '']),\n ...order.items.map((item) => `${item.quantity} × ${nameOf(item)} ${money(item.amountTotal)}`),\n `${shippingLabel} ${money(order.amountShipping)}`,\n `Total ${money(order.amountTotal)}`,\n '',\n 'Ship to:',\n ...address,\n '',\n 'Contact:',\n ...contact,\n '',\n `In Stripe: ${link}`,\n ].join('\\n')\n\n const rows = order.items\n .map(\n (item) =>\n `<tr><td style=\"padding:4px 12px 4px 0\">${item.quantity} ×</td><td style=\"padding:4px 12px 4px 0\">${escapeHtml(nameOf(item))}</td><td style=\"padding:4px 0;text-align:right\">${money(item.amountTotal)}</td></tr>`,\n )\n .join('')\n const html = `<div style=\"font-family:Helvetica,Arial,sans-serif;font-size:15px;line-height:1.45;color:#191919\">\n<p>${heading} on ${date}${order.livemode ? '' : ' <strong>(test mode, no money moved)</strong>'}</p>\n${warnings.map((warning) => `<p><strong>${escapeHtml(warning)}</strong></p>\\n`).join('')}<table style=\"border-collapse:collapse\">${rows}\n<tr><td></td><td style=\"padding:4px 12px 4px 0\">${escapeHtml(shippingLabel)}</td><td style=\"padding:4px 0;text-align:right\">${money(order.amountShipping)}</td></tr>\n<tr><td></td><td style=\"padding:4px 12px 4px 0\"><strong>Total</strong></td><td style=\"padding:4px 0;text-align:right\"><strong>${money(order.amountTotal)}</strong></td></tr>\n</table>\n<p><strong>Ship to</strong><br>${address.map(escapeHtml).join('<br>')}</p>\n<p><strong>Contact</strong><br>${contact.map(escapeHtml).join('<br>')}</p>\n<p><a href=\"${link}\">Open the payment in Stripe</a></p>\n</div>`\n\n return {subject, text, html}\n}\n","import type Stripe from 'stripe'\n\n/** Checks the signature and parses the event. Throws when the signature doesn't match. */\nexport function verifyEvent(\n stripe: Stripe,\n payload: string,\n signature: string | null,\n secret: string,\n): Stripe.Event {\n if (!signature) throw new Error('Missing Stripe-Signature header')\n return stripe.webhooks.constructEvent(payload, signature, secret)\n}\n\nexport type CheckoutEvent =\n /** Paid: fulfil the order. */\n | {type: 'paid'; session: Stripe.Checkout.Session}\n /** Completed, but a delayed payment method hasn't settled yet. Wait for async_payment_succeeded. */\n | {type: 'pending'; session: Stripe.Checkout.Session}\n /** A delayed payment failed. Nothing to fulfil. */\n | {type: 'failed'; session: Stripe.Checkout.Session}\n /** An invoice was paid. One that renews a subscription is fulfilled like an order. */\n | {type: 'invoicePaid'; invoice: Stripe.Invoice}\n /** A payment for an invoice failed. Stripe tries again and writes to the customer itself. */\n | {type: 'invoiceFailed'; invoice: Stripe.Invoice}\n /** A subscription ended: cancelled, or given up on after failed payments. */\n | {type: 'subscriptionEnded'; subscription: Stripe.Subscription}\n | {type: 'ignored'}\n\n/**\n * The events a webhook endpoint for the shop listens to. The last three are for subscriptions; a\n * shop without any can leave them out.\n */\nexport const WEBHOOK_EVENTS = [\n 'checkout.session.completed',\n 'checkout.session.async_payment_succeeded',\n 'checkout.session.async_payment_failed',\n 'invoice.paid',\n 'invoice.payment_failed',\n 'customer.subscription.deleted',\n] as const\n\n/** What a webhook event means for the shop. Only `paid` and a renewal's `invoicePaid` are fulfilled. */\nexport function classifyEvent(event: Stripe.Event): CheckoutEvent {\n switch (event.type) {\n case 'checkout.session.completed':\n case 'checkout.session.async_payment_succeeded': {\n const session = event.data.object\n return session.payment_status === 'unpaid'\n ? {type: 'pending', session}\n : {type: 'paid', session}\n }\n case 'checkout.session.async_payment_failed':\n return {type: 'failed', session: event.data.object}\n case 'invoice.paid':\n return {type: 'invoicePaid', invoice: event.data.object}\n case 'invoice.payment_failed':\n return {type: 'invoiceFailed', invoice: event.data.object}\n case 'customer.subscription.deleted':\n return {type: 'subscriptionEnded', subscription: event.data.object}\n default:\n return {type: 'ignored'}\n }\n}\n","import {SHOP_SETTINGS_ID, SHOP_SETTINGS_TYPE} from './ids'\n\n/** Where a site keeps its products, for the queries the plugin writes. */\nexport interface ProductQueryOptions {\n /** Document types that can be bought. Default `['product']`. */\n types?: readonly string[]\n /** The name of the `stripeCommerce` field on those types. Default `commerce`. */\n field?: string\n /** GROQ for the product's name. Default `title`. */\n title?: string\n /** GROQ for the product's slug. Default `slug.current`. */\n slug?: string\n /** GROQ for the URL of the image shown in the cart and on Stripe's page. */\n image?: string\n}\n\nconst NAME = /^[A-Za-z_]\\w*$/\nconst TYPE_NAME = /^[A-Za-z_][\\w.-]*$/\n\n/** The options with their defaults, checked: names end up inside GROQ. */\nexport function resolveProductQuery(options: ProductQueryOptions = {}) {\n const types = options.types?.length ? [...options.types] : ['product']\n const field = options.field ?? 'commerce'\n for (const type of types) {\n if (!TYPE_NAME.test(type))\n throw new TypeError(`sanity-plugin-stripe: \"${type}\" is not a document type name`)\n }\n if (!NAME.test(field)) throw new TypeError(`sanity-plugin-stripe: \"${field}\" is not a field name`)\n return {\n types,\n field,\n title: options.title ?? 'title',\n slug: options.slug ?? 'slug.current',\n image:\n options.image ?? 'coalesce(images[0].asset->url, image.asset->url, mainImage.asset->url)',\n }\n}\n\nconst typeFilter = (types: readonly string[]) =>\n `_type in [${types.map((type) => JSON.stringify(type)).join(', ')}]`\n\n/** The price-and-availability field as the cart and the checkout read it. No stock counts. */\nexport const COMMERCE_PROJECTION = `{availability, price, taxClass, variants[]{_key, label, availability}, subscription{enabled, price, every, unit}}`\n\n/**\n * What the cart and the checkout need of a product, for the site's own queries:\n *\n * ```ts\n * defineQuery(`*[_type == \"product\"]{${cartProductProjection()}}`)\n * ```\n */\nexport function cartProductProjection(options?: ProductQueryOptions): string {\n const {field, title, slug, image} = resolveProductQuery(options)\n return `_id, \"title\": ${title}, \"slug\": ${slug}, \"imageUrl\": ${image}, \"commerce\": ${field}${COMMERCE_PROJECTION}`\n}\n\n/** Every product that has the commerce field filled in, for the cart page. */\nexport function cartProductsQuery(options?: ProductQueryOptions): string {\n const {types, field} = resolveProductQuery(options)\n return `*[${typeFilter(types)} && defined(${field})]{${cartProductProjection(options)}}`\n}\n\n/** The products with the ids in `$ids`, for the checkout. */\nexport function checkoutProductsQuery(options?: ProductQueryOptions): string {\n const {types} = resolveProductQuery(options)\n return `*[${typeFilter(types)} && _id in $ids]{${cartProductProjection(options)}}`\n}\n\n/**\n * Every version of the products in `productIds` (published, draft, and copies inside a Content\n * Release), with the keys of its variants: what the webhook marks sold out. It needs the raw\n * perspective, a token, and API version 2025-02-19 or later.\n */\nexport function productVersionsQuery(\n productIds: readonly string[],\n options?: ProductQueryOptions,\n): {query: string; params: Record<string, string>} {\n const {types, field} = resolveProductQuery(options)\n const params: Record<string, string> = {}\n const anyVersion = productIds.map((id, index) => {\n params[`product${index}`] = id\n return `sanity::versionOf($product${index})`\n })\n return {\n query: `*[${typeFilter(types)} && defined(${field}) && (${anyVersion.join(' || ') || 'false'})]{_id, \"variants\": ${field}.variants[]._key}`,\n params,\n }\n}\n\n/** The shop settings: shipping regions, selling out at 0, and delivery for subscriptions. */\nexport const SHOP_SETTINGS_QUERY = `*[_type == \"${SHOP_SETTINGS_TYPE}\" && _id == \"${SHOP_SETTINGS_ID}\"][0]{shippingRegions[]{_key, title, everywhereElse, countries, rate, freeFrom}, autoSoldOut, subscriptionDelivery}`\n","import type Stripe from 'stripe'\n\nimport {\n lineName,\n lineStatus,\n MAX_QUANTITY,\n sanitizeLines,\n type CartLine,\n type CartProduct,\n type CheckoutProblem,\n type Interval,\n} from '../core/cart'\nimport {toMinorUnits} from '../core/money'\nimport {resolveRegions, type ShippingRegion, type SubscriptionDelivery} from '../core/regions'\nimport {stockLeft, type ProductStock} from '../core/stock'\n\n/**\n * Builds a Checkout Session from the cart and fresh product data. Nothing here trusts a price\n * sent by the browser: the cart holds references, and prices come from `products`.\n */\n\ntype SessionParams = Stripe.Checkout.SessionCreateParams\ntype LineItemParams = Stripe.Checkout.SessionCreateParams.LineItem\n\n/** The key, in a Stripe product's metadata, that marks a subscription's delivery charge. */\nexport const DELIVERY_KEY = 'delivery'\n\nexport interface CheckoutInput {\n /** The cart as the browser sent it. Checked here, so pass the raw value. */\n lines: unknown\n /** The shipping region the buyer chose, as the browser sent it. */\n regionId: unknown\n /** Fresh product data (not from a CDN cache) for every product in the cart. */\n products: readonly CartProduct[]\n /** Stock by product id, from `readStock`. Without it nothing is counted. */\n stock?: ReadonlyMap<string, ProductStock>\n /** Shipping regions from the shop settings, before `resolveRegions`. */\n regions: readonly ShippingRegion[]\n /** ISO 4217 code, such as `eur`. */\n currency: string\n successUrl: string\n cancelUrl: string\n /** Marks the session as this shop's, for the webhook and the Stripe Dashboard. */\n integrationIdentifier: string\n /** Stripe tax rate ids (`txr_…`) by tax class, so receipts show the tax. */\n taxRates?: Partial<Record<string, string>>\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n /** Don't say how many are left when a cart asks for too many. */\n hideRemaining?: boolean\n /** When the session stops taking payment, as a Unix time. Stripe's default is 24 hours on. */\n expiresAt?: number\n /** How a subscription pays for delivery, from the shop settings. Default `free`. */\n subscriptionDelivery?: SubscriptionDelivery\n /** Shows the field for a promotion code on Stripe's page. */\n promotionCodes?: boolean\n}\n\nexport type CheckoutResult =\n | {ok: true; params: SessionParams; lines: CartLine[]; region: ShippingRegion}\n | {ok: false; error: 'empty' | 'region' | 'unavailable'; problems: CheckoutProblem[]}\n\nconst sameInterval = (a: Interval, b: Interval) => a.unit === b.unit && a.every === b.every\n\nexport function buildCheckoutSession(input: CheckoutInput): CheckoutResult {\n const max = input.maxQuantity ?? MAX_QUANTITY\n const currency = input.currency.toLowerCase()\n const lines = sanitizeLines(input.lines, max)\n if (!lines.length) return {ok: false, error: 'empty', problems: []}\n\n const region = resolveRegions(input.regions).find((candidate) => candidate.id === input.regionId)\n if (!region) return {ok: false, error: 'region', problems: []}\n\n const products = new Map(input.products.map((product) => [product._id, product]))\n const problems: CheckoutProblem[] = []\n const lineItems: LineItemParams[] = []\n // A product can be in the cart twice, bought once and subscribed to: both draw on one count.\n const taken = new Map<string, number>()\n // One checkout takes one interval: the first subscription in the cart sets it.\n let interval: Interval | undefined\n // In the currency's minor unit: everything in the cart, and the part of it that repeats.\n let goods = 0\n let repeating = 0\n\n for (const line of lines) {\n const product = products.get(line.productId)\n const status = lineStatus(product, line.variantKey, max, line.plan)\n const ref = {\n productId: line.productId,\n ...(line.variantKey ? {variantKey: line.variantKey} : {}),\n }\n const asked = {...ref, ...(line.plan ? {plan: line.plan} : {})}\n if (!product || !status.available) {\n problems.push({\n ...asked,\n ...(product?.title ? {name: product.title} : {}),\n reason: status.available ? 'missing' : status.reason,\n })\n continue\n }\n const name = lineName(product, status.variant)\n\n // Counts are private, so this is the one place a buyer meets them.\n const counted = stockLeft(input.stock?.get(line.productId), line.variantKey)\n const stockKey = `${line.productId}:${line.variantKey ?? ''}`\n const left = counted === null ? null : counted - (taken.get(stockKey) ?? 0)\n if (left !== null && left <= 0) {\n problems.push({...asked, name, reason: 'soldOut'})\n continue\n }\n const most = left === null ? status.maxQuantity : Math.min(status.maxQuantity, left)\n if (line.quantity > most) {\n problems.push({\n ...asked,\n name,\n reason: 'tooMany',\n ...(input.hideRemaining ? {} : {maxQuantity: most}),\n })\n continue\n }\n taken.set(stockKey, (taken.get(stockKey) ?? 0) + line.quantity)\n\n if (status.interval) {\n interval ??= status.interval\n if (!sameInterval(status.interval, interval)) {\n problems.push({...asked, name, reason: 'otherInterval'})\n continue\n }\n }\n\n const unitAmount = toMinorUnits(status.price, currency)\n goods += unitAmount * line.quantity\n if (status.interval) repeating += unitAmount * line.quantity\n\n const taxClass = product.commerce?.taxClass\n const taxRate = taxClass ? input.taxRates?.[taxClass] : undefined\n lineItems.push({\n quantity: line.quantity,\n price_data: {\n currency,\n unit_amount: unitAmount,\n ...(status.interval\n ? {recurring: {interval: status.interval.unit, interval_count: status.interval.every}}\n : {}),\n product_data: {\n name,\n ...(product.imageUrl ? {images: [product.imageUrl]} : {}),\n // Read back by the webhook, to know which product and variant was bought.\n metadata: ref,\n },\n },\n ...(taxRate ? {tax_rates: [taxRate]} : {}),\n })\n }\n\n if (problems.length) return {ok: false, error: 'unavailable', problems}\n\n // Free delivery goes by the goods before any promotion code: the code is typed on Stripe's\n // page, after this charge is set.\n const rate = toMinorUnits(region.rate, currency)\n const freeFrom = region.freeFrom === undefined ? null : toMinorUnits(region.freeFrom, currency)\n const charge = (amount: number) => (freeFrom !== null && amount >= freeFrom ? 0 : rate)\n\n // Stripe has no shipping options for a subscription. Delivery is either in the subscription's\n // price, or a line of its own that repeats with it; then it goes by what each later parcel\n // holds, the lines that repeat.\n if (interval && input.subscriptionDelivery === 'rate' && charge(repeating) > 0) {\n lineItems.push({\n quantity: 1,\n price_data: {\n currency,\n unit_amount: charge(repeating),\n recurring: {interval: interval.unit, interval_count: interval.every},\n product_data: {name: `Delivery: ${region.title}`, metadata: {[DELIVERY_KEY]: region.id}},\n },\n })\n }\n\n const where = {region: region.id, regionTitle: region.title}\n return {\n ok: true,\n lines,\n region,\n params: {\n mode: interval ? 'subscription' : 'payment',\n line_items: lineItems,\n // No payment method list: Stripe shows what the Dashboard has switched on.\n shipping_address_collection: {allowed_countries: region.countries},\n ...(interval\n ? {subscription_data: {metadata: where}}\n : {\n shipping_options: [\n {\n shipping_rate_data: {\n type: 'fixed_amount',\n display_name: region.title,\n fixed_amount: {amount: charge(goods), currency},\n },\n },\n ],\n }),\n ...(input.promotionCodes ? {allow_promotion_codes: true} : {}),\n success_url: input.successUrl,\n cancel_url: input.cancelUrl,\n metadata: where,\n integration_identifier: input.integrationIdentifier,\n ...(input.expiresAt ? {expires_at: input.expiresAt} : {}),\n },\n }\n}\n","import type Stripe from 'stripe'\n\nconst keyOf = (step: string) => `fulfilled_${step}`\nconst stamp = (step: string) => ({metadata: {[keyOf(step)]: new Date().toISOString()}})\n\n/**\n * Runs a fulfilment step once per Checkout Session, even when Stripe delivers the event again.\n * The step's completion is recorded in the session's own metadata, so a failed step is retried on\n * the next delivery while finished steps are skipped. It works for free orders too, which have no\n * PaymentIntent.\n *\n * `session` must be read fresh from Stripe's API. Two deliveries arriving in the same moment can\n * both run the step, so it is for steps where a rare repeat is harmless, such as an email sent\n * with an idempotency key. Stock is guarded on its own, in `decrementStock`.\n */\nexport async function once(\n stripe: Stripe,\n session: Stripe.Checkout.Session,\n step: string,\n fn: () => Promise<void>,\n): Promise<'done' | 'skipped'> {\n if (session.metadata?.[keyOf(step)]) return 'skipped'\n await fn()\n await stripe.checkout.sessions.update(session.id, stamp(step))\n return 'done'\n}\n\n/**\n * The same for a renewal: once per invoice, recorded in the invoice's metadata. `invoice` must be\n * read fresh from Stripe's API.\n */\nexport async function oncePerInvoice(\n stripe: Stripe,\n invoice: Stripe.Invoice,\n step: string,\n fn: () => Promise<void>,\n): Promise<'done' | 'skipped'> {\n if (invoice.metadata?.[keyOf(step)]) return 'skipped'\n await fn()\n await stripe.invoices.update(invoice.id, stamp(step))\n return 'done'\n}\n","import type Stripe from 'stripe'\n\nimport type {Interval} from '../core/cart'\nimport {DELIVERY_KEY} from './checkout'\n\nexport interface OrderItem {\n /** Sanity document id of the product. */\n productId?: string\n variantKey?: string\n name: string\n quantity: number\n /** For the whole line, in the currency's minor unit (cents). */\n amountTotal: number\n /** True on a line that is a subscription: it is delivered and charged again. */\n subscription?: boolean\n}\n\n/**\n * A paid order, read into what a shop needs to send it: a paid Checkout Session, or a renewal of\n * a subscription that one started. It is never saved in Sanity.\n */\nexport interface Order {\n /** The Checkout Session that took the order. For a renewal, the one that started the subscription. */\n sessionId: string\n paymentIntentId: string | null\n /** Set on a renewal: the invoice Stripe charged for it. */\n invoiceId?: string\n /** Set when the order started a subscription, and on each of its renewals. */\n subscription?: {\n id: string\n /** Null when Stripe holds an interval the plugin doesn't offer, such as days. */\n interval: Interval | null\n }\n /** False for a payment made in a sandbox or in test mode. */\n livemode: boolean\n currency: string\n /** Including shipping, in the currency's minor unit. */\n amountTotal: number\n amountShipping: number\n regionId?: string\n regionTitle?: string\n customer: {name: string | null; email: string | null; phone: string | null}\n shipping: {name: string | null; address: Stripe.Address | null}\n items: OrderItem[]\n createdAt: Date\n}\n\nfunction readInterval(recurring: Stripe.Price.Recurring | null | undefined): Interval | null {\n if (!recurring) return null\n const every = recurring.interval_count\n if (recurring.interval === 'week') return {unit: 'week', every}\n return recurring.interval === 'month' ? {unit: 'month', every} : null\n}\n\nconst metadataOf = (product: string | Stripe.Product | Stripe.DeletedProduct | undefined) =>\n product && typeof product === 'object' && !product.deleted ? product.metadata : {}\n\nconst idOf = (value: string | {id: string} | null | undefined) =>\n typeof value === 'string' ? value : (value?.id ?? null)\n\n/** Reads a completed session and its line items into an order. */\nexport async function getOrder(stripe: Stripe, session: Stripe.Checkout.Session): Promise<Order> {\n const lineItems = await stripe.checkout.sessions.listLineItems(session.id, {\n limit: 100,\n expand: ['data.price.product'],\n })\n const items: OrderItem[] = []\n let interval: Interval | null = null\n // A subscription's delivery charge is a line of its own: it is shipping, not something to pack.\n let delivery = 0\n for (const item of lineItems.data) {\n const metadata = metadataOf(item.price?.product)\n if (metadata[DELIVERY_KEY]) {\n delivery += item.amount_total\n continue\n }\n const ordered: OrderItem = {\n name: item.description ?? 'Item',\n quantity: item.quantity ?? 1,\n amountTotal: item.amount_total,\n }\n if (metadata.productId) ordered.productId = metadata.productId\n if (metadata.variantKey) ordered.variantKey = metadata.variantKey\n if (item.price?.recurring) {\n ordered.subscription = true\n interval ??= readInterval(item.price.recurring)\n }\n items.push(ordered)\n }\n const shippingDetails = session.collected_information?.shipping_details\n const subscriptionId = idOf(session.subscription)\n return {\n sessionId: session.id,\n paymentIntentId: idOf(session.payment_intent),\n ...(subscriptionId ? {subscription: {id: subscriptionId, interval}} : {}),\n livemode: session.livemode,\n currency: session.currency ?? '',\n amountTotal: session.amount_total ?? 0,\n amountShipping: (session.shipping_cost?.amount_total ?? 0) + delivery,\n ...(session.metadata?.region ? {regionId: session.metadata.region} : {}),\n ...(session.metadata?.regionTitle ? {regionTitle: session.metadata.regionTitle} : {}),\n customer: {\n name: session.customer_details?.name ?? null,\n email: session.customer_details?.email ?? null,\n phone: session.customer_details?.phone ?? null,\n },\n shipping: {name: shippingDetails?.name ?? null, address: shippingDetails?.address ?? null},\n items,\n createdAt: new Date(session.created * 1000),\n }\n}\n\n/**\n * Reads a paid renewal into an order: what the subscription sends again, and where. The address\n * is the customer's as it is now, so one changed in the customer portal is the one used.\n *\n * `session` is the Checkout Session that started the subscription.\n */\nexport async function getRenewal(\n stripe: Stripe,\n invoice: Stripe.Invoice,\n session: Stripe.Checkout.Session,\n subscriptionId: string,\n): Promise<Order> {\n // An invoice line names its Stripe product by id. The subscription's items carry the products\n // themselves, with the Sanity ids the checkout put in their metadata.\n const subscription = await stripe.subscriptions.retrieve(subscriptionId, {\n expand: ['items.data.price.product'],\n })\n const sold = new Map<\n string,\n {name: string; metadata: Stripe.Metadata; interval: Interval | null}\n >()\n for (const item of subscription.items.data) {\n const {product, recurring} = item.price\n if (typeof product === 'object' && !product.deleted)\n sold.set(product.id, {\n name: product.name,\n metadata: product.metadata,\n interval: readInterval(recurring),\n })\n }\n\n const items: OrderItem[] = []\n let interval: Interval | null = null\n let delivery = 0\n for (const line of invoice.lines.data) {\n const product = sold.get(line.pricing?.price_details?.product ?? '')\n if (product?.metadata[DELIVERY_KEY]) {\n delivery += line.amount\n continue\n }\n const ordered: OrderItem = {\n name: product?.name ?? line.description ?? 'Item',\n quantity: line.quantity ?? 1,\n amountTotal: line.amount,\n subscription: true,\n }\n if (product?.metadata.productId) ordered.productId = product.metadata.productId\n if (product?.metadata.variantKey) ordered.variantKey = product.metadata.variantKey\n interval ??= product?.interval ?? null\n items.push(ordered)\n }\n\n const started = session.collected_information?.shipping_details\n const where = invoice.parent?.subscription_details?.metadata ?? session.metadata\n return {\n sessionId: session.id,\n paymentIntentId: null,\n invoiceId: invoice.id,\n subscription: {id: subscriptionId, interval},\n livemode: invoice.livemode,\n currency: invoice.currency,\n amountTotal: invoice.total,\n amountShipping: delivery,\n ...(where?.region ? {regionId: where.region} : {}),\n ...(where?.regionTitle ? {regionTitle: where.regionTitle} : {}),\n customer: {\n name: invoice.customer_name,\n email: invoice.customer_email,\n phone: invoice.customer_phone,\n },\n shipping: {\n name: invoice.customer_shipping?.name ?? started?.name ?? null,\n address: invoice.customer_shipping?.address ?? started?.address ?? null,\n },\n items,\n createdAt: new Date((invoice.status_transitions.paid_at ?? invoice.created) * 1000),\n }\n}\n","/**\n * As much of a Sanity client as the plugin uses, so `/server` needs no Sanity package of its own.\n * A client from `@sanity/client` or `next-sanity` fits all three.\n */\n\n/** What the plugin asks of a client that can see private documents: everything, uncached. */\nexport interface RawConfig {\n perspective: 'raw'\n useCdn: false\n apiVersion: string\n}\n\n/** Reads from Sanity. For products and the shop settings, use one that skips the CDN. */\nexport interface SanityReader {\n fetch(query: string, params?: Record<string, unknown>): Promise<unknown>\n /** When the client has it, the plugin asks for the raw perspective to read stock. */\n withConfig?(config: RawConfig): SanityReader\n}\n\nexport interface SanityTransaction {\n create(document: {_id: string; _type: string; [field: string]: unknown}): SanityTransaction\n patch(\n documentId: string,\n operations: {dec?: Record<string, number>; set?: Record<string, unknown>},\n ): SanityTransaction\n delete(documentId: string): SanityTransaction\n commit(): Promise<unknown>\n}\n\n/** Reads and lowers stock counts. It needs a token that can write: Editor rights are enough. */\nexport interface SanityWriter extends SanityReader {\n transaction(): SanityTransaction\n withConfig?(config: RawConfig): SanityWriter\n}\n\n/**\n * Stock documents and every version of a product are only visible with a token, the raw\n * perspective and an API version that knows Content Releases.\n */\nconst RAW: RawConfig = {perspective: 'raw', useCdn: false, apiVersion: '2025-02-19'}\n\nexport function rawReader(client: SanityReader): SanityReader {\n return client.withConfig?.(RAW) ?? client\n}\n\nexport function rawWriter(client: SanityWriter): SanityWriter {\n return client.withConfig?.(RAW) ?? client\n}\n","import {asArray, isObject} from '../core/guards'\nimport {publishedId, STOCK_CHANGE_TYPE, stockChangeId, stockId} from '../core/ids'\nimport {productVersionsQuery, resolveProductQuery, type ProductQueryOptions} from '../core/queries'\nimport {readStockDocuments, STOCK_QUERY, stockLeft, type ProductStock} from '../core/stock'\nimport type {OrderItem} from './order'\nimport {rawReader, rawWriter, type SanityReader, type SanityWriter} from './sanity'\n\nconst SAFE_KEY = /^[\\w-]+$/\n\n/** Stripe stops redelivering an event after three days; a month leaves a wide margin. */\nconst KEEP_CHANGES_DAYS = 30\n\n/**\n * The stock of the given products, by product id, read from their private stock documents. The\n * client needs a token. A product without a stock document isn't counted and has no entry.\n *\n * A site can use it on the server to say \"only 2 left\"; it should never send the counts of\n * everything to the browser.\n */\nexport async function readStock(\n client: SanityReader,\n productIds: readonly string[],\n): Promise<Map<string, ProductStock>> {\n const ids = productIds.filter((id) => id && !id.includes('.')).map((id) => stockId(id))\n if (!ids.length) return new Map()\n return readStockDocuments(await rawReader(client).fetch(STOCK_QUERY, {ids}))\n}\n\n/** What an order did to one stock count. */\nexport interface StockChange {\n productId: string\n variantKey?: string\n from: number\n /** Below 0 when more was sold than was in stock. */\n to: number\n}\n\nexport interface StockResult {\n /**\n * `lowered`: counts were changed now. `already`: this session's stock was lowered before, and\n * `changes` is what happened then. `untracked`: nothing in the order has a stock count.\n */\n status: 'lowered' | 'already' | 'untracked'\n changes: StockChange[]\n}\n\nexport interface StockOptions {\n /** Mark a product or variant sold out when its count reaches 0. Default true. */\n autoSoldOut?: boolean\n /** Where the site keeps its products. */\n products?: ProductQueryOptions\n /** The current time, for tests. */\n now?: Date\n}\n\nfunction readChanges(value: unknown): StockChange[] {\n return asArray(value).flatMap((item) => {\n if (!isObject(item)) return []\n const {productId, variantKey, from, to} = item\n if (typeof productId !== 'string') return []\n if (typeof from !== 'number' || typeof to !== 'number') return []\n const change: StockChange = {productId, from, to}\n if (typeof variantKey === 'string') change.variantKey = variantKey\n return [change]\n })\n}\n\ninterface ProductVersion {\n _id: string\n variants: string[]\n}\n\nfunction readVersions(value: unknown): ProductVersion[] {\n return asArray(value).flatMap((item) =>\n isObject(item) && typeof item._id === 'string'\n ? [\n {\n _id: item._id,\n variants: asArray(item.variants).filter(\n (key): key is string => typeof key === 'string',\n ),\n },\n ]\n : [],\n )\n}\n\nconst isConflict = (error: unknown) => isObject(error) && error.statusCode === 409\n\n/**\n * Lowers the stock count of every ordered product or variant that has one. Products and variants\n * without a count are left alone. When a count reaches 0 and `autoSoldOut` is on, the product or\n * variant is marked sold out in every version of the product document, so publishing a draft or a\n * release can't put it back on sale.\n *\n * It happens once per Checkout Session, however often it is called: the counts change in one\n * transaction with a record of the session, and Sanity refuses a second record with the same id.\n * The record holds product ids and counts only.\n */\nexport async function decrementStock(\n client: SanityWriter,\n order: {sessionId: string; items: readonly OrderItem[]},\n options: StockOptions = {},\n): Promise<StockResult> {\n const {field} = resolveProductQuery(options.products)\n const autoSoldOut = options.autoSoldOut ?? true\n const now = options.now ?? new Date()\n\n const ordered: Array<{productId: string; variantKey?: string; quantity: number}> = []\n for (const item of order.items) {\n const {productId, variantKey, quantity} = item\n if (!productId || productId.includes('.')) continue\n if (variantKey && !SAFE_KEY.test(variantKey)) continue\n const same = ordered.find(\n (line) => line.productId === productId && line.variantKey === variantKey,\n )\n if (same) same.quantity += quantity\n else ordered.push(variantKey ? {productId, variantKey, quantity} : {productId, quantity})\n }\n if (!ordered.length) return {status: 'untracked', changes: []}\n\n const raw = rawWriter(client)\n const recordId = stockChangeId(order.sessionId)\n const readRecord = async (): Promise<StockChange[] | null> => {\n const record = await raw.fetch(`*[_id == $id][0]{changes}`, {id: recordId})\n return isObject(record) ? readChanges(record.changes) : null\n }\n\n // Three reads at once: Stripe waits for the webhook's answer before it sends the buyer on.\n const productIds = [...new Set(ordered.map((line) => line.productId))]\n const cutoff = new Date(now.getTime() - KEEP_CHANGES_DAYS * 24 * 60 * 60 * 1000)\n const [before, stock, oldRecords] = await Promise.all([\n readRecord(),\n readStock(raw, productIds),\n raw.fetch(`*[_type == $type && loweredAt < $before][0...25]._id`, {\n type: STOCK_CHANGE_TYPE,\n before: cutoff.toISOString(),\n }),\n ])\n if (before) return {status: 'already', changes: before}\n const old = asArray(oldRecords).filter((id): id is string => typeof id === 'string')\n\n const changes: StockChange[] = []\n const lower = new Map<string, Record<string, number>>()\n for (const {productId, variantKey, quantity} of ordered) {\n const from = stockLeft(stock.get(productId), variantKey)\n if (from === null) continue\n const dec = lower.get(productId) ?? {}\n dec[variantKey ? `variants[_key==\"${variantKey}\"].stockCount` : 'stockCount'] = quantity\n lower.set(productId, dec)\n const change: StockChange = {productId, from, to: from - quantity}\n if (variantKey) change.variantKey = variantKey\n changes.push(change)\n }\n if (!changes.length) return {status: 'untracked', changes: []}\n\n // The public sees \"sold out\", never the count: set it on every version of the product.\n const soldOut = autoSoldOut ? changes.filter((change) => change.to <= 0) : []\n const mark = new Map<string, Record<string, string>>()\n if (soldOut.length) {\n const {query, params} = productVersionsQuery(\n [...new Set(soldOut.map((change) => change.productId))],\n options.products,\n )\n for (const version of readVersions(await raw.fetch(query, params))) {\n const productId = publishedId(version._id)\n const set: Record<string, string> = {}\n for (const change of soldOut) {\n if (change.productId !== productId) continue\n if (!change.variantKey) set[`${field}.availability`] = 'soldOut'\n else if (version.variants.includes(change.variantKey))\n set[`${field}.variants[_key==\"${change.variantKey}\"].availability`] = 'soldOut'\n }\n if (Object.keys(set).length) mark.set(version._id, set)\n }\n }\n\n const transaction = client.transaction().create({\n _id: recordId,\n _type: STOCK_CHANGE_TYPE,\n sessionId: order.sessionId,\n loweredAt: now.toISOString(),\n changes: changes.map((change, index) => Object.assign({_key: `change-${index}`}, change)),\n })\n for (const [productId, dec] of lower) transaction.patch(stockId(productId), {dec})\n for (const [documentId, set] of mark) transaction.patch(documentId, {set})\n for (const documentId of old) transaction.delete(documentId)\n\n try {\n await transaction.commit()\n } catch (error) {\n // The record exists: another delivery of the same event got here first, and nothing changed now.\n if (isConflict(error)) {\n const winner = await readRecord()\n if (winner) return {status: 'already', changes: winner}\n }\n throw error\n }\n return {status: 'lowered', changes}\n}\n","import type Stripe from 'stripe'\n\nimport {readCartProducts, sanitizeLines, type CartLine, type CartProduct} from '../core/cart'\nimport {isObject} from '../core/guards'\nimport {\n checkoutProductsQuery,\n resolveProductQuery,\n SHOP_SETTINGS_QUERY,\n type ProductQueryOptions,\n} from '../core/queries'\nimport {\n readShopSettings,\n regionsFromSettings,\n type ShippingRegion,\n type ShopSettings,\n} from '../core/regions'\nimport type {ProductStock} from '../core/stock'\nimport {buildCheckoutSession} from './checkout'\nimport {classifyEvent, verifyEvent} from './events'\nimport {once, oncePerInvoice} from './once'\nimport {getOrder, getRenewal, type Order} from './order'\nimport type {SanityReader, SanityWriter} from './sanity'\nimport {decrementStock, readStock, type StockChange} from './stock'\n\ntype Handler = (request: Request) => Promise<Response>\ntype Log = (level: 'info' | 'warn' | 'error', message: string) => void\n\n// Info lines go to the server's log too: they say what each payment did to stock and orders.\nconst consoleLog: Log = (level, message) => console[level](message)\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), {\n status,\n headers: {'Content-Type': 'application/json', 'Cache-Control': 'no-store'},\n })\n\nconst reason = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\n/** Stripe wants 30 minutes at least, counted on its own clock: leave a minute's margin. */\nconst SHORTEST_EXPIRY = 31 * 60\n\n// Checkout\n\nexport interface CheckoutMessages {\n /** The checkout can't be started: no key, no shipping regions, or Stripe refused. */\n unavailable: string\n /** The request couldn't be read. */\n invalid: string\n empty: string\n /** No shipping region, or one that no longer exists. */\n region: string\n /** A product in the cart sold out, changed or went away. */\n changed: string\n}\n\nconst CHECKOUT_MESSAGES: CheckoutMessages = {\n unavailable: \"Checkout isn't available right now. Please try again later.\",\n invalid: 'The cart could not be read.',\n empty: 'Your cart is empty.',\n region: 'Choose where to ship the order.',\n changed: 'Some things in your cart have changed. Check them and try again.',\n}\n\nexport interface CheckoutRouteOptions {\n /** From `createStripe`. Without one the route answers 503, so a site runs before it has keys. */\n stripe: Stripe | null | undefined\n /**\n * Reads products and the shop settings, without a token, so it sees published documents only.\n * It must skip the CDN, so prices are fresh.\n */\n client: SanityReader\n /**\n * A Sanity client with a token, to read stock counts, which the public can't. Without it nothing\n * is counted and the shop sells by the on-sale switch alone.\n */\n stockClient?: SanityReader | null\n /** ISO 4217 code, the same one the Studio plugin has. */\n currency: string\n /**\n * Where Stripe sends the buyer after paying: a path on the site or a full address. Include\n * `{CHECKOUT_SESSION_ID}`, such as `/shop/thanks?session_id={CHECKOUT_SESSION_ID}`.\n */\n successUrl: string\n /** Where Stripe's back link goes: the cart. A path or a full address. */\n cancelUrl: string\n /**\n * Marks this shop's sessions: the webhook fulfils only sessions that carry it, and the Stripe\n * Dashboard groups them by it. Give the webhook route the same value. End it with 8 random\n * letters, such as `myshop_qhzvmkpt`.\n */\n integrationIdentifier: string\n /** Where the site keeps its products. Default: `product` documents with a `commerce` field. */\n products?: ProductQueryOptions\n /** Stripe tax rate ids (`txr_…`) by tax class, so receipts show the tax. */\n taxRates?: Partial<Record<string, string>>\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n /** Answer \"not that many left\" without saying how many, when a cart asks for too many. */\n hideRemaining?: boolean\n /** Minutes until an unpaid session expires, about 30 at least. Stripe's default is 24 hours. */\n expiresInMinutes?: number\n /**\n * Shows a field for a promotion code on Stripe's page. Codes are made in the Stripe Dashboard,\n * one per checkout. On a subscription, the code's coupon says whether renewals are discounted.\n */\n promotionCodes?: boolean\n /** Changes the session before it is created: phone numbers, consent, a customer. */\n extend?: (\n params: Stripe.Checkout.SessionCreateParams,\n context: {lines: CartLine[]; region: ShippingRegion; request: Request},\n ) => Stripe.Checkout.SessionCreateParams | Promise<Stripe.Checkout.SessionCreateParams>\n /** What buyers read when the checkout can't start. */\n messages?: Partial<CheckoutMessages>\n /** Adds the reason to error replies. For development only. */\n debug?: boolean\n log?: Log\n}\n\n/**\n * The site's checkout route. The browser posts the cart's product references and the chosen\n * shipping region; prices, stock and shipping are read fresh from Sanity here. A cart with a\n * subscription in it becomes a subscription checkout; anything bought once in the same cart is\n * charged with the first payment.\n *\n * ```ts\n * // app/api/checkout/route.ts\n * export const POST = checkoutRoute({stripe, client, stockClient, currency: 'eur', …})\n * ```\n *\n * It answers `{url}` to go to, or `{error, message, problems}`.\n */\nexport function checkoutRoute(options: CheckoutRouteOptions): Handler {\n const {client, currency, log = consoleLog} = options\n const messages = {...CHECKOUT_MESSAGES, ...options.messages}\n const productsQuery = checkoutProductsQuery(options.products)\n let saidUncounted = false\n\n const unavailable = (detail: string, status = 503) => {\n log('error', `[checkout] ${detail}`)\n return json(\n {error: 'unavailable', message: messages.unavailable, ...(options.debug ? {detail} : {})},\n status,\n )\n }\n\n return async (request) => {\n const {stripe, stockClient} = options\n if (!stripe) return unavailable('There is no Stripe key. Set one in the environment.')\n\n const body: unknown = await request.json().catch(() => null)\n if (!isObject(body)) return json({error: 'invalid', message: messages.invalid}, 400)\n\n const lines = sanitizeLines(body.lines, options.maxQuantity)\n const ids = [...new Set(lines.map((line) => line.productId))]\n let products: CartProduct[]\n let settings: ShopSettings | null\n let stock: Map<string, ProductStock> | undefined\n try {\n const [foundProducts, foundSettings, foundStock] = await Promise.all([\n ids.length ? client.fetch(productsQuery, {ids}) : [],\n client.fetch(SHOP_SETTINGS_QUERY),\n stockClient ? readStock(stockClient, ids) : undefined,\n ])\n products = readCartProducts(foundProducts)\n settings = readShopSettings(foundSettings)\n stock = foundStock\n } catch (error) {\n return unavailable(`Sanity could not be read: ${reason(error)}`, 502)\n }\n if (!stockClient && !saidUncounted) {\n saidUncounted = true\n log('info', '[checkout] No stockClient: stock counts are not checked at checkout.')\n }\n const regions = regionsFromSettings(settings)\n if (!regions.length)\n return unavailable('No shipping regions. Publish the shop settings in the Studio.')\n\n const origin = new URL(request.url).origin\n const result = buildCheckoutSession({\n lines,\n regionId: body.regionId,\n products,\n regions,\n currency,\n successUrl: absoluteUrl(options.successUrl, origin),\n cancelUrl: absoluteUrl(options.cancelUrl, origin),\n integrationIdentifier: options.integrationIdentifier,\n ...(stock ? {stock} : {}),\n ...(options.hideRemaining ? {hideRemaining: true} : {}),\n ...(options.promotionCodes ? {promotionCodes: true} : {}),\n ...(settings?.subscriptionDelivery\n ? {subscriptionDelivery: settings.subscriptionDelivery}\n : {}),\n ...(options.taxRates ? {taxRates: options.taxRates} : {}),\n ...(options.maxQuantity ? {maxQuantity: options.maxQuantity} : {}),\n ...(options.expiresInMinutes\n ? {\n expiresAt:\n Math.floor(Date.now() / 1000) +\n Math.max(SHORTEST_EXPIRY, options.expiresInMinutes * 60),\n }\n : {}),\n })\n\n if (!result.ok) {\n const message = {\n empty: messages.empty,\n region: messages.region,\n unavailable: messages.changed,\n }[result.error]\n return json({error: result.error, message, problems: result.problems}, 409)\n }\n\n try {\n const params = options.extend\n ? await options.extend(result.params, {lines: result.lines, region: result.region, request})\n : result.params\n const session = await stripe.checkout.sessions.create(params)\n if (!session.url)\n return unavailable(`Checkout Session ${session.id} came back without a URL.`, 502)\n return json({url: session.url})\n } catch (error) {\n return unavailable(`Stripe refused the Checkout Session: ${reason(error)}`, 502)\n }\n }\n}\n\n/** Stripe needs full addresses. `{CHECKOUT_SESSION_ID}` must reach it as written, not encoded. */\nfunction absoluteUrl(url: string, origin: string): string {\n return /^https?:\\/\\//.test(url) ? url : `${origin}${url.startsWith('/') ? '' : '/'}${url}`\n}\n\n// Webhook\n\nconst subscriptionOf = (invoice: Stripe.Invoice): string | null => {\n const subscription = invoice.parent?.subscription_details?.subscription\n return typeof subscription === 'string' ? subscription : (subscription?.id ?? null)\n}\n\nexport interface OrderContext {\n /** The shop settings as published. Null before they are published. */\n settings: ShopSettings | null\n /** What the order did to stock counts. Empty when nothing ordered is counted. */\n stock: StockChange[]\n /**\n * Set when stock could not be lowered, with the reason: say so to whoever packs the order.\n * Stripe is asked to deliver the event again, and only the stock is tried again.\n */\n stockError?: string\n /** The Checkout Session that took the order. */\n session: Stripe.Checkout.Session\n stripe: Stripe\n}\n\nexport interface RenewalContext extends OrderContext {\n /** The paid invoice. `session` is the Checkout Session that started the subscription. */\n invoice: Stripe.Invoice\n}\n\nexport interface SubscriptionContext {\n /** The Checkout Session that started the subscription. */\n session: Stripe.Checkout.Session\n stripe: Stripe\n}\n\nexport interface WebhookRouteOptions {\n /** From `createStripe`. */\n stripe: Stripe | null | undefined\n /** The endpoint's signing secret (`whsec_…`), from the environment. */\n webhookSecret: string | undefined\n /** The same value the checkout route has. Sessions without it are not this shop's. */\n integrationIdentifier: string\n /** Reads the shop settings, without a token. It must skip the CDN. */\n client: SanityReader\n /**\n * A Sanity client with a token that can write, to lower stock counts when an order is paid.\n * Without one, stock is left as it is.\n */\n stockClient?: SanityWriter | null\n /** Where the site keeps its products. The same as the checkout route's. */\n products?: ProductQueryOptions\n /**\n * Called once for each paid order, after stock is lowered: send the order email here. Throw\n * and Stripe delivers the event again later; stock isn't lowered twice.\n */\n onOrder?: (order: Order, context: OrderContext) => void | Promise<void>\n /** Called when a delayed payment (a bank transfer, for one) fails. Nothing was fulfilled. */\n onPaymentFailed?: (session: Stripe.Checkout.Session) => void | Promise<void>\n /**\n * Called once for each paid renewal of a subscription, after stock is lowered: there is a\n * parcel to send again. The order's address is the customer's as it is now. The first payment\n * of a subscription is not a renewal: it reaches `onOrder`.\n */\n onRenewal?: (order: Order, context: RenewalContext) => void | Promise<void>\n /**\n * Called when a renewal's payment fails. Stripe tries again and writes to the customer by\n * itself, so there is nothing that has to be done here. It can be called more than once.\n */\n onRenewalFailed?: (invoice: Stripe.Invoice, context: SubscriptionContext) => void | Promise<void>\n /** Called when a subscription ends: cancelled, or given up on after failed payments. */\n onSubscriptionEnded?: (\n subscription: Stripe.Subscription,\n context: SubscriptionContext,\n ) => void | Promise<void>\n /**\n * Called for every other event the endpoint receives, once its signature is checked: a\n * dispute, a refund. These are not sorted by shop: they are the Stripe account's.\n */\n onEvent?: (event: Stripe.Event, context: {stripe: Stripe}) => void | Promise<void>\n log?: Log\n}\n\n/**\n * The site's Stripe webhook. A paid order of this shop lowers stock and calls `onOrder`, each\n * once per payment however often Stripe delivers the event. A paid renewal of a subscription\n * does the same and calls `onRenewal`. Payments made any other way on the same Stripe account (a\n * Payment Link, another site) are left alone. Nothing is fulfilled on the thank-you page.\n *\n * ```ts\n * // app/api/stripe/webhook/route.ts\n * export const POST = webhookRoute({stripe, webhookSecret, integrationIdentifier, client, …})\n * ```\n *\n * Point a Stripe webhook endpoint at it with the events in `WEBHOOK_EVENTS`.\n */\nexport function webhookRoute(options: WebhookRouteOptions): Handler {\n const {client, log = consoleLog} = options\n // Fail when the site starts, not on the first order.\n resolveProductQuery(options.products)\n let saidUncounted = false\n\n /**\n * The session as this client's API version shapes it, or null when it isn't this shop's. The\n * event carries the session as the endpoint's version shapes it, so it is read again.\n */\n const ownSession = async (stripe: Stripe, sessionId: string) => {\n const session = await stripe.checkout.sessions.retrieve(sessionId)\n const own =\n session.mode !== 'setup' && session.integration_identifier === options.integrationIdentifier\n if (!own) log('info', `[stripe webhook] ${sessionId}: not one of this shop's, left alone.`)\n return own ? session : null\n }\n\n /** Lowers stock for a paid order or renewal, once per `id`: a session's or an invoice's. */\n const lowerStock = async (id: string, order: Order, settings: ShopSettings | null) => {\n if (!options.stockClient) {\n if (!saidUncounted) {\n saidUncounted = true\n log('info', '[stripe webhook] No stockClient: stock counts are left as they are.')\n }\n return {stock: []}\n }\n try {\n const result = await decrementStock(\n options.stockClient,\n {sessionId: id, items: order.items},\n {\n autoSoldOut: settings?.autoSoldOut ?? true,\n ...(options.products ? {products: options.products} : {}),\n },\n )\n if (result.status === 'lowered') {\n const summary = result.changes\n .map(\n ({productId, variantKey, from, to}) =>\n `${productId}${variantKey ? ` ${variantKey}` : ''} ${from} → ${to}`,\n )\n .join(', ')\n log('info', `[stripe webhook] ${id}: stock ${summary}`)\n }\n return {stock: result.changes}\n } catch (error) {\n // The order still has to reach the shop. It is told, and Stripe is asked to come back.\n const stockError = reason(error)\n log('error', `[stripe webhook] ${id}: stock was not lowered: ${stockError}`)\n return {stock: [], stockError}\n }\n }\n\n const fulfil = async (stripe: Stripe, sessionId: string) => {\n const session = await ownSession(stripe, sessionId)\n if (!session) return\n // Trust the payment status Stripe's API gives now over the one in the event.\n if (session.payment_status === 'unpaid') {\n log('info', `[stripe webhook] ${sessionId}: not paid yet.`)\n return\n }\n const [order, foundSettings] = await Promise.all([\n getOrder(stripe, session),\n client.fetch(SHOP_SETTINGS_QUERY),\n ])\n const settings = readShopSettings(foundSettings)\n const {stock, stockError} = await lowerStock(sessionId, order, settings)\n\n const {onOrder} = options\n if (onOrder) {\n const outcome = await once(stripe, session, 'order', async () => {\n await onOrder(order, {\n settings,\n stock,\n ...(stockError ? {stockError} : {}),\n session,\n stripe,\n })\n })\n log('info', `[stripe webhook] ${sessionId}: order ${outcome}.`)\n }\n\n if (stockError) throw new Error(`Stock was not lowered: ${stockError}`)\n }\n\n /**\n * The Checkout Session that started a subscription, or null when the subscription isn't this\n * shop's. It is looked up in Stripe, not taken from a label on the invoice.\n */\n const startedBy = async (stripe: Stripe, subscriptionId: string) => {\n const {data} = await stripe.checkout.sessions.list({subscription: subscriptionId, limit: 1})\n const session = data[0]\n const own = session?.integration_identifier === options.integrationIdentifier\n if (!own) log('info', `[stripe webhook] ${subscriptionId}: not one of this shop's, left alone.`)\n return own ? session : null\n }\n\n const renew = async (stripe: Stripe, invoiceId: string) => {\n // Read again: as this client's API version shapes it, and with the status it has now.\n const invoice = await stripe.invoices.retrieve(invoiceId)\n const subscriptionId = subscriptionOf(invoice)\n // A subscription's first invoice is the order the checkout's own event fulfils.\n if (!subscriptionId || invoice.billing_reason !== 'subscription_cycle') return\n const session = await startedBy(stripe, subscriptionId)\n if (!session) return\n if (invoice.status !== 'paid') {\n log('info', `[stripe webhook] ${invoiceId}: not paid yet.`)\n return\n }\n const [order, foundSettings] = await Promise.all([\n getRenewal(stripe, invoice, session, subscriptionId),\n client.fetch(SHOP_SETTINGS_QUERY),\n ])\n const settings = readShopSettings(foundSettings)\n // The renewal is paid already, so it can take a count below 0: `onRenewal` is told.\n const {stock, stockError} = await lowerStock(invoiceId, order, settings)\n\n const {onRenewal} = options\n if (onRenewal) {\n const outcome = await oncePerInvoice(stripe, invoice, 'renewal', async () => {\n await onRenewal(order, {\n settings,\n stock,\n ...(stockError ? {stockError} : {}),\n session,\n invoice,\n stripe,\n })\n })\n log('info', `[stripe webhook] ${invoiceId}: renewal ${outcome}.`)\n }\n\n if (stockError) throw new Error(`Stock was not lowered: ${stockError}`)\n }\n\n return async (request) => {\n const {stripe, webhookSecret} = options\n if (!stripe || !webhookSecret) {\n log('error', '[stripe webhook] The Stripe key or the webhook secret is not set.')\n return json({error: 'Webhook not configured'}, 503)\n }\n\n // The signature covers the exact bytes Stripe sent, so read the body as text before parsing.\n const payload = await request.text()\n let event: Stripe.Event\n try {\n event = verifyEvent(stripe, payload, request.headers.get('stripe-signature'), webhookSecret)\n } catch (error) {\n log('warn', `[stripe webhook] Signature check failed: ${reason(error)}`)\n return json({error: 'Invalid signature'}, 400)\n }\n\n const result = classifyEvent(event)\n try {\n switch (result.type) {\n case 'paid':\n await fulfil(stripe, result.session.id)\n break\n case 'pending':\n log('info', `[stripe webhook] ${result.session.id}: waiting for a delayed payment.`)\n break\n case 'failed': {\n const session = await ownSession(stripe, result.session.id)\n if (session) {\n log('warn', `[stripe webhook] ${session.id}: the delayed payment failed.`)\n await options.onPaymentFailed?.(session)\n }\n break\n }\n case 'invoicePaid':\n await renew(stripe, result.invoice.id)\n break\n case 'invoiceFailed': {\n const {onRenewalFailed} = options\n if (!onRenewalFailed) break\n const invoice = await stripe.invoices.retrieve(result.invoice.id)\n const subscriptionId = subscriptionOf(invoice)\n const session = subscriptionId ? await startedBy(stripe, subscriptionId) : null\n if (session) {\n log('warn', `[stripe webhook] ${invoice.id}: a renewal's payment failed.`)\n await onRenewalFailed(invoice, {session, stripe})\n }\n break\n }\n case 'subscriptionEnded': {\n const {onSubscriptionEnded} = options\n if (!onSubscriptionEnded) break\n const session = await startedBy(stripe, result.subscription.id)\n if (session) {\n const subscription = await stripe.subscriptions.retrieve(result.subscription.id)\n log('info', `[stripe webhook] ${subscription.id}: the subscription ended.`)\n await onSubscriptionEnded(subscription, {session, stripe})\n }\n break\n }\n case 'ignored':\n await options.onEvent?.(event, {stripe})\n break\n }\n } catch (error) {\n // A 500 makes Stripe deliver the event again. Steps that already finished are skipped then.\n log('error', `[stripe webhook] ${event.type} ${event.id} failed: ${reason(error)}`)\n return json({error: 'Fulfilment failed'}, 500)\n }\n\n return json({received: true})\n }\n}\n\n// Customer portal\n\nexport interface PortalRouteOptions {\n /** From `createStripe`. */\n stripe: Stripe | null | undefined\n /** What a visitor reads when the portal can't be opened. */\n message?: string\n log?: Log\n}\n\n/** How long the portal's address is kept before Stripe is asked for it again. */\nconst PORTAL_LINK_AGE = 5 * 60 * 1000\n\n/**\n * Sends a visitor to Stripe's customer portal, where a subscriber changes their card or address,\n * pauses or cancels, and finds their invoices. There are no accounts to build: the visitor types\n * their email on Stripe's page and Stripe emails them a link in.\n *\n * ```ts\n * // app/account/route.ts\n * export const GET = portalRoute({stripe})\n * ```\n *\n * Switch the portal's login link on once, in the Stripe Dashboard under Settings, Billing,\n * Customer portal, and choose there what subscribers may change.\n */\nexport function portalRoute(options: PortalRouteOptions): () => Promise<Response> {\n const {log = consoleLog} = options\n let link: {url: string; at: number} | undefined\n\n const unavailable = (detail: string) => {\n log('error', `[portal] ${detail}`)\n return new Response(options.message ?? \"The account page isn't available right now.\", {\n status: 503,\n headers: {'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store'},\n })\n }\n\n return async () => {\n const {stripe} = options\n if (!stripe) return unavailable('There is no Stripe key. Set one in the environment.')\n if (!link || Date.now() - link.at > PORTAL_LINK_AGE) {\n try {\n // The Dashboard's own portal settings are the default configuration. A site can also\n // have made one through the API, which never is the default: either will do.\n const {data} = await stripe.billingPortal.configurations.list({active: true, limit: 100})\n const linked = data.filter((configuration) => configuration.login_page.url)\n const url = (linked.find((configuration) => configuration.is_default) ?? linked[0])\n ?.login_page.url\n if (!url)\n return unavailable(\n 'The customer portal has no login link. Activate it in the Stripe Dashboard: Settings, Billing, Customer portal.',\n )\n link = {url, at: Date.now()}\n } catch (error) {\n return unavailable(`Stripe could not be asked for the portal's address: ${reason(error)}`)\n }\n }\n return new Response(null, {\n status: 303,\n headers: {'Location': link.url, 'Cache-Control': 'no-store'},\n })\n }\n}\n","/** The package's version, sent to Stripe with each call. A test keeps it in step with package.json. */\nexport const VERSION = '0.2.0'\n","import {Stripe} from 'stripe'\n\nimport {VERSION} from '../version'\n\n/** The Stripe API version the plugin is written and tested against (stripe-node 23). */\nexport const STRIPE_API_VERSION = '2026-10-28.endive'\n\n/**\n * A Stripe client pinned to the API version the plugin is tested against, so a Stripe upgrade\n * never changes the API under the shop. Pass the key from the site's environment, never from code:\n *\n * ```ts\n * const stripe = process.env.STRIPE_SECRET_KEY ? createStripe(process.env.STRIPE_SECRET_KEY) : null\n * ```\n *\n * A restricted key (`rk_…`) is enough, and safer than a secret key. `config` takes the rest of\n * stripe-node's options, such as `maxNetworkRetries` or `httpClient`.\n */\nexport function createStripe(\n secretKey: string,\n config: Omit<Stripe.StripeConfig, 'apiVersion' | 'appInfo'> = {},\n): Stripe {\n return new Stripe(secretKey, {\n ...config,\n apiVersion: STRIPE_API_VERSION,\n appInfo: {\n name: 'sanity-plugin-stripe',\n version: VERSION,\n url: 'https://github.com/ConfederationStudio/sanity-plugin-stripe',\n },\n })\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { i as isObject, n as asNumber, t as asArray } from "./guards-CAcETsf7.js";
1
+ import { i as asNumber, o as isObject, r as asArray } from "./countries-Csjmv4lY.js";
2
2
  /** Schema type names. They are saved in content, so they never change. */
3
- const COMMERCE_TYPE = "stripeCommerce", VARIANT_TYPE = "stripeVariant", SHIPPING_REGION_TYPE = "stripeShippingRegion", SHOP_SETTINGS_TYPE = "stripeShopSettings", SHOP_SETTINGS_ID = "stripeShopSettings", STOCK_TYPE = "stripe.stock";
3
+ const COMMERCE_TYPE = "stripeCommerce", VARIANT_TYPE = "stripeVariant", SUBSCRIPTION_TYPE = "stripeSubscription", SHIPPING_REGION_TYPE = "stripeShippingRegion", SHOP_SETTINGS_TYPE = "stripeShopSettings", SHOP_SETTINGS_ID = "stripeShopSettings", STOCK_TYPE = "stripe.stock";
4
4
  function stockId(productId) {
5
5
  return `stripe.stock.${productId}`;
6
6
  }
@@ -35,12 +35,6 @@ function readStockDocuments(value) {
35
35
  function stockLeft(stock, variantKey) {
36
36
  return stock ? variantKey ? stock.variants.find((variant) => variant._key === variantKey)?.stockCount ?? null : stock.stockCount : null;
37
37
  }
38
- /**
39
- * Every country Stripe Checkout accepts in `shipping_address_collection.allowed_countries`
40
- * (stripe-node 23, API version 2026-10-28.endive). A region set to "everywhere else" ships to
41
- * these, minus the countries the other regions list.
42
- */
43
- const STRIPE_SHIPPING_COUNTRIES = /* @__PURE__ */ "AC.AD.AE.AF.AG.AI.AL.AM.AO.AQ.AR.AT.AU.AW.AX.AZ.BA.BB.BD.BE.BF.BG.BH.BI.BJ.BL.BM.BN.BO.BQ.BR.BS.BT.BV.BW.BY.BZ.CA.CD.CF.CG.CH.CI.CK.CL.CM.CN.CO.CR.CV.CW.CY.CZ.DE.DJ.DK.DM.DO.DZ.EC.EE.EG.EH.ER.ES.ET.FI.FJ.FK.FO.FR.GA.GB.GD.GE.GF.GG.GH.GI.GL.GM.GN.GP.GQ.GR.GS.GT.GU.GW.GY.HK.HN.HR.HT.HU.ID.IE.IL.IM.IN.IO.IQ.IS.IT.JE.JM.JO.JP.KE.KG.KH.KI.KM.KN.KR.KW.KY.KZ.LA.LB.LC.LI.LK.LR.LS.LT.LU.LV.LY.MA.MC.MD.ME.MF.MG.MK.ML.MM.MN.MO.MQ.MR.MS.MT.MU.MV.MW.MX.MY.MZ.NA.NC.NE.NG.NI.NL.NO.NP.NR.NU.NZ.OM.PA.PE.PF.PG.PH.PK.PL.PM.PN.PR.PS.PT.PY.QA.RE.RO.RS.RU.RW.SA.SB.SC.SD.SE.SG.SH.SI.SJ.SK.SL.SM.SN.SO.SR.SS.ST.SV.SX.SZ.TA.TC.TD.TF.TG.TH.TJ.TK.TL.TM.TN.TO.TR.TT.TV.TW.TZ.UA.UG.US.UY.UZ.VA.VC.VE.VG.VN.VU.WF.WS.XK.YE.YT.ZA.ZM.ZW".split("."), EU_COUNTRIES = /* @__PURE__ */ "AT.BE.BG.CY.CZ.DE.DK.EE.ES.FI.FR.GR.HR.HU.IE.IT.LT.LU.LV.MT.NL.PL.PT.RO.SE.SI.SK".split(".");
44
- export { stockLeft as a, SHOP_SETTINGS_ID as c, STOCK_TYPE as d, VARIANT_TYPE as f, stockId as h, readStockDocuments as i, SHOP_SETTINGS_TYPE as l, stockChangeId as m, STRIPE_SHIPPING_COUNTRIES as n, COMMERCE_TYPE as o, publishedId as p, STOCK_QUERY as r, SHIPPING_REGION_TYPE as s, EU_COUNTRIES as t, STOCK_CHANGE_TYPE as u };
38
+ export { SHIPPING_REGION_TYPE as a, STOCK_CHANGE_TYPE as c, VARIANT_TYPE as d, publishedId as f, COMMERCE_TYPE as i, STOCK_TYPE as l, stockId as m, readStockDocuments as n, SHOP_SETTINGS_ID as o, stockChangeId as p, stockLeft as r, SHOP_SETTINGS_TYPE as s, STOCK_QUERY as t, SUBSCRIPTION_TYPE as u };
45
39
 
46
- //# sourceMappingURL=countries-BuqQF1AW.js.map
40
+ //# sourceMappingURL=stock-DAsSGayt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stock-DAsSGayt.js","names":[],"sources":["../src/core/ids.ts","../src/core/stock.ts"],"sourcesContent":["/** Schema type names. They are saved in content, so they never change. */\nexport const COMMERCE_TYPE = 'stripeCommerce'\nexport const VARIANT_TYPE = 'stripeVariant'\nexport const SUBSCRIPTION_TYPE = 'stripeSubscription'\nexport const SHIPPING_REGION_TYPE = 'stripeShippingRegion'\nexport const SHOP_SETTINGS_TYPE = 'stripeShopSettings'\n\n/**\n * The one shop settings document, named after its type. Its id has no dot, so the site reads it\n * without a token: it holds what the cart shows anyway.\n */\nexport const SHOP_SETTINGS_ID = 'stripeShopSettings'\n\n/**\n * A product's stock counts. They are kept out of the product document, which anyone can read\n * from a public dataset: a dot in an id means Sanity serves the document to members and tokens\n * only.\n */\nexport const STOCK_TYPE = 'stripe.stock'\n\nexport function stockId(productId: string): string {\n return `stripe.stock.${productId}`\n}\n\n/**\n * What one paid Checkout Session did to stock, written in the same transaction as the change, so\n * the same payment never lowers stock twice. It holds product ids and counts, nothing about the\n * buyer, and is deleted after a month.\n */\nexport const STOCK_CHANGE_TYPE = 'stripe.stockChange'\n\nexport function stockChangeId(sessionId: string): string {\n return `stripe.stockChange.${sessionId.replaceAll(/[^\\w-]/g, '-')}`\n}\n\n/** The id a product is published under, whichever version of it a form is editing. */\nexport function publishedId(documentId: string): string {\n if (documentId.startsWith('drafts.')) return documentId.slice('drafts.'.length)\n if (documentId.startsWith('versions.')) return documentId.split('.').slice(2).join('.')\n return documentId\n}\n","import {asArray, asNumber, isObject} from './guards'\nimport {STOCK_TYPE, stockId} from './ids'\n\n/** A product's stock, as its private stock document holds it. */\nexport interface ProductStock {\n /** For a product without variants. Null when it isn't counted. */\n stockCount: number | null\n /** One entry per counted variant, under the variant's `_key`. */\n variants: Array<{_key: string; stockCount: number | null}>\n}\n\n/** The stock documents with the ids in `$ids`. Only a client with a token gets an answer. */\nexport const STOCK_QUERY = `*[_type == \"${STOCK_TYPE}\" && _id in $ids]{_id, stockCount, variants[]{_key, stockCount}}`\n\n/** Stock documents as Sanity returned them, by product id. */\nexport function readStockDocuments(value: unknown): Map<string, ProductStock> {\n const prefix = stockId('')\n const stock = new Map<string, ProductStock>()\n for (const item of asArray(value)) {\n if (!isObject(item) || typeof item._id !== 'string' || !item._id.startsWith(prefix)) continue\n stock.set(item._id.slice(prefix.length), {\n stockCount: asNumber(item.stockCount),\n variants: asArray(item.variants).flatMap((variant) =>\n isObject(variant) && typeof variant._key === 'string'\n ? [{_key: variant._key, stockCount: asNumber(variant.stockCount)}]\n : [],\n ),\n })\n }\n return stock\n}\n\n/** How many are left of a product or one of its variants. Null when it isn't counted. */\nexport function stockLeft(stock: ProductStock | undefined, variantKey?: string): number | null {\n if (!stock) return null\n if (!variantKey) return stock.stockCount\n return stock.variants.find((variant) => variant._key === variantKey)?.stockCount ?? null\n}\n"],"mappings":";;AACA,MAAa,gBAAgB,kBAChB,eAAe,iBACf,oBAAoB,sBACpB,uBAAuB,wBACvB,qBAAqB,sBAMrB,mBAAmB,sBAOnB,aAAa;AAE1B,SAAgB,QAAQ,WAA2B;CACjD,OAAO,gBAAgB;AACzB;;;;;;AAOA,MAAa,oBAAoB;AAEjC,SAAgB,cAAc,WAA2B;CACvD,OAAO,sBAAsB,UAAU,WAAW,WAAW,GAAG;AAClE;;AAGA,SAAgB,YAAY,YAA4B;CAGtD,OAFI,WAAW,WAAW,SAAS,IAAU,WAAW,MAAM,CAAgB,IAC1E,WAAW,WAAW,WAAW,IAAU,WAAW,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAC/E;AACT;;AC5BA,MAAa,cAAc,eAAe,WAAW;;AAGrD,SAAgB,mBAAmB,OAA2C;CAC5E,IAAM,SAAS,QAAQ,EAAE,GACnB,wBAAQ,IAAI,IAA0B;CAC5C,KAAK,IAAM,QAAQ,QAAQ,KAAK,GAC9B,AAAK,SAAS,IAAI,KAAK,OAAO,KAAK,OAAQ,YAAa,KAAK,IAAI,WAAW,MAAM,KAClF,MAAM,IAAI,KAAK,IAAI,MAAM,OAAO,MAAM,GAAG;EACvC,YAAY,SAAS,KAAK,UAAU;EACpC,UAAU,QAAQ,KAAK,QAAQ,CAAC,CAAC,SAAS,YACxC,SAAS,OAAO,KAAK,OAAO,QAAQ,QAAS,WACzC,CAAC;GAAC,MAAM,QAAQ;GAAM,YAAY,SAAS,QAAQ,UAAU;EAAC,CAAC,IAC/D,CAAC,CACP;CACF,CAAC;CAEH,OAAO;AACT;;AAGA,SAAgB,UAAU,OAAiC,YAAoC;CAG7F,OAFK,QACA,aACE,MAAM,SAAS,MAAM,YAAY,QAAQ,SAAS,UAAU,CAAC,EAAE,cAAc,OAD5D,MAAM,aADX;AAGrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-stripe",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Sell from a Sanity site with Stripe Checkout: price, stock and variants in the Studio, a cart for React, and checkout and webhook routes for the site.",
5
5
  "keywords": [
6
6
  "checkout",
@@ -31,39 +31,11 @@
31
31
  "sideEffects": false,
32
32
  "types": "./dist/index.d.ts",
33
33
  "exports": {
34
- ".": {
35
- "source": "./src/index.ts",
36
- "default": "./dist/index.js"
37
- },
38
- "./react": {
39
- "source": "./src/react/index.ts",
40
- "default": "./dist/react.js"
41
- },
42
- "./server": {
43
- "source": "./src/server/index.ts",
44
- "default": "./dist/server.js"
45
- },
34
+ ".": "./dist/index.js",
35
+ "./react": "./dist/react.js",
36
+ "./server": "./dist/server.js",
46
37
  "./package.json": "./package.json"
47
38
  },
48
- "publishConfig": {
49
- "exports": {
50
- ".": "./dist/index.js",
51
- "./react": "./dist/react.js",
52
- "./server": "./dist/server.js",
53
- "./package.json": "./package.json"
54
- }
55
- },
56
- "scripts": {
57
- "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
58
- "check": "tsc --noEmit && oxlint && vitest run",
59
- "format": "oxfmt",
60
- "link-watch": "plugin-kit link-watch",
61
- "lint": "oxlint",
62
- "prepublishOnly": "npm run build",
63
- "test": "vitest run",
64
- "typecheck": "tsc --noEmit",
65
- "watch": "pkg-utils watch --strict"
66
- },
67
39
  "dependencies": {
68
40
  "@sanity/icons": "^5.2.3",
69
41
  "@sanity/ui": "^4.4.10"
@@ -109,5 +81,14 @@
109
81
  "engines": {
110
82
  "node": ">=20.19 <22 || >=22.12"
111
83
  },
112
- "packageManager": "pnpm@12.6.0"
113
- }
84
+ "scripts": {
85
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
86
+ "check": "tsc --noEmit && oxlint && vitest run",
87
+ "format": "oxfmt",
88
+ "link-watch": "plugin-kit link-watch",
89
+ "lint": "oxlint",
90
+ "test": "vitest run",
91
+ "typecheck": "tsc --noEmit",
92
+ "watch": "pkg-utils watch --strict"
93
+ }
94
+ }
@@ -1,111 +0,0 @@
1
- import { i as isObject, t as asArray } from "./guards-CAcETsf7.js";
2
- /**
3
- * Products as a query with `cartProductProjection` returns them. Anything that isn't a document
4
- * is dropped; the fields are checked where they are used, in `lineStatus`.
5
- */
6
- function readCartProducts(value) {
7
- return asArray(value).filter((item) => isObject(item) && typeof item._id == "string");
8
- }
9
- /** One cart line per product and variant. */
10
- function lineKey(line) {
11
- return `${line.productId}:${line.variantKey ?? ""}`;
12
- }
13
- /**
14
- * Turns anything (stored JSON, a request body) into valid cart lines: drops what doesn't parse,
15
- * merges duplicates and keeps quantities between 1 and `max`.
16
- *
17
- * A product id with a dot is dropped: a published document's id has none, so a draft or a copy
18
- * inside a release can never be put in a cart, whatever the site's Sanity client is able to read.
19
- */
20
- function sanitizeLines(input, max = 10) {
21
- let merged = /* @__PURE__ */ new Map();
22
- for (let item of asArray(input).slice(0, 100)) {
23
- if (!isObject(item)) continue;
24
- let { productId, variantKey, quantity } = item;
25
- if (typeof productId != "string" || !productId || productId.length > 200 || productId.includes(".") || variantKey !== void 0 && (typeof variantKey != "string" || variantKey.length > 200) || typeof quantity != "number" || !Number.isFinite(quantity)) continue;
26
- let line = {
27
- productId,
28
- quantity: 0,
29
- ...variantKey ? { variantKey } : {}
30
- }, key = lineKey(line), total = (merged.get(key)?.quantity ?? 0) + Math.floor(quantity);
31
- total < 1 || merged.set(key, {
32
- ...line,
33
- quantity: Math.min(total, max)
34
- });
35
- }
36
- return [...merged.values()];
37
- }
38
- /**
39
- * Whether a product (and variant) is on sale, as far as anyone can see. Stock counts are private,
40
- * so they are checked by the checkout route, on the server.
41
- */
42
- function lineStatus(product, variantKey, max = 10) {
43
- let commerce = product?.commerce;
44
- if (!product || !commerce) return {
45
- available: !1,
46
- reason: "missing"
47
- };
48
- if (commerce.availability === "soldOut") return {
49
- available: !1,
50
- reason: "soldOut"
51
- };
52
- if (typeof commerce.price != "number") return {
53
- available: !1,
54
- reason: "noPrice"
55
- };
56
- let variants = commerce.variants ?? [], variant;
57
- if (variants.length) {
58
- if (!variantKey) return {
59
- available: !1,
60
- reason: "chooseVariant"
61
- };
62
- if (variant = variants.find((candidate) => candidate._key === variantKey), !variant) return {
63
- available: !1,
64
- reason: "variantMissing"
65
- };
66
- if (variant.availability === "soldOut") return {
67
- available: !1,
68
- reason: "soldOut"
69
- };
70
- } else if (variantKey) return {
71
- available: !1,
72
- reason: "variantMissing"
73
- };
74
- return {
75
- available: !0,
76
- maxQuantity: max,
77
- price: commerce.price,
78
- variant
79
- };
80
- }
81
- /** Display name for a line: the product title, plus the variant label when there is one. */
82
- function lineName(product, variant) {
83
- let title = product.title ?? "Product";
84
- return variant?.label ? `${title} – ${variant.label}` : title;
85
- }
86
- /** The problems in a checkout route's reply, as the browser reads them. */
87
- function readCheckoutProblems(value) {
88
- return asArray(value).filter((item) => isObject(item) && typeof item.productId == "string" && typeof item.reason == "string");
89
- }
90
- /** Joins the cart's lines with their products, as read from Sanity for the cart page. */
91
- function cartRows(lines, products, max = 10) {
92
- let byId = new Map(products.map((product) => [product._id, product]));
93
- return lines.map((line) => {
94
- let product = byId.get(line.productId), status = lineStatus(product, line.variantKey, max), name = product ? lineName(product, status.available ? status.variant : void 0) : "Item", quantity = status.available ? Math.min(line.quantity, status.maxQuantity) : 0;
95
- return {
96
- line,
97
- product,
98
- status,
99
- name,
100
- quantity,
101
- total: status.available ? status.price * quantity : 0
102
- };
103
- });
104
- }
105
- /** What the cart's lines add up to, in currency units, before shipping. */
106
- function cartSubtotal(rows) {
107
- return rows.reduce((sum, row) => sum + row.total, 0);
108
- }
109
- export { lineStatus as a, sanitizeLines as c, lineName as i, cartSubtotal as n, readCartProducts as o, lineKey as r, readCheckoutProblems as s, cartRows as t };
110
-
111
- //# sourceMappingURL=cart-CSjxtjq-.js.map