Checkout endpoint: GET /checkout/first-payment with Stripe redirect #368

Closed
opened 2026-04-07 01:11:29 +00:00 by forgejo_admin · 2 comments

Type

Feature

Lineage

Decomposed from forgejo_admin/basketball-api#366. Ticket B of 3 — depends on A (migration, merged as PR #370). Independent of C (email).

Repo

forgejo_admin/basketball-api

User Story

As a parent who clicks the payment link in my email
I want to be redirected straight to Stripe's checkout page with my prorated amount
So that I can pay in one click and have my card saved for future monthly billing

Context

The email CTA links to GET /checkout/first-payment?token={contract_token}. This endpoint validates the contract token, calculates the player's prorated fee from Player.monthly_fee, creates a Stripe Checkout Session, and 302-redirects to Stripe's hosted page. No frontend intermediary needed.

Critical: save card for recurring billing. The Checkout Session must include payment_intent_data={"setup_future_usage": "off_session"} so Stripe saves the payment method to the Customer. This allows creating subscriptions on May 1 using the saved card — no second card entry needed.

Proration formula (matches westside-contracts +page.svelte:9):

prorated_dollars = round(monthly_fee * 25 / 30 / 5) * 5

Existing patterns to follow: checkout.py Stripe Customer create/reuse, Order creation, _SUCCESS_URL/_CANCEL_URL redirects. Existing webhook _handle_generic_order_completed handles payment completion — no webhook changes needed.

File Targets

Files to modify:

  • src/basketball_api/routes/checkout.py — add GET /checkout/first-payment endpoint, add _calculate_prorated_cents() helper, add imports for ContractStatus, Player, ProductCategory, RedirectResponse

Files to create:

  • tests/test_first_payment.py — endpoint tests

Files NOT to touch:

  • src/basketball_api/routes/webhooks.py — existing handler covers this
  • src/basketball_api/services/email.py — that's ticket C
  • src/basketball_api/routes/admin.py — that's ticket C

Acceptance Criteria

  • GET /checkout/first-payment?token={valid_signed_token} returns 307 redirect to Stripe checkout URL
  • Invalid token returns 404
  • Unsigned contract token returns 404
  • Duplicate pending/paid order returns 409
  • Proration correct: $200→$165 ($16500 cents), $180→$150 ($15000 cents), $160→$135 ($13500 cents)
  • Null monthly_fee defaults to $200 (prorated $165)
  • Order record created with correct amount_cents, product_id, stripe_checkout_session_id
  • Stripe Customer created/reused (saves to player.stripe_customer_id)
  • Stripe Checkout Session includes payment_intent_data={"setup_future_usage": "off_session"} to save card for future billing

Test Expectations

  • Unit test: valid token → Stripe mock → 307 redirect + Order created with correct amount
  • Unit test: invalid token → 404
  • Unit test: unsigned contract → 404
  • Unit test: duplicate order → 409
  • Unit test: $180 fee → $150 prorated ($15000 cents)
  • Unit test: null monthly_fee → defaults to $200 → $165 prorated ($16500 cents)
  • Unit test: verify setup_future_usage passed to Stripe Checkout Session create call
  • Run: pytest tests/test_first_payment.py -v

Constraints

  • Use contract_token (Player model, unique) not registration_token (Parent model)
  • Stripe calls must be mocked in tests (@patch("basketball_api.routes.checkout.stripe"))
  • Use RedirectResponse(url=..., status_code=307) for the redirect
  • Product lookup: ProductCategory.monthly + active=True
  • conftest.py provides client fixture with DB override (no auth needed — token-based)
  • MUST include payment_intent_data={"setup_future_usage": "off_session"} in Checkout Session

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • westside-basketball — project
  • Parent: forgejo_admin/basketball-api#366
  • Depends on: #367 (migration, merged PR #370)
  • Plan: ~/pal-e-platform/docs/superpowers/plans/2026-04-06-first-payment-email.md (Task 2)
### Type Feature ### Lineage Decomposed from `forgejo_admin/basketball-api#366`. Ticket B of 3 — depends on A (migration, merged as PR #370). Independent of C (email). ### Repo `forgejo_admin/basketball-api` ### User Story As a parent who clicks the payment link in my email I want to be redirected straight to Stripe's checkout page with my prorated amount So that I can pay in one click and have my card saved for future monthly billing ### Context The email CTA links to `GET /checkout/first-payment?token={contract_token}`. This endpoint validates the contract token, calculates the player's prorated fee from `Player.monthly_fee`, creates a Stripe Checkout Session, and 302-redirects to Stripe's hosted page. No frontend intermediary needed. **Critical: save card for recurring billing.** The Checkout Session must include `payment_intent_data={"setup_future_usage": "off_session"}` so Stripe saves the payment method to the Customer. This allows creating subscriptions on May 1 using the saved card — no second card entry needed. Proration formula (matches westside-contracts `+page.svelte:9`): ``` prorated_dollars = round(monthly_fee * 25 / 30 / 5) * 5 ``` Existing patterns to follow: `checkout.py` Stripe Customer create/reuse, Order creation, `_SUCCESS_URL`/`_CANCEL_URL` redirects. Existing webhook `_handle_generic_order_completed` handles payment completion — no webhook changes needed. ### File Targets Files to modify: - `src/basketball_api/routes/checkout.py` — add `GET /checkout/first-payment` endpoint, add `_calculate_prorated_cents()` helper, add imports for `ContractStatus`, `Player`, `ProductCategory`, `RedirectResponse` Files to create: - `tests/test_first_payment.py` — endpoint tests Files NOT to touch: - `src/basketball_api/routes/webhooks.py` — existing handler covers this - `src/basketball_api/services/email.py` — that's ticket C - `src/basketball_api/routes/admin.py` — that's ticket C ### Acceptance Criteria - [ ] `GET /checkout/first-payment?token={valid_signed_token}` returns 307 redirect to Stripe checkout URL - [ ] Invalid token returns 404 - [ ] Unsigned contract token returns 404 - [ ] Duplicate pending/paid order returns 409 - [ ] Proration correct: $200→$165 ($16500 cents), $180→$150 ($15000 cents), $160→$135 ($13500 cents) - [ ] Null `monthly_fee` defaults to $200 (prorated $165) - [ ] Order record created with correct `amount_cents`, `product_id`, `stripe_checkout_session_id` - [ ] Stripe Customer created/reused (saves to `player.stripe_customer_id`) - [ ] Stripe Checkout Session includes `payment_intent_data={"setup_future_usage": "off_session"}` to save card for future billing ### Test Expectations - [ ] Unit test: valid token → Stripe mock → 307 redirect + Order created with correct amount - [ ] Unit test: invalid token → 404 - [ ] Unit test: unsigned contract → 404 - [ ] Unit test: duplicate order → 409 - [ ] Unit test: $180 fee → $150 prorated ($15000 cents) - [ ] Unit test: null monthly_fee → defaults to $200 → $165 prorated ($16500 cents) - [ ] Unit test: verify `setup_future_usage` passed to Stripe Checkout Session create call - Run: `pytest tests/test_first_payment.py -v` ### Constraints - Use `contract_token` (Player model, unique) not `registration_token` (Parent model) - Stripe calls must be mocked in tests (`@patch("basketball_api.routes.checkout.stripe")`) - Use `RedirectResponse(url=..., status_code=307)` for the redirect - Product lookup: `ProductCategory.monthly` + `active=True` - `conftest.py` provides `client` fixture with DB override (no auth needed — token-based) - MUST include `payment_intent_data={"setup_future_usage": "off_session"}` in Checkout Session ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `westside-basketball` — project - Parent: `forgejo_admin/basketball-api#366` - Depends on: #367 (migration, merged PR #370) - Plan: `~/pal-e-platform/docs/superpowers/plans/2026-04-06-first-payment-email.md` (Task 2)
Author
Owner

Scope Review: READY

Review note: review-874-2026-04-06
Ticket is fully scoped — all template sections present, file targets verified against codebase, traceability triangle complete (story:WS-S7 confirmed on project page), dependency on migration ticket A (#366) correctly documented. 8 acceptance criteria are specific and testable. Fits single agent pass (~3-4 min, 2 discrete changes).

## Scope Review: READY Review note: `review-874-2026-04-06` Ticket is fully scoped — all template sections present, file targets verified against codebase, traceability triangle complete (story:WS-S7 confirmed on project page), dependency on migration ticket A (#366) correctly documented. 8 acceptance criteria are specific and testable. Fits single agent pass (~3-4 min, 2 discrete changes).
Author
Owner

Scope update (2026-04-06): Added payment_intent_data={"setup_future_usage": "off_session"} requirement. Parents enter card once during prorated checkout; Stripe saves it for recurring monthly billing starting May 1. Also updated lineage — migration dependency (#367) merged as PR #370.

**Scope update (2026-04-06):** Added `payment_intent_data={"setup_future_usage": "off_session"}` requirement. Parents enter card once during prorated checkout; Stripe saves it for recurring monthly billing starting May 1. Also updated lineage — migration dependency (#367) merged as PR #370.
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#368
No description provided.