fix: don't set jersey_option at checkout — wait for Stripe webhook #140
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!140
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "137-fix-jersey-option-timing"
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
The checkout endpoint was prematurely setting
jersey_optionon the player record before Stripe payment was confirmed. If a user abandoned the Stripe checkout session, they appeared as having selected a jersey option they never paid for. This fix removes that premature assignment sojersey_optionis only set by the Stripe webhook on payment confirmation.Changes
src/basketball_api/routes/jersey.py: Removedp.jersey_option = jersey_optfrom the checkout path.jersey_order_statusis still set topendingso we know a checkout was started. The opt-out path is unchanged.tests/test_jersey.py: Updated existing tests to assertjersey_option is Noneafter checkout. Updated webhook test to simulate the real flow (jersey_option NULL before webhook). Added 3 new tests covering checkout-without-option, opt-out-sets-immediately, and webhook-sets-from-metadata.Test Plan
pytest tests/ -k jersey— 28 tests passruff formatandruff checkpassjersey_optionjersey_optionremains NULL after checkout creationjersey_optionon payment confirmationjersey_optionimmediatelyReview Checklist
Related
Self-Review
Verdict: LGTM -- no issues found.
Code change (jersey.py)
p.jersey_option = jersey_optfrom the checkout path (line 170)jersey_order_status = pendingstill set so we track that a checkout was startedjersey_optionimmediately since no payment is involvedwebhooks.pyalready setsjersey_optionfrom Stripe metadata oncheckout.session.completed-- no changes needed therejersey_optis still used for validation and the opt-out branch)Tests (test_jersey.py)
test_checkout_sets_pending_status->test_checkout_sets_pending_status_but_not_jersey_option: assertsjersey_option is Noneafter checkouttest_webhook_updates_player_jersey_fields->test_webhook_sets_jersey_option_and_paid_status: pre-setsjersey_option = None(notreversible) to match real flowtest_checkout_creates_stripe_session_without_setting_jersey_option: verifies Stripe session created, jersey_option stays NULLtest_opt_out_sets_jersey_option_immediately: confirms opt-out path still sets jersey_option at checkout timetest_webhook_sets_jersey_option_from_stripe_metadata: verifies webhook reads option from Stripe metadata, not player recordVerification
PR #140 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Stripe webhooks
Fix correctness: The change is surgically correct. The checkout endpoint (
jersey.py) was prematurely assigningjersey_optionon the player record before Stripe payment confirmation. Now it only setsjersey_order_status = pendingto track that a checkout was initiated. Thejersey_optionvalue is preserved in Stripe session metadata ("jersey_option": body.optionat line 162), and the webhook handler (_handle_jersey_checkout_completedinwebhooks.py:171-209) reads it back from metadata and sets it on the player only after confirmed payment. The data flow is sound.Opt-out path: Correctly unchanged. Opt-out sets
jersey_optionimmediately since no payment is involved.Model compatibility: The
Player.jersey_optioncolumn isMapped[JerseyOption | None]withnullable=True, so leaving it asNoneafter checkout is valid at the database level.Test quality: Three new tests cover the critical scenarios:
jersey_optionstays NULLjersey_optionimmediately (regression guard)jersey_optionfrom Stripe metadata (the real flow)Existing tests updated to reflect the new behavior (asserting
jersey_option is Noneafter checkout, setting up webhook test with NULL initial state). Docstrings added explaining the why behind each assertion. All tests includedb.refresh(player)before assertions, which is the correct SQLAlchemy pattern.No changes to webhooks.py: The webhook handler already reads
jersey_optionfrom Stripe metadata and sets it on confirmation. This PR correctly identified that the webhook was already doing the right thing -- the bug was only in the checkout endpoint setting the value too early.BLOCKERS
None.
NITS
Multi-player family gap (pre-existing, not introduced by this PR): The checkout endpoint sets
jersey_order_status = pendingon ALL of a parent's players (for p in parent.players), but Stripe metadata only carriesplayer_id: parent.players[0].id. When the webhook fires, only that one player getsjersey_optionset andjersey_order_status = paid. Siblings remain stuck atpendingwithjersey_option = None. This is a pre-existing design gap -- recommend tracking as a separate issue.PR body Related section: References
Closes #137but does not reference the plan slugplan-wkq. Per SOP template, the Related section should link to the parent plan.SOP COMPLIANCE
137-fix-jersey-option-timingreferences #137)plan-wkqreference)PROCESS OBSERVATIONS
jersey_optionassignment from metadata. The fix reduces data inconsistency risk (abandoned checkouts no longer leave stalejersey_optionvalues).VERDICT: APPROVED