Skip to content

Build Checkout Session creation endpoint (cart validation + Stripe session) #1586

Description

@ArendPeter

Question / Task

Part of the Phase 1 Payment System: Design & Build Map, graduated from Design backend data model & Stripe integration flow for Phase 1 payments and re-designed for a cart by Redesign checkout data model & flow for a multi-product cart (see that ticket's resolution comment for the full rationale). Blocked by the migration/config ticket (stripeCheckoutSessionsDB table, pricing constants) — and by the follow-up schema amendment flagged on PR #1589 / #1584 (product column → jsonb).

Build the Checkout Session creation endpoint, e.g. POST /API/Elections/:id/CheckoutSession (adjust to match this repo's existing route conventions in elections.routes.ts/roll.routes.ts).

Product catalog: a small in-repo registry (shared config or a dedicated module), one entry per product type, each knowing how to price itself, build its own Stripe price_data (name/description/image), validate a requested quantity, and — used by the webhook ticket — apply its own fulfillment effect. Phase 1 registers exactly one entry: voter-limit blocks (BLOCK_SIZE voters per unit, PRICE_PER_BLOCK_CENTS each, capped so voter_limit can't exceed 5,000 — the Custom tier stays contact-us only, no Checkout Session path for it). This registry is the extensibility point for future add-ons (e.g. a custom-slug product) — building a second entry is explicitly out of scope for this ticket and this map; only the one-entry shape needs to exist.

Request — the frontend sends the cart it displayed, as a list of items, not raw prices:

{ "items": [ { "type": "voter_limit_block", "quantity": 3 } ] }

Validation, per item, in order:

  1. Reject any type not present in the Phase-1 catalog (i.e. anything but voter_limit_block) — there's nothing else to accept yet, so this is a flat reject, not a partial-fulfillment case.
  2. For voter_limit_block: recompute new_voter_limit = election.voter_limit + quantity * BLOCK_SIZE; reject if > 5000.

On success: build one Stripe price_data line item per cart item from the catalog (never client-supplied price/copy — the client's request only carries type + quantity), compute the total (amount_cents) and the aggregate voter_count_granted (sum across any voter_limit_block items) from the backend's own catalog numbers, assemble the full product JSON blob (an exact snapshot of every line item's price_data + type-specific payload, e.g. { type: 'voter_limit_block', blocks: 3, ...price_data }), insert one pending row into stripeCheckoutSessionsDB, then create the Stripe Checkout Session (mode: 'payment', hosted page, line_items built from the catalog price_data per item, submit_type: 'pay' — never 'donate', client_reference_id = election_id, metadata = {election_id, user_id, checkout_session_row_id}, custom_text.after_submit carrying the "this is a program service fee, not tax-deductible" disclaimer).

Both the reactive trigger (organizer blocked while adding voters) and the proactive "Upgrade Election" button/cart should hit this same endpoint/contract — no separate code paths.

Response: the Stripe-hosted Checkout URL for the frontend to redirect to.

Requires STRIPE_SECRET_KEY (test-mode value already retrieved and stored per the "Retrieve Stripe test-mode API keys" ticket).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions