Summary
Add Stripe Customer Portal access for Pro users so they can update payment details or cancel renewal. The app will create short-lived portal sessions on demand, return users to /user, sync Stripe subscription state after portal return, and rely on the US-001 entitlement resolver to keep canceling users Pro until currentPeriodEndUtc.
Key Changes
- Extend users-service billing configuration:
- Add
Stripe:PortalConfigurationId and Stripe:PortalReturnUrl.
- Use the Stripe Dashboard portal configuration ID, with cancellation configured to end at period end.
- Add authenticated users-service billing endpoints:
POST /users/billing/portal-session: require logged-in Pro/canceling user with StripeCustomerId; create Stripe Billing Portal session with customer, portal configuration, and return URL; return { portalUrl }.
POST /users/billing/sync: retrieve the current Stripe subscription for the user’s stored StripeCustomerId/StripeSubscriptionId, update local status, current period end, and cancel-at-period-end flag, then return entitlement fields.
- Return clear non-portal errors for Free users, missing Stripe customer/subscription IDs, expired Pro users, and Stripe failures.
- Update Angular user page:
- Show plan status from
AuthSessionStore: Free, Pro active, Pro canceling, Pro expired.
- Show canceling access end date from
currentPeriodEndUtc.
- Show “Manage subscription” for
pro_active and pro_canceling; clicking calls portal-session endpoint and redirects to portalUrl.
- On
/user?billing=returned, call billing sync, refresh session, and show a non-blocking status message.
- Entitlement behavior:
- Canceling keeps
isPro=true until currentPeriodEndUtc.
- Once
currentPeriodEndUtc is in the past, /session resolves the user as Free/expired even if no webhook has arrived yet.
- US-004 webhooks remain responsible for long-term event-driven consistency.
Test Plan
- Users-service tests:
- Unauthenticated portal session returns
401.
- Free user cannot create portal session.
- Pro user with Stripe customer/subscription gets a portal URL.
- Missing Stripe customer/subscription returns a recoverable billing error.
- Billing sync maps
cancel_at_period_end=true to pro_canceling and preserves access through period end.
- Billing sync maps expired/canceled subscription to non-Pro entitlement.
- Angular tests:
- User page renders Free, Pro active, Pro canceling with end date, and Pro expired states.
- Manage button calls portal-session endpoint and redirects to returned URL.
billing=returned query param triggers sync and session refresh.
- Sync/portal failures show recoverable messages without breaking progress loading.
Assumptions
- US-001 and US-002 fields/endpoints exist before this starts, including
StripeCustomerId, StripeSubscriptionId, StripeSubscriptionStatus, SubscriptionCurrentPeriodEndUtc, and SubscriptionCancelAtPeriodEnd.
- Portal configuration is created in Stripe Dashboard and its ID is provided through app config.
- Portal return URL is
/user?billing=returned.
- Stripe webhooks are not implemented in this story; US-003 adds explicit return sync to make portal changes visible immediately after return.
- Official Stripe references: Customer Portal sessions and integration docs: https://docs.stripe.com/api/customer_portal/sessions and https://docs.stripe.com/customer-management/integrate-customer-portal
Summary
Add Stripe Customer Portal access for Pro users so they can update payment details or cancel renewal. The app will create short-lived portal sessions on demand, return users to
/user, sync Stripe subscription state after portal return, and rely on the US-001 entitlement resolver to keep canceling users Pro untilcurrentPeriodEndUtc.Key Changes
Stripe:PortalConfigurationIdandStripe:PortalReturnUrl.POST /users/billing/portal-session: require logged-in Pro/canceling user withStripeCustomerId; create Stripe Billing Portal session with customer, portal configuration, and return URL; return{ portalUrl }.POST /users/billing/sync: retrieve the current Stripe subscription for the user’s storedStripeCustomerId/StripeSubscriptionId, update local status, current period end, and cancel-at-period-end flag, then return entitlement fields.AuthSessionStore: Free, Pro active, Pro canceling, Pro expired.currentPeriodEndUtc.pro_activeandpro_canceling; clicking calls portal-session endpoint and redirects toportalUrl./user?billing=returned, call billing sync, refresh session, and show a non-blocking status message.isPro=trueuntilcurrentPeriodEndUtc.currentPeriodEndUtcis in the past,/sessionresolves the user as Free/expired even if no webhook has arrived yet.Test Plan
401.cancel_at_period_end=truetopro_cancelingand preserves access through period end.billing=returnedquery param triggers sync and session refresh.Assumptions
StripeCustomerId,StripeSubscriptionId,StripeSubscriptionStatus,SubscriptionCurrentPeriodEndUtc, andSubscriptionCancelAtPeriodEnd./user?billing=returned.