Skip to content

Decide whether Relay-owned course email campaigns belong in the target product #298

Description

@alexeygrigorev

Parent/reference: #50 (approved-purpose wiring and Datamailer retirement)
Related authorities: #21 (Relay ownership), #48 (Relay-owned templates), #49 (website delivery intent/status), #227 (course/event/marketing preferences), #290 (static Datamailer inventory), #297 (adoption-ledger reconciliation)
Explicitly separate from: #149 (system-evaluation, peer-task, score-visibility, and peer/score-notification decisions)

Decision outcome

Record whether the target website wants any Relay-owned course email-campaign product semantics. The answer must be one independent ADOPT, DEFER, or REJECT decision for the campaign family; silence, “port CMP,” or a partial source diff is not approval.

This is a product decision gate, not permission to copy the CMP tree. It must never become a wholesale Datamailer port. Until the decision and the Relay contract are accepted, do not add a campaign model, migration, route, outbox event, provider call, sender configuration, source pin, or new Datamailer send.

Evidence boundary

The current CMP main is 6d3cc0e0e0b9a1d28be1a8d53209555269e1d3fe (tree 45f1f8e2548b25e3ef89d44b2370efbb647ff088, parent e49c575ce3f567cc14a8f66a89d3ccaca3bfe2f4), titled Dispatch campaign Queue asynchronously; fix missing campaign from_email. The commit changes exactly these nine source paths:

  • api/views/email_campaign_serializers.py
  • cadmin/templates/cadmin/include/email_campaign_status_badge.html
  • cadmin/tests/test_campaign_datamailer_views.py
  • cadmin/views/campaign_datamailer.py
  • course_management/datamailer/payloads/email_campaigns.py
  • course_management/datamailer_outbox_senders.py
  • courses/migrations/0044_alter_emailcampaign_status.py
  • courses/models/course.py
  • courses/tests/test_datamailer_outbox_campaign_queue.py

The reviewed change introduces or completes these semantics in the CMP line:

  1. A separate EmailCampaign model and QUEUE_PENDING status/migration.
  2. A Studio campaign queue action that enqueues campaign.queue and returns before the potentially long-running provider call; an asynchronous outbox sender later calls Datamailer and marks the campaign queued.
  3. Campaign payloads that include configured from_email through the CMP Datamailer configuration.
  4. API serialization/UI status changes and tests for the new lifecycle.

The website authority remains origin/main face8e4808d65afbf0374d1ced7a88079950d663 (tree c8496fb2a37529eb7c63e1eeb9e35e29a2aadada) and still pins CMP 98a235283904b4ef9ad29e196298540756cf1bcc. On that target, the campaign domain is RegistrationCampaign in courses/models/cohort.py; the adopted Studio path studio_courses/views/campaign_datamailer.py currently calls Datamailer campaign queueing synchronously and there is no target EmailCampaign, campaign.queue outbox event, queue_pending status, or email-campaign API surface. This current implementation is characterization evidence, not authorization for another Datamailer send.

Required owner decision

Choose one disposition for the whole campaign family:

ADOPT

