Review: Stripe webhook not syncing checkout.session.completed to order status
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_completedfunction (lines 164-202) is the handler forcheckout.session.completedwithorder_idmetadata. Code path: line 291-294 dispatches to this handler first. - Secondary target:
src/basketball_api/services/jersey_sync.py-- exists. Thesync_player_jersey_from_orderfunction syncs jersey fields from paid orders. Called by the webhook handler at line 192. - Checkout route:
src/basketball_api/routes/checkout.py-- confirmed thatorder_idIS set in Stripe metadata (line 235), so the webhook handler should match. - Existing tests:
tests/test_checkout.py,tests/test_jersey.pycover 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.completedevent type may not be enabled in the Stripe webhook endpoint configuration. - Webhook secret mismatch:
stripe_webhook_secretenv 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_orderreturns 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.