chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled

This commit is contained in:
Wira Basalamah
2026-04-21 09:29:29 +07:00
commit adde003fba
222 changed files with 37657 additions and 0 deletions

View File

@ -0,0 +1,380 @@
-- CreateTable
CREATE TABLE "SubscriptionPlan" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"priceMonthly" DECIMAL NOT NULL,
"messageQuota" INTEGER NOT NULL,
"seatQuota" INTEGER NOT NULL,
"broadcastQuota" INTEGER NOT NULL,
"featuresJson" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Tenant" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"timezone" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'TRIAL',
"planId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Tenant_planId_fkey" FOREIGN KEY ("planId") REFERENCES "SubscriptionPlan" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Role" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"permissionsJson" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Role_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"roleId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'INVITED',
"avatarUrl" TEXT,
"lastLoginAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "User_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelName" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"wabaId" TEXT,
"phoneNumberId" TEXT,
"displayPhoneNumber" TEXT,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"webhookStatus" TEXT,
"lastSyncAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Channel_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Contact" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelId" TEXT,
"externalRef" TEXT,
"fullName" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"email" TEXT,
"avatarUrl" TEXT,
"countryCode" TEXT,
"optInStatus" TEXT NOT NULL DEFAULT 'UNKNOWN',
"lastInteractionAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Contact_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Contact_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Tag" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"color" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Tag_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ContactTag" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ContactTag_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ContactTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Conversation" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"subject" TEXT,
"status" TEXT NOT NULL DEFAULT 'OPEN',
"priority" TEXT NOT NULL DEFAULT 'NORMAL',
"assignedUserId" TEXT,
"firstMessageAt" DATETIME,
"lastMessageAt" DATETIME,
"lastInboundAt" DATETIME,
"lastOutboundAt" DATETIME,
"resolvedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Conversation_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Conversation_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Conversation_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Conversation_assignedUserId_fkey" FOREIGN KEY ("assignedUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Message" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"contactId" TEXT,
"direction" TEXT NOT NULL,
"type" TEXT NOT NULL DEFAULT 'TEXT',
"providerMessageId" TEXT,
"replyToMessageId" TEXT,
"contentText" TEXT,
"mediaUrl" TEXT,
"mimeType" TEXT,
"deliveryStatus" TEXT NOT NULL DEFAULT 'QUEUED',
"failedReason" TEXT,
"sentByUserId" TEXT,
"sentAt" DATETIME,
"deliveredAt" DATETIME,
"readAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Message_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "Message_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Message_sentByUserId_fkey" FOREIGN KEY ("sentByUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ConversationNote" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ConversationNote_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ConversationNote_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ConversationNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ConversationTag" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"tagId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ConversationTag_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ConversationTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ConversationActivity" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"actorUserId" TEXT,
"activityType" TEXT NOT NULL,
"metadataJson" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ConversationActivity_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ConversationActivity_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "ConversationActivity_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ContactSegment" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"rulesJson" JSONB,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ContactSegment_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "SegmentMember" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"segmentId" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SegmentMember_segmentId_fkey" FOREIGN KEY ("segmentId") REFERENCES "ContactSegment" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "SegmentMember_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "MessageTemplate" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT NOT NULL,
"languageCode" TEXT NOT NULL,
"templateType" TEXT,
"bodyText" TEXT NOT NULL,
"headerType" TEXT,
"footerText" TEXT,
"buttonsJson" JSONB,
"providerTemplateId" TEXT,
"approvalStatus" TEXT NOT NULL DEFAULT 'DRAFT',
"rejectedReason" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "MessageTemplate_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "MessageTemplate_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "BroadcastCampaign" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
"createdByUserId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"campaignType" TEXT NOT NULL DEFAULT 'BROADCAST',
"audienceType" TEXT NOT NULL,
"segmentId" TEXT,
"scheduledAt" DATETIME,
"startedAt" DATETIME,
"finishedAt" DATETIME,
"status" TEXT NOT NULL DEFAULT 'DRAFT',
"totalRecipients" INTEGER NOT NULL DEFAULT 0,
"totalSent" INTEGER NOT NULL DEFAULT 0,
"totalDelivered" INTEGER NOT NULL DEFAULT 0,
"totalRead" INTEGER NOT NULL DEFAULT 0,
"totalFailed" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "BroadcastCampaign_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "BroadcastCampaign_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "BroadcastCampaign_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "MessageTemplate" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "BroadcastCampaign_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "BroadcastCampaign_segmentId_fkey" FOREIGN KEY ("segmentId") REFERENCES "ContactSegment" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "CampaignRecipient" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"campaignId" TEXT NOT NULL,
"contactId" TEXT NOT NULL,
"phoneNumber" TEXT NOT NULL,
"sendStatus" TEXT NOT NULL DEFAULT 'QUEUED',
"failureReason" TEXT,
"providerMessageId" TEXT,
"sentAt" DATETIME,
"deliveredAt" DATETIME,
"readAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CampaignRecipient_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "BroadcastCampaign" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "CampaignRecipient_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "Contact" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"actorUserId" TEXT,
"entityType" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"metadataJson" JSONB,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "AuditLog_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "WebhookEvent" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"channelId" TEXT,
"eventType" TEXT NOT NULL,
"providerEventId" TEXT,
"payloadJson" JSONB NOT NULL,
"processStatus" TEXT NOT NULL,
"failedReason" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processedAt" DATETIME,
CONSTRAINT "WebhookEvent_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "WebhookEvent_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "UsageMetric" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"metricDate" DATETIME NOT NULL,
"inboundMessages" INTEGER NOT NULL DEFAULT 0,
"outboundMessages" INTEGER NOT NULL DEFAULT 0,
"activeContacts" INTEGER NOT NULL DEFAULT 0,
"activeAgents" INTEGER NOT NULL DEFAULT 0,
"broadcastSent" INTEGER NOT NULL DEFAULT 0,
"storageUsedMb" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "UsageMetric_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "BillingInvoice" (
"id" TEXT NOT NULL PRIMARY KEY,
"tenantId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"invoiceNumber" TEXT NOT NULL,
"periodStart" DATETIME NOT NULL,
"periodEnd" DATETIME NOT NULL,
"subtotal" DECIMAL NOT NULL,
"taxAmount" DECIMAL NOT NULL,
"totalAmount" DECIMAL NOT NULL,
"paymentStatus" TEXT NOT NULL DEFAULT 'UNPAID',
"dueDate" DATETIME NOT NULL,
"paidAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "BillingInvoice_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "BillingInvoice_planId_fkey" FOREIGN KEY ("planId") REFERENCES "SubscriptionPlan" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "SubscriptionPlan_code_key" ON "SubscriptionPlan"("code");
CREATE UNIQUE INDEX "Tenant_slug_key" ON "Tenant"("slug");
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
CREATE UNIQUE INDEX "Contact_tenantId_phoneNumber_key" ON "Contact"("tenantId", "phoneNumber");
CREATE UNIQUE INDEX "Tag_tenantId_name_key" ON "Tag"("tenantId", "name");
CREATE UNIQUE INDEX "ContactTag_contactId_tagId_key" ON "ContactTag"("contactId", "tagId");
CREATE INDEX "Conversation_tenantId_status_idx" ON "Conversation"("tenantId", "status");
CREATE INDEX "Conversation_tenantId_assignedUserId_idx" ON "Conversation"("tenantId", "assignedUserId");
CREATE INDEX "Conversation_tenantId_lastMessageAt_idx" ON "Conversation"("tenantId", "lastMessageAt");
CREATE UNIQUE INDEX "Message_providerMessageId_key" ON "Message"("providerMessageId");
CREATE UNIQUE INDEX "ConversationTag_conversationId_tagId_key" ON "ConversationTag"("conversationId", "tagId");
CREATE UNIQUE INDEX "SegmentMember_segmentId_contactId_key" ON "SegmentMember"("segmentId", "contactId");
CREATE UNIQUE INDEX "UsageMetric_tenantId_metricDate_key" ON "UsageMetric"("tenantId", "metricDate");
CREATE UNIQUE INDEX "BillingInvoice_invoiceNumber_key" ON "BillingInvoice"("invoiceNumber");

View File

@ -0,0 +1,4 @@
ALTER TABLE "CampaignRecipient" ADD COLUMN "sendAttempts" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "CampaignRecipient" ADD COLUMN "maxSendAttempts" INTEGER NOT NULL DEFAULT 3;
ALTER TABLE "CampaignRecipient" ADD COLUMN "lastAttemptAt" DATETIME;
ALTER TABLE "CampaignRecipient" ADD COLUMN "nextRetryAt" DATETIME;

View File

@ -0,0 +1,22 @@
CREATE TABLE "BackgroundJobState" (
"id" TEXT NOT NULL PRIMARY KEY,
"jobName" TEXT NOT NULL,
"lockedBy" TEXT NOT NULL,
"lockedUntil" DATETIME,
"runs" INTEGER NOT NULL DEFAULT 0,
"lastRunStartedAt" DATETIME,
"lastRunCompletedAt" DATETIME,
"lastRunStatus" TEXT,
"lastRunSummaryJson" JSONB,
"lastError" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "BackgroundJobState_jobName_key" UNIQUE ("jobName")
);
CREATE INDEX "BackgroundJobState_jobName_idx" ON "BackgroundJobState"("jobName");
CREATE INDEX "CampaignRecipient_campaignId_sendStatus_nextRetryAt_idx" ON "CampaignRecipient"("campaignId", "sendStatus", "nextRetryAt");
CREATE INDEX "CampaignRecipient_campaignId_sendStatus_idx" ON "CampaignRecipient"("campaignId", "sendStatus");
CREATE INDEX "BroadcastCampaign_tenantId_status_idx" ON "BroadcastCampaign"("tenantId", "status");
CREATE INDEX "BroadcastCampaign_status_scheduledAt_idx" ON "BroadcastCampaign"("status", "scheduledAt");

View File

@ -0,0 +1,2 @@
ALTER TABLE "BackgroundJobState" ADD COLUMN "consecutiveFailures" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "BackgroundJobState" ADD COLUMN "lastFailureAt" DATETIME;

View File

@ -0,0 +1,3 @@
ALTER TABLE "WebhookEvent" ADD COLUMN "eventHash" TEXT;
CREATE INDEX "WebhookEvent_tenantId_channelId_eventHash_idx" ON "WebhookEvent"("tenantId", "channelId", "eventHash");

View File

@ -0,0 +1,17 @@
CREATE TABLE "AuthToken" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"tokenType" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"expiresAt" DATETIME NOT NULL,
"consumedAt" DATETIME,
"createdByUser" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"metadataJson" JSONB,
CONSTRAINT "AuthToken_tokenHash_key" UNIQUE ("tokenHash"),
CONSTRAINT "AuthToken_tokenType_check" CHECK ("tokenType" IN ('PASSWORD_RESET', 'INVITE_ACCEPTANCE'))
);
CREATE INDEX "AuthToken_userId_tokenType_idx" ON "AuthToken"("userId", "tokenType");
CREATE INDEX "AuthToken_tenantId_tokenType_expiresAt_idx" ON "AuthToken"("tenantId", "tokenType", "expiresAt");

