Files
2026-05-27 23:29:53 +01:00

6.1 KiB
Raw Permalink Blame History

OpenSpec Change: enterprise-campaigns

  • Change ID: enterprise-campaigns
  • Package: @suaveplan/campaigns
  • Category: universal
  • Target path: universal/email/campaigns
  • Status: IN PROGRESS
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/campaigns.md

Summary

Deliver @suaveplan/campaigns on the genesis branch from scratch at enterprise quality. The package provides email campaign orchestration: campaign lifecycle state machine (draft → scheduled → sending → sent / cancelled / failed), audience resolution via @suaveplan/audience-rules, template selection via @suaveplan/email-templates, per-recipient variable injection, send-rate throttling with configurable concurrency, schedule management (immediate, future ISO-8601, recurring via cron expression), campaign analytics aggregation, and A/B variant support. Genesis adds structured errors, Zod schemas, OTel instrumentation, AbortSignal cancellation, idempotent send via @suaveplan/idempotency, and retry with circuit breaker via @suaveplan/retry.


Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) CampaignError; CAMPAIGN.* error codes
@suaveplan/types (workspace:^) LoggerLike, Clock, EventBusLike
@suaveplan/events (workspace:^) Campaign lifecycle event bus
@suaveplan/email-templates (workspace:^) Template rendering per recipient
@suaveplan/audience-rules (workspace:^) Audience segment resolution
@suaveplan/idempotency (workspace:^) Per-recipient idempotency key guard
@suaveplan/retry (workspace:^) Retry + circuit breaker for delivery calls
@suaveplan/suppression (workspace:^) Suppression list check before send
zod (catalog:forms) Option schema validation
@opentelemetry/api (catalog:observability) Spans + counters + histograms

Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).


Public API surface

Core types

type CampaignStatus =
  | 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed'

interface Campaign {
  id: string
  name: string
  templateName: string
  audienceRuleId: string
  schedule: CampaignSchedule
  status: CampaignStatus
  abVariants?: AbVariant[]
  metadata: Record<string, unknown>
  createdAt: Date
  updatedAt: Date
}

interface CampaignSchedule {
  type: 'immediate' | 'at' | 'recurring'
  at?: string          // ISO-8601 for type:'at'
  cron?: string        // cron expression for type:'recurring'
  timezone?: string
}

interface AbVariant {
  name: string
  templateName: string
  fraction: number     // 0–1, fractions must sum to ≤1
}

interface CampaignSendOptions {
  concurrency?: number   // max parallel sends; default 10
  ratePerSecond?: number // max sends/second; default 100
  signal?: AbortSignal
  dryRun?: boolean
}

interface CampaignAnalytics {
  campaignId: string
  sent: number
  failed: number
  suppressed: number
  durationMs: number
}

CampaignManager

class CampaignManager {
  constructor(options: CampaignManagerOptions)
  create(draft: Omit<Campaign, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Campaign
  schedule(id: string, schedule: CampaignSchedule): Campaign
  send(id: string, options?: CampaignSendOptions): Promise<CampaignAnalytics>
  cancel(id: string): Campaign
  get(id: string): Campaign | undefined
  list(filter?: CampaignFilter): Campaign[]
}

interface CampaignManagerOptions {
  sender: CampaignSender       // injectable delivery adapter
  clock?: Clock
  logger?: LoggerLike
  bus?: EventBusLike
}

interface CampaignSender {
  send(recipient: string, subject: string, html: string, text: string): Promise<void>
}

Error codes

type CampaignErrorCode =
  | 'CAMPAIGN_NOT_FOUND'
  | 'CAMPAIGN_INVALID_TRANSITION'
  | 'CAMPAIGN_SEND_FAILED'
  | 'CAMPAIGN_TEMPLATE_NOT_FOUND'
  | 'CAMPAIGN_AUDIENCE_FAILED'
  | 'CAMPAIGN_ABORTED'

Genesis additions

  1. CampaignError extends SuaveplanError with six stable codes.
  2. Zod schemas for Campaign, CampaignSchedule, AbVariant, CampaignSendOptions, CampaignAnalytics.
  3. A/B variant support — variants are bucketed deterministically by recipient hash (FNV-1a) against configured fractions.
  4. Suppression list integration — calls @suaveplan/suppression before every send; suppressed recipients are counted in analytics without being sent to.
  5. OTel instrumentation — span campaign.send, counter campaign.send_total, histogram campaign.send_duration_ms, attributes campaign.id, campaign.status, campaign.recipient_count.
  6. Property-based tests — A/B bucketing distribution, state machine transition invariants.

File layout

universal/email/campaigns/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      campaign-manager/
        campaign-manager.ts
        campaign-manager.md
        campaign-manager.types.ts
        campaign-manager.test.ts
        index.ts
      campaign-fsm/
        campaign-fsm.ts
        campaign-fsm.md
        campaign-fsm.test.ts
        index.ts
      ab-bucketing/
        ab-bucketing.ts
        ab-bucketing.md
        ab-bucketing.test.ts
        index.ts
    scheduling/
      schedule-parser/
        schedule-parser.ts
        schedule-parser.md
        schedule-parser.test.ts
        index.ts
    analytics/
      campaign-analytics/
        campaign-analytics.ts
        campaign-analytics.md
        campaign-analytics.test.ts
        index.ts
    __benchmarks__/
      campaigns.bench.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream consumers: any server-side campaign dispatch service.

Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/email-templates, @suaveplan/audience-rules, @suaveplan/idempotency, @suaveplan/retry, @suaveplan/suppression.