Jersey Stripe checkout + second tryout announcement email #103

Closed
opened 2026-03-18 20:29:56 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

plan-wkq → Phase 11 → Second tryout operations + jersey ordering

Repo

forgejo_admin/basketball-api

User Story

As an admin
I want to send all parents an announcement email with second tryout details, jersey ordering via Stripe, and profile completion link
So that parents can complete profiles, order jerseys, and show up to the second tryout

Context

Marcus communicated the following on 2026-03-18:

  • Second tryouts: Tuesday March 24, 4-5:30 PM at Kongo Basketball Gym (1261 S 650 W Bldg 2, Farmington UT 84025). Everyone is making a team — official teams announced after Tuesday.
  • Jersey options: $90 reversible (home+away), $130 jersey+warmup, or opt out (bring your own). Need jerseys in time for April 10-11 invite-only tournament.
  • Tournament: April 10-11, invite-only, all top travel clubs.
  • Email must include: tryout details, jersey Stripe checkout, profile completion CTA, login credentials.
  • Account consistency: Token-based links ensure payments track to the right Stripe customer.
  • Send Marcus a test first before blasting to all parents.

The Gmail OAuth email service exists (services/email.py) with branded HTML templates and _brand_wrapper() helper. Stripe integration exists in subscriptions.py (checkout sessions, webhooks, customer management). Parent model has registration_token for token-based auth. Keycloak admin API integration exists in services/keycloak.py.

Auto-generated passwords use pattern Westside-{FirstName}-{2digits} but are NOT stored in the DB — only shown once during registration. To include credentials in the email, generate a new password via Keycloak Admin API and include it.

Stripe secrets: env var BASKETBALL_STRIPE_API_KEY. Test keys available at ~/secrets/stripe/test-secret-key.

File Targets

Files the agent should modify or create:

  • src/basketball_api/models.py — add JerseyOption enum (reversible, jersey_warmup, opt_out), add jersey_option and jersey_order_status fields to Player model. jersey_order_status enum: none, pending, paid, shipped.
  • src/basketball_api/routes/admin.py — add POST /admin/email/tryout-announcement endpoint that sends branded email to all parents (or single parent for test mode via ?test_email=marcus@email.com query param)
  • src/basketball_api/routes/jersey.py — NEW file:
    • GET /jersey/options — returns available jersey options with prices (public, no auth needed)
    • POST /jersey/checkout — token-authenticated (via registration_token query param), creates Stripe Checkout Session for selected jersey option, returns checkout URL. Must set stripe_customer_id from player record as the Stripe customer and include player_id + jersey_option in session metadata.
    • Include this router in main.py
  • src/basketball_api/routes/webhooks.py — add handler for jersey checkout session completed: update player.jersey_option and player.jersey_order_status = paid
  • src/basketball_api/services/email.py — add send_tryout_announcement_email(tenant, parent, players, credentials, db) function with branded HTML including: second tryout details, jersey options with link to https://westside.tail5b443a.ts.net/jersey?token={registration_token}, profile CTA with link to https://westside.tail5b443a.ts.net/players/{player_id}, login credentials (email + new password)
  • src/basketball_api/services/keycloak.py — add reset_parent_password(parent_email, new_password) function using existing Keycloak Admin API to set a new password. Use existing generate_password() from services/password.py to create the new password.
  • alembic/versions/012_add_jersey_fields.py — migration for new enum + fields

Files the agent should NOT touch:

  • src/basketball_api/routes/register.py — registration flow is separate
  • src/basketball_api/routes/subscriptions.py — monthly dues are separate from jersey one-time purchases

