feat(billing): local-currency plan prices with a regional discount on Solo

Merged
RI
4d ago
+2166-141

Summary

Plan prices in the user's local currency, with a regional (PPP) discount on Solo only. Only amounts that exist on the Stripe prices are shown or charged; nothing is converted client-side.

  • One price per plan and interval: each plan has a Stripe price with the same USD amount as before, plus ~120 currencies at the market exchange rate. Users can pick any currency. A customer Stripe already bills in a currency is locked to it.
  • Solo regional discount. A Stripe price has one amount per currency, so euro countries need separate prices. Solo has four discount tiers (85 / 70 / 60 / 50 % of the full price), listed in plans.regional_price_ids. country_price_tier.ts maps countries to tiers from the World Bank 2025 price level, never above the full price. Examples: Germany/UK 85 %, France/Italy 70 %, Spain/Portugal 60 %, Croatia/Poland/India/Brazil 50 %. Maker, Team, Enterprise and credits cost the same everywhere.
  • No VPN discounts. The tier granted is the most expensive one among the visitor IP, the billing address and the saved card countries.
    • A discounted checkout requires a billing address.
    • On customer.subscription.created, and when a subscription activates or changes card, the webhook checks the paying card and the billing address. If either is from a pricier region, it moves the subscription to that region's price and invoices the difference (proration_behavior: always_invoice). If nothing proves the region, the full price applies.
  • Visible discount. The Plans page shows the full price struck through, a "{n}% regional discount" badge, and the card/address requirement. The trial plan picker strikes through the full price too.
  • Trial auto-start charges the same tier, using the saved card.
  • private/plan_prices?orgId= returns the tier's amounts plus discount and regularPrices. It is cached per billing account, tier and price version, and returns complete: false on partial catalogs (the page shows Retry and checkout stays disabled).
  • Existing subscriptions keep resolving. alt_price_m_ids / alt_price_y_ids hold replaced and regional prices, synced from regional_price_ids by a trigger, so subscriptions still resolve as monthly/yearly.

Release

  • Stripe, both accounts: 16 full prices (all plans) plus 8 Solo discount prices are active. Unused tier and PPP sets are archived. Old prices stay active for existing subscribers.
  • Migration: 20261027120000_plan_alt_price_ids.sql adds the columns and trigger, then switches plans. It only touches rows still on the old live IDs, so it is a no-op on local/test databases.
  • Rollback: point plans back at the old IDs and clear regional_price_ids.

Test plan

  • bunx vitest run tests/regional-price.unit.test.ts tests/plan-prices.unit.test.ts tests/stripe-redirects.unit.test.ts tests/stripe-trial-plan.unit.test.ts tests/stripe-event.unit.test.ts
  • pgTAP 90_test_plan_alt_price_ids.sql (11 assertions)
  • Release migration dry-run on simulated prod rows (Solo gets 4 tiers, Maker none)
  • bun typecheck, bun lint:ox, eslint, typos
  • After release:
    • Plans page from FR / DE / US / IN shows the right Solo discount
    • Checkout with a foreign card gets corrected to the right price
    • An existing USD subscriber stays on USD

๐Ÿค– Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added currency-aware plan pricing to organization billing settings, with a selector for available currencies.
    • Plan prices, discounts, monthly and yearly breakdowns, and Enterprise MAU pricing display in the selected currency.
    • The selected currency carries through to subscription checkout, which only accepts currencies supported by the plan and billing account.
    • Accounts with a locked currency see the applicable restriction. Added currency availability notices and a retry option if prices fail to load.
    • Added country-based regional discounts to eligible plan and trial pricing, with regular prices shown alongside discounted trial prices.
    • Checkout collects a billing address when required for regional pricing.