Skip to content

fix: clear scheduled_at on unschedule, return on template failures, add SES config LocalStack (testing) - #367

Open
williamchiii wants to merge 8 commits into
devfrom
wchi/email_campaigns_backend
Open

fix: clear scheduled_at on unschedule, return on template failures, add SES config LocalStack (testing)#367
williamchiii wants to merge 8 commits into
devfrom
wchi/email_campaigns_backend

Conversation

@williamchiii

@williamchiii williamchiii commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Description

Three small backend fixes found while building the campaign UI and verifying the
send pipeline end to end.

Unscheduling left a stale send time. PATCH /status with status: "draft" never
cleared scheduled_at, so an unscheduled campaign kept advertising a time it would
never send at. The handler cannot fix this itself — ScheduledAt is a *time.Time,
so an omitted field and an explicit null both arrive as nil and it has to assume
"leave it alone". Handled in the service instead, where "a draft has no send time" is
a business rule rather than a transport detail.

Not currently user-visible, since every read is gated on status: the sweep filters
status = 'scheduled', and the frontend checks the same before showing or prefilling
a time. It would mislead anyone reading the table directly, or any future delivery
stats joining on the column.

Template failures panicked the worker. SendHtmlEmail logged parse and execute
failures but did not return, so a missing or malformed template left template nil
and the next line called .Execute on it. Both paths now return, so asynq retries and
a genuinely broken template ends up archived with the reason visible. The parse error
also never said which file failed, so both log lines now carry the template path.

This affects the transactional emails (welcome, confirmation, decision), not campaigns.

Dev tooling. .env.dev.example was missing every AWS/SES variable, so a fresh
clone could not run the email worker at all. Adds AWS_ACCESS_KEY,
AWS_ACCESS_KEY_SECRET, AWS_REGION, and EMAIL_TEMPLATE_DIRECTORY (credentials
left blank).

Adds an optional localstack service that emulates SES locally. It is inert unless
AWS_ENDPOINT_URL is set, so it changes nothing by default. With it, campaigns can be
sent end to end without touching the real SES account or risking mail to real users.

Type of Change

  • Bug fix (non-breaking change)

Checklist

  • My code follows the project's style guidelines
  • I have commented on complex parts of the code
  • I have updated documentation if necessary
  • I have updated or added tests to cover my changes
  • I have updated the OpenAPI YAML or other API schema files if applicable

Additional Notes

  • No new tests. The template fix is a missing return on an error path; the
    scheduled_at change is worth one asserting a draft transition sets
    ScheduledAtDoUpdate with a nil value, and I can add it if you'd like. Existing
    email-domain tests, build, and vet are all green.
  • To use LocalStack: uncomment AWS_ENDPOINT_URL=http://localstack:4566 in
    .env.dev, run docker compose up localstack, verify the sender with
    awslocal ses verify-email-identity --email-address noreply@swamphacks.com,
    then read delivered mail from http://localhost:4566/_aws/ses.
  • EMAIL_TEMPLATE_DIRECTORY should point at internal/emailutils/templates/. My
    local .env.dev had internal/email/templates/, which does not exist — worth
    checking whatever is set in the deployed environments, since that path failing is
    what surfaced the panic above.
  • Still missing from .env.dev.example: the CF_* and CORE_BUCKETS_* variables.
    Left alone since I did not want to guess at intended values.
  • The campaign admin UI is a separate branch and will come as its own PR; it does not
    depend on anything here

Resolves applicant recipient groups to deduped contact emails (excluding
is_fake seeded rows), adds raw-HTML task type and SES send methods, registers
all task types in the email worker mux, and adds SendCampaign with
draft/scheduled -> sending -> sent/failed transitions plus an admin-gated
POST /email/campaigns/{campaignId}/send endpoint.
Adds GetUserContactEmailsByRoles (role-based lookup on users, excluding
is_fake rows) and a recipientRoles map, so resolveRecipients now dispatches
applicant groups by application status and role groups by user role.
Only interest_subscribers remains unsupported.
Introduces campaignStore and campaignMailer interfaces in the email package
so the campaign service can be tested with fakes (concrete repository and
email service satisfy them unchanged). Adds 8 tests covering per-recipient
queueing, format routing, cross-group dedup, status transitions including
sending->failed with last_error, and the guard paths.
test: cover SendCampaign send pipeline

Introduces campaignStore and campaignMailer interfaces in the email package
so the campaign service can be tested with fakes (concrete repository and
email service satisfy them unchanged). Adds 8 tests covering per-recipient
queueing, format routing, cross-group dedup, status transitions including
sending->failed with last_error, and the guard paths.
Adds DeleteEmailCampaign query, repo method, and a DeleteCampaign service
method guarded by canDeleteCampaign so draft, scheduled, and failed campaigns
can be removed while sent and sending ones stay on the record. Exposes
DELETE /email/campaigns/{campaignId} with OpenAPI docs.
Adds an asynq Scheduler in cmd/email_worker that sweeps every minute for
campaigns whose scheduled_at has passed, failing any more than two hours
late so an outage cannot trigger a surprise blast.

Sends are now claimed with a conditional UPDATE, so overlapping ticks or a
double-clicked Send cannot dispatch the same campaign twice. Also resolves
interest_subscribers, completing all seven recipient types.
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
core Ready Ready Preview Aug 30, 2026 5:48am
xii-front-page Error Error Aug 30, 2026 5:48am

@williamchiii williamchiii changed the title fix: return on email template failures, add SES config and LocalStack for testing fix: clear scheduled_at on unschedule, return on template failures, add SES config LocalStack (testing) Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant