Admin blast endpoint: /email/blast with layout + query + data params #295

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

Type

Feature

Lineage

Depends on MJML system + send_templated_email() tickets (same session). Supersedes board item #724 (contract reminder email endpoint).

Repo

forgejo_admin/basketball-api

User Story

As an admin,
I want to send templated email blasts to defined audiences by specifying layout and content,
So that new email campaigns don't require new endpoints or code changes.

Context

Current pattern: each email blast has its own endpoint (/email/profile-reminder, /email/jersey-reminder) with hardcoded query logic and response models. The contract reminder blast doesn't exist yet.

Decision: one generic /email/blast endpoint with a query registry. Predefined audience queries return per-recipient data. Admin specifies layout, content, and query name. Per-recipient placeholders (player_name, contract_url) merge into the template.

The test_email query param safety valve is proven and stays — limits send to one recipient for testing.

The first query is unsigned_contracts: parents with a contract_token on their player but no contract_signed_at timestamp.

Architecture reference: arch-email in pal-e-docs.

Blockers

This ticket CANNOT be built until both upstream tickets are merged and deployed:

  • forgejo_admin/basketball-api#293 — MJML email system (compiled templates must exist at /app/templates/email/compiled/)
  • forgejo_admin/basketball-api#294send_templated_email() function (this endpoint calls it)

Do NOT start work on this ticket until #293 and #294 are merged to main.

File Targets

Files to create:

  • src/basketball_api/services/email_queries.py — query registry with unsigned_contracts query

Files to modify:

  • src/basketball_api/routes/admin.py — add POST /email/blast endpoint

Files NOT to touch:

  • Existing /email/profile-reminder and /email/jersey-reminder endpoints — stay for now
  • src/basketball_api/services/email.py — uses send_templated_email() from prior ticket

Acceptance Criteria

  • POST /email/blast accepts JSON body with layout, email_type, query, subject, data, optional test_email
  • unsigned_contracts query returns parents whose players have contract_token but no contract signature
  • Per-recipient placeholders ({{player_name}}, {{contract_url}}, {{parent_name}}) merge into subject and data values
  • test_email param limits send to one matching recipient
  • Response: {"sent_count": N, "errors": [...]}
  • EmailLog entry per send with correct email_type
  • Requires admin auth (existing require_admin dependency)

Test Expectations

  • Unit test: blast with test_email sends to one recipient only
  • Unit test: blast with no matching recipients returns sent_count: 0
  • Unit test: invalid query name returns 400
  • Unit test: invalid layout name returns 404 (from load_email_template)
  • Unit test: unsigned_contracts query returns correct players (signed excluded, unsigned included)
  • Run command: pytest tests/test_admin_email.py -v

Constraints

  • Query functions must return list[dict] with at minimum {"email": str, "name": str}
  • Additional keys in the dict become per-recipient placeholders
  • Subject string also supports {{key}} replacement per-recipient
  • Admin auth via existing require_admin dependency
  • Match existing admin endpoint patterns in routes/admin.py

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • project-westside-basketball — westside project
  • arch-email — architecture reference
  • Board item #724 — superseded by this ticket
  • MJML system (#293) + send_templated_email() (#294) — hard blockers
### Type Feature ### Lineage Depends on MJML system + send_templated_email() tickets (same session). Supersedes board item #724 (contract reminder email endpoint). ### Repo `forgejo_admin/basketball-api` ### User Story As an admin, I want to send templated email blasts to defined audiences by specifying layout and content, So that new email campaigns don't require new endpoints or code changes. ### Context Current pattern: each email blast has its own endpoint (`/email/profile-reminder`, `/email/jersey-reminder`) with hardcoded query logic and response models. The contract reminder blast doesn't exist yet. Decision: one generic `/email/blast` endpoint with a query registry. Predefined audience queries return per-recipient data. Admin specifies layout, content, and query name. Per-recipient placeholders (player_name, contract_url) merge into the template. The `test_email` query param safety valve is proven and stays — limits send to one recipient for testing. The first query is `unsigned_contracts`: parents with a `contract_token` on their player but no `contract_signed_at` timestamp. Architecture reference: `arch-email` in pal-e-docs. ### Blockers **This ticket CANNOT be built until both upstream tickets are merged and deployed:** - `forgejo_admin/basketball-api#293` — MJML email system (compiled templates must exist at `/app/templates/email/compiled/`) - `forgejo_admin/basketball-api#294` — `send_templated_email()` function (this endpoint calls it) Do NOT start work on this ticket until #293 and #294 are merged to main. ### File Targets Files to create: - `src/basketball_api/services/email_queries.py` — query registry with `unsigned_contracts` query Files to modify: - `src/basketball_api/routes/admin.py` — add `POST /email/blast` endpoint Files NOT to touch: - Existing `/email/profile-reminder` and `/email/jersey-reminder` endpoints — stay for now - `src/basketball_api/services/email.py` — uses `send_templated_email()` from prior ticket ### Acceptance Criteria - [ ] `POST /email/blast` accepts JSON body with layout, email_type, query, subject, data, optional test_email - [ ] `unsigned_contracts` query returns parents whose players have contract_token but no contract signature - [ ] Per-recipient placeholders ({{player_name}}, {{contract_url}}, {{parent_name}}) merge into subject and data values - [ ] `test_email` param limits send to one matching recipient - [ ] Response: `{"sent_count": N, "errors": [...]}` - [ ] EmailLog entry per send with correct email_type - [ ] Requires admin auth (existing `require_admin` dependency) ### Test Expectations - [ ] Unit test: blast with `test_email` sends to one recipient only - [ ] Unit test: blast with no matching recipients returns `sent_count: 0` - [ ] Unit test: invalid query name returns 400 - [ ] Unit test: invalid layout name returns 404 (from load_email_template) - [ ] Unit test: `unsigned_contracts` query returns correct players (signed excluded, unsigned included) - Run command: `pytest tests/test_admin_email.py -v` ### Constraints - Query functions must return `list[dict]` with at minimum `{"email": str, "name": str}` - Additional keys in the dict become per-recipient placeholders - Subject string also supports `{{key}}` replacement per-recipient - Admin auth via existing `require_admin` dependency - Match existing admin endpoint patterns in routes/admin.py ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `project-westside-basketball` — westside project - `arch-email` — architecture reference - Board item #724 — superseded by this ticket - MJML system (#293) + send_templated_email() (#294) — hard blockers
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-752-2026-04-03
Ticket is well-scoped with complete template, verified file targets, and solid acceptance criteria, but has three refinement items before it can move to execution:

  • [SCOPE] arch-email architecture note does not exist in pal-e-docs. Create it to complete the traceability triangle.
  • [BODY] Dependencies on #293 (MJML system) and #294 (send_templated_email) are mentioned in Lineage but not marked as hard blockers. Add explicit "BLOCKED BY" note — neither dependency exists in the codebase yet.
  • [LABEL] Add a blocked-by:751,750 label (or equivalent) to make the dependency chain machine-readable on the board.
## Scope Review: NEEDS_REFINEMENT Review note: `review-752-2026-04-03` Ticket is well-scoped with complete template, verified file targets, and solid acceptance criteria, but has three refinement items before it can move to execution: - **[SCOPE]** `arch-email` architecture note does not exist in pal-e-docs. Create it to complete the traceability triangle. - **[BODY]** Dependencies on #293 (MJML system) and #294 (send_templated_email) are mentioned in Lineage but not marked as hard blockers. Add explicit "BLOCKED BY" note — neither dependency exists in the codebase yet. - **[LABEL]** Add a `blocked-by:751,750` label (or equivalent) to make the dependency chain machine-readable on the board.
Author
Owner

Scope Review: READY

Review note: review-752-2026-04-03-r2
Re-review after refinement. All three items from previous review (review-752-2026-04-03) resolved: arch-email note created, explicit Blockers section added, blocked-by labels applied. Traceability triangle complete. File targets verified. Ready for execution once #293 and #294 are merged.

## Scope Review: READY Review note: `review-752-2026-04-03-r2` Re-review after refinement. All three items from previous review (`review-752-2026-04-03`) resolved: arch-email note created, explicit Blockers section added, blocked-by labels applied. Traceability triangle complete. File targets verified. Ready for execution once #293 and #294 are merged.
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#295
No description provided.