Support Stripe discount coupons in registration (WESTSIDE50, GIRLSHOOPS) #391

Closed
opened 2026-04-07 20:51:09 +00:00 by forgejo_admin · 1 comment

Type

Feature

Lineage

Standalone — discovered during registration flow audit (2026-04-07). Stripe has active promotion codes (WESTSIDE50 50% off, GIRLSHOOPS 20% off) but the registration form only supports free promo codes.

Repo

forgejo_admin/basketball-api

User Story

As a parent with a discount code from Coach Marcus
I want to enter it during registration and pay the reduced amount
So that I get the discount without needing a separate payment arrangement

Context

Current promo code flow: code is checked against BASKETBALL_TRYOUT_PROMO_CODES env var (comma-separated list: TESTFREE,GIRLS2026). If it matches, registration is free — Stripe is bypassed entirely. If it doesn't match, the code is rejected.

Stripe already has active promotion codes:

  • WESTSIDE50 — 50% off ($15 instead of $30), active, 6 redemptions
  • GIRLSHOOPS — 20% off ($24 instead of $30), active
  • CASHPAID — 100% off, active
  • TESTFREE — 100% off, active

The fix: when a promo code doesn't match the free list, check Stripe for a valid promotion code. If found, create a Checkout Session with the discount applied and redirect to Stripe for the reduced amount.

File Targets

Files to modify:

  • src/basketball_api/routes/register.py lines 1187-1197 — after free code check, add Stripe promotion code lookup. If valid, fall through to the card payment path with discounts parameter on Session.create()

Specifically in the Session.create() call (line 1349), add:

discounts=[{"promotion_code": promo_code_id}]

Files NOT to touch:

  • src/basketball_api/services/email.py — email works the same regardless of discount
  • westside-app form — already handles redirect responses from promo path

Acceptance Criteria

  • When I enter WESTSIDE50 as promo code, I'm redirected to Stripe checkout showing $15
  • When I enter GIRLSHOOPS as promo code, I'm redirected to Stripe checkout showing $24
  • When I enter TESTFREE as promo code, registration is still free (no Stripe redirect)
  • When I enter an invalid code, I get "Invalid promo code" error
  • When I complete discounted Stripe payment, webhook fires and registration completes normally
  • Stripe receipt shows the discount applied

Test Expectations

  • Unit test: valid Stripe promo code returns redirect_url (mock Stripe)
  • Unit test: free code still bypasses Stripe
  • Unit test: invalid code returns 400
  • Run command: pytest tests/test_promo_registration.py

Constraints

  • Use stripe.PromotionCode.list(code=code_str, active=True) to validate — returns list, check if non-empty
  • Extract the promotion_code ID from the response for the discounts param
  • The promo code path currently sets payment_status=paid immediately — discount codes should set payment_status=pending like card payments (payment completes via webhook)
  • Keycloak account creation should happen in the webhook for discounted payments, same as full-price card payments

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • westside-basketball
### Type Feature ### Lineage Standalone — discovered during registration flow audit (2026-04-07). Stripe has active promotion codes (WESTSIDE50 50% off, GIRLSHOOPS 20% off) but the registration form only supports free promo codes. ### Repo `forgejo_admin/basketball-api` ### User Story As a parent with a discount code from Coach Marcus I want to enter it during registration and pay the reduced amount So that I get the discount without needing a separate payment arrangement ### Context Current promo code flow: code is checked against `BASKETBALL_TRYOUT_PROMO_CODES` env var (comma-separated list: `TESTFREE,GIRLS2026`). If it matches, registration is free — Stripe is bypassed entirely. If it doesn't match, the code is rejected. Stripe already has active promotion codes: - `WESTSIDE50` — 50% off ($15 instead of $30), active, 6 redemptions - `GIRLSHOOPS` — 20% off ($24 instead of $30), active - `CASHPAID` — 100% off, active - `TESTFREE` — 100% off, active The fix: when a promo code doesn't match the free list, check Stripe for a valid promotion code. If found, create a Checkout Session with the discount applied and redirect to Stripe for the reduced amount. ### File Targets Files to modify: - `src/basketball_api/routes/register.py` lines 1187-1197 — after free code check, add Stripe promotion code lookup. If valid, fall through to the card payment path with `discounts` parameter on `Session.create()` Specifically in the `Session.create()` call (line 1349), add: ```python discounts=[{"promotion_code": promo_code_id}] ``` Files NOT to touch: - `src/basketball_api/services/email.py` — email works the same regardless of discount - westside-app form — already handles redirect responses from promo path ### Acceptance Criteria - [ ] When I enter WESTSIDE50 as promo code, I'm redirected to Stripe checkout showing $15 - [ ] When I enter GIRLSHOOPS as promo code, I'm redirected to Stripe checkout showing $24 - [ ] When I enter TESTFREE as promo code, registration is still free (no Stripe redirect) - [ ] When I enter an invalid code, I get "Invalid promo code" error - [ ] When I complete discounted Stripe payment, webhook fires and registration completes normally - [ ] Stripe receipt shows the discount applied ### Test Expectations - [ ] Unit test: valid Stripe promo code returns redirect_url (mock Stripe) - [ ] Unit test: free code still bypasses Stripe - [ ] Unit test: invalid code returns 400 - Run command: `pytest tests/test_promo_registration.py` ### Constraints - Use `stripe.PromotionCode.list(code=code_str, active=True)` to validate — returns list, check if non-empty - Extract the promotion_code ID from the response for the `discounts` param - The promo code path currently sets `payment_status=paid` immediately — discount codes should set `payment_status=pending` like card payments (payment completes via webhook) - Keycloak account creation should happen in the webhook for discounted payments, same as full-price card payments ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `westside-basketball`
Author
Owner

