Phase 2b: Scheduled send — send_at + CronJob worker #5

Open
opened 2026-03-22 18:56:54 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

plan-pal-e-mail → Phase 2b

Repo

forgejo_admin/pal-e-mail

User Story

As a platform admin (Lucas)
I want to schedule an email for future delivery via POST /send with a send_at field
So that I can prepare emails now and have them delivered at the right time without manual intervention

Context

Phase 2 delivers immediate send only. Lucas needs to schedule emails for specific dates (e.g., jersey reminders for next Tuesday). The send_at field on POST /send queues instead of sending immediately. A k8s CronJob polls the scheduled_emails table every minute and sends due emails. Apps resolve their own recipients and timing — pal-e-mail just handles queued delivery.

File Targets

Files to create:

  • src/pal_e_mail/models.py — add ScheduledEmail model (sender, to, subject, project, template, html, data, brand_name, send_at, status, created_at)
  • src/pal_e_mail/routes/scheduled.py — GET /scheduled (list pending), DELETE /scheduled/{id} (cancel)
  • src/pal_e_mail/services/scheduler.py — send_due_emails() function, called by worker
  • alembic/versions/002_scheduled_emails.py — migration for scheduled_emails table

Files to modify:

  • src/pal_e_mail/schemas.py — add send_at: datetime | None to SendRequest, add ScheduledEntry/ScheduledResponse models
  • src/pal_e_mail/routes/send.py — if send_at provided, queue instead of send
  • src/pal_e_mail/main.py — include scheduled router

Infrastructure:

  • k8s CronJob manifest (terraform or kustomize) — runs every minute, calls POST /internal/send-due

Acceptance Criteria

  • POST /send with send_at stores in scheduled_emails table, returns schedule_id
  • POST /send without send_at continues to send immediately (no regression)
  • CronJob sends due emails and updates status to sent/failed
  • GET /scheduled returns pending emails with filters
  • DELETE /scheduled/{id} cancels a pending email
  • Cancelled emails are not sent by the worker

Test Expectations

  • Unit test: POST /send with send_at queues, returns scheduled status
  • Unit test: POST /send without send_at sends immediately (regression)
  • Unit test: send_due_emails() picks up due items, skips future items
  • Unit test: DELETE /scheduled/{id} prevents sending
  • Unit test: GET /scheduled filtering and pagination
  • Run command: cd ~/pal-e-mail && .venv/bin/pytest tests/ -v

Constraints

  • Sync, not async (consistent with Phase 2)
  • CronJob pattern matches tofu-state backup pattern
  • send_at in UTC always
  • No recurring — apps handle their own scheduling logic

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • project-pal-e-mail — parent project
  • plan-pal-e-mail — parent plan Phase 2b
  • forgejo_admin/pal-e-mail#3 — Phase 2 (predecessor)
### Type Feature ### Lineage `plan-pal-e-mail` → Phase 2b ### Repo `forgejo_admin/pal-e-mail` ### User Story As a platform admin (Lucas) I want to schedule an email for future delivery via POST /send with a send_at field So that I can prepare emails now and have them delivered at the right time without manual intervention ### Context Phase 2 delivers immediate send only. Lucas needs to schedule emails for specific dates (e.g., jersey reminders for next Tuesday). The send_at field on POST /send queues instead of sending immediately. A k8s CronJob polls the scheduled_emails table every minute and sends due emails. Apps resolve their own recipients and timing — pal-e-mail just handles queued delivery. ### File Targets Files to create: - `src/pal_e_mail/models.py` — add ScheduledEmail model (sender, to, subject, project, template, html, data, brand_name, send_at, status, created_at) - `src/pal_e_mail/routes/scheduled.py` — GET /scheduled (list pending), DELETE /scheduled/{id} (cancel) - `src/pal_e_mail/services/scheduler.py` — send_due_emails() function, called by worker - `alembic/versions/002_scheduled_emails.py` — migration for scheduled_emails table Files to modify: - `src/pal_e_mail/schemas.py` — add send_at: datetime | None to SendRequest, add ScheduledEntry/ScheduledResponse models - `src/pal_e_mail/routes/send.py` — if send_at provided, queue instead of send - `src/pal_e_mail/main.py` — include scheduled router Infrastructure: - k8s CronJob manifest (terraform or kustomize) — runs every minute, calls POST /internal/send-due ### Acceptance Criteria - [ ] POST /send with send_at stores in scheduled_emails table, returns schedule_id - [ ] POST /send without send_at continues to send immediately (no regression) - [ ] CronJob sends due emails and updates status to sent/failed - [ ] GET /scheduled returns pending emails with filters - [ ] DELETE /scheduled/{id} cancels a pending email - [ ] Cancelled emails are not sent by the worker ### Test Expectations - [ ] Unit test: POST /send with send_at queues, returns scheduled status - [ ] Unit test: POST /send without send_at sends immediately (regression) - [ ] Unit test: send_due_emails() picks up due items, skips future items - [ ] Unit test: DELETE /scheduled/{id} prevents sending - [ ] Unit test: GET /scheduled filtering and pagination - Run command: `cd ~/pal-e-mail && .venv/bin/pytest tests/ -v` ### Constraints - Sync, not async (consistent with Phase 2) - CronJob pattern matches tofu-state backup pattern - send_at in UTC always - No recurring — apps handle their own scheduling logic ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `project-pal-e-mail` — parent project - `plan-pal-e-mail` — parent plan Phase 2b - `forgejo_admin/pal-e-mail#3` — Phase 2 (predecessor)
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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/pal-e-mail#5
No description provided.