Generic send_templated_email() + EmailType migration (contract_offer, contract_reminder) #294

Closed
opened 2026-04-03 18:38:24 +00:00 by forgejo_admin · 2 comments

Type

Feature

Lineage

Depends on MJML email system ticket (same session). Part of email architecture overhaul scoped 2026-04-03.

Repo

forgejo_admin/basketball-api

User Story

As a developer,
I want one generic send function that takes a layout name and data dict,
So that new email types don't require new Python functions.

Context

Currently every email type has its own function (80-150 lines). send_confirmation_email(), send_profile_reminder_email(), send_jersey_reminder_email(), etc. The pattern is identical: build HTML, call gmail_sdk, log to EmailLog. Only the content differs.

Decision: one send_templated_email() function handles all sends. Layout name maps to a compiled MJML template. Data dict fills {{key}} placeholders. EmailLog records the type.

Also need contract_offer and contract_reminder values in the EmailType enum for the contract email wave.

Existing functions stay untouched — this ticket adds the new path alongside them. Migration of old functions is future work.

File Targets

Files to modify:

  • src/basketball_api/services/email.py — add send_templated_email() function (~30 lines)
  • src/basketball_api/models.py — add contract_offer, contract_reminder to EmailType enum
  • alembic/versions/ — new migration adding enum values

Files NOT to touch:

  • Existing send functions in email.py — do not modify or delete
  • src/basketball_api/routes/admin.py — blast endpoint is a separate ticket

Acceptance Criteria

  • send_templated_email(tenant, layout="action", data={...}, to=email, subject=subj, email_type=EmailType.contract_reminder, db=db) sends a branded email
  • EmailLog entry created with correct email_type, recipient, gmail_message_id
  • Plain text fallback generated from HTML
  • EmailType.contract_offer and EmailType.contract_reminder exist in enum
  • Alembic migration runs cleanly (upgrade and downgrade)
  • Existing send functions still work unchanged

Test Expectations

  • Unit test: send_templated_email() calls gmail_sdk and creates EmailLog on success
  • Unit test: send_templated_email() logs failure and returns None on gmail error
  • Unit test: missing template raises FileNotFoundError
  • Unit test: EmailType enum has contract_offer and contract_reminder values
  • Run command: pytest tests/ -k "test_templated_email or test_models" -v

Constraints

  • Function signature must accept all fields needed for EmailLog (tenant_id, parent_id, player_id, email_type, recipient_email)
  • Must use existing get_gmail_client(tenant, db=db) for OAuth
  • Must use existing load_email_template() for template rendering
  • Plain text fallback: strip HTML tags, preserve link URLs

Checklist

  • PR opened
  • Tests pass
  • Migration tested (up + down)
  • No unrelated changes
  • project-westside-basketball — westside project
  • MJML email system ticket — dependency (templates must exist)
### Type Feature ### Lineage Depends on MJML email system ticket (same session). Part of email architecture overhaul scoped 2026-04-03. ### Repo `forgejo_admin/basketball-api` ### User Story As a developer, I want one generic send function that takes a layout name and data dict, So that new email types don't require new Python functions. ### Context Currently every email type has its own function (80-150 lines). `send_confirmation_email()`, `send_profile_reminder_email()`, `send_jersey_reminder_email()`, etc. The pattern is identical: build HTML, call gmail_sdk, log to EmailLog. Only the content differs. Decision: one `send_templated_email()` function handles all sends. Layout name maps to a compiled MJML template. Data dict fills `{{key}}` placeholders. EmailLog records the type. Also need `contract_offer` and `contract_reminder` values in the EmailType enum for the contract email wave. Existing functions stay untouched — this ticket adds the new path alongside them. Migration of old functions is future work. ### File Targets Files to modify: - `src/basketball_api/services/email.py` — add `send_templated_email()` function (~30 lines) - `src/basketball_api/models.py` — add `contract_offer`, `contract_reminder` to EmailType enum - `alembic/versions/` — new migration adding enum values Files NOT to touch: - Existing send functions in email.py — do not modify or delete - `src/basketball_api/routes/admin.py` — blast endpoint is a separate ticket ### Acceptance Criteria - [ ] `send_templated_email(tenant, layout="action", data={...}, to=email, subject=subj, email_type=EmailType.contract_reminder, db=db)` sends a branded email - [ ] EmailLog entry created with correct email_type, recipient, gmail_message_id - [ ] Plain text fallback generated from HTML - [ ] `EmailType.contract_offer` and `EmailType.contract_reminder` exist in enum - [ ] Alembic migration runs cleanly (upgrade and downgrade) - [ ] Existing send functions still work unchanged ### Test Expectations - [ ] Unit test: `send_templated_email()` calls gmail_sdk and creates EmailLog on success - [ ] Unit test: `send_templated_email()` logs failure and returns None on gmail error - [ ] Unit test: missing template raises FileNotFoundError - [ ] Unit test: EmailType enum has contract_offer and contract_reminder values - Run command: `pytest tests/ -k "test_templated_email or test_models" -v` ### Constraints - Function signature must accept all fields needed for EmailLog (tenant_id, parent_id, player_id, email_type, recipient_email) - Must use existing `get_gmail_client(tenant, db=db)` for OAuth - Must use existing `load_email_template()` for template rendering - Plain text fallback: strip HTML tags, preserve link URLs ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] Migration tested (up + down) - [ ] No unrelated changes ### Related - `project-westside-basketball` — westside project - MJML email system ticket — dependency (templates must exist)
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-751-2026-04-03

Ticket is well-scoped with all template sections present, verified file targets, and correct traceability to WS-S7 and WS-S22. Single fixable issue:

  • [SCOPE] No arch-email architecture note exists in pal-e-docs. 8+ board items reference arch:email but the backing note is missing. Create arch-email to document the email subsystem (send functions, EmailType enum, EmailLog, template loading, Gmail OAuth).

Dependencies verified: #293 (MJML templates) is a soft dependency (not blocking code merge, but blocking production use). #295 (blast endpoint) correctly identified as downstream consumer.

## Scope Review: NEEDS_REFINEMENT Review note: `review-751-2026-04-03` Ticket is well-scoped with all template sections present, verified file targets, and correct traceability to WS-S7 and WS-S22. Single fixable issue: - **[SCOPE]** No `arch-email` architecture note exists in pal-e-docs. 8+ board items reference `arch:email` but the backing note is missing. Create `arch-email` to document the email subsystem (send functions, EmailType enum, EmailLog, template loading, Gmail OAuth). Dependencies verified: #293 (MJML templates) is a soft dependency (not blocking code merge, but blocking production use). #295 (blast endpoint) correctly identified as downstream consumer.
Author
Owner

Scope Review: READY

Review note: review-751-2026-04-03-r2
Re-review after arch-email note creation. All template sections present, traceability triangle complete (WS-S7, WS-S22, arch-email verified), file targets confirmed, dependencies mapped. Ready for dispatch.

## Scope Review: READY Review note: `review-751-2026-04-03-r2` Re-review after arch-email note creation. All template sections present, traceability triangle complete (WS-S7, WS-S22, arch-email verified), file targets confirmed, dependencies mapped. Ready for dispatch.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#294
No description provided.