chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
This commit is contained in:
380
prisma/migrations/20260420143000_init/migration.sql
Normal file
380
prisma/migrations/20260420143000_init/migration.sql
Normal 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");
|
||||
@ -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;
|
||||
@ -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");
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "BackgroundJobState" ADD COLUMN "consecutiveFailures" INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE "BackgroundJobState" ADD COLUMN "lastFailureAt" DATETIME;
|
||||
@ -0,0 +1,3 @@
|
||||
ALTER TABLE "WebhookEvent" ADD COLUMN "eventHash" TEXT;
|
||||
|
||||
CREATE INDEX "WebhookEvent_tenantId_channelId_eventHash_idx" ON "WebhookEvent"("tenantId", "channelId", "eventHash");
|
||||
@ -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
570
prisma/schema.prisma
Normal 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
589
prisma/seed.cjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user