100 lines
3.7 KiB
Plaintext
100 lines
3.7 KiB
Plaintext
// Marketing-Funnel Phase 3 (Phase 05 Task #80).
|
|
//
|
|
// Email-sequence engine: operator-defined drip campaigns triggered by outbox
|
|
// events. `EmailSequence` is the sequence definition (steps as a JSON
|
|
// array). `EmailSequenceRun` is one in-flight execution against a single
|
|
// Lead / Tenant target. `EmailSequenceStepDelivery` records the per-step
|
|
// send + open + click outcome and powers the statistics dashboard.
|
|
//
|
|
// V1 sequences are linear (step 0, 1, 2, ...). Conditions per step and
|
|
// per-step delay-from-trigger make sequences reusable without date-math
|
|
// in the definition schema. Branching / parallel paths would require a
|
|
// schema-refactor (separate Step table) but are not in scope today.
|
|
|
|
model EmailSequence {
|
|
id String @id @default(cuid())
|
|
/// NULL for system-wide sequences; set for tenant-scoped ones.
|
|
tenantId String?
|
|
name String
|
|
description String?
|
|
|
|
/// `lead.captured` | `lead.qualified` | `lead.stale_60d` |
|
|
/// `lead.converted_to_trial` | `lead.converted_to_customer` |
|
|
/// `tenant.signup` | `tenant.trial_expiring` | `tenant.cancelled` |
|
|
/// `custom`. Matched against outbox eventType (dotted-snake suffix).
|
|
triggerEvent String
|
|
|
|
/// JSON array of `EmailSequenceStep` objects. Shape:
|
|
/// `{ stepIndex, delayHours, template, subject, conditions?, variables? }`.
|
|
/// See packages/email-sequences/src/types.ts for the runtime type.
|
|
stepsJson Json
|
|
|
|
/// Optional sequence-level conditions (e.g. "only customers with plan=pro").
|
|
/// JSON object — same DSL as per-step conditions.
|
|
conditionsJson Json?
|
|
|
|
/// Operator kill-switch. When false, trigger handler skips this sequence
|
|
/// even if the event matches.
|
|
isEnabled Boolean @default(true)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
runs EmailSequenceRun[]
|
|
|
|
@@index([triggerEvent, isEnabled])
|
|
@@index([tenantId, createdAt(sort: Desc)])
|
|
}
|
|
|
|
model EmailSequenceRun {
|
|
id String @id @default(cuid())
|
|
sequenceId String
|
|
/// Target lead — set when the trigger came from a Lead event.
|
|
leadId String?
|
|
/// Target tenant — set when the trigger came from a Tenant event.
|
|
tenantId String?
|
|
/// Outbox eventId that started this run, for idempotency / debugging.
|
|
triggerEventId String?
|
|
/// Lifecycle: pending → running → (completed | cancelled | failed).
|
|
/// `unsubscribed` is a terminal status set when the recipient opted out.
|
|
status String @default("pending")
|
|
startedAt DateTime @default(now())
|
|
completedAt DateTime?
|
|
/// Free-form per-step state ({ lastStepIndex, lastScheduledAt, ... }).
|
|
stepStateJson Json?
|
|
|
|
sequence EmailSequence @relation(fields: [sequenceId], references: [id], onDelete: Cascade)
|
|
deliveries EmailSequenceStepDelivery[]
|
|
|
|
/// Prevent a duplicate run from the same outbox event-id.
|
|
@@unique([sequenceId, triggerEventId])
|
|
@@index([sequenceId, status])
|
|
@@index([leadId])
|
|
@@index([tenantId])
|
|
@@index([startedAt(sort: Desc)])
|
|
}
|
|
|
|
model EmailSequenceStepDelivery {
|
|
id String @id @default(cuid())
|
|
runId String
|
|
stepIndex Int
|
|
/// `pending` (queued for send) | `sent` | `bounced` | `skipped` (condition
|
|
/// false) | `failed` (transient error after retries).
|
|
status String @default("pending")
|
|
scheduledAt DateTime
|
|
sentAt DateTime?
|
|
openedAt DateTime?
|
|
clickedAt DateTime?
|
|
bouncedAt DateTime?
|
|
/// Mail-provider message-id for correlating webhooks.
|
|
providerMessageId String?
|
|
errorMessage String?
|
|
|
|
run EmailSequenceRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([runId, stepIndex])
|
|
@@index([status, scheduledAt])
|
|
@@index([providerMessageId], name: "email_sequence_step_delivery_provider_message_id_idx")
|
|
@@index([scheduledAt])
|
|
}
|