242 lines
7.5 KiB
Plaintext
242 lines
7.5 KiB
Plaintext
// Embeddable Form-Widget (Phase 05 Task #58).
|
|
//
|
|
// `Form` is the tenant-authored definition + theme + delivery config that
|
|
// powers the iframe-embedded mini-app. `FormSubmission` is the inbox for
|
|
// payloads landing through the public submission endpoint. Both rows are
|
|
// tenant-scoped so existing tenant-isolation tooling applies as-is.
|
|
|
|
model Form {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
/// Public key used in the embed `<script data-form-key="…">`.
|
|
key String @unique @default(cuid())
|
|
/// Human-readable public URL slug for `/f/:tenant/:slug`.
|
|
slug String?
|
|
name String
|
|
subtype FormSubtype @default(standard)
|
|
status FormStatus @default(draft)
|
|
createdByUserId String?
|
|
publishedAt DateTime?
|
|
isPublic Boolean @default(false)
|
|
isDemoData Boolean @default(false)
|
|
/// Schema-versioned form definition JSON (validated by `FormDefinitionSchema`).
|
|
definition Json
|
|
/// Theme JSON (validated by `FormThemeSchema`). Empty object inherits tenant brand.
|
|
theme Json @default("{}")
|
|
/// Legacy route target. New AK-P16c action routing lives in FormAction.
|
|
targetType String @default("generic")
|
|
targetEntityId String?
|
|
/// Mapping from form field-key -> target row property-key.
|
|
fieldMapping Json @default("{}")
|
|
/// Allowlist of origins permitted to POST submissions. Empty -> no submissions accepted.
|
|
allowedOrigins String[]
|
|
notifyEmails String[]
|
|
webhookUrl String?
|
|
/// Anti-spam / processing config (honeypotEnabled, turnstileEnabled, successMessage override).
|
|
config Json @default("{}")
|
|
isEnabled Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
submissions FormSubmission[]
|
|
fields FormField[]
|
|
actions FormAction[]
|
|
files FormFile[]
|
|
|
|
@@unique([tenantId, slug])
|
|
@@index([tenantId, createdAt(sort: Desc)])
|
|
@@index([tenantId, createdByUserId, createdAt(sort: Desc)])
|
|
@@index([tenantId, status, createdAt(sort: Desc)])
|
|
@@index([key])
|
|
}
|
|
|
|
model FormSubmission {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
tenantId String
|
|
/// Authenticated submissions have exactly one internal or PortalUser actor.
|
|
/// The merge migration adds the database CHECK after legacy public-form
|
|
/// backfill; portal services enforce the invariant immediately.
|
|
createdByUserId String?
|
|
createdByPortalUserId String?
|
|
projectId String?
|
|
/// Validated payload as { values: Record<string, string|number|boolean|null> }.
|
|
payload Json
|
|
/// Origin header recorded at submission time, for audit / forensics.
|
|
sourceOrigin String?
|
|
/// SHA-256 hash of the submitter IP — no raw IP storage for PII reasons.
|
|
ipHash String?
|
|
userAgent String?
|
|
/// "received" | "processed" | "failed" | "spam".
|
|
status String @default("received")
|
|
/// JSON describing routing/webhook delivery outcome.
|
|
result Json?
|
|
linkedEntityType String?
|
|
linkedEntityId String?
|
|
idempotencyKey String?
|
|
gdprAccepted Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
createdByPortalUser PortalUser? @relation("PortalFormSubmissionActor", fields: [createdByPortalUserId, tenantId], references: [id, tenantId], onDelete: Restrict)
|
|
files FormFile[]
|
|
actionLogs FormActionLog[]
|
|
|
|
@@unique([formId, idempotencyKey])
|
|
@@index([formId, createdAt(sort: Desc)])
|
|
@@index([tenantId, createdAt(sort: Desc)])
|
|
@@index([status])
|
|
@@index([tenantId, linkedEntityType, linkedEntityId])
|
|
@@index([tenantId, createdByPortalUserId, createdAt(sort: Desc)])
|
|
@@index([tenantId, projectId, createdAt(sort: Desc)])
|
|
}
|
|
|
|
enum FormStatus {
|
|
draft
|
|
published
|
|
archived
|
|
}
|
|
|
|
enum FormSubtype {
|
|
standard
|
|
acceptance_form
|
|
onboarding
|
|
survey
|
|
booking_intake
|
|
hr_request
|
|
/// WF-P16 S01 type selection: submissions feed the lead inbox by default.
|
|
lead
|
|
/// WF-P16 S01 type selection: staff-only form without a public link.
|
|
internal
|
|
portal_form
|
|
}
|
|
|
|
enum FormFieldType {
|
|
text
|
|
textarea
|
|
number
|
|
date
|
|
time
|
|
select
|
|
multiselect
|
|
boolean
|
|
email
|
|
phone
|
|
file
|
|
photo
|
|
honeypot
|
|
}
|
|
|
|
enum FormActionType {
|
|
auto_match
|
|
create_lead
|
|
create_contact
|
|
create_project
|
|
create_task
|
|
send_email_to_team
|
|
send_email_to_submitter
|
|
webhook
|
|
trigger_automation
|
|
start_double_opt_in
|
|
}
|
|
|
|
enum FormActionLogStatus {
|
|
pending
|
|
success
|
|
failed
|
|
skipped
|
|
}
|
|
|
|
model FormField {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
tenantId String
|
|
fieldKey String
|
|
fieldType FormFieldType
|
|
label String
|
|
helpText String?
|
|
placeholder String?
|
|
required Boolean @default(false)
|
|
position Int
|
|
options Json @default("[]")
|
|
validationRules Json @default("{}")
|
|
conditionalLogic Json @default("[]")
|
|
isSystem Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([formId, fieldKey])
|
|
@@index([tenantId, formId, position])
|
|
}
|
|
|
|
model FormFile {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
submissionId String
|
|
tenantId String
|
|
fieldKey String
|
|
fileId String
|
|
kind String @default("file")
|
|
createdAt DateTime @default(now())
|
|
|
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
submission FormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId, formId, fieldKey])
|
|
@@index([tenantId, submissionId])
|
|
@@index([fileId])
|
|
}
|
|
|
|
model FormAction {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
tenantId String
|
|
actionType FormActionType
|
|
name String
|
|
position Int
|
|
enabled Boolean @default(true)
|
|
fieldMapping Json @default("{}")
|
|
actionConfig Json @default("{}")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
logs FormActionLog[]
|
|
|
|
@@index([tenantId, formId, position])
|
|
@@index([tenantId, actionType])
|
|
}
|
|
|
|
model FormActionLog {
|
|
id String @id @default(cuid())
|
|
formActionId String
|
|
submissionId String
|
|
formId String
|
|
tenantId String
|
|
actionType FormActionType
|
|
status FormActionLogStatus @default(pending)
|
|
attempt Int @default(1)
|
|
inputSnapshot Json @default("{}")
|
|
outputSnapshot Json?
|
|
errorMessage String?
|
|
targetType String?
|
|
targetId String?
|
|
idempotencyKey String
|
|
rerunOfLogId String?
|
|
startedAt DateTime @default(now())
|
|
finishedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
action FormAction @relation(fields: [formActionId], references: [id], onDelete: Cascade)
|
|
submission FormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([formActionId, submissionId, idempotencyKey])
|
|
@@index([tenantId, formId, createdAt(sort: Desc)])
|
|
@@index([tenantId, submissionId])
|
|
@@index([tenantId, status])
|
|
}
|