feat: save card registration as pending before Stripe redirect #111
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
ldraney/basketball-api!111
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "110-feat-stripe-card-payment-saves-registrat"
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
pending/stripe) before redirecting to Stripe Payment Linkcheckout.session.completedevents to pending registrations by parent emailpaidwith Stripe session/payment intent IDs storedChanges
routes/register.py: Moved card redirect after player/registration creation, setspending/striperoutes/webhooks.py: New pending registration matching block before jersey checkout handlertests/test_promo_registration.py: Updated test comment for new behaviorTest Plan
pytest tests/test_promo_registration.py -vpassesReview Checklist
Related
plan-wkq— Phase 1 (Commit Uncommitted Work)PR #111 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Stripe webhooks
register.py changes -- The save-before-redirect logic is structurally correct. Card payments now flow through full player/registration creation with
payment_status=pendingandsignup_method="stripe", then return the Stripe Payment Link URL. The placement afterdb.commit()at line 1273 ensures the record is persisted before the user is redirected. This is the right pattern.webhooks.py changes -- The new pending-registration matching block at lines 198-229 queries for a pending registration by joining
Registration -> Player -> Parentand matching on lowercased parent email. When found, it updatespayment_statustopaidand stores Stripe session/payment IDs. The logic is functionally sound for the happy path, but has edge case issues documented below.BLOCKERS
1. Card registration test does not verify DB state (missing test coverage)
test_card_payment_returns_redirect_url(line 149 oftest_promo_registration.py) was updated with a new docstring ("should save registration as pending") but the test body was NOT updated. It only asserts:response.status_code == 200"redirect_url" in body"stripe.com" in body["redirect_url"]It does NOT verify that a
Registrationrecord was created withpayment_status=pendingandsignup_method="stripe". The docstring claims the test covers DB persistence, but it does not. This is the core new behavior of this PR and it is untested.2. Zero test coverage for webhook pending-registration matching
The new webhook handler block (33 lines of new logic in
webhooks.py) has no tests at all. Required test cases:This is new functionality with zero test coverage -- a BLOCKER per review criteria.
3. Webhook handler ordering can intercept jersey checkouts (correctness bug)
The pending-registration check (line 197-229) runs BEFORE the jersey checkout check (line 232). The pending-registration handler matches on
customer_email+payment_status == pending. The jersey handler matches onmetadata.jersey_option.If a parent has a pending card registration AND later buys a jersey, the jersey
checkout.session.completedevent will have their email incustomer_details.email. The pending-registration handler will match first, incorrectly marking the registration as paid with the jersey checkout session ID, and the jersey order will never be processed.Fix: Either check for jersey metadata first (swap ordering), or add
signup_method == "stripe"to the webhook filter, or check that the checkout session ID does NOT have jersey metadata before matching as a registration payment.NITS
1. Inline import in hot path --
from basketball_api.models import Parent, PaymentStatus, Registrationat line 199 insidestripe_webhook(). Move these to the module-level imports at the top ofwebhooks.pywherePlayerand other models are already imported. The inline import adds per-request overhead and is inconsistent with the rest of the file.2.
funcimport ordering --from sqlalchemy import funcat line 9 is placed between thebasketball_api.configandbasketball_api.modelsimports, breaking the standard import grouping (stdlib, third-party, local). Should be grouped with thesqlalchemy.ormimport on line 5.3. Multiple pending registrations ambiguity --
.first()at line 216 picks an arbitrary pending registration when a parent has multiple children registered with card payment. The PR body acknowledges Phase 4 will fix this with metadata-based matching. Consider adding anORDER BY created_at DESCso at least the most recent registration is matched, or logging a warning when multiple matches exist.SOP COMPLIANCE
110-feat-stripe-card-payment-saves-registrat-> issue #110)plan-wkq)PROCESS OBSERVATIONS
test_subscriptions.pyandtest_jersey.pyboth have webhook test classes that mockstripe.Webhook.construct_event. The same pattern should be used to test the new pending-registration webhook handler.VERDICT: NOT APPROVED
Three blockers must be resolved:
test_card_payment_returns_redirect_url(verifypending/striperecord created)PR #111 Review
Re-review after fixes for 3 previously reported blockers.
BLOCKER RESOLUTION STATUS
Blocker 1: Card registration test should assert DB state -- FIXED
TestCardRegistration.test_card_payment_returns_redirect_urlnow accepts adb: Sessionfixture and asserts:reg.payment_status == PaymentStatus.pendingreg.signup_method == "stripe"Clean, comprehensive fix.
Blocker 2: New webhook tests for pending registration email matching -- FIXED
Three new tests added in
TestRegistrationWebhookCompletion:test_webhook_marks_pending_registration_as_paid-- full end-to-end: register card -> webhook -> verify paid + Stripe IDs storedtest_webhook_email_matching_is_case_insensitive-- registers with mixed-case email, webhook fires with different casing, still matchestest_webhook_no_match_falls_through-- webhook with unknown email does not crash, falls through toprocess_checkout_completedAll three tests use the correct pattern: mock
stripe.Webhook.construct_event, create real DB state via the registration endpoint, then assert DB mutations. Well-structured.Blocker 3: Webhook handler ordering (jersey metadata first, then email matching) -- FIXED
In
webhooks.pyline 200,_handle_jersey_checkout_completed(db, data_object)runs first. Only if it returnsFalse(nojersey_optionin metadata) does the code proceed to email-based pending registration matching (line 203+). The comment at line 197-199 explicitly documents the rationale. Correct ordering.DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Stripe webhooks
Registration flow (
register.py):redirect_url. Previously it returned the redirect immediately without saving any DB state. The fix correctly places the redirect afterdb.commit()at line 1278.elif body.payment_method == "card"branch at line 1243 correctly setspayment_status = PaymentStatus.pendingandsignup_method = "stripe".Webhook flow (
webhooks.py):customer_details.email,customer_email, and empty string fallback. Normalized with.strip().lower().func.lower(Parent.email)for DB-side case-insensitive comparison. Correct.payment_status = paid, storesstripe_checkout_session_id,stripe_payment_intent_id,amount_cents. All fields exist on the Registration model and are nullable. Correct.Import placement: The
from basketball_api.models import Parent, PaymentStatus, Registrationat line 204 is a function-level import inside the webhook handler.Playeris already imported at module level (line 10). This works but is slightly inconsistent --ParentandRegistrationcould be at module level too. Non-blocking.BLOCKERS
One new issue found:
The webhook pending registration query (lines 216-225) filters only on
PaymentStatus.pendingbut does NOT filter onRegistration.signup_method == "stripe". Bothcashandcardregistrations creatependingrecords. If a parent registers a child with cash (pending/cash), then later registers another child with card (pending/stripe), the webhook could match the wrong registration -- whichever.first()returns.The fix is adding one line to the filter:
This is a correctness bug. While it may be unlikely in the current user base, the email-based matching is already acknowledged as a temporary approach (PR body notes Phase 4 will use metadata-based matching). Adding the
signup_methodfilter costs nothing and prevents a real data integrity issue.Severity: BLOCKER -- unfiltered query could update the wrong registration record.
NITS
Function-level import (
webhooks.pyline 204):Parent,PaymentStatus, andRegistrationare imported inside the function body. Consider moving to module-level imports alongside the existingPlayerimport on line 10. This is consistent with the rest of the file.Test file naming: The webhook completion tests live in
test_promo_registration.py, which was originally scoped to registration endpoint tests. The webhook tests are conceptually distinct. Consider whether they should eventually move to a dedicatedtest_webhook.py. Non-blocking for this PR.PR body Test Plan checkboxes: All three items are unchecked (
- [ ]). These should be checked off before merge to reflect that testing was actually performed.SOP COMPLIANCE
110-feat-stripe-card-payment-saves-registratreferences issue #110)plan-wkq)PROCESS OBSERVATIONS
VERDICT: NOT APPROVED
One remaining blocker: the webhook query must also filter on
signup_method == "stripe"to avoid matching cash registrations for the same parent email. Single-line fix.PR #111 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Stripe webhooks / pytest
register.py -- card payment flow
The old code returned the Stripe redirect URL immediately (before creating any DB records). The new code correctly moves the redirect after player/parent/registration creation, setting
payment_status=pendingandsignup_method="stripe". The control flow is clean:cardbranch (pending/stripe)redirect_urlfor card payments (before Keycloak account creation, which only applies topaidregistrations)The early return at line 1278 is correctly placed after
db.commit()and before the Keycloak block, which is guarded bypayment_status == PaymentStatus.paidanyway. No logic leak.webhooks.py -- pending registration matching
The handler ordering is correct and well-commented:
process_checkout_completedis the fallbackThe query at lines 216-226 is the focus of this round. It filters on three conditions:
func.lower(Parent.email) == customer_email(case-insensitive email match)Registration.payment_status == PaymentStatus.pendingRegistration.signup_method == "stripe"This is the fix from round 2. The
signup_method == "stripe"filter correctly prevents matching cash-pending registrations, which was the previous blocker. The join chainRegistration -> Player -> Parentfollows the established relationships in the model.Email extraction handles both
customer_details.emailandcustomer_emailwith.strip().lower()normalization. Empty-email case is guarded (if customer_email:).Inline import:
from basketball_api.models import Parent, PaymentStatus, Registrationat line 204 is inside the function body.Playeris already imported at the module level (line 10).Parent,PaymentStatus, andRegistrationare not in the top-level imports. This is a minor style inconsistency but not a blocker -- likely done to avoid circular imports or to keep the diff minimal.test_promo_registration.py -- test coverage
The test suite covers:
test_card_payment_returns_redirect_url-- verifies redirect URL returned AND DB state (pending/stripe), including Player record existence. This was the round 1 blocker fix.test_webhook_marks_pending_registration_as_paid-- full end-to-end: creates pending registration, fires webhook, assertspaidstatus + Stripe session/payment intent IDs + amount_cents. This was the round 1 blocker fix.test_webhook_email_matching_is_case_insensitive-- registers with mixed-case email, webhook arrives with different case, still matches. Good edge case.test_webhook_no_match_falls_through-- webhook for unknown email does not crash, falls through toprocess_checkout_completed. Good error path coverage.All tests properly mock
stripe.Webhook.construct_eventand use the_cleanfixture for DB isolation.BLOCKERS
None.
All three previous blockers have been verified as resolved:
test_card_payment_returns_redirect_urlnow assertsreg.payment_status == PaymentStatus.pendingandreg.signup_method == "stripe".signup_method == "stripe"filter: Fixed. The webhook query at line 223 includesRegistration.signup_method == "stripe", preventing false matches on cash-pending registrations.NITS
Inline import in webhook handler (webhooks.py:204):
Parent,PaymentStatus, andRegistrationcould be moved to the module-level imports alongsidePlayerfor consistency. Not a blocker -- the code works correctly as-is.PR body test plan uses manual checkboxes: The test plan lists
pytest tests/test_promo_registration.py -vbut the checkboxes are unchecked. Minor -- CI validates this.SOP COMPLIANCE
110-feat-stripe-card-payment-saves-registratreferences issue #110)plan-wkq)settingsobject)PROCESS OBSERVATIONS
signup_methodandpayment_statuscolumns already exist in the model. Low-risk deploy.signup_method == "stripe"filter prevents cross-contamination with cash registrations.VERDICT: APPROVED