Files
HiveGrid-Wireframes/hivegrid-cloud/prisma/schema/emailSequences.prisma
T

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])
}