fix: contract email sends offer copy, not reminder #467
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!467
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "462-contract-offer-email"
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
The
POST /admin/email/contract/{player_id}endpoint was reusingsend_contract_reminder_email, which sends reminder copy ("we noticed you haven't signed yet"). For a first-time contract send, parents should receive offer copy ("your child has been placed on the team"). This addssend_contract_offer_emailand wires it into the single-player endpoint.Changes
src/basketball_api/services/email.py— Addedsend_contract_offer_emailfunction that sends branded offer copy with Queens pink (#e91e8c) or Kings red (#d42026) accent based on team name. Uses_brand_wrapper, logs toemail_logwithEmailType.contract_offer, includes plain-text fallback.src/basketball_api/routes/admin.py— UpdatedPOST /admin/email/contract/{player_id}to callsend_contract_offer_emailinstead ofsend_contract_reminder_email. Simplified call signature (single player instead of list).Test Plan
POST /admin/email/contract/{player_id}?test_email=draneylucas@gmail.comand verify offer copy appears (not reminder copy)contract_offertype/admin/email/contract-reminderendpoint unchanged (still uses reminder copy for bulk sends)Review Checklist
send_contract_offer_emailimports cleanly)Related Notes
Related
The single-player contract email endpoint was reusing send_contract_reminder_email which sends "we noticed you haven't signed yet" copy. Added send_contract_offer_email for the initial offer ("your child has been placed on the team") and wired it into POST /admin/email/contract/{player_id}. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>QA Review — PR #467
What was reviewed
src/basketball_api/services/email.py(+113 lines),src/basketball_api/routes/admin.py(+3/-8 lines)send_contract_signed_email,send_contract_reminder_email)Findings
No issues found. Clean implementation.
Pattern compliance —
send_contract_offer_emailfollows the established email function pattern exactly:get_gmail_client-> build HTML with_brand_wrapper-> plain-text fallback ->client.send_message->EmailLogcommit -> return message_id. Matchessend_contract_signed_emailstructure.Queens/Kings branding — Uses
_BRAND_QUEENS_PINK/_BRAND_REDconstants already defined in the module. Team name check ("Queens" in team_name) is consistent with other Queens-specific logic in the codebase.EmailType.contract_offer — Already exists in the enum (models.py line 72). No migration needed.
Import hygiene —
send_contract_offer_emailadded alphabetically.send_contract_reminder_emailretained (still used by bulk endpoint at line 1113).Admin route — Clean substitution. The
db.expunge(parent)pattern for test_email is preserved. Docstring updated to reflect offer vs reminder distinction.Edge case —
player.teams[0].name if player.teams else "Westside"fallback matches the same pattern insend_contract_reminder_emailline 1691.VERDICT: APPROVED
PR #467 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Gmail SDK
What this PR does: Adds
send_contract_offer_email()toemail.py(initial offer copy -- "your child has been placed on the team") and wires it intoPOST /admin/email/contract/{player_id}, replacing the previous call tosend_contract_reminder_email(reminder copy -- "we noticed you haven't signed yet"). Clean separation of offer vs. reminder email paths.Code quality observations:
_brand_wrapperaccent mismatch -- The function correctly computesaccent(Queens pink vs. Kings red) and uses it in inline body styles (CTA button, player name highlight, team name highlight). However,_brand_wrapper(tenant.name, body_html)is called WITHOUT passingaccent_color, so the wrapper's header border (border-bottom: 3px solid {accent_color}) will always be_BRAND_REDeven for Queens players. The fix is_brand_wrapper(tenant.name, body_html, accent_color=accent). This is a visual inconsistency, not a functional bug, but it undermines the Queens/Kings branding logic the function explicitly implements.Hardcoded season string --
"Spring/Summer 2026 season"is hardcoded in both HTML and plain-text bodies. The reminder function uses a_CONTRACT_DEADLINEconstant. Consider extracting the season string to a module-level constant to avoid stale copy in future seasons.escape()usage -- Properly applied toparent.name,player.name,team_name, andcontract_urlin HTML. Plain-text fallback correctly omits escaping. Good.Error handling -- Catches all exceptions, logs with
logger.exception, returnsNone. Consistent with the existingsend_contract_reminder_emailpattern. The caller in the route handler handlesNonegracefully (returnsmessage_id: nullin the response).Session safety -- The
db.expunge(parent)pattern in the route handler fortest_emailis preserved correctly, preventing the test email swap from being flushed to the database.Type hints and docstring -- Return type
str | Noneis correct. Docstring clearly distinguishes offer vs. reminder semantics. PEP 484/257 compliant.BLOCKERS
Test coverage gap for the new email function -- The existing tests in
test_single_contract_email.pymockload_email_templateandget_gmail_client. The newsend_contract_offer_emaildoes NOT callload_email_template(it builds HTML inline), so themock_load_templatepatch is now a dead mock that never fires. The tests still pass because they only assert on the response shape (player_name, parent_email, contract_link, message_id), but they do NOT verify:EmailType.contract_offer(notcontract_reminder) is loggedThe tests validate the route plumbing but not the behavioral change this PR introduces. A unit test for
send_contract_offer_emailthat asserts on the HTML content and email log type is needed to prevent regression back to reminder copy.NITS
The
_brand_wrapperaccent mismatch (item 1 above) -- while visual-only, it should be fixed in this PR since the function explicitly branches on Queens vs. Kings.Hardcoded
"Spring/Summer 2026 season"-- extract to a constant like_CURRENT_SEASON = "Spring/Summer 2026"alongside_CONTRACT_DEADLINE.The dead
mock_load_templatepatches intest_single_contract_email.pyshould be removed since the new code path no longer callsload_email_template. Dead mocks are misleading.SOP COMPLIANCE
462-contract-offer-emailmatches issue #462)PROCESS OBSERVATIONS
test_single_contract_email.pytests were written for the original implementation (which usedload_email_templateviasend_contract_reminder_email). They need updating to match the new code path, otherwise they provide false confidence.VERDICT: NOT APPROVED
Reason: The test file
test_single_contract_email.pyhas dead mocks (mock_load_template) and does not verify the behavioral change this PR introduces (offer copy vs. reminder copy, email type logging, team-based accent color). The tests need to be updated to mockget_gmail_clientonly (removing the deadload_email_templatemock) and assert that the Gmail client receives HTML containing offer copy and thatEmailType.contract_offeris written toemail_log. Fix the_brand_wrapperaccent passthrough as well.- Remove dead mock_load_template patches from tests (send_contract_offer_email builds HTML inline, never calls load_email_template) - Add tests verifying offer copy ("has been placed on"), not reminder copy - Add tests verifying EmailType.contract_offer is logged - Add tests verifying Queens pink (#e91e8c) vs Kings red (#d42026) accent - Fix _brand_wrapper call to pass accent_color=accent - Extract "Spring/Summer 2026" to _CONTRACT_SEASON constant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>PR #467 Re-Review
Re-review after fix commit addressing 1 blocker and 2 nits from the initial review.
Previous Findings -- Verification
BLOCKER (dead test mocks) -- FIXED.
@patch("basketball_api.services.email.load_email_template")andmock_load_templateremoved fromtest_send_returns_200andtest_test_email_param_redirects. The newsend_contract_offer_emailuses inline HTML +_brand_wrapper, notload_email_template, so these mocks were dead. Now correctly only mockingget_gmail_client.NIT (accent not passed to
_brand_wrapper) -- FIXED.send_contract_offer_emailcomputesaccentfrom team name (_BRAND_QUEENS_PINKfor Queens,_BRAND_REDotherwise) and passesaccent_color=accentto_brand_wrapper. Correctly uses the canonical_BRAND_QUEENS_PINKconstant rather than the duplicate_CONTRACT_PINK.NIT (hardcoded season string) -- FIXED. Extracted to
_CONTRACT_SEASON = "Spring/Summer 2026"module-level constant at line 1615 (in the diff). Used in both HTML and plain-text body.DOMAIN REVIEW
Python/FastAPI/SQLAlchemy:
send_contract_offer_emailfollows the same pattern as other email functions in this module: get client, build HTML + plain text, send, log toemail_log, return message ID or None.EmailType.contract_offeralready exists in the enum (model line 72, migration 033). No migration needed.escape()correctly applied to all user-supplied data (parent.name,player.name,team_name,contract_url).try/exceptwithlogger.exceptionon failure, returnsNone.EmailLogentry committed with correct fields.-> str | None).Test coverage (5 new tests added):
test_email_body_contains_offer_copy-- verifies offer copy present, reminder copy absent (both HTML and plain text)test_email_log_uses_contract_offer_type-- verifiesEmailLogentry uses correctEmailTypetest_kings_player_gets_red_accent-- verifies#d42026in HTML for non-Queens teamtest_queens_player_gets_pink_accent-- verifies#e91e8cin HTML for Queens team (with dedicatedqueens_parent_with_playerfixture)BLOCKERS
None.
NITS
_CONTRACT_SEASONwill need updating each season. It is at least a named constant now (easily greppable), but consider whether this should come from tenant config or a more central "current season" constant shared across email functions. Low priority -- current approach is fine for now._CONTRACT_PINKis now redundant with_BRAND_QUEENS_PINK. Both are#e91e8c. The reminder function still uses_CONTRACT_PINKbut that is out of scope for this PR. Worth a cleanup ticket.SOP COMPLIANCE
462-contract-offer-emailCloses #462PROCESS OBSERVATIONS
Clean fix cycle. All three items from the initial review addressed in a single commit. Test coverage went from 3 tests to 8 tests, covering offer copy correctness, email type logging, and accent color branching for both team types. The blocker (dead mocks) was a real correctness issue -- those tests would have passed even if
send_contract_offer_emailwas never called.VERDICT: APPROVED