Skip to content

Add voter_limit column & stripeCheckoutSessionsDB table - #1589

Open
ArendPeter wants to merge 4 commits into
Equal-Vote:mainfrom
ArendPeter:voter-limit-migration-1584
Open

ArendPeter wants to merge 4 commits into
Equal-Vote:mainfrom
ArendPeter:voter-limit-migration-1584

Conversation

@ArendPeter

@ArendPeter ArendPeter commented Aug 24, 2026

Copy link
Copy Markdown
Member

Human Summary

This is the first in a series of PRs adding stripe integration and payment support. I'm starting by storing the voter limit per election, and setting up a stripe table for managing tracking the transactions.

Summary

  • Adds voter_limit (integer, not null) to electionDB, backfilled to GREATEST(100, current head roll count) so no election that already has more voters loses them.
  • New elections get voter_limit = FREE_TIER_LIMIT (100), set server-side in ElectionsDB.createElection, matching the existing pattern for create_date/update_date/head.
  • Creates stripeCheckoutSessionsDB (Stripe checkout bookkeeping + webhook idempotency key), with a thin-wrapper model class (StripeCheckoutSessionsDB) and mock, mirroring EmailEvents.ts's style.
  • Adds shared pricing constants: FREE_TIER_LIMIT (100), BLOCK_SIZE (200), PRICE_PER_BLOCK_CENTS (1000).
  • Fixes packages/shared's "./config" export map, which pointed directly at SharedConfig.js/.d.ts and so silently bypassed config/index.ts (and anything re-exported from it, including the new pricing config) for any consumer importing @equal-vote/star-vote-shared/config.

This is the first of 4 graduated build tickets off Design backend data model & Stripe integration flow for Phase 1 payments, part of the Phase 1 Payment System map. Resolves #1584.

Test plan

  • npm run build -ws (shared, backend, frontend) — all green
  • npm test -w @equal-vote/star-vote-backend — 27 suites / 211 tests pass
  • Migration up/down round-tripped against a disposable Postgres container
  • Backfill verified by hand: election with 2 rolls → 100, election with 150 head rolls → 150 (historical roll rows and non-head election versions correctly excluded/included), election with 0 rolls → 100

🤖 Generated with Claude Code

@netlify

netlify Bot commented Aug 24, 2026

Copy link
Copy Markdown

Deploy Preview for bettervoting ready!

Name Link
🔨 Latest commit f6cb070
🔍 Latest deploy log https://app.netlify.com/projects/bettervoting/deploys/6a8e8f769ebc280008bfaae3
😎 Deploy Preview https://deploy-preview-1589--bettervoting.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@ArendPeter

Copy link
Copy Markdown
Member Author

Flagging a needed follow-up before merge (or as a quick amendment here): the checkout flow was redesigned around a cart — see #1590. stripeCheckoutSessionsDB.product should be jsonb, not varchar — a row now holds a JSON array of cart line items (a full snapshot of the Stripe price_data per item), since one Checkout Session can carry multiple products even though Phase 1 only ships one product type. voter_count_granted is unaffected — still the aggregate across any voter-limit line items in the row. Details on #1584.

@ArendPeter ArendPeter left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments


