Bug: Generic checkout webhook doesn't sync jersey status back to players table #170
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#170
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Type
Bug
Lineage
standalone — discovered during operations/data audit
Repo
forgejo_admin/basketball-apiWhat Broke
The generic checkout system (migration 013) introduced an
orderstable to replace denormalizedplayers.jersey_*fields, but the Stripe webhook handler for the new system (_handle_generic_order_completedinwebhooks.py) only updatesorders.status = paid— it never syncs back toplayers.jersey_option,jersey_order_status,jersey_size, orjersey_number. Parents who order through the new/checkout/create-sessionpath appear to have no jersey order when queryingplayers.Confirmed affected players:
orders.status = paid,players.jersey_order_status = noneorders.status = paid,players.jersey_order_status = noneRoot cause: Two parallel checkout systems (legacy
jersey.pyand genericcheckout.py) both active, neither decommissioned. Webhook routing inwebhooks.py:256-262checksorder_idfirst — if present, generic handler runs and returns, legacy handler (which DOES update players) is never reached.Repro Steps
/checkout/create-session(new generic path)checkout.session.completedwithorder_idin metadata_handle_generic_order_completedsetsorders.status = paid, returnsSELECT jersey_order_status FROM players WHERE name = 'Anaiyah Fesolai';— returnsnoneExpected Behavior
After successful Stripe payment via either checkout path,
players.jersey_option,jersey_order_status,jersey_size, andjersey_numbershould reflect the completed order. Admin queries againstplayersshould return accurate jersey status.Environment
basketball-apiFile Targets
src/basketball_api/routes/webhooks.py—_handle_generic_order_completed()(lines 137-169). Add player sync afterorder.status = paid.src/basketball_api/routes/checkout.py—create_checkout_session()opt-out path (lines 140-160). Add player field sync on opt-out order creation.src/basketball_api/models.py—Productmodel (line 328),ProductCategoryenum (line 114). Used for jersey category detection.alembic/versions/023_backfill_player_jersey_from_orders.py— NEW. One-time data migration to reconcile existing paid orders → player records.tests/test_checkout.py—test_webhook_updates_order_to_paid()(line 321). Extend to assert player fields. Add new tests.Product → JerseyOption Mapping
The webhook must map
product.nametoJerseyOptionenum whenproduct.category == 'jersey':reversiblejersey_warmupopt_outorder.custom_dataJSONB contains sizing:{top_size: S, shorts_size: S, jersey_number: 1}. Maptop_size→player.jersey_size,jersey_number→player.jersey_number.Test Expectations
Run:
cd ~/basketball-api && python -m pytest tests/test_checkout.py tests/test_jersey.py -vExtend existing test:
test_webhook_updates_order_to_paid(test_checkout.py:321) — after assertingorder.status == paid, also assertplayer.jersey_option,player.jersey_order_status == paid,player.jersey_size,player.jersey_numberNew tests needed:
test_webhook_syncs_jersey_fields_to_player— generic order webhook for a jersey-category product updates player recordtest_webhook_skips_player_sync_for_non_jersey_product— tournament/equipment orders don't touch player jersey fieldstest_opt_out_checkout_syncs_player_fields— opt-out via/checkout/create-sessionsetsplayer.jersey_option = opt_outandplayer.jersey_order_status = paidtest_duplicate_order_prevention— second checkout for same player+product returns existing pending order or errorsAcceptance Criteria
_handle_generic_order_completedsyncsplayer.jersey_option,jersey_order_status,jersey_size,jersey_numberwhenproduct.category == jerseycheckout.pyopt-out path sets player jersey fields (same as legacyjersey.pyopt-out does)023_backfill_player_jersey_from_ordersreconciles all existingorders.status = paid+product.category = jerseyrows → player records/checkout/create-sessionchecks for existing pending order for same player+product before creating new oneConstraints
WHERE players.jersey_order_status = 'none'guard.Decisions
/jersey/checkout): Keep active for now. Add a deprecationlogger.warningon each call. Full retirement is a separate future ticket.Related
#171— Baby Betty contradictory state (split from this ticket)project-westside-basketball— project this affectsalembic/versions/013_generic_checkout_system.py— migration that introduced orders tablewestside-contracts— frontend SvelteKit app that drives the checkout flowScope Review: NEEDS_REFINEMENT
Review note:
review-392-2026-03-26Root cause analysis and code references are accurate — all file targets verified against the codebase. The bug is real and well-documented.
Six issues prevent READY status:
src/basketball_api/routes/webhooks.py, notwebhooks.py)orders.custom_dataJSONBRefinement (post review-392-2026-03-26)
Addressing 6 review findings. Updating issue body with refined scope below.
Decisions made
Refined issue body follows in next edit.
Scope Review: READY
Review note:
review-392-2026-03-26bRe-review pass: all 6 issues from previous review (review-392-2026-03-26) have been addressed. All file targets verified against codebase, all ACs are agent-testable, template complete.
Observation (not a blocker): Product
custom_fieldsoptions fortop_sizeincludeS, M, L, XLbutJerseySizeenum usesAS, AM, AL, AXL. Agent can handle this during implementation using the existing try/except pattern from the legacy handler (webhooks.py:207-209).