Email function + blast endpoint: send_first_payment_email + POST /admin/email/first-payment #369

Closed
opened 2026-04-07 01:11:50 +00:00 by forgejo_admin · 2 comments

Type

Feature

Lineage

Decomposed from forgejo_admin/basketball-api#366. Ticket C of 3 — depends on A (migration, merged as PR #370). Independent of B (checkout endpoint).

Repo

forgejo_admin/basketball-api

User Story

As an admin (Marcus)
I want to blast a branded payment email to all parents who signed contracts
So that they receive a personalized Stripe payment link for their prorated first monthly fee

Context

Send date: Wednesday April 8, 8:00 AM — after the first practice (Tuesday April 7). Email framing is post-practice: "Great first practice! Here's how billing works."

Two components: (1) send_first_payment_email() in services/email.py builds a branded HTML email using _brand_wrapper() with per-player CTA linking to the checkout endpoint. (2) POST /admin/email/first-payment in routes/admin.py queries all contract_status=signed players and sends the email to each, with a test_email safety param.

Email tone: Friendly-firm, Marcus voice. Post-practice energy. Clearly explain:

  • What "prorated" means in plain language (partial month, remaining days in April)
  • The exact prorated amount for this player
  • That this card will be saved and used for automatic monthly billing on the 1st of each month
  • The full monthly fee amount so parents know what to expect going forward

Email framing (post-practice, not pre-practice):

  • Opening: "Great first practice!" (not "first practice is tomorrow")
  • Explain the prorated first payment
  • Explain the recurring billing model (same card, 1st of each month, full monthly fee)
  • CTA: "Pay ${prorated_fee} Now"
  • Friendly close: questions → reply to this email

CTA links to {base_url}/checkout/first-payment?token={contract_token} — this is basketball-api's base_url, NOT frontend_url, because it's a redirect endpoint.

File Targets

Files to modify:

  • src/basketball_api/services/email.py — add _calculate_prorated_fee() helper and send_first_payment_email() function (before the MJML template loader section ~line 1102)
  • src/basketball_api/routes/admin.py — add FirstPaymentBlastResponse schema and POST /admin/email/first-payment endpoint (after jersey-reminder ~line 895), add send_first_payment_email to imports, add ContractStatus to model imports

Files to create:

  • tests/test_first_payment_email.py — email function tests
  • tests/test_first_payment_blast.py — blast endpoint tests (needs local admin_client fixture with auth override, same pattern as tests/test_players.py:84)

Files NOT to touch:

  • src/basketball_api/routes/checkout.py — that's ticket B
  • src/basketball_api/routes/webhooks.py — no changes needed

Acceptance Criteria

  • send_first_payment_email() sends branded HTML email with player name, prorated amount, and checkout CTA
  • Email subject: "Great First Practice — Here's Your April Fee | {tenant_name}"
  • Email opens with post-practice framing ("Great first practice!")
  • Email clearly explains proration in plain language (partial month, remaining days in April)
  • Email states the prorated amount and the full monthly fee
  • Email explains card will be saved for automatic monthly billing on the 1st
  • CTA link: {base_url}/checkout/first-payment?token={contract_token}
  • EmailLog written with email_type=EmailType.first_payment
  • POST /admin/email/first-payment sends to all contract_status=signed players
  • test_email param restricts blast to one parent's players
  • Returns { sent_count, errors }

Test Expectations

  • Unit test: email function sends with correct content (prorated amount, player name, contract_token in CTA URL)
  • Unit test: email mentions full monthly fee amount
  • Unit test: email function logs to EmailLog with first_payment type
  • Unit test: $180 fee → email shows $150 prorated and $180 full monthly
  • Unit test: blast sends to signed players only (unsigned excluded)
  • Unit test: test_email param filters to one parent
  • Unit test: test_email not found → sent_count=0
  • Run: pytest tests/test_first_payment_email.py tests/test_first_payment_blast.py -v

Constraints

  • Follow existing _brand_wrapper() pattern (Westside red/black palette)
  • Follow existing jersey-reminder blast pattern in admin.py:847-895
  • admin_client fixture must be defined in test file (not in conftest) — same pattern as test_players.py:84
  • Mock get_gmail_client in email tests (@patch("basketball_api.services.email.get_gmail_client"))
  • Mock send_first_payment_email in blast tests (@patch("basketball_api.routes.admin.send_first_payment_email"))
  • Plaintext fallback required alongside HTML
  • Email content must use plain language — parents are not technical. "Prorated" needs explanation.

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • westside-basketball — project
  • Parent: forgejo_admin/basketball-api#366
  • Depends on: #367 (migration, merged PR #370)
  • Plan: ~/pal-e-platform/docs/superpowers/plans/2026-04-06-first-payment-email.md (Tasks 3 + 4)
