Fix: confirmation email skipped for existing Keycloak accounts #396
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
forgejo_admin/basketball-api!396
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "390-fix-confirmation-email-existing-keycloak"
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
Confirmation emails were not sent to parents who already had a Keycloak account because
send_confirmation_email()was nested insideif keycloak_credentials:in both the promo and webhook registration paths. Additionally, the promo path never generatedregistration_token, causingtoken=Nonein profile URLs.Changes
src/basketball_api/routes/register.py— Addedimport secrets; generateregistration_tokenfor parents in the promo path (v2 endpoint) before waiver signing; movedsend_confirmation_email()outside theif keycloak_credentials:guard so it fires unconditionally, passingcredentials=keycloak_credentials(None for existing accounts, dict for new ones).src/basketball_api/routes/webhooks.py— Movedsend_confirmation_email()outside theif keycloak_credentials:guard in the webhook checkout path, same pattern as register.py.tests/test_promo_registration.py— Added 3 tests: existing Keycloak sends email with credentials=None, new Keycloak includes credentials (no regression), and registration_token is generated (not None).tests/test_webhooks.py— Added 1 test: webhook path sends confirmation email when Keycloak account already exists.Test Plan
pytest tests/ -k "test_register or test_promo or test_webhook or test_keycloak"— 124 passed, 0 failuresReview Checklist
ruff formatandruff checkpassRelated Notes
None.
Related
Closes #390
PR #396 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy
Bug fix correctness -- The root cause was correct:
send_confirmation_email()was nested insideif keycloak_credentials:in both the promo (register.py) and webhook (webhooks.py) paths. Parents with existing Keycloak accounts gotNonefromcreate_account_for_parent(), skipping the email entirely.The fix is sound:
register.py --
keycloak_credentialsis initialized toNoneat line 1387 before the Keycloak conditional, so it is always in scope when the email call fires. The email call is moved to fire unconditionally after the Keycloak block, passingcredentials=keycloak_credentials(None for existing accounts, dict for new). Thesend_confirmation_email()function already handlescredentials=Nonegracefully (line 96-103 inemail.py--cred_textis empty string).register.py token fix --
registration_tokengeneration viasecrets.token_urlsafe(32)is guarded byif not parent.registration_token:, preventing overwrite for returning parents. This fixes thetoken=Noneprofile URL bug. Placement before waiver signing is correct.webhooks.py -- Same pattern: email call moved outside
if keycloak_credentials:but kept insideif player and player.parent:andif tenant:. Scope is correct.No regressions -- Card/Stripe payments still return early at line 1384 (
return {"redirect_url": ...}) before reaching the email section, so only promo registrations hit this path. The webhook path handles card payments separately. Thecredentialskwarg is passed through, preserving behavior for new accounts.send_confirmation_emailsignature compatibility -- Verified at/home/ldraney/basketball-api/src/basketball_api/services/email.py:65-72. The function acceptscredentials: Credentials | None = Noneas a keyword argument. Passingcredentials=Noneis the default behavior. No interface mismatch.BLOCKERS
None.
credentials=Noneis passed andmock_email.assert_called_once()confirms the email fires.NITS
Lazy import inside hot path --
from basketball_api.services.email import send_confirmation_emailis imported inline at call sites in both register.py and webhooks.py rather than at module top. This is an existing pattern in the codebase (not introduced by this PR), so not a blocker, but worth noting for future cleanup.Test setup duplication -- The three promo tests in
TestConfirmationEmailExistingKeycloakeach repeat the same Settings/monkeypatch boilerplate (lines 633-639, 662-668, 694-698). A shared fixture would reduce duplication. Minor -- the tests are readable as-is.Hardcoded tryout details in email body --
email.py:119has hardcoded date "Tuesday, March 24", time, and location. Not introduced by this PR, but this will need updating for future seasons.SOP COMPLIANCE
390-fix-confirmation-email-existing-keycloakPROCESS OBSERVATIONS
Clean bug fix with strong test coverage. The fix addresses both code paths (promo + webhook) that shared the same bug pattern, which is good -- partial fixes would have left the webhook path broken. The 4 new tests directly map to the bug scenarios described in issue #390. Change failure risk is low given the narrow scope and the fact that
send_confirmation_emailalready handledcredentials=None.VERDICT: APPROVED
PR #396 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / pytest
Bug fix correctness: The fix addresses two distinct bugs from #390:
Confirmation email gated behind
if keycloak_credentials:-- Bothregister.py(promo path) andwebhooks.py(Stripe path) hadsend_confirmation_email()nested inside a conditional that only fired when a new Keycloak account was created. Parents with existing accounts got no confirmation email. The fix correctly moves the email call outside the guard and passescredentials=keycloak_credentials(which isNonefor existing accounts, dict for new ones). Thesend_confirmation_emailfunction signature already acceptscredentials: Credentials | None = None, so this is safe.registration_tokenwas never generated in the promo path -- The promo v2 endpoint skipped token generation, resulting intoken=Noneappearing literally in profile URLs (confirmed:email.py:92interpolatesparent.registration_tokeninto f-strings). The fix generates asecrets.token_urlsafe(32)token whenparent.registration_tokenis falsy. This is correct and uses the standard library's cryptographically secure token generator.Third call site:
src/basketball_api/services/registration.py:126also callssend_confirmation_emailbut was already correct -- it calls unconditionally without a Keycloak guard and without passingcredentials. No change needed there.Pattern consistency: Both
register.pyandwebhooks.pynow follow the same pattern: try/except aroundsend_confirmation_email()withcredentials=keycloak_credentials, structured log message indicating whether credentials were included or omitted. Good symmetry.Indentation in webhooks.py: The email block in webhooks.py is deeply nested (inside the Stripe event processing chain). The indentation looks correct relative to the surrounding try/except for Keycloak account creation.
BLOCKERS
None.
NITS
Inline import pattern -- Both paths use
from basketball_api.services.email import send_confirmation_emailinside the function body rather than at module top-level. This is a pre-existing pattern (not introduced by this PR), so not actionable here, but worth noting as tech debt.confirmation_email_sentflag not set -- Theregistration.pypath setsregistration.confirmation_email_sent = Trueafter sending. Neitherregister.pynorwebhooks.pyset this flag after sending the email. This is pre-existing behavior unchanged by this PR, but could cause confusion for any code that checks that field. Consider a follow-up ticket.Log message consistency --
register.pylogs"Sent confirmation email to %s (promo registration, credentials=%s)"whilewebhooks.pylogs"Sent confirmation email to %s (credentials=%s)". Minor -- the promo path includes the registration type context, the webhook path does not. Not blocking.SOP COMPLIANCE
390-fix-confirmation-email-existing-keycloaktest-password,whsec_test,tok_existing)PROCESS OBSERVATIONS
send_confirmation_emailalready handlescredentials=Nonegracefully.VERDICT: APPROVED