570
prisma/schema.prisma Normal file
View File

@ -0,0 +1,570 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
enum TenantStatus {
ACTIVE
SUSPENDED
TRIAL
INACTIVE
}
enum UserStatus {
ACTIVE
INVITED
DISABLED
}
enum RoleCode {
SUPER_ADMIN
ADMIN_CLIENT
AGENT
}
enum ChannelStatus {
CONNECTED
DISCONNECTED
PENDING
ERROR
}
enum ConversationStatus {
OPEN
PENDING
RESOLVED
ARCHIVED
SPAM
}
enum ConversationPriority {
LOW
NORMAL
HIGH
URGENT
}
enum MessageDirection {
INBOUND
OUTBOUND
}
enum AuthTokenType {
PASSWORD_RESET
INVITE_ACCEPTANCE
}
enum MessageType {
TEXT
IMAGE
VIDEO
AUDIO
DOCUMENT
TEMPLATE
INTERACTIVE
}
enum DeliveryStatus {
QUEUED
SENT
DELIVERED
READ
FAILED
}
enum OptInStatus {
UNKNOWN
OPTED_IN
OPTED_OUT
}
enum TemplateApprovalStatus {
DRAFT
PENDING
APPROVED
REJECTED
DISABLED
}
enum CampaignStatus {
DRAFT
SCHEDULED
PROCESSING
COMPLETED
PARTIAL_FAILED
FAILED
CANCELED
}
enum CampaignAudienceType {
SEGMENT
IMPORT
MANUAL
}
enum CampaignType {
BROADCAST
BULK_FOLLOWUP
}
enum PaymentStatus {
UNPAID
PAID
OVERDUE
VOID
}
model SubscriptionPlan {
id String @id @default(cuid())
name String
code String @unique
priceMonthly Int
messageQuota Int
seatQuota Int
broadcastQuota Int
featuresJson String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenants Tenant[]
invoices BillingInvoice[]
}
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
companyName String
timezone String
status TenantStatus @default(TRIAL)
planId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plan SubscriptionPlan @relation(fields: [planId], references: [id])
users User[]
roles Role[]
channels Channel[]
contacts Contact[]
tags Tag[]
conversations Conversation[]
messages Message[]
notes ConversationNote[]
activities ConversationActivity[]
segments ContactSegment[]
templates MessageTemplate[]
campaigns BroadcastCampaign[]
auditLogs AuditLog[]
webhookEvents WebhookEvent[]
usageMetrics UsageMetric[]
billingInvoices BillingInvoice[]
}
model Role {
id String @id @default(cuid())
tenantId String?
name String
code RoleCode
permissionsJson String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant? @relation(fields: [tenantId], references: [id])
users User[]
}
model User {
id String @id @default(cuid())
tenantId String
fullName String
email String @unique
passwordHash String
roleId String
status UserStatus @default(INVITED)
avatarUrl String?
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
role Role @relation(fields: [roleId], references: [id])
assignedConversations Conversation[]
notes ConversationNote[]
sentMessages Message[]
activities ConversationActivity[] @relation("ActorActivities")
campaigns BroadcastCampaign[]
auditLogs AuditLog[]
}
model AuthToken {
id String @id @default(cuid())
userId String
tenantId String
tokenType AuthTokenType
tokenHash String @unique
expiresAt DateTime
consumedAt DateTime?
createdByUser String?
createdAt DateTime @default(now())
metadataJson String?
@@index([userId, tokenType])
@@index([tenantId, tokenType, expiresAt])
}
model Channel {
id String @id @default(cuid())
tenantId String
channelName String
provider String
wabaId String?
phoneNumberId String?
displayPhoneNumber String?
status ChannelStatus @default(PENDING)
webhookStatus String?
lastSyncAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
contacts Contact[]
conversations Conversation[]
messages Message[]
templates MessageTemplate[]
campaigns BroadcastCampaign[]
webhookEvents WebhookEvent[]
}
model Contact {
id String @id @default(cuid())
tenantId String
channelId String?
externalRef String?
fullName String
phoneNumber String
email String?
avatarUrl String?
countryCode String?
optInStatus OptInStatus @default(UNKNOWN)
lastInteractionAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
channel Channel? @relation(fields: [channelId], references: [id])
conversations Conversation[]
messages Message[]
contactTags ContactTag[]
segmentMembers SegmentMember[]
campaignRecipients CampaignRecipient[]
@@unique([tenantId, phoneNumber])
}
model Tag {
id String @id @default(cuid())
tenantId String
name String
color String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
contactTags ContactTag[]
conversationTags ConversationTag[]
@@unique([tenantId, name])
}
model ContactTag {
id String @id @default(cuid())
tenantId String
contactId String
tagId String
createdAt DateTime @default(now())
contact Contact @relation(fields: [contactId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@unique([contactId, tagId])
}
model Conversation {
id String @id @default(cuid())
tenantId String
channelId String
contactId String
subject String?
status ConversationStatus @default(OPEN)
priority ConversationPriority @default(NORMAL)
assignedUserId String?
firstMessageAt DateTime?
lastMessageAt DateTime?
lastInboundAt DateTime?
lastOutboundAt DateTime?
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
channel Channel @relation(fields: [channelId], references: [id])
contact Contact @relation(fields: [contactId], references: [id])
assignedUser User? @relation(fields: [assignedUserId], references: [id])
messages Message[]
notes ConversationNote[]
activities ConversationActivity[]
conversationTags ConversationTag[]
@@index([tenantId, status])
@@index([tenantId, assignedUserId])
@@index([tenantId, lastMessageAt])
}
model Message {
id String @id @default(cuid())
tenantId String
conversationId String
channelId String
contactId String?
direction MessageDirection
type MessageType @default(TEXT)
providerMessageId String? @unique
replyToMessageId String?
contentText String?
mediaUrl String?
mimeType String?
deliveryStatus DeliveryStatus @default(QUEUED)
failedReason String?
sentByUserId String?
sentAt DateTime?
deliveredAt DateTime?
readAt DateTime?
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
conversation Conversation @relation(fields: [conversationId], references: [id])
channel Channel @relation(fields: [channelId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
sentByUser User? @relation(fields: [sentByUserId], references: [id])
}
model ConversationNote {
id String @id @default(cuid())
tenantId String
conversationId String
userId String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
conversation Conversation @relation(fields: [conversationId], references: [id])
user User @relation(fields: [userId], references: [id])
}
model ConversationTag {
id String @id @default(cuid())
tenantId String
conversationId String
tagId String
createdAt DateTime @default(now())
conversation Conversation @relation(fields: [conversationId], references: [id])
tag Tag @relation(fields: [tagId], references: [id])
@@unique([conversationId, tagId])
}
model ConversationActivity {
id String @id @default(cuid())
tenantId String
conversationId String
actorUserId String?
activityType String
metadataJson String?
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
conversation Conversation @relation(fields: [conversationId], references: [id])
actorUser User? @relation("ActorActivities", fields: [actorUserId], references: [id])
}
model ContactSegment {
id String @id @default(cuid())
tenantId String
name String
description String?
rulesJson String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
members SegmentMember[]
campaigns BroadcastCampaign[]
}
model SegmentMember {
id String @id @default(cuid())
tenantId String
segmentId String
contactId String
createdAt DateTime @default(now())
segment ContactSegment @relation(fields: [segmentId], references: [id])
contact Contact @relation(fields: [contactId], references: [id])
@@unique([segmentId, contactId])
}
model MessageTemplate {
id String @id @default(cuid())
tenantId String
channelId String
name String
category String
languageCode String
templateType String?
bodyText String
headerType String?
footerText String?
buttonsJson String?
providerTemplateId String?
approvalStatus TemplateApprovalStatus @default(DRAFT)
rejectedReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
channel Channel @relation(fields: [channelId], references: [id])
campaigns BroadcastCampaign[]
}
model BroadcastCampaign {
id String @id @default(cuid())
tenantId String
channelId String
templateId String
createdByUserId String
name String
campaignType CampaignType @default(BROADCAST)
audienceType CampaignAudienceType
segmentId String?
scheduledAt DateTime?
startedAt DateTime?
finishedAt DateTime?
status CampaignStatus @default(DRAFT)
totalRecipients Int @default(0)
totalSent Int @default(0)
totalDelivered Int @default(0)
totalRead Int @default(0)
totalFailed Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
channel Channel @relation(fields: [channelId], references: [id])
template MessageTemplate @relation(fields: [templateId], references: [id])
createdByUser User @relation(fields: [createdByUserId], references: [id])
segment ContactSegment? @relation(fields: [segmentId], references: [id])
recipients CampaignRecipient[]
@@index([tenantId, status])
@@index([status, scheduledAt])
}
model CampaignRecipient {
id String @id @default(cuid())
tenantId String
campaignId String
contactId String
phoneNumber String
sendStatus DeliveryStatus @default(QUEUED)
sendAttempts Int @default(0)
maxSendAttempts Int @default(3)
lastAttemptAt DateTime?
nextRetryAt DateTime?
failureReason String?
providerMessageId String?
sentAt DateTime?
deliveredAt DateTime?
readAt DateTime?
createdAt DateTime @default(now())
campaign BroadcastCampaign @relation(fields: [campaignId], references: [id])
contact Contact @relation(fields: [contactId], references: [id])
@@index([campaignId, sendStatus, nextRetryAt])
@@index([campaignId, sendStatus])
}
model BackgroundJobState {
id String @id @default(cuid())
jobName String @unique
lockedBy String
lockedUntil DateTime?
runs Int @default(0)
consecutiveFailures Int @default(0)
lastRunStartedAt DateTime?
lastRunCompletedAt DateTime?
lastRunStatus String?
lastRunSummaryJson String?
lastError String?
lastFailureAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AuditLog {
id String @id @default(cuid())
tenantId String
actorUserId String?
entityType String
entityId String
action String
metadataJson String?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
actorUser User? @relation(fields: [actorUserId], references: [id])
}
model WebhookEvent {
id String @id @default(cuid())
tenantId String
channelId String?
eventType String
providerEventId String?
eventHash String?
payloadJson String
processStatus String
failedReason String?
createdAt DateTime @default(now())
processedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id])
channel Channel? @relation(fields: [channelId], references: [id])
@@index([tenantId, channelId, eventHash])
}
model UsageMetric {
id String @id @default(cuid())
tenantId String
metricDate DateTime
inboundMessages Int @default(0)
outboundMessages Int @default(0)
activeContacts Int @default(0)
activeAgents Int @default(0)
broadcastSent Int @default(0)
storageUsedMb Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
@@unique([tenantId, metricDate])
}
model BillingInvoice {
id String @id @default(cuid())
tenantId String
planId String
invoiceNumber String @unique
periodStart DateTime
periodEnd DateTime
subtotal Int
taxAmount Int
totalAmount Int
paymentStatus PaymentStatus @default(UNPAID)
dueDate DateTime
paidAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id])
plan SubscriptionPlan @relation(fields: [planId], references: [id])
}

589
prisma/seed.cjs Normal file
View File

@ -0,0 +1,589 @@
const { PrismaClient, RoleCode, TenantStatus, UserStatus, ChannelStatus, ConversationStatus, ConversationPriority, MessageDirection, MessageType, DeliveryStatus, OptInStatus, TemplateApprovalStatus, CampaignStatus, CampaignAudienceType, CampaignType, PaymentStatus } = require("@prisma/client");
const { pbkdf2Sync, randomBytes } = require("crypto");
const prisma = new PrismaClient();
function hashPassword(password) {
const iterations = 120000;
const salt = randomBytes(16);
const digest = pbkdf2Sync(password, salt, iterations, 32, "sha256");
return `pbkdf2$${iterations}$${salt.toString("base64url")}$${digest.toString("base64url")}`;
}
async function main() {
await prisma.campaignRecipient.deleteMany();
await prisma.broadcastCampaign.deleteMany();
await prisma.messageTemplate.deleteMany();
await prisma.segmentMember.deleteMany();
await prisma.contactSegment.deleteMany();
await prisma.conversationActivity.deleteMany();
await prisma.conversationTag.deleteMany();
await prisma.conversationNote.deleteMany();
await prisma.message.deleteMany();
await prisma.conversation.deleteMany();
await prisma.contactTag.deleteMany();
await prisma.tag.deleteMany();
await prisma.contact.deleteMany();
await prisma.webhookEvent.deleteMany();
await prisma.auditLog.deleteMany();
await prisma.usageMetric.deleteMany();
await prisma.billingInvoice.deleteMany();
await prisma.authToken.deleteMany();
await prisma.channel.deleteMany();
await prisma.user.deleteMany();
await prisma.role.deleteMany();
await prisma.tenant.deleteMany();
await prisma.subscriptionPlan.deleteMany();
await prisma.subscriptionPlan.createMany({
data: [
{
id: "plan-basic",
name: "Basic",
code: "basic",
priceMonthly: 1500000,
messageQuota: 10000,
seatQuota: 5,
broadcastQuota: 2000,
featuresJson: JSON.stringify({ inbox: true, contacts: true, analytics: "basic" })
},
{
id: "plan-pro",
name: "Pro",
code: "pro",
priceMonthly: 2500000,
messageQuota: 50000,
seatQuota: 15,
broadcastQuota: 10000,
featuresJson: JSON.stringify({ inbox: true, contacts: true, analytics: "extended", broadcast: true })
}
]
});
await prisma.tenant.createMany({
data: [
{
id: "tenant-acme",
name: "Acme Co",
slug: "acme-co",
companyName: "Acme Co",
timezone: "Asia/Jakarta",
status: TenantStatus.ACTIVE,
planId: "plan-pro"
},
{
id: "tenant-sinar",
name: "Sinar Abadi",
slug: "sinar-abadi",
companyName: "PT Sinar Abadi",
timezone: "Asia/Jakarta",
status: TenantStatus.TRIAL,
planId: "plan-basic"
}
]
});
await prisma.role.createMany({
data: [
{ id: "role-super-admin", name: "Super Admin", code: RoleCode.SUPER_ADMIN, permissionsJson: JSON.stringify({ global: true }) },
{ id: "role-admin-acme", tenantId: "tenant-acme", name: "Admin Client", code: RoleCode.ADMIN_CLIENT, permissionsJson: JSON.stringify({ tenant: true }) },
{ id: "role-agent-acme", tenantId: "tenant-acme", name: "Agent", code: RoleCode.AGENT, permissionsJson: JSON.stringify({ inbox: true }) },
{ id: "role-admin-sinar", tenantId: "tenant-sinar", name: "Admin Client", code: RoleCode.ADMIN_CLIENT, permissionsJson: JSON.stringify({ tenant: true }) }
]
});
await prisma.user.createMany({
data: [
{
id: "user-super-admin",
tenantId: "tenant-acme",
fullName: "Platform Owner",
email: "owner@inboxsuite.test",
passwordHash: hashPassword("demo123"),
roleId: "role-super-admin",
status: UserStatus.ACTIVE,
lastLoginAt: new Date("2026-04-20T08:00:00Z")
},
{
id: "user-admin-001",
tenantId: "tenant-acme",
fullName: "Admin Operations",
email: "admin@acme.test",
passwordHash: hashPassword("admin123"),
roleId: "role-admin-acme",
status: UserStatus.ACTIVE,
lastLoginAt: new Date("2026-04-20T09:00:00Z")
},
{
id: "user-agent-001",
tenantId: "tenant-acme",
fullName: "Farhan",
email: "farhan@acme.test",
passwordHash: hashPassword("agent123"),
roleId: "role-agent-acme",
status: UserStatus.ACTIVE,
lastLoginAt: new Date("2026-04-20T09:12:00Z")
},
{
id: "user-agent-002",
tenantId: "tenant-acme",
fullName: "Tiara",
email: "tiara@acme.test",
passwordHash: hashPassword("agent123"),
roleId: "role-agent-acme",
status: UserStatus.ACTIVE,
lastLoginAt: new Date("2026-04-20T08:54:00Z")
},
{
id: "user-admin-sinar",
tenantId: "tenant-sinar",
fullName: "Sinar Admin",
email: "admin@sinar.test",
passwordHash: hashPassword("sinar123"),
roleId: "role-admin-sinar",
status: UserStatus.INVITED
}
]
});
await prisma.channel.createMany({
data: [
{
id: "channel-main-acme",
tenantId: "tenant-acme",
channelName: "Main WA",
provider: "Meta BSP",
wabaId: "waba-acme-main",
phoneNumberId: "phone-acme-main",
displayPhoneNumber: "+628111111111",
status: ChannelStatus.CONNECTED,
webhookStatus: "healthy",
lastSyncAt: new Date("2026-04-20T09:10:00Z")
},
{
id: "channel-ops-acme",
tenantId: "tenant-acme",
channelName: "Ops WA",
provider: "Meta BSP",
wabaId: "waba-acme-ops",
phoneNumberId: "phone-acme-ops",
displayPhoneNumber: "+628222222222",
status: ChannelStatus.CONNECTED,
webhookStatus: "healthy",
lastSyncAt: new Date("2026-04-20T08:45:00Z")
},
{
id: "channel-sinar",
tenantId: "tenant-sinar",
channelName: "Sales WA",
provider: "Meta BSP",
wabaId: "waba-sinar-main",
phoneNumberId: "phone-sinar-main",
displayPhoneNumber: "+628333333333",
status: ChannelStatus.PENDING,
webhookStatus: "pending"
}
]
});
await prisma.tag.createMany({
data: [
{ id: "tag-enterprise", tenantId: "tenant-acme", name: "Enterprise", color: "#155eef" },
{ id: "tag-hot-lead", tenantId: "tenant-acme", name: "Hot Lead", color: "#ff7a00" },
{ id: "tag-payment", tenantId: "tenant-acme", name: "Payment Follow-up", color: "#b54708" },
{ id: "tag-b2b", tenantId: "tenant-acme", name: "B2B", color: "#067647" }
]
});
await prisma.contact.createMany({
data: [
{
id: "contact-001",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
fullName: "Nadia Pratama",
phoneNumber: "+628123456789",
email: "nadia@example.com",
countryCode: "ID",
optInStatus: OptInStatus.OPTED_IN,
lastInteractionAt: new Date("2026-04-20T09:12:00Z")
},
{
id: "contact-002",
tenantId: "tenant-acme",
channelId: "channel-ops-acme",
fullName: "PT Sinar Abadi",
phoneNumber: "+62219876543",
email: "ops@sinarabadi.co.id",
countryCode: "ID",
optInStatus: OptInStatus.UNKNOWN,
lastInteractionAt: new Date("2026-04-19T11:00:00Z")
},
{
id: "contact-003",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
fullName: "Rizky Saputra",
phoneNumber: "+628777777777",
email: "rizky@example.com",
countryCode: "ID",
optInStatus: OptInStatus.OPTED_IN,
lastInteractionAt: new Date("2026-04-18T12:00:00Z")
}
]
});
await prisma.contactTag.createMany({
data: [
{ id: "ct-001", tenantId: "tenant-acme", contactId: "contact-001", tagId: "tag-enterprise" },
{ id: "ct-002", tenantId: "tenant-acme", contactId: "contact-001", tagId: "tag-hot-lead" },
{ id: "ct-003", tenantId: "tenant-acme", contactId: "contact-002", tagId: "tag-b2b" },
{ id: "ct-004", tenantId: "tenant-acme", contactId: "contact-003", tagId: "tag-payment" }
]
});
await prisma.contactSegment.create({
data: {
id: "segment-hot-leads",
tenantId: "tenant-acme",
name: "Hot Leads",
description: "Enterprise prospects",
rulesJson: JSON.stringify({ tags: ["Enterprise", "Hot Lead"] })
}
});
await prisma.segmentMember.create({
data: {
id: "segment-member-001",
tenantId: "tenant-acme",
segmentId: "segment-hot-leads",
contactId: "contact-001"
}
});
await prisma.conversation.createMany({
data: [
{
id: "conv-001",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
contactId: "contact-001",
subject: "Enterprise pricing",
status: ConversationStatus.OPEN,
priority: ConversationPriority.HIGH,
assignedUserId: "user-agent-001",
firstMessageAt: new Date("2026-04-20T09:00:00Z"),
lastMessageAt: new Date("2026-04-20T09:12:00Z"),
lastInboundAt: new Date("2026-04-20T09:12:00Z")
},
{
id: "conv-002",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
contactId: "contact-003",
subject: "Payment follow-up",
status: ConversationStatus.PENDING,
priority: ConversationPriority.NORMAL,
firstMessageAt: new Date("2026-04-20T08:30:00Z"),
lastMessageAt: new Date("2026-04-20T08:47:00Z"),
lastInboundAt: new Date("2026-04-20T08:47:00Z")
},
{
id: "conv-003",
tenantId: "tenant-acme",
channelId: "channel-ops-acme",
contactId: "contact-002",
subject: "Template request",
status: ConversationStatus.RESOLVED,
priority: ConversationPriority.NORMAL,
assignedUserId: "user-agent-002",
firstMessageAt: new Date("2026-04-19T09:00:00Z"),
lastMessageAt: new Date("2026-04-19T11:00:00Z"),
lastInboundAt: new Date("2026-04-19T10:20:00Z"),
lastOutboundAt: new Date("2026-04-19T11:00:00Z"),
resolvedAt: new Date("2026-04-19T11:10:00Z")
}
]
});
await prisma.conversationTag.createMany({
data: [
{ id: "cvt-001", tenantId: "tenant-acme", conversationId: "conv-001", tagId: "tag-enterprise" },
{ id: "cvt-002", tenantId: "tenant-acme", conversationId: "conv-001", tagId: "tag-hot-lead" },
{ id: "cvt-003", tenantId: "tenant-acme", conversationId: "conv-002", tagId: "tag-payment" },
{ id: "cvt-004", tenantId: "tenant-acme", conversationId: "conv-003", tagId: "tag-b2b" }
]
});
await prisma.message.createMany({
data: [
{
id: "msg-001",
tenantId: "tenant-acme",
conversationId: "conv-001",
channelId: "channel-main-acme",
contactId: "contact-001",
direction: MessageDirection.INBOUND,
type: MessageType.TEXT,
contentText: "Halo, saya mau tanya soal paket enterprise untuk tim saya.",
deliveryStatus: DeliveryStatus.READ,
createdAt: new Date("2026-04-20T09:00:00Z"),
readAt: new Date("2026-04-20T09:00:00Z")
},
{
id: "msg-002",
tenantId: "tenant-acme",
conversationId: "conv-001",
channelId: "channel-main-acme",
direction: MessageDirection.OUTBOUND,
type: MessageType.TEXT,
contentText: "Tentu. Boleh saya tahu estimasi jumlah agent yang akan memakai sistem?",
deliveryStatus: DeliveryStatus.DELIVERED,
sentByUserId: "user-agent-001",
createdAt: new Date("2026-04-20T09:05:00Z"),
sentAt: new Date("2026-04-20T09:05:00Z"),
deliveredAt: new Date("2026-04-20T09:05:05Z")
},
{
id: "msg-003",
tenantId: "tenant-acme",
conversationId: "conv-002",
channelId: "channel-main-acme",
contactId: "contact-003",
direction: MessageDirection.INBOUND,
type: MessageType.TEXT,
contentText: "Sudah saya transfer, mohon dicek ya.",
deliveryStatus: DeliveryStatus.READ,
createdAt: new Date("2026-04-20T08:47:00Z"),
readAt: new Date("2026-04-20T08:47:00Z")
}
]
});
await prisma.conversationNote.create({
data: {
id: "note-001",
tenantId: "tenant-acme",
conversationId: "conv-001",
userId: "user-agent-001",
content: "Prospek enterprise, follow-up sore ini."
}
});
await prisma.conversationActivity.createMany({
data: [
{
id: "act-001",
tenantId: "tenant-acme",
conversationId: "conv-001",
actorUserId: "user-agent-001",
activityType: "assigned",
metadataJson: JSON.stringify({ assignee: "Farhan" })
},
{
id: "act-002",
tenantId: "tenant-acme",
conversationId: "conv-001",
actorUserId: "user-agent-001",
activityType: "message_sent",
metadataJson: JSON.stringify({ messageId: "msg-002" })
}
]
});
await prisma.messageTemplate.createMany({
data: [
{
id: "tpl-001",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
name: "promo_april_2026",
category: "Marketing",
languageCode: "id",
templateType: "text",
bodyText: "Halo {{1}}, ada promo spesial untuk bisnis Anda minggu ini.",
approvalStatus: TemplateApprovalStatus.APPROVED
},
{
id: "tpl-002",
tenantId: "tenant-acme",
channelId: "channel-ops-acme",
name: "billing_reminder",
category: "Utility",
languageCode: "id",
templateType: "text",
bodyText: "Halo {{1}}, berikut pengingat pembayaran Anda.",
approvalStatus: TemplateApprovalStatus.PENDING
}
]
});
await prisma.broadcastCampaign.createMany({
data: [
{
id: "campaign-001",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
templateId: "tpl-001",
createdByUserId: "user-admin-001",
name: "Promo April",
campaignType: CampaignType.BROADCAST,
audienceType: CampaignAudienceType.SEGMENT,
segmentId: "segment-hot-leads",
scheduledAt: new Date("2026-04-20T10:00:00Z"),
startedAt: new Date("2026-04-20T10:00:00Z"),
finishedAt: new Date("2026-04-20T10:30:00Z"),
status: CampaignStatus.COMPLETED,
totalRecipients: 1203,
totalSent: 1203,
totalDelivered: 1180,
totalRead: 873,
totalFailed: 23
},
{
id: "campaign-002",
tenantId: "tenant-acme",
channelId: "channel-ops-acme",
templateId: "tpl-002",
createdByUserId: "user-admin-001",
name: "Billing Reminder",
campaignType: CampaignType.BULK_FOLLOWUP,
audienceType: CampaignAudienceType.MANUAL,
status: CampaignStatus.DRAFT,
totalRecipients: 0
}
]
});
await prisma.campaignRecipient.createMany({
data: [
{
id: "recipient-001",
tenantId: "tenant-acme",
campaignId: "campaign-001",
contactId: "contact-001",
phoneNumber: "+628123456789",
sendStatus: DeliveryStatus.DELIVERED,
sentAt: new Date("2026-04-20T10:01:00Z"),
deliveredAt: new Date("2026-04-20T10:01:05Z"),
readAt: new Date("2026-04-20T11:00:00Z")
},
{
id: "recipient-002",
tenantId: "tenant-acme",
campaignId: "campaign-001",
contactId: "contact-003",
phoneNumber: "+628777777777",
sendStatus: DeliveryStatus.FAILED,
failureReason: "Template mismatch"
}
]
});
await prisma.usageMetric.createMany({
data: [
{
id: "usage-001",
tenantId: "tenant-acme",
metricDate: new Date("2026-04-20T00:00:00Z"),
inboundMessages: 128,
outboundMessages: 96,
activeContacts: 3,
activeAgents: 2,
broadcastSent: 1203,
storageUsedMb: 512
}
]
});
await prisma.billingInvoice.createMany({
data: [
{
id: "invoice-001",
tenantId: "tenant-acme",
planId: "plan-pro",
invoiceNumber: "INV-2026-001",
periodStart: new Date("2026-04-01T00:00:00Z"),
periodEnd: new Date("2026-04-30T23:59:59Z"),
subtotal: 2500000,
taxAmount: 275000,
totalAmount: 2775000,
paymentStatus: PaymentStatus.PAID,
dueDate: new Date("2026-04-30T00:00:00Z"),
paidAt: new Date("2026-04-15T00:00:00Z")
},
{
id: "invoice-002",
tenantId: "tenant-sinar",
planId: "plan-basic",
invoiceNumber: "INV-2026-002",
periodStart: new Date("2026-04-01T00:00:00Z"),
periodEnd: new Date("2026-04-30T23:59:59Z"),
subtotal: 1500000,
taxAmount: 165000,
totalAmount: 1665000,
paymentStatus: PaymentStatus.UNPAID,
dueDate: new Date("2026-05-01T00:00:00Z")
}
]
});
await prisma.auditLog.createMany({
data: [
{
id: "audit-001",
tenantId: "tenant-acme",
actorUserId: "user-agent-001",
entityType: "conversation",
entityId: "conv-001",
action: "message_sent",
metadataJson: JSON.stringify({ messageId: "msg-002" })
},
{
id: "audit-002",
tenantId: "tenant-acme",
actorUserId: "user-admin-001",
entityType: "campaign",
entityId: "campaign-001",
action: "campaign_created",
metadataJson: JSON.stringify({ recipients: 1203 })
}
]
});
await prisma.webhookEvent.createMany({
data: [
{
id: "webhook-001",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
eventType: "message.inbound",
providerEventId: "evt_001",
payloadJson: JSON.stringify({ source: "meta", conversationId: "conv-001" }),
processStatus: "processed",
processedAt: new Date("2026-04-20T09:00:02Z")
},
{
id: "webhook-002",
tenantId: "tenant-acme",
channelId: "channel-main-acme",
eventType: "message.status",
providerEventId: "evt_002",
payloadJson: JSON.stringify({ source: "meta", campaignId: "campaign-001" }),
processStatus: "failed",
failedReason: "Temporary provider mismatch"
}
]
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (error) => {
console.error(error);
await prisma.$disconnect();
process.exit(1);
});