Acceptance Criteria

  • GET /jersey/options returns 3 options with correct prices
  • POST /jersey/checkout?token={reg_token} creates a Stripe Checkout Session tied to the correct customer, returns checkout URL
  • Stripe webhook updates jersey_option and jersey_order_status on the player after successful payment
  • POST /admin/email/tryout-announcement sends branded email to all parents with: tryout details, jersey link, profile link, login credentials
  • POST /admin/email/tryout-announcement?test_email=someone@test.com sends only to that email (for Marcus test)
  • Password reset via Keycloak Admin API works — new password included in email
  • All emails logged in EmailLog
  • Jersey order status tracked in DB (jersey_option + jersey_order_status fields)
  • Admin-only access on email endpoint, token-auth on jersey checkout

Test Expectations

  • Unit test: jersey options endpoint returns correct data
  • Unit test: jersey checkout creates Stripe session with correct metadata
  • Unit test: webhook handler updates player jersey fields
  • Unit test: email contains all required sections (tryout, jersey, profile, credentials)
  • Unit test: admin-only access enforced on email endpoint
  • Unit test: token auth enforced on jersey checkout
  • Run command: pytest tests/ -k "jersey or tryout_announcement"

Constraints

  • Follow existing Stripe patterns in subscriptions.py and webhooks.py
  • Follow existing email patterns in services/email.py — use _brand_wrapper() and brand colors
  • Follow existing admin route patterns in routes/admin.py
  • Jersey checkout is a ONE-TIME payment (not subscription) — use Stripe Checkout Session in payment mode, not subscription
  • Stripe product names: "Reversible Jersey (Home & Away)" at $90, "Jersey + Warmup Package" at $130
  • Use stripe.checkout.Session.create() with mode="payment" and customer=player.stripe_customer_id
  • If player has no stripe_customer_id, create one using parent email (same pattern as subscriptions.py)
  • Token lookup: query Parent by registration_token, then get their players

Checklist

  • PR opened
  • Tests pass
  • ruff check clean
  • No unrelated changes
  • Migration tested
  • phase-wkq-11-girls-tryout — second tryout operations
  • PR #102 (merged) — admin email infrastructure this builds on