export async function up(db: Kysely<any>): Promise<void> {
await db.schema.alterTable('electionDB')
.addColumn('voter_limit', 'integer')

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about if the voter_limit should be it's own column or part of the election settings.

It could technically be part of the settings, but on principle I think items under the settings should be assumed to by editable by the admin, and anything that requires more careful state control should be surfaced as a column.

// today becomes retroactively unable to keep its current voters.
await sql`
UPDATE "electionDB" e
SET voter_limit = GREATEST(100, (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to hardcode this rather than use the variable from the shared package. If the variable in the shared package ever changes, then that would require an additional migration.


// Backfill: no election that already has more than 100 voters on its roll
// today becomes retroactively unable to keep its current voters.
await sql`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is written using sql instead of the kysely ORM functions because kysely would have been more awkward. Here's an example of what that would have looked like

await db
  .updateTable("electionDB")
  .set((eb) => ({
    voter_limit: eb.fn("greatest", [
      eb.val(100),
      eb
        .selectFrom("electionRollDB")
        .select(eb.fn.countAll().$castTo<number>().as("count"))
        .whereRef("electionRollDB.election_id", "=", "electionDB.election_id")
        .where("head", "=", true),
    ]),
  }))
  .execute();

Comment thread packages/shared/src/config/PricingConfig.ts
@ArendPeter

Copy link
Copy Markdown
Member Author

Pushed a fixup addressing the follow-up above: stripeCheckoutSessionsDB.product is now jsonb (migration + shared StripeCheckoutSession/new StripeCheckoutSessionLineItem type), and sumVoterLimitPurchases no longer filters on product (it just sums voter_count_granted, which is unaffected). Build and full test suite (27 suites / 211 tests) pass.

@ArendPeter
ArendPeter force-pushed the voter-limit-migration-1584 branch from d14017f to 02cafae Compare August 26, 2026 07:00
ArendPeter and others added 4 commits August 26, 2026 00:02
Per Equal-Vote#1590 the checkout flow moved to a cart model: one Checkout
Session can carry multiple line items, so `product` now stores a
JSON array of line items (each a full Stripe price_data snapshot
tagged with its internal product type) instead of a single varchar
product type. voter_count_granted stays the authoritative aggregate
on the row, so sumVoterLimitPurchases no longer filters by product.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e7hk5frznvBj8kbB3uu4M
Replaced 'product' and 'amount_cents' columns with 'line_items' column in the stripeCheckoutSessionsDB table.
@ArendPeter
ArendPeter force-pushed the voter-limit-migration-1584 branch from 02cafae to f6cb070 Compare August 26, 2026 07:02
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('election_id', 'varchar', (col) => col.notNull())
.addColumn('user_id', 'varchar', (col) => col.notNull())
.addColumn('line_items', 'jsonb', (col) => col.notNull())

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line items will be a json blob matching what was passed to stripe. Here's an example for adding 600 voters to the vote limit

line_items: [
    {
      price_data: {
        currency: "usd",
        unit_amount: 500,
        product_data: {
          name: "Voter Batch",
          description: "Add a batch of voters to the vote limit (200 per batch)",
          images: [
            "https://example.com/images/product.jpg",
          ],
        },
      },
      quantity: 3,
}
]

This is written to be generic so that the transaction could also theoretically include both a voter limit increase and an add-on, such as adding a custom slug.

@ArendPeter
ArendPeter marked this pull request as ready for review August 26, 2026 07:05
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 56 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 807f4e51-341b-4040-8e19-9e822d3cb054

📥 Commits

Reviewing files that changed from the base of the PR and between 58aea31 and f6cb070.

📒 Files selected for processing (16)
  • packages/backend/src/DevElections/elections/emailtracking.ts
  • packages/backend/src/DevElections/elections/starprordering.ts
  • packages/backend/src/DevElections/elections/tiechecks.ts
  • packages/backend/src/DevElections/elections/wizardstar.ts
  • packages/backend/src/DevElections/elections/writeins.ts
  • packages/backend/src/Migrations/2026_08_24_voter_limit_and_stripe_sessions.ts
  • packages/backend/src/Models/Database.ts
  • packages/backend/src/Models/Elections.ts
  • packages/backend/src/Models/StripeCheckoutSessions.ts
  • packages/backend/src/Models/__mocks__/StripeCheckoutSessions.ts
  • packages/backend/src/test/database_sandbox.ts
  • packages/shared/package.json
  • packages/shared/src/config/PricingConfig.ts
  • packages/shared/src/config/index.ts
  • packages/shared/src/domain_model/Election.ts
  • packages/shared/src/domain_model/StripeCheckoutSession.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

await db.schema
.createTable('stripeCheckoutSessionsDB')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('election_id', 'varchar', (col) => col.notNull())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need amount_cents here as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can be derived from the itemized info in line_items

election.update_date = Date.now().toString()// Use now() because it doesn't change with time zone
election.head = true
election.create_date = new Date().toISOString()
election.voter_limit = pricingConfig.FREE_TIER_LIMIT

@jacksonloper jacksonloper Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We set voter_limit here to free tier limit. But I think they can edit it later with a suitable devious CURL using the updateElection endpoint. (like POST {"Election": {..., "voter_limit": 1000000}} to /API/Election/:id/edit)

@ArendPeter ArendPeter Sep 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I've got it on the roadmap for the next PRs

The automated ticket generation got really messy, but it's tracked at #1585

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add voter_limit column & stripeCheckoutSessionsDB table (migration + shared pricing config)

2 participants