APPROVED

Scope review findings:

1. File targets verified

  • register.py lines 1187-1197: promo code validation block confirmed. Checks body.promo_code against settings.tryout_promo_codes (comma-separated env var). Returns 400 if no match. This is the insertion point for Stripe promotion code fallback.
  • register.py line 1349: stripe.checkout.Session.create() confirmed at this line, inside the if body.payment_method == "card" block. Currently uses line_items with price_data (dynamic pricing, $30 tryout / $40 remote). No discounts param yet.

2. Stripe API confirmed

  • stripe.PromotionCode.list() accepts code: str and active: bool filter params (verified in stripe/params/_promotion_code_list_params.py). Case-insensitive matching per Stripe docs.
  • Session.create() accepts discounts param (list of dicts with promotion_code key). Confirmed via Session model having discounts: Optional[List[Discount]] attribute. Stripe SDK v14.4.1 installed.

3. Frontend handles redirect

  • westside-app register/+page.svelte line 154: if (result?.redirect_url) { window.location.href = result.redirect_url; return; } — already handles redirect responses. The promo path returning a redirect_url instead of credentials will work without frontend changes.

4. No scope conflicts

  • No open PRs touch register.py. Branch 108-register-photo-upload is stale (no associated PR). Safe to proceed.

5. Test file exists

  • tests/test_promo_registration.py exists with 4 existing tests in TestPromoCodeRegistration class: valid code, case-insensitive matching, invalid code returns 400, missing code returns 400. New Stripe discount tests can extend this class.

All file targets, line numbers, and API assumptions check out. Ready for dispatch.

APPROVED Scope review findings: **1. File targets verified** - `register.py` lines 1187-1197: promo code validation block confirmed. Checks `body.promo_code` against `settings.tryout_promo_codes` (comma-separated env var). Returns 400 if no match. This is the insertion point for Stripe promotion code fallback. - `register.py` line 1349: `stripe.checkout.Session.create()` confirmed at this line, inside the `if body.payment_method == "card"` block. Currently uses `line_items` with `price_data` (dynamic pricing, $30 tryout / $40 remote). No `discounts` param yet. **2. Stripe API confirmed** - `stripe.PromotionCode.list()` accepts `code: str` and `active: bool` filter params (verified in `stripe/params/_promotion_code_list_params.py`). Case-insensitive matching per Stripe docs. - `Session.create()` accepts `discounts` param (list of dicts with `promotion_code` key). Confirmed via Session model having `discounts: Optional[List[Discount]]` attribute. Stripe SDK v14.4.1 installed. **3. Frontend handles redirect** - `westside-app register/+page.svelte` line 154: `if (result?.redirect_url) { window.location.href = result.redirect_url; return; }` — already handles redirect responses. The promo path returning a `redirect_url` instead of credentials will work without frontend changes. **4. No scope conflicts** - No open PRs touch `register.py`. Branch `108-register-photo-upload` is stale (no associated PR). Safe to proceed. **5. Test file exists** - `tests/test_promo_registration.py` exists with 4 existing tests in `TestPromoCodeRegistration` class: valid code, case-insensitive matching, invalid code returns 400, missing code returns 400. New Stripe discount tests can extend this class. All file targets, line numbers, and API assumptions check out. Ready for dispatch.
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#391
No description provided.