# 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 ```ts 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 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` ```ts class CampaignManager { constructor(options: CampaignManagerOptions) create(draft: Omit): Campaign schedule(id: string, schedule: CampaignSchedule): Campaign send(id: string, options?: CampaignSendOptions): Promise 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 } ``` ### Error codes ```ts 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 ```text 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`.