### Type Feature ### Lineage `plan-wkq` → Phase 11 → Second tryout operations + jersey ordering ### Repo `forgejo_admin/basketball-api` ### User Story As an admin I want to send all parents an announcement email with second tryout details, jersey ordering via Stripe, and profile completion link So that parents can complete profiles, order jerseys, and show up to the second tryout ### Context Marcus communicated the following on 2026-03-18: - **Second tryouts:** Tuesday March 24, 4-5:30 PM at Kongo Basketball Gym (1261 S 650 W Bldg 2, Farmington UT 84025). Everyone is making a team — official teams announced after Tuesday. - **Jersey options:** $90 reversible (home+away), $130 jersey+warmup, or opt out (bring your own). Need jerseys in time for April 10-11 invite-only tournament. - **Tournament:** April 10-11, invite-only, all top travel clubs. - **Email must include:** tryout details, jersey Stripe checkout, profile completion CTA, login credentials. - **Account consistency:** Token-based links ensure payments track to the right Stripe customer. - **Send Marcus a test first** before blasting to all parents. The Gmail OAuth email service exists (`services/email.py`) with branded HTML templates and `_brand_wrapper()` helper. Stripe integration exists in `subscriptions.py` (checkout sessions, webhooks, customer management). Parent model has `registration_token` for token-based auth. Keycloak admin API integration exists in `services/keycloak.py`. Auto-generated passwords use pattern `Westside-{FirstName}-{2digits}` but are NOT stored in the DB — only shown once during registration. To include credentials in the email, generate a new password via Keycloak Admin API and include it. Stripe secrets: env var `BASKETBALL_STRIPE_API_KEY`. Test keys available at `~/secrets/stripe/test-secret-key`. ### File Targets Files the agent should modify or create: - `src/basketball_api/models.py` — add `JerseyOption` enum (reversible, jersey_warmup, opt_out), add `jersey_option` and `jersey_order_status` fields to Player model. `jersey_order_status` enum: none, pending, paid, shipped. - `src/basketball_api/routes/admin.py` — add `POST /admin/email/tryout-announcement` endpoint that sends branded email to all parents (or single parent for test mode via `?test_email=marcus@email.com` query param) - `src/basketball_api/routes/jersey.py` — NEW file: - `GET /jersey/options` — returns available jersey options with prices (public, no auth needed) - `POST /jersey/checkout` — token-authenticated (via `registration_token` query param), creates Stripe Checkout Session for selected jersey option, returns checkout URL. Must set `stripe_customer_id` from player record as the Stripe customer and include `player_id` + `jersey_option` in session metadata. - Include this router in `main.py` - `src/basketball_api/routes/webhooks.py` — add handler for jersey checkout session completed: update `player.jersey_option` and `player.jersey_order_status = paid` - `src/basketball_api/services/email.py` — add `send_tryout_announcement_email(tenant, parent, players, credentials, db)` function with branded HTML including: second tryout details, jersey options with link to `https://westside.tail5b443a.ts.net/jersey?token={registration_token}`, profile CTA with link to `https://westside.tail5b443a.ts.net/players/{player_id}`, login credentials (email + new password) - `src/basketball_api/services/keycloak.py` — add `reset_parent_password(parent_email, new_password)` function using existing Keycloak Admin API to set a new password. Use existing `generate_password()` from `services/password.py` to create the new password. - `alembic/versions/012_add_jersey_fields.py` — migration for new enum + fields Files the agent should NOT touch: - `src/basketball_api/routes/register.py` — registration flow is separate - `src/basketball_api/routes/subscriptions.py` — monthly dues are separate from jersey one-time purchases ### Acceptance Criteria - [ ] `GET /jersey/options` returns 3 options with correct prices - [ ] `POST /jersey/checkout?token={reg_token}` creates a Stripe Checkout Session tied to the correct customer, returns checkout URL - [ ] Stripe webhook updates `jersey_option` and `jersey_order_status` on the player after successful payment - [ ] `POST /admin/email/tryout-announcement` sends branded email to all parents with: tryout details, jersey link, profile link, login credentials - [ ] `POST /admin/email/tryout-announcement?test_email=someone@test.com` sends only to that email (for Marcus test) - [ ] Password reset via Keycloak Admin API works — new password included in email - [ ] All emails logged in EmailLog - [ ] Jersey order status tracked in DB (jersey_option + jersey_order_status fields) - [ ] Admin-only access on email endpoint, token-auth on jersey checkout ### Test Expectations - [ ] Unit test: jersey options endpoint returns correct data - [ ] Unit test: jersey checkout creates Stripe session with correct metadata - [ ] Unit test: webhook handler updates player jersey fields - [ ] Unit test: email contains all required sections (tryout, jersey, profile, credentials) - [ ] Unit test: admin-only access enforced on email endpoint - [ ] Unit test: token auth enforced on jersey checkout - Run command: `pytest tests/ -k "jersey or tryout_announcement"` ### Constraints - Follow existing Stripe patterns in `subscriptions.py` and `webhooks.py` - Follow existing email patterns in `services/email.py` — use `_brand_wrapper()` and brand colors - Follow existing admin route patterns in `routes/admin.py` - Jersey checkout is a ONE-TIME payment (not subscription) — use Stripe Checkout Session in `payment` mode, not `subscription` - Stripe product names: "Reversible Jersey (Home & Away)" at $90, "Jersey + Warmup Package" at $130 - Use `stripe.checkout.Session.create()` with `mode="payment"` and `customer=player.stripe_customer_id` - If player has no `stripe_customer_id`, create one using parent email (same pattern as subscriptions.py) - Token lookup: query Parent by `registration_token`, then get their players ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] `ruff check` clean - [ ] No unrelated changes - [ ] Migration tested ### Related - `phase-wkq-11-girls-tryout` — second tryout operations - PR #102 (merged) — admin email infrastructure this builds on
forgejo_admin 2026-03-18 20:49:41 +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#103
No description provided.