feat: tournament product creation + per-player Stripe checkout links #460
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api!460
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "457-tournament-checkout-links"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds admin endpoints to create tournaments with team-specific pricing and generate per-player Stripe checkout URLs. First consumer: Utah Invitational (17U Elite $55, 17U Select $65, 16U Elite $55).
Changes
src/basketball_api/models.py— AddedTournamentandTournamentProductmodels with tenant FK, unique constraint on (tournament_id, team_id)alembic/versions/039_add_tournament_tables.py— Migration creatingtournamentsandtournament_productstables with indexessrc/basketball_api/routes/admin.py— AddedPOST /admin/tournaments(create with team-specific products),GET /admin/tournaments/{id}, andPOST /admin/tournaments/{id}/checkout-links(generate per-player Stripe sessions)src/basketball_api/routes/checkout.py— Extractedcreate_player_checkout_session()helper for admin-driven checkout flowtests/test_tournament.py— 11 tests: creation, validation, 404s, checkout links with Stripe mocks, duplicate order handling, model linkageTest Plan
pytest tests/test_tournament.py -v— 11/11 passcategory='tournament',type='one_time'Review Checklist
Related Notes
{{checkout_url}}placeholder consumerstory:WS-S33— tournament billing user storyCloses #457
Adds admin endpoints to create tournaments with team-specific pricing and generate per-player Stripe checkout URLs for billing tournament entry fees. - Tournament and TournamentProduct models with migration 039 - POST /admin/tournaments creates tournament + N products in one call - GET /admin/tournaments/{id} retrieves tournament with products - POST /admin/tournaments/{id}/checkout-links generates per-player URLs - Extracted create_player_checkout_session helper from checkout.py - 11 tests covering creation, retrieval, links, duplicates, and models Closes #457 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>PR #460 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Alembic / Stripe / Pydantic
Models (
src/basketball_api/models.py):TournamentandTournamentProductare well-structured.Tournamenthas id, name, event_date, description, tenant_id -- matches spec.TournamentProductlinks tournament to product with team_id FK and unique constraint on(tournament_id, team_id)-- prevents duplicate team entries per tournament. Cascade deletes are correct. Relationships bidirectional withback_populates.Migration (
alembic/versions/039_add_tournament_tables.py): Schema matches models exactly. Indexes on all FK columns. Unique constraint matches model__table_args__. Downgrade drops tables in correct dependency order.Routes (
src/basketball_api/routes/admin.py):POST /admin/tournamentsvalidates teams exist (404 for missing), validates non-empty teams list (422), parses event_date with error handling (422), creates tournament + products in single transaction. All correct.GET /admin/tournaments/{id}returns 404 for missing tournament. Lazy-loads tournament_products via relationship. Clean.POST /admin/tournaments/{id}/checkout-linksiterates team players, checks for existing paid/pending orders (idempotent skip with error message), calls extracted helper. 404 for missing tournament, 400 for no products configured.Checkout helper (
src/basketball_api/routes/checkout.py):create_player_checkout_sessionextracts the duplicate-check, customer creation, order creation, and Stripe session creation pattern. Uses_SUCCESS_URL/_CANCEL_URLfrom module-level settings (not hardcoded). Stripe API key configured via module-levelimport stripewith global config from settings. The helper raisesHTTPException(409)for duplicates, which the caller catches gracefully.DRY observation: The helper duplicates logic from the existing
create_checkout_sessionendpoint (lines 149-253). Both do: duplicate order check, customer create-or-reuse, order creation, Stripe session creation. Ideally the existing endpoint would be refactored to call the helper too. However, this is additive code that does not modify the existing endpoint, and the duplication is in checkout logic (not auth/security), so this is a nit, not a blocker.Tests (
tests/test_tournament.py): 11 tests across 4 test classes:TestCreateTournament: happy path (201, products created with correct category/type), empty teams (422), invalid team ID (404), no date (201 with null)TestGetTournament: happy path (200), not found (404)TestCheckoutLinks: happy path with Stripe mocks (verifies metadata includes tournament_id/name/team_name), not found (404), duplicate order skipped (errors list populated, other players still get links)TestTournamentModel: direct ORM creation and relationship linkageAuth is properly overridden via
require_admindependency in test fixtures.BLOCKERS
1. Migration number collision (039 vs remote 044)
The PR creates
alembic/versions/039_add_tournament_tables.pywithrevision = "039"anddown_revision = "038". You stated remote main has migrations up to 044. This migration will fail to apply -- Alembic will not find revision "038" in the chain since the head is already at "044". The migration must be rebased to045(withdown_revision = "044").This is a hard blocker: the migration will not run on production.
NITS
DRY: checkout session creation --
create_player_checkout_sessionlargely duplicates the Stripe session flow increate_checkout_session(lines 149-253 of checkout.py). Consider refactoring the existing endpoint to call the helper, reducing two copies of customer-create + order-create + session-create to one. Not blocking since both paths work independently and the existing endpoint is untouched.Double duplicate-order check --
generate_checkout_linkschecks for existing orders before callingcreate_player_checkout_session, which also checks. The outer check handles the graceful skip (errors list), while the inner check is a safety net that raises 409. This is defensively correct but could be simplified by having only one check point.event_dateas string in request model --CreateTournamentRequest.event_dateisstr | Nonewith manualdate.fromisoformat()parsing. Usingdatetime.date | Nonedirectly in the Pydantic model would let FastAPI handle validation automatically and remove the manual parse block.Response model
created_atas string --TournamentResponse.created_atisstrwith manual.isoformat(). Usingdatetimetype with Pydantic's JSON serialization would be cleaner.N+1 query potential in
get_tournament-- Iteratingtournament.tournament_productsthen accessingtp.team.nameandtp.product.price_centstriggers lazy loads per item. For small tournament rosters this is fine; for scale, considerjoinedloadorselectinloadon the query.SOP COMPLIANCE
457-tournament-checkout-linksstory:WS-S33and linked issue #456PROCESS OBSERVATIONS
VERDICT: NOT APPROVED
Single blocker: migration 039 collides with existing migrations on main (remote head is 044). Rebase the migration to 045 with
down_revision = "044", then this PR is ready to merge.bca32a5ed1d3b334eb4c