138 lines
5.8 KiB
Plaintext
138 lines
5.8 KiB
Plaintext
// R-LEADS / M040a - HiveGrid's operator-wide sales lead intake.
|
|
//
|
|
// Platform leads deliberately carry no tenantId. They are isolated by the
|
|
// platform-admin RLS contract and are visible only to authenticated operators.
|
|
// Anonymous S2S intake uses a reviewed SECURITY DEFINER capability; the API
|
|
// runtime role never receives direct table access outside those policies.
|
|
|
|
enum AdminLeadStatus {
|
|
new
|
|
contacted
|
|
qualified
|
|
disqualified
|
|
}
|
|
|
|
enum AdminLeadActivityType {
|
|
captured
|
|
imported
|
|
manually_created
|
|
status_changed
|
|
assigned
|
|
note_added
|
|
archived
|
|
restored
|
|
duplicate_hint_recorded
|
|
legacy_event
|
|
}
|
|
|
|
model AdminLeadSource {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
key String @unique
|
|
displayName String
|
|
enabled Boolean @default(true)
|
|
defaultAssigneeAdminUserId String?
|
|
notifyAdminUserIds String[] @default([])
|
|
rateLimitPerMinute Int @default(60)
|
|
metadata Json?
|
|
|
|
leads AdminLead[]
|
|
credentials AdminLeadSourceCredential[]
|
|
|
|
@@index([enabled, displayName], name: "admin_lead_source_enabled_display_name_idx")
|
|
@@index([defaultAssigneeAdminUserId], name: "admin_lead_source_default_assignee_idx")
|
|
}
|
|
|
|
/// Rotatable source credential. Only a one-way digest is persisted; the raw
|
|
/// bearer value is returned once by the source administration service.
|
|
model AdminLeadSourceCredential {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
sourceId String
|
|
keyHash String @unique
|
|
keyPrefix String
|
|
expiresAt DateTime?
|
|
revokedAt DateTime?
|
|
lastUsedAt DateTime?
|
|
|
|
source AdminLeadSource @relation(fields: [sourceId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([sourceId, revokedAt, expiresAt], name: "admin_lead_source_credential_active_idx")
|
|
@@index([sourceId, keyPrefix], name: "admin_lead_source_credential_prefix_idx")
|
|
}
|
|
|
|
model AdminLead {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
capturedAt DateTime @default(now())
|
|
archivedAt DateTime?
|
|
sourceId String
|
|
sourceDetail String?
|
|
externalId String?
|
|
idempotencyKey String?
|
|
firstName String?
|
|
lastName String?
|
|
email String?
|
|
normalizedEmail String?
|
|
phone String?
|
|
normalizedPhone String?
|
|
company String?
|
|
message String?
|
|
utmSource String?
|
|
utmMedium String?
|
|
utmCampaign String?
|
|
utmTerm String?
|
|
utmContent String?
|
|
referrer String?
|
|
landingPage String?
|
|
status AdminLeadStatus @default(new)
|
|
assignedToAdminUserId String?
|
|
duplicateHintOfLeadId String?
|
|
metadata Json?
|
|
|
|
source AdminLeadSource @relation(fields: [sourceId], references: [id], onDelete: Restrict)
|
|
activities AdminLeadActivity[]
|
|
duplicateHint AdminLead? @relation("AdminLeadDuplicateHint", fields: [duplicateHintOfLeadId], references: [id], onDelete: SetNull)
|
|
duplicates AdminLead[] @relation("AdminLeadDuplicateHint")
|
|
convertedCompany AdminCompany? @relation("AdminLeadConvertedCompany")
|
|
convertedContact AdminContact? @relation("AdminLeadConvertedContact")
|
|
convertedDeal AdminDeal? @relation("AdminLeadConvertedDeal")
|
|
salesActivities AdminActivity[]
|
|
qualification AdminLeadQualification?
|
|
duplicateCandidatesPrimary AdminDuplicateCandidate[] @relation("AdminDuplicatePrimaryLead")
|
|
duplicateCandidatesSecondary AdminDuplicateCandidate[] @relation("AdminDuplicateSecondaryLead")
|
|
mergeRecordsSurvived AdminMergeRecord[] @relation("AdminMergeSurvivorLead")
|
|
mergeRecordMerged AdminMergeRecord? @relation("AdminMergeMergedLead")
|
|
emailMessages AdminEmailMessage[]
|
|
callSessions AdminCallSession[]
|
|
callMatchCandidates AdminCallMatchCandidate[]
|
|
|
|
@@unique([sourceId, externalId], name: "admin_lead_source_external_id_key")
|
|
@@unique([sourceId, idempotencyKey], name: "admin_lead_source_idempotency_key")
|
|
@@index([status, archivedAt, capturedAt(sort: Desc)], name: "admin_lead_status_archive_captured_idx")
|
|
@@index([sourceId, capturedAt(sort: Desc)], name: "admin_lead_source_captured_idx")
|
|
@@index([assignedToAdminUserId, status, capturedAt(sort: Desc)], name: "admin_lead_assignee_status_captured_idx")
|
|
@@index([normalizedEmail], name: "admin_lead_normalized_email_idx")
|
|
@@index([normalizedPhone], name: "admin_lead_normalized_phone_idx")
|
|
@@index([duplicateHintOfLeadId], name: "admin_lead_duplicate_hint_idx")
|
|
}
|
|
|
|
/// Append-only business timeline. Notes are activities rather than a mutable
|
|
/// blob on AdminLead so every operator-visible change remains attributable.
|
|
model AdminLeadActivity {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
leadId String
|
|
type AdminLeadActivityType
|
|
actorAdminUserId String?
|
|
payload Json
|
|
|
|
lead AdminLead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([leadId, createdAt(sort: Desc)], name: "admin_lead_activity_lead_created_idx")
|
|
@@index([type, createdAt(sort: Desc)], name: "admin_lead_activity_type_created_idx")
|
|
@@index([actorAdminUserId, createdAt(sort: Desc)], name: "admin_lead_activity_actor_created_idx")
|
|
}
|