If campaigns are wanted, answer all fields below. Adoption approves target product semantics only; it does not approve any CMP bytes, Datamailer call, or Relay activation.

  • Domain and lifecycle: Is a campaign a first-class target-owned object, a versioned message intent, or another bounded abstraction? Define its relationship to Course, Cohort, and any existing RegistrationCampaign; draft/preview/test/queue/cancel/replace/archive states; immutable content/version and correction/history rules; and whether an EmailCampaign analogue is actually needed.
  • Actor and parity: Name the capabilities allowed to create, edit, preview, test-send, queue, cancel, inspect, or resend. Define exact Course/Cohort scope, reauthentication/confirmation, CSRF, audit, private/no-store responses, and parity between Studio and /api/v1/admin/. No copied CMP staff-token or broad admin behavior is accepted by inference.
  • Purpose and audience: Choose the approved Decision: Confirm the MVP transactional email purpose catalog #22 purpose ID(s), trigger/action model, exact recipient query and cohort boundary, account/preference/suppression rules, excluded/removed/ambiguous recipients, and whether the campaign is course-update, newsletter/marketing, or a new purpose requiring authority. Define subject/body/context ownership without storing an unapproved local renderer or raw recipient/body data.
  • Relay contract and sender: Bind each campaign to a Relay template key and immutable version, typed context, sender/reply-to policy, and development-only courses sender where applicable. from_email must mean a Relay-owned sender identity, not a new website DATAMAILER_FROM_EMAIL or SES fallback. Define accepted-versus-delivered, callback/reconciliation, suppression, and provider-unavailable behavior.
  • Async and idempotency semantics: Decide whether queueing creates one website delivery intent plus one durable after-commit job, the exact idempotency/version key and conflict rule, pending/accepted/delivered/retryable/ambiguous/dead/suppressed states, lease/retry/worker-restart behavior, and how response loss or ambiguous Relay acknowledgement is resolved. A request transaction must never contact Relay or Datamailer.
  • Migration and retirement: Define how existing RegistrationCampaign/Datamailer history and any pending legacy work are characterized, retained, reconciled, suppressed, or excluded; no campaign import may send or requeue. Preserve target migration identities, redaction/retention, rollback, one-active-sender, and Wire approved messages through Relay and retire Datamailer #50 retirement gates.

DEFER

Preserve the target's current campaign data/read/history characterization but do not activate or extend new campaign sending. Name a concrete reopen condition (for example, an accepted Relay campaign API plus an owner-approved purpose/audience contract). Record all nine CMP paths and any dependent campaign bytes as unapplied source drift; no Datamailer queue/test-send/upsert or sender configuration is permitted as a temporary implementation.

REJECT

State why course email campaigns are outside the target product, preserve only the minimum read-only historical evidence required by #50, and record the entire CMP campaign family as an explicit source-sync exclusion. Do not create a local replacement model, renderer, queue, or provider adapter.

Scope after an ADOPT decision

Only after the owner response does PM split the smallest target-native implementation slices under #48/#49/#50, refresh #297/#290 source/overlay evidence, and re-groom each slice against one exact current website/CMP identity. A future implementation must use the website application-service and Relay boundary, not copy the CMP EmailCampaign/Datamailer implementation literally.

Explicit non-goals

  • No CMP source-pin movement, sync_course_platform.py --apply, copied files, migration transplant, migration squash, or adoption-ledger rewrite in this issue.
  • No website EmailCampaign model, API/Studio route, queue/outbox sender, from_email setting, Datamailer/SES/Relay provider call, credential, sender/domain activation, real/test recipient, or deployment.
  • No wholesale Datamailer port, local template renderer, second provider/event stack, synchronous request-time network call, blind retry after ambiguity, or dual sender.
  • No decision about Decision: Choose CMP system-evaluation, peer-assignment, score, and notification behavior #149's four behavior families, general registration/enrollment semantics, peer assignment, system evaluations, scoring, certificates, or unrelated email purposes.
  • No production/protected-data/queue access and no import, drain, freeze, retirement, or canary action.

Acceptance criteria

Browser and verification scenarios

This decision issue has no implementation or render change; screenshots and provider/browser execution are not applicable at this stage. Any future ADOPT child must include independent desktop/mobile scenarios, including:

  1. authorized Studio and admin-API campaign draft/preview with exact Course/Cohort scope and masked data;
  2. unauthorized, stale-revision, missing-Relay, suppressed-recipient, and ambiguous-acknowledgement states with no existence/body/recipient leakage;
  3. queue request returning a safe pending/accepted status, refresh/reconciliation to terminal state, retry/dead handling, and no request-time network call;
  4. preference opt-out, duplicate/replay, changed-version conflict, cancellation, worker restart, and rollback behavior.

Required lifecycle

This issue remains blocked for engineering until the owner response is recorded. PM then either closes the decision gate with an explicit source exclusion or creates/grooms only the minimum implementation children. No code, source pin, provider, deployment, commit, or merge is authorized by this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important follow-upadminArea: admincoursesArea: coursesdecisionOwner decision requiredemailArea: emailintegrationArea: integrationsecurityArea: securitytestingArea: testing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions