Skip to content

feat(subscription_manager): Stripe customers, Customer Portal, idempotency - #117

Merged
calmmage merged 1 commit into
mainfrom
feat/stripe-customer-portal
Sep 25, 2026
Merged

calmmage merged 1 commit into
mainfrom
feat/stripe-customer-portal

Conversation

@calmmage

Copy link
Copy Markdown
Owner

Connect Stripe Checkout to a reused customer so the Customer Portal has something to open, and close the other gaps the Stripe planner flagged on the existing adapter.

What

  • Customer reuse. stripe_customer_id lives on the per-user billing_accounts row. First Checkout POST /v1/customers with metadata[telegram_user_id], name/description = telegram username if known, no email. Later sessions pass customer=<id> for both mode=payment and mode=subscription. client_reference_id and signed metadata are unchanged.
  • Customer Portal. create_stripe_portal_url(user_id) -> str → POST /v1/billing_portal/sessions with return_url = public_base_url + "/billing/success". Raises SubscriptionPaymentError with a user message when the user has no Stripe customer yet. /manage (behind register_commands) replies with the portal link. Portal configuration is not created from code.
  • Idempotency. Idempotency-Key: <invoice_id> on Checkout Session create; Idempotency-Key: customer:<telegram_user_id> on customer create. stripe_request_headers takes an optional key.
  • Stable integration_identifier. botspot-credits / botspot-plan (settable via BOTSPOT_SUBSCRIPTION_MANAGER_STRIPE_INTEGRATION_IDENTIFIER_PREFIX, default botspot). No random suffix.
  • invoice.payment_failed. In STRIPE_FAIL_EVENT_TYPES. Resolves the subscription via invoice.subscription or Basil+ parent.subscription_details.subscription, marks the local row past_due (does not cancel), and sends a one-line Telegram notice with the portal link if stripe_customer_id exists. Smart Retries + customer.subscription.deleted still handle the end.
  • Webhook dedupe. Replayed event ids return False immediately except invoice.paid (current renew behaviour kept).

No stripe SDK; raw REST + Stripe-Version as today. Placeholders only.

Tests

uv run pytest tests/components/test_subscription_manager.py — existing tests plus:

  • test_stripe_request_headers_optional_idempotency_key
  • test_stripe_integration_identifier_is_stable
  • test_stripe_integration_identifier_prefix_env
  • test_stripe_customer_reused_on_checkout
  • test_stripe_plan_checkout_passes_customer_and_stable_id
  • test_create_stripe_portal_url
  • test_stripe_invoice_payment_failed_marks_past_due
  • test_stripe_invoice_payment_failed_parent_subscription_no_notice_without_customer
  • test_stripe_replayed_event_returns_false_before_fulfill
  • test_stripe_invoice_paid_replay_still_processes

ruff clean. pyright 0 errors on botspot/components/new/subscription_manager. Full suite 267 passed, coverage 71% (gate 60%). No Telegram token, no network.

Env names added (values never)

BOTSPOT_SUBSCRIPTION_MANAGER_STRIPE_INTEGRATION_IDENTIFIER_PREFIX

Default botspot → identifiers botspot-credits / botspot-plan. Existing Stripe env names unchanged (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_ALLOW_LIVE, STRIPE_PRICE_IDS_JSON, PUBLIC_BASE_URL).

Do not merge.

🤖 Generated with Claude Code

Reuse one Stripe customer per telegram user (stored on billing_accounts),
pass customer= on Checkout, open the Customer Portal via /manage, send
Idempotency-Key on customer and session creates, and keep
integration_identifier stable (botspot-credits / botspot-plan).
invoice.payment_failed marks the local sub past_due and notifies with a
portal link; replayed webhook event ids return False except invoice.paid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@calmmage
calmmage merged commit a89d904 into main Sep 25, 2026
10 of 11 checks passed
@calmmage
calmmage deleted the feat/stripe-customer-portal branch September 25, 2026 13:25
@calmmage
calmmage restored the feat/stripe-customer-portal branch October 5, 2026 04:58
@calmmage
calmmage deleted the feat/stripe-customer-portal branch October 9, 2026 12:22
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