feat: Stripe Subscription endpoints + webhooks for monthly payments (Phase 9a) #83

Closed
opened 2026-03-14 22:31:33 +00:00 by forgejo_admin · 0 comments

Lineage

plan-2026-03-08-tryout-prep → Phase 9 (Monthly Club Payments) → Phase 9a (Backend)

Repo

forgejo_admin/basketball-api

User Story

As an admin
I want to create monthly subscriptions for players and see their payment status
So that the club collects $200/month per player and I know who's current vs behind

Context

The club charges $200/month per player for the season. Currently payments are one-time tryout fees ($30) via Stripe Checkout. This phase adds recurring billing via Stripe Subscriptions.

Key decisions from planning:

  • $200/month per player via Stripe Subscriptions (confirm amount with Marcus if changed)
  • Admin creates subscriptions for players (not self-service initially)
  • Existing Stripe API key (BASKETBALL_STRIPE_API_KEY) and webhook secret (BASKETBALL_STRIPE_WEBHOOK_SECRET) are already deployed in k8s and sufficient
  • The existing webhook handler at /webhooks/stripe handles checkout.session.completed — extend it for subscription events
  • gmail-sdk notifications for past-due are DEFERRED (no outbound emails)
  • Lucas's EIN is in ~/secrets for Stripe Connect (coach payouts are Phase 7, not this phase)

Stripe Subscriptions flow:

  1. Admin creates a Stripe Product + Price (one-time setup, can be API or dashboard)
  2. For each player, admin hits endpoint → creates Stripe Customer (if not exists) + Subscription
  3. Stripe bills monthly, sends webhook events
  4. App updates subscription_status on Player model based on events

File Targets

Files the agent should modify:

  • src/basketball_api/models.py — add subscription_status (enum: active/past_due/canceled/none), stripe_customer_id, stripe_subscription_id to Player model
  • alembic/versions/xxx_add_subscription_fields.py — new migration for Player subscription fields
  • src/basketball_api/routes/webhooks.py — extend existing webhook handler with subscription event types
  • src/basketball_api/main.py — register subscriptions router

Files the agent should create:

  • src/basketball_api/routes/subscriptions.py — new route file: subscription CRUD, payment overview
  • tests/test_subscriptions.py — tests for all subscription endpoints and webhook handlers

Files the agent should NOT touch:

  • src/basketball_api/routes/teams.py — Phase 10, separate concern
  • src/basketball_api/routes/register.py — registration flow unchanged
  • src/basketball_api/auth.py — auth already working

Acceptance Criteria

  • Player model has subscription_status enum field (active/past_due/canceled/none, default none)
  • Player model has stripe_customer_id (nullable String)
  • Player model has stripe_subscription_id (nullable String)
  • Alembic migration adds all 3 fields
  • POST /api/subscriptions/setup — admin-only, one-time: creates Stripe Product + Price if not exists, returns product/price IDs (idempotent)
  • POST /api/subscriptions/{player_id} — admin-only: creates Stripe Customer (using parent email) + Subscription for player, stores IDs on Player model, sets status to active
  • GET /api/subscriptions — admin-only: list all players with subscription status, grouped by status
  • GET /api/subscriptions/overview — admin-only: summary (total active, past_due, canceled, none; total monthly revenue; count by status)
  • DELETE /api/subscriptions/{player_id} — admin-only: cancels Stripe Subscription, updates status to canceled
  • Webhook handler processes invoice.paid → confirms status active
  • Webhook handler processes invoice.payment_failed → sets status to past_due
  • Webhook handler processes customer.subscription.updated → syncs status
  • Webhook handler processes customer.subscription.deleted → sets status to canceled
  • GET /api/subscriptions/mine — authenticated player/parent: returns own subscription status + Stripe Customer Portal link

Test Expectations

  • Unit test: create subscription for player, verify stripe fields set
  • Unit test: cancel subscription, verify status changes
  • Unit test: webhook invoice.paid updates status to active
  • Unit test: webhook invoice.payment_failed updates status to past_due
  • Unit test: webhook customer.subscription.deleted updates status to canceled
  • Unit test: non-admin cannot create/cancel subscriptions (403)
  • Unit test: overview endpoint returns correct counts and revenue
  • Unit test: /mine endpoint returns correct status for authenticated user
  • Mock Stripe API calls in tests (use unittest.mock or pytest-mock)
  • Run command: pytest tests/test_subscriptions.py -v

Constraints

  • Follow existing auth pattern: require_role("admin") from src/basketball_api/auth.py
  • Follow existing webhook pattern in src/basketball_api/routes/webhooks.py
  • Use existing Stripe env vars: BASKETBALL_STRIPE_API_KEY, BASKETBALL_STRIPE_WEBHOOK_SECRET
  • Install stripe Python package if not already in requirements
  • Multi-tenant: scope all queries by tenant_id
  • Mock all Stripe API calls in tests — do not make real API calls
  • Run ruff format before committing
  • Stripe Customer Portal URL: use stripe.billing_portal.Session.create() to generate a portal link for players to manage their payment method

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • westside-basketball — project
  • Phase 9b (westside-app payment dashboard) blocked by this
  • Phase 9c (past-due email notifications) DEFERRED
  • Existing webhook handler at routes/webhooks.py handles checkout.session.completed
### Lineage `plan-2026-03-08-tryout-prep` → Phase 9 (Monthly Club Payments) → Phase 9a (Backend) ### Repo `forgejo_admin/basketball-api` ### User Story As an admin I want to create monthly subscriptions for players and see their payment status So that the club collects $200/month per player and I know who's current vs behind ### Context The club charges $200/month per player for the season. Currently payments are one-time tryout fees ($30) via Stripe Checkout. This phase adds recurring billing via Stripe Subscriptions. Key decisions from planning: - $200/month per player via Stripe Subscriptions (confirm amount with Marcus if changed) - Admin creates subscriptions for players (not self-service initially) - Existing Stripe API key (`BASKETBALL_STRIPE_API_KEY`) and webhook secret (`BASKETBALL_STRIPE_WEBHOOK_SECRET`) are already deployed in k8s and sufficient - The existing webhook handler at `/webhooks/stripe` handles `checkout.session.completed` — extend it for subscription events - gmail-sdk notifications for past-due are DEFERRED (no outbound emails) - Lucas's EIN is in ~/secrets for Stripe Connect (coach payouts are Phase 7, not this phase) Stripe Subscriptions flow: 1. Admin creates a Stripe Product + Price (one-time setup, can be API or dashboard) 2. For each player, admin hits endpoint → creates Stripe Customer (if not exists) + Subscription 3. Stripe bills monthly, sends webhook events 4. App updates `subscription_status` on Player model based on events ### File Targets Files the agent should modify: - `src/basketball_api/models.py` — add `subscription_status` (enum: active/past_due/canceled/none), `stripe_customer_id`, `stripe_subscription_id` to Player model - `alembic/versions/xxx_add_subscription_fields.py` — new migration for Player subscription fields - `src/basketball_api/routes/webhooks.py` — extend existing webhook handler with subscription event types - `src/basketball_api/main.py` — register subscriptions router Files the agent should create: - `src/basketball_api/routes/subscriptions.py` — new route file: subscription CRUD, payment overview - `tests/test_subscriptions.py` — tests for all subscription endpoints and webhook handlers Files the agent should NOT touch: - `src/basketball_api/routes/teams.py` — Phase 10, separate concern - `src/basketball_api/routes/register.py` — registration flow unchanged - `src/basketball_api/auth.py` — auth already working ### Acceptance Criteria - [ ] Player model has `subscription_status` enum field (active/past_due/canceled/none, default none) - [ ] Player model has `stripe_customer_id` (nullable String) - [ ] Player model has `stripe_subscription_id` (nullable String) - [ ] Alembic migration adds all 3 fields - [ ] `POST /api/subscriptions/setup` — admin-only, one-time: creates Stripe Product + Price if not exists, returns product/price IDs (idempotent) - [ ] `POST /api/subscriptions/{player_id}` — admin-only: creates Stripe Customer (using parent email) + Subscription for player, stores IDs on Player model, sets status to active - [ ] `GET /api/subscriptions` — admin-only: list all players with subscription status, grouped by status - [ ] `GET /api/subscriptions/overview` — admin-only: summary (total active, past_due, canceled, none; total monthly revenue; count by status) - [ ] `DELETE /api/subscriptions/{player_id}` — admin-only: cancels Stripe Subscription, updates status to canceled - [ ] Webhook handler processes `invoice.paid` → confirms status active - [ ] Webhook handler processes `invoice.payment_failed` → sets status to past_due - [ ] Webhook handler processes `customer.subscription.updated` → syncs status - [ ] Webhook handler processes `customer.subscription.deleted` → sets status to canceled - [ ] `GET /api/subscriptions/mine` — authenticated player/parent: returns own subscription status + Stripe Customer Portal link ### Test Expectations - [ ] Unit test: create subscription for player, verify stripe fields set - [ ] Unit test: cancel subscription, verify status changes - [ ] Unit test: webhook invoice.paid updates status to active - [ ] Unit test: webhook invoice.payment_failed updates status to past_due - [ ] Unit test: webhook customer.subscription.deleted updates status to canceled - [ ] Unit test: non-admin cannot create/cancel subscriptions (403) - [ ] Unit test: overview endpoint returns correct counts and revenue - [ ] Unit test: /mine endpoint returns correct status for authenticated user - Mock Stripe API calls in tests (use unittest.mock or pytest-mock) - Run command: `pytest tests/test_subscriptions.py -v` ### Constraints - Follow existing auth pattern: `require_role("admin")` from `src/basketball_api/auth.py` - Follow existing webhook pattern in `src/basketball_api/routes/webhooks.py` - Use existing Stripe env vars: `BASKETBALL_STRIPE_API_KEY`, `BASKETBALL_STRIPE_WEBHOOK_SECRET` - Install `stripe` Python package if not already in requirements - Multi-tenant: scope all queries by tenant_id - Mock all Stripe API calls in tests — do not make real API calls - Run `ruff format` before committing - Stripe Customer Portal URL: use `stripe.billing_portal.Session.create()` to generate a portal link for players to manage their payment method ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `westside-basketball` — project - Phase 9b (westside-app payment dashboard) blocked by this - Phase 9c (past-due email notifications) DEFERRED - Existing webhook handler at `routes/webhooks.py` handles `checkout.session.completed`
forgejo_admin 2026-03-14 23:33:58 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#83
No description provided.