Skip to content

Per-user retirement jurisdiction from Stripe billing address (#119)#134

Open
brawlaphant wants to merge 1 commit into
regen-network:mainfrom
brawlaphant:feature/119-customize-retirement-jurisdiction
Open

Per-user retirement jurisdiction from Stripe billing address (#119)#134
brawlaphant wants to merge 1 commit into
regen-network:mainfrom
brawlaphant:feature/119-customize-retirement-jurisdiction

Conversation

@brawlaphant

Copy link
Copy Markdown
Contributor

Closes the autopilot half of #119.

Problem

Subscribers had no way to set their retirement jurisdiction — every scheduled retirement used the hardcoded config.defaultJurisdiction ("US"), making on-chain retirement records inaccurate for non-US subscribers. Marie raised this in the issue.

What this PR does

Wires the resolution chain for the on-chain side without requiring any new UI. Stripe already collects the billing-address country during Checkout — we just plumb it through.

Resolution order (per the issue spec):

  1. Explicit jurisdiction param (MCP tool callers) — already worked
  2. user.countrynew: synced from Stripe billing address at checkout
  3. config.defaultJurisdiction — fallback

Changes

  • src/server/db.ts
    • ALTER TABLE users ADD COLUMN country migration (ISO 3166-1 alpha-2, nullable)
    • User type gains country: string | null
    • setUserCountry / setUserCountryIfMissing / getUserCountryBySubscriberId helpers
    • normalizeCountryCode validator: accepts \"US\" and sub-national \"US-OR\", rejects invalid input
  • src/server/routes.ts
    • checkout.session.completed webhook reads session.customer_details.address.country and calls setUserCountryIfMissing so a Stripe-provided value never overwrites an explicit user setting
  • src/services/retire-subscriber.ts
    • Resolves subscriberJurisdiction once at the top of retireForSubscriber (getUserCountryBySubscriberId(...) ?? config.defaultJurisdiction)
    • Replaces the two hardcoded config.defaultJurisdiction sites in the MsgSend and MsgBuyDirect retirement paths
  • src/__tests__/user-country.test.ts — 11 new tests

Out of scope (deliberately, flagged in commit)

  • Dashboard settings UI for editing country post-signup. The POST /profile/display-name pattern can be mirrored, but the form to call it is design work — better as its own PR.
  • customer.updated webhook sync for billing-address edits in the customer portal. Requires enabling that event in the Stripe webhook config.

Test plan

  • npm run typecheck — clean
  • npm test — 60/60 passing (49 prior + 11 new)
  • npm run build — clean
  • Reviewer: confirm Stripe Checkout currently collects address for the active price IDs (needed for the webhook sync to populate)
  • Reviewer: spot-check the migration on a fresh DB and a populated DB (country column appears, no data loss)

Refs: #119

🤖 Generated with Claude Code

… billing address

Closes the autopilot half of regen-network#119. Subscribers had no way to set their
retirement jurisdiction — every scheduled retirement used the
hardcoded config.defaultJurisdiction ("US"), which made retirement
records inaccurate for non-US subscribers.

This wires the resolution chain for the on-chain side without
requiring any new UI:

  1. Explicit jurisdiction param (MCP tool callers) — already worked
  2. user.country — NEW: synced from Stripe billing address at checkout
  3. config.defaultJurisdiction — fallback

Changes:

- db.ts: ALTER TABLE users ADD COLUMN country (ISO 3166-1 alpha-2,
  nullable). Migration follows the existing pattern.
- db.ts: User type gains country: string | null.
- db.ts: setUserCountry / setUserCountryIfMissing /
  getUserCountryBySubscriberId helpers, plus a normalizeCountryCode
  validator that accepts alpha-2 ("US") and sub-national ("US-OR")
  forms. Invalid input is rejected, never persisted.
- routes.ts: checkout.session.completed webhook reads
  session.customer_details.address.country and sets it on the user
  with setUserCountryIfMissing — never overwrites a value the user
  set explicitly.
- retire-subscriber.ts: resolves subscriberJurisdiction once at
  the top of retireForSubscriber from
  getUserCountryBySubscriberId(...) ?? config.defaultJurisdiction,
  then uses it in the two MsgSend / MsgBuyDirect retirement paths
  (replacing the two hardcoded config.defaultJurisdiction sites).

Test: 11 new vitest cases — code normalization (lowercase → upper,
sub-national pass-through, invalid → null), if-missing semantics
(set when null, no-overwrite, no-op on invalid, no-op on empty),
and getUserCountryBySubscriberId join behavior.
Full suite: 60/60 passing.

Out of scope (deferred to follow-up PR — flagged in regen-network#119):
- Dashboard settings UI for editing country after signup. The
  POST /profile/display-name pattern can be mirrored, but the form
  to call it is design work.
- customer.updated webhook sync. Would catch billing-address edits
  in the customer portal, but requires enabling that event in the
  Stripe webhook config.

Refs: regen-network#119

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces functionality to track and use a user's country for retirement jurisdictions. It includes a database migration to add a country column to the users table, helper functions for normalizing and setting country codes, and logic to sync country data from Stripe billing addresses. The retirement service was updated to prioritize the user's country over the default system jurisdiction. Feedback was provided regarding an inaccurate docstring for the getUserCountryBySubscriberId function, which incorrectly describes its fallback behavior and return type.

Comment thread src/server/db.ts
Comment on lines +721 to +725
* Resolve the retirement jurisdiction for a subscriber per #119:
* 1. user.country (explicit setting)
* 2. defaultFallback (typically config.defaultJurisdiction)
* Returns the resolved code (always non-null because fallback is required).
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The docstring for getUserCountryBySubscriberId is inaccurate. It claims to return a resolved code that is "always non-null because fallback is required," but the function signature returns string | null and it does not implement any fallback logic (the fallback is correctly handled by the caller in retire-subscriber.ts). The documentation should be updated to accurately reflect the function's behavior.

/**
 * Get the user's country code for a subscriber.
 * Returns the ISO 3166-1 alpha-2 code (or sub-national code) if set, otherwise null.
 */

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.

1 participant