feat: wire tournament checkout URLs into blast query for per-player payment links #465
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!465
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "463-wire-checkout-urls"
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
stripe_checkout_urlcolumn to the Order model and wires it through the checkout link generation and blast query pipeline so tournament fee emails include each player's unique Stripe payment link.Changes
src/basketball_api/models.py— Addedstripe_checkout_url(VARCHAR 500, nullable) to Order modelalembic/versions/047_add_stripe_checkout_url_to_orders.py— NEW migration adding the columnsrc/basketball_api/routes/checkout.py—create_player_checkout_session()now persistssession.urltoOrder.stripe_checkout_urlsrc/basketball_api/services/email_queries.py—query_tournament_committed()acceptstournament_idinquery_params, joins through TournamentProduct -> Product -> Order to resolve per-player checkout URLsscripts/backfill_checkout_urls.py— NEW backfill script that re-fetches URLs from Stripe for existing orders missingstripe_checkout_urltests/test_tournament.py— Added tests for URL persistence on Order and model column existencetests/test_tournament_blast.py— Added tests for checkout_url resolution with/without tournament_idTest Plan
pytest tests/ -k "tournament"— 30 tests pass (4 new)python scripts/backfill_checkout_urls.py(dry run) then--commitfor the 21 Utah Invitational ordersReview Checklist
ruff checkandruff formatpass on all changed filesscripts/backfill_stripe.pyRelated Notes
Related
Closes #463
PR #465 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Alembic / Stripe SDK
Migration (047_add_stripe_checkout_url_to_orders.py)
down_revision = "046"chains properly.String(500), nullable=True-- matches the model. Clean upgrade/downgrade.Model (models.py)
stripe_checkout_url: Mapped[str | None] = mapped_column(String(500), nullable=True)-- correctly placed afterstripe_checkout_session_id. PEP 484 type hint present.Checkout persistence (checkout.py)
order.stripe_checkout_url = checkout_session.urladded immediately afterorder.stripe_checkout_session_id = checkout_session.idat the samedb.flush()boundary. Clean single-write pattern -- no extra flush needed.Admin endpoint (admin.py)
generate_checkout_links()callscreate_player_checkout_session()which now saves the URL on the Order. Thedb.commit()at line 2311 persists it. No changes needed in admin.py itself -- the URL propagates through the existingcreate_player_checkout_sessionreturn path. Correct.Blast query (email_queries.py)
query_tournament_committed()now accepts optionaltournament_idinquery_params.(player_id, team_id) -> checkout_urlmapping by joining TournamentProduct -> Order.OrderStatus.pendingandstripe_checkout_url.isnot(None)-- correct: only returns URLs that exist for active pending orders.checkout_urls.get((player.id, matched_team.id), "")returns empty string when no order exists or no tournament_id given. Verified by tests."checkout_url": ""comment ("populated by body.data override") is now replaced with the actual resolution. Good.Backfill script (scripts/backfill_checkout_urls.py)
scripts/backfill_stripe.py(dry-run by default,--committo apply).stripe_checkout_session_id IS NOT NULL AND stripe_checkout_url IS NULL-- correct targeting.stripe.checkout.Session.retrieve()to get the URL from Stripe.StripeErrorper-order (doesn't abort the whole batch). Good resilience.session.url = Nonefor expired sessions. The script handles this with the warning log. This is a known Stripe limitation, not a code bug -- but operationally, the backfill may need to regenerate sessions for expired orders. Worth noting for the operator.Test coverage (test_tournament.py, test_tournament_blast.py)
test_generate_checkout_links_saves_url_to_order-- verifies Order.stripe_checkout_url is set after checkout-links endpointtest_order_has_stripe_checkout_url-- model-level column existence testtest_returns_checkout_url_from_order-- query with tournament_id resolves URL from Ordertest_checkout_url_empty_without_tournament_id-- no tournament_id yields empty stringtest_checkout_url_empty_when_no_order-- tournament_id with no order yields empty stringBLOCKERS
None.
NITS
Backfill expiry caveat: The docstring for
backfill_checkout_urls.pycould note that Stripe checkout session URLs expire after 24 hours, so the backfill only works for recently-created sessions. Expired sessions will log a warning but the operator might not know why.product_by_teamassumes 1:1 team:product: The dict comprehension{tp.team_id: tp.product_id for tp in tp_rows}will silently overwrite if a tournament ever has multiple products per team. Current schema has a unique constraint on(tournament_id, team_id)so this is safe today, but a comment noting the assumption would help future readers.Query efficiency:
query_tournament_committedloads all TournamentProduct rows and all matching Orders into memory. For the current scale (dozens of players) this is fine. If tournaments scale to hundreds of teams, consider a single joined query.SOP COMPLIANCE
463-wire-checkout-urlsfollows{issue-number}-{kebab-case-purpose}PROCESS OBSERVATIONS
VERDICT: APPROVED