### Type Feature ### Lineage Decomposed from `forgejo_admin/basketball-api#366`. Ticket C of 3 — depends on A (migration, merged as PR #370). Independent of B (checkout endpoint). ### Repo `forgejo_admin/basketball-api` ### User Story As an admin (Marcus) I want to blast a branded payment email to all parents who signed contracts So that they receive a personalized Stripe payment link for their prorated first monthly fee ### Context **Send date: Wednesday April 8, 8:00 AM** — after the first practice (Tuesday April 7). Email framing is post-practice: "Great first practice! Here's how billing works." Two components: (1) `send_first_payment_email()` in `services/email.py` builds a branded HTML email using `_brand_wrapper()` with per-player CTA linking to the checkout endpoint. (2) `POST /admin/email/first-payment` in `routes/admin.py` queries all `contract_status=signed` players and sends the email to each, with a `test_email` safety param. **Email tone:** Friendly-firm, Marcus voice. Post-practice energy. Clearly explain: - What "prorated" means in plain language (partial month, remaining days in April) - The exact prorated amount for this player - That this card will be saved and used for automatic monthly billing on the 1st of each month - The full monthly fee amount so parents know what to expect going forward **Email framing (post-practice, not pre-practice):** - Opening: "Great first practice!" (not "first practice is tomorrow") - Explain the prorated first payment - Explain the recurring billing model (same card, 1st of each month, full monthly fee) - CTA: "Pay ${prorated_fee} Now" - Friendly close: questions → reply to this email CTA links to `{base_url}/checkout/first-payment?token={contract_token}` — this is basketball-api's base_url, NOT frontend_url, because it's a redirect endpoint. ### File Targets Files to modify: - `src/basketball_api/services/email.py` — add `_calculate_prorated_fee()` helper and `send_first_payment_email()` function (before the MJML template loader section ~line 1102) - `src/basketball_api/routes/admin.py` — add `FirstPaymentBlastResponse` schema and `POST /admin/email/first-payment` endpoint (after jersey-reminder ~line 895), add `send_first_payment_email` to imports, add `ContractStatus` to model imports Files to create: - `tests/test_first_payment_email.py` — email function tests - `tests/test_first_payment_blast.py` — blast endpoint tests (needs local `admin_client` fixture with auth override, same pattern as `tests/test_players.py:84`) Files NOT to touch: - `src/basketball_api/routes/checkout.py` — that's ticket B - `src/basketball_api/routes/webhooks.py` — no changes needed ### Acceptance Criteria - [ ] `send_first_payment_email()` sends branded HTML email with player name, prorated amount, and checkout CTA - [ ] Email subject: "Great First Practice — Here's Your April Fee | {tenant_name}" - [ ] Email opens with post-practice framing ("Great first practice!") - [ ] Email clearly explains proration in plain language (partial month, remaining days in April) - [ ] Email states the prorated amount and the full monthly fee - [ ] Email explains card will be saved for automatic monthly billing on the 1st - [ ] CTA link: `{base_url}/checkout/first-payment?token={contract_token}` - [ ] EmailLog written with `email_type=EmailType.first_payment` - [ ] `POST /admin/email/first-payment` sends to all `contract_status=signed` players - [ ] `test_email` param restricts blast to one parent's players - [ ] Returns `{ sent_count, errors }` ### Test Expectations - [ ] Unit test: email function sends with correct content (prorated amount, player name, contract_token in CTA URL) - [ ] Unit test: email mentions full monthly fee amount - [ ] Unit test: email function logs to EmailLog with `first_payment` type - [ ] Unit test: $180 fee → email shows $150 prorated and $180 full monthly - [ ] Unit test: blast sends to signed players only (unsigned excluded) - [ ] Unit test: test_email param filters to one parent - [ ] Unit test: test_email not found → sent_count=0 - Run: `pytest tests/test_first_payment_email.py tests/test_first_payment_blast.py -v` ### Constraints - Follow existing `_brand_wrapper()` pattern (Westside red/black palette) - Follow existing jersey-reminder blast pattern in `admin.py:847-895` - `admin_client` fixture must be defined in test file (not in conftest) — same pattern as `test_players.py:84` - Mock `get_gmail_client` in email tests (`@patch("basketball_api.services.email.get_gmail_client")`) - Mock `send_first_payment_email` in blast tests (`@patch("basketball_api.routes.admin.send_first_payment_email")`) - Plaintext fallback required alongside HTML - Email content must use plain language — parents are not technical. "Prorated" needs explanation. ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `westside-basketball` — project - Parent: `forgejo_admin/basketball-api#366` - Depends on: #367 (migration, merged PR #370) - Plan: `~/pal-e-platform/docs/superpowers/plans/2026-04-06-first-payment-email.md` (Tasks 3 + 4)
Author
Owner

Scope Review: READY

Review note: review-875-2026-04-06
Scope is solid — all template sections present, file targets verified against codebase, traceability triangle complete (story:WS-S7 confirmed on project page). Hard dependency on #367 (migration) correctly documented. 9 AC are concrete and machine-verifiable. Single-repo, follows established blast pattern. No decomposition needed.

[SCOPE] arch-basketball-api and arch-email notes do not exist in pal-e-docs (pre-existing platform gap, not a blocker).

## Scope Review: READY Review note: `review-875-2026-04-06` Scope is solid — all template sections present, file targets verified against codebase, traceability triangle complete (story:WS-S7 confirmed on project page). Hard dependency on #367 (migration) correctly documented. 9 AC are concrete and machine-verifiable. Single-repo, follows established blast pattern. No decomposition needed. **[SCOPE]** arch-basketball-api and arch-email notes do not exist in pal-e-docs (pre-existing platform gap, not a blocker).
Author
Owner

Scope update (2026-04-06): Email timing changed from pre-practice (April 7) to post-practice (Wednesday April 8, 8:00 AM). Framing flipped to "Great first practice!" tone. Added requirements to explain proration in plain language and inform parents that their card will be saved for automatic monthly billing on the 1st. Subject line updated accordingly.

**Scope update (2026-04-06):** Email timing changed from pre-practice (April 7) to post-practice (Wednesday April 8, 8:00 AM). Framing flipped to "Great first practice!" tone. Added requirements to explain proration in plain language and inform parents that their card will be saved for automatic monthly billing on the 1st. Subject line updated accordingly.
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#369
No description provided.