Payment processing with Stripe, Square, and SumUp.
A provider-agnostic payment interface with adapters for all three providers. Handles checkout sessions, webhook verification, refunds, and idempotent payment processing.
Provider Interface
PaymentProvider defines the common contract:
- Create single and multi-listing checkout sessions
- Validate incoming webhooks using each provider's checks
- Retrieve session details and process refunds
Error subclass for user-facing payment validation errors (e.g. invalid phone number). These propagate through safeAsync so the message can be shown to the user.
Build a checkout's signed metadata the way the given provider needs it, with the caps read from the provider registry: build the logical shape within the per-value and entry caps, pack the small fields into one entry when the provider needs that to fit, and enforce the caps on the shape that reaches the wire. SumUp's caps are unbounded (its metadata is stored locally, never sent to the provider), which makes the enforcement a no-op for it.
Build checkout session metadata from booking data (items already compact).
Render a priced order into a provider's line-item array: each ticket line via
line, each extra (booking fee, …) via extra. Providers supply the two
shape callbacks; the ordering (tickets, then extras) matches what Stripe and
Square built by hand before.
Cache a provider API client keyed on its config. Reuses the cached client while the config is unchanged and recreates it when the config changes; returns null when the provider is unconfigured.
Build a standalone-line CheckoutItem for one listing — the shared
shape every single-listing checkout (direct-to-provider QR booking, the
plain public booking form) builds its one-item items array from.
Create a withClient helper that runs an operation with a lazily-resolved client. Returns null if the client is not available or the operation fails.
Only the fields that grow with what the buyer selected can realistically exceed the per-value limit. Form validation already holds every other field well below the smallest provider limit of 255.
Normalize validated session metadata into the canonical SessionMetadata shape.
Resolve the provider for callbacks and completion of payments that already exist. New sales use getActivePaymentProvider.
True when the listing still has spots for this quantity on this date (no date for a date-less listing, whose capacity is one running total).
Load one explicitly named provider implementation.
Build a provider's createCheckoutSession: call the provider's own create
function, read the session id and URL off whatever shape it returns, and map
that to a shared CheckoutSessionResult — all inside the standard checkout
error guard. Each provider only supplies its create call and how to read the
id/url. A null create answer means the provider is not configured; a non-null
answer must contain both documented fields.
Collapse the packable small fields into one JSON b entry, dropping them
from the top level. Falsy values are omitted (the "" = absent convention), so
the b entry only appears when at least one packed field is actually present.
The one door every webhook body comes through. A body that is not JSON is refused the same way for every provider. What the body then means differs — a signed provider posts the event itself, SumUp posts two fields we build one from — so each provider passes its own reading, run outside the catch so a bug in it never reads as bad JSON. A reading that answers nothing means the body is JSON this provider cannot read, which earns the same refusal.
Process a single-listing booking.
Convert single-listing answerIds to the per-listing format used in metadata
Convert registration line items to compact, edge-tagged booking items (v2).
Each package member line carries ITS OWN package edge (k:"p", r=its group
id) so the webhook can revalidate each line's nodeKey — an order can book
several packages, so the edge is per line, never order-wide; folded children
(in allocations) and standalone lines stay untagged. See signed-metadata.ts.
Normalise a provider timestamp to the ledger's canonical ISO 8601 form
(YYYY-MM-DDTHH:mm:ss.sssZ), or undefined when it's absent or unparseable.
Compact the resolved modifier specs to id/quantity references for metadata.
Wrap a checkout operation, converting PaymentUserError to { error } and letting unexpected failures propagate. Used by both provider adapters.
Operations every configured payment provider supplies.
Booking result — callers map this to their response format
Registration intent for checkout (one or more listings)
Single item within a checkout — one bookable PATH. A listing booked through two overlapping packages (or a package plus its own standalone row) in one order is one item per path, each with its own quantity and price.
Result of creating a checkout session.
Run an operation with the lazily-resolved client. Returns null when the client is unconfigured or the operation fails (unless the error should propagate). The named type keeps the contract visible to callers of the widely-used provider runners so a signature drift fails at the definition instead of leaking to callers.
Shared shape for a provider credential check in connection-test results.
Provider implementation for work on an existing payment.
A modifier resolved for a specific checkout — the input the pricing pipeline
applies. Eligibility (scope, stock, codes) is decided upstream; by the time a
spec reaches pricing it is known to apply. value is the signed calc value
(see modifierDelta); listingIds scopes which items it is charged on
(null = the whole order); quantity is how many the buyer took (1 for an
automatic or code modifier, more for an opt-in add-on).
Supported payment provider identifiers
Valid payment status value
Everything retrieving a checkout session by id can come back as.
Validated logical metadata. Missing wire values are represented by "";
provider-specific packing is removed at extraction.
Set up a webhook endpoint for a provider. Some providers (e.g. Stripe) support programmatic creation; recreating any existing endpoint returns a fresh signing secret. Shared by the provider interface and each provider's own implementation so the signature can't drift.
A validated payment session returned after checkout completion
Everything resolving a webhook's session can come back as: the session; "skip" to acknowledge without processing; "retry" to answer with the fixed retryable refusal; a rejection carrying a paid charge the boundary could not read; or null for an event that is provably not ours.
Result of webhook endpoint setup
Result of webhook signature verification
Stubbable API for internal calls (testable via spyOn, like stripeApi/squareApi)
Schema for valid payment status values. "failed" is a terminal non-payment (declined or expired checkout) — distinct from "unpaid", which may still complete.
Safely execute async operation, returning null on error. Re-throws PaymentUserError so user-facing messages propagate.
Usage
import * as mod from "docs/payments.ts";