Review: Stripe webhook for subscription status sync
Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- split from #23
- [x] Repo -- ldraney/westside-ror
- [x] User Story -- system actor, auto-updates roster
- [x] Context -- explains gap (no webhook endpoint, DB columns exist)
- [x] File Targets -- 4 modify/create, 3 do-not-touch
- [x] Acceptance Criteria -- 8 criteria
- [x] Test Expectations -- 7 test cases + run command
- [x] Constraints -- 5 constraints listed
- [x] Checklist -- standard 3-item
- [x] Related -- references project, companion tickets, spike, arch doc
Traceability
- [x] story:subscription-links label -- present on board item
- [ ] story note MISSING --
project-westside-rornote does not exist in pal-e-docs (404). [SCOPE] Create project pageproject-westside-rorwith user-stories section including subscription-links story. - [x] arch:rails label -- present on board item
- [ ] arch note MISSING -- no
arch-railsnote found in pal-e-docs. [SCOPE] Create architecture notearch-railsfor the Rails application component. Note:docs/stripe-subscription-architecture.mdexists in-repo but is not a pal-e-docs note. - [x] Forgejo issue -- ldraney/westside-ror#27, state: open
File Targets
- [x]
config/routes.rb-- verified: exists, currently has no webhook route (line 1-39). Route addition is straightforward. - [x]
app/controllers/webhooks_controller.rb-- correctly marked as NEW. Does not exist yet. Will needskip_forgery_protectionsince ApplicationController inherits from ActionController::Base (no existing CSRF skip patterns in codebase). - [x]
app/services/stripe_client.rb-- verified: exists (127 lines). Uses raw net/http, no Stripe gem. Addingverify_webhook_signaturemethod here is consistent with existing patterns. - [x] Deployment config (env var STRIPE_WEBHOOK_SECRET) -- no existing references. New secret needed.
- [x] DB columns verified in schema.rb:
Repo Placement
OK. Issue filed on ldraney/westside-ror, all file targets are in that repo. Single-repo change.
Dependencies
- #24 (editable fees) -- architecture doc says #24 ships first, but the issue correctly notes this webhook can ship independently. The webhook does not require fee editing to work. No blocking dependency.
- #26 (copy links UI) -- companion ticket, explicitly independent. No dependency.
- #22 (spike) -- closed. Produced the architecture doc. No dependency.
- #23 (original combined ticket) -- still on board in backlog. Should be closed or updated once #26 and #27 replace it. Not a blocker but a housekeeping item.
- No items in in_progress block this ticket. Items in in_progress (#2 Stripe payment links, #13 email) are unrelated to webhook functionality.
Acceptance Criteria
All 8 criteria are testable by an agent. The test expectations map cleanly to acceptance criteria. Run command (
rails test) is valid.Observations:
- AC #4 references reading metadata (player_id, team) from checkout.session.completed. The ticket should clarify HOW player_id gets into session metadata -- this is set when creating the payment link (which happens in #26). The webhook needs to handle the case where metadata is missing or incomplete.
- AC #5 references finding payment_link by
stripe_payment_link_id. The checkout session object from Stripe includes apayment_linkfield -- this is the correct lookup key. - Idempotency constraint is stated but not in AC. Could be an explicit test case (processing same event twice should not error).
Blast Radius
subscription_statusis read byadmin/roster_report_controller.rb(lines 14, 24, 29, 30). Webhook updates to this field will surface automatically in the roster report. No code changes needed there -- this is the desired effect.paid_atis read byStripeClient#db_payment_summary(lines 70, 84), used by the payments dashboard. Again, webhook updates will surface automatically. No blast radius concern.- No other controllers or services reference these columns.
- Adding
skip_forgery_protectionto the new WebhooksController is scoped -- it only affects that controller, not the app globally.
Decomposition Assessment
4 file targets in 1 repo. 8 acceptance criteria. 7 test expectations. Estimated agent work: ~8-10 minutes (new controller, service method, route, tests). This is above the 5-minute threshold but all targets are in one repo and tightly coupled -- decomposing would create artificial seams. The 8 AC are all part of the same webhook handler. A single agent pass is appropriate if the agent is given clear guidance. No decomposition needed.
Recommendations
[SCOPE]Create project pageproject-westside-rorwith user-stories section including thesubscription-linksstory entry.[SCOPE]Create architecture notearch-railsfor the Rails application component in pal-e-docs (or re-evaluate whetherarch:railsis the right label -- possiblyarch:stripewould be more specific).[BODY]Add note to AC #4 about handling missing metadata gracefully (payment links created outside the app may lack player_id metadata).[BODY]Add explicit AC for idempotency: "Processing the same webhook event twice does not create duplicate records or errors."[BODY]Clarify that #23 (the original combined ticket) should be closed now that #26 and #27 replace it.