Review: Stripe webhook not syncing checkout.session.completed to order status

review-719-stripe-webhook-sync Doc

review ready

Verdict: APPROVED

Ticket forgejo_admin/basketball-api#264 (board item #719) is ready to move from backlog to todo. Well-scoped bug with concrete evidence, clear acceptance criteria, and verified file targets.

Template Completeness

Section Status Notes
Type PASS Bug
Lineage PASS Standalone, discovered during jersey validation 2026-03-30. Includes specific player/order details.
Repo PASS <code>forgejo_admin/basketball-api</code>
What Broke PASS Excellent specificity: Stripe session ID, order #18, player #113, payment_status=paid but DB stuck on pending.
Repro Steps PASS 4 clear steps with observable outcome.
Expected Behavior PASS 3 concrete behaviors: update order status, sync jersey fields, handle previous opt-out.
Environment PASS Cluster (prod), commit SHA (4cd0e1b5), affected records, Stripe session ID.
Acceptance Criteria PASS 4 checkboxes, all verifiable: webhook processes events, order status updates, jersey fields sync, structured logging.
Related PASS project-westside-basketball, story:WS-S18, basketball-api#263.

File Target Verification

  • Primary target: src/basketball_api/routes/webhooks.py -- exists. The _handle_generic_order_completed function (lines 164-202) is the handler for checkout.session.completed with order_id metadata. Code path: line 291-294 dispatches to this handler first.
  • Secondary target: src/basketball_api/services/jersey_sync.py -- exists. The sync_player_jersey_from_order function syncs jersey fields from paid orders. Called by the webhook handler at line 192.
  • Checkout route: src/basketball_api/routes/checkout.py -- confirmed that order_id IS set in Stripe metadata (line 235), so the webhook handler should match.
  • Existing tests: tests/test_checkout.py, tests/test_jersey.py cover related functionality.

Root Cause Hypothesis

The code path looks correct on paper: _handle_generic_order_completed receives the session data, finds the order by metadata.order_id, sets order.status = OrderStatus.paid, and calls sync_player_jersey_from_order. Possible failure modes the agent should investigate:
  • Webhook not configured in Stripe: The checkout.session.completed event type may not be enabled in the Stripe webhook endpoint configuration.
  • Webhook secret mismatch: stripe_webhook_secret env var may be wrong, causing signature verification to fail (400 response, no logging).
  • Silent failure in jersey_sync: If the product category is not jersey, sync_player_jersey_from_order returns early without logging.
  • Missing structured logging: Acceptance criteria #4 asks for visible webhook logs -- current logging exists but may not be surfacing in production.

Traceability

  • Story: story:WS-S18 -- "As a parent, I want to receive a branded email with jersey ordering link so that I can order without bringing cash." Confirmed in project-westside-basketball user stories.
  • Board item: #719, labels: type:bug,arch:basketball-api,story:WS-S18. Correct.

Dependencies

  • basketball-api#263 (remove opt-out from jersey ordering) -- related but independent. No blocking dependency.
  • basketball-api#262 (extract shared _resolve_player helper) -- refactoring ticket, not a dependency.
  • No blockers identified.

Decomposition Assessment

Well-scoped for a single agent. Discrete changes expected:
  • Diagnose root cause (webhook config vs code bug)
  • Fix the handler or configuration
  • Add/improve structured logging for webhook events
All within the 3-change limit. Estimated agent time: under 5 minutes.

Recommendations

  • Agent should check Stripe webhook endpoint config (which events are enabled) as part of diagnosis.
  • Agent should add a test case for the specific scenario: player with previous opt-out order gets a new paid order via generic checkout.