05aad75272
Multi-tenant AI help desk SaaS for the firearms industry. Full monorepo: API (Express/Prisma), Worker (BullMQ), Frontend (React/Vite/Tailwind). PostgreSQL 16 + pgvector, Redis 7, JWT auth, RLS tenant isolation. Dark Armory theme with tactical branding throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
368 lines
13 KiB
SQL
368 lines
13 KiB
SQL
-- CreateTable
|
|
CREATE TABLE "plans" (
|
|
"id" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"slug" TEXT NOT NULL,
|
|
"maxUsers" INTEGER NOT NULL DEFAULT 3,
|
|
"maxEmailAccounts" INTEGER NOT NULL DEFAULT 1,
|
|
"maxTicketsPerMonth" INTEGER NOT NULL DEFAULT 500,
|
|
"aiDraftsEnabled" BOOLEAN NOT NULL DEFAULT false,
|
|
"knowledgeBaseEnabled" BOOLEAN NOT NULL DEFAULT false,
|
|
"priceMonthly" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
"priceYearly" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "plans_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "tenants" (
|
|
"id" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"slug" TEXT NOT NULL,
|
|
"plan_id" TEXT NOT NULL,
|
|
"settings" JSONB NOT NULL DEFAULT '{}',
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "users" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"email" TEXT NOT NULL,
|
|
"password_hash" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"role" TEXT NOT NULL DEFAULT 'agent',
|
|
"avatar_url" TEXT,
|
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
|
"last_login_at" TIMESTAMP(3),
|
|
"refresh_token" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "email_accounts" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"name" TEXT NOT NULL,
|
|
"email_address" TEXT NOT NULL,
|
|
"imap_host" TEXT NOT NULL,
|
|
"imap_port" INTEGER NOT NULL DEFAULT 993,
|
|
"imap_user" TEXT NOT NULL,
|
|
"imap_password" TEXT NOT NULL,
|
|
"imap_tls" BOOLEAN NOT NULL DEFAULT true,
|
|
"smtp_host" TEXT NOT NULL,
|
|
"smtp_port" INTEGER NOT NULL DEFAULT 587,
|
|
"smtp_user" TEXT NOT NULL,
|
|
"smtp_password" TEXT NOT NULL,
|
|
"smtp_tls" BOOLEAN NOT NULL DEFAULT true,
|
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
|
"last_poll_at" TIMESTAMP(3),
|
|
"last_error" TEXT,
|
|
"poll_interval_seconds" INTEGER NOT NULL DEFAULT 60,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "email_accounts_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "tickets" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"ticket_number" TEXT NOT NULL,
|
|
"email_account_id" TEXT NOT NULL,
|
|
"subject" TEXT NOT NULL,
|
|
"status" TEXT NOT NULL DEFAULT 'incoming',
|
|
"priority" TEXT NOT NULL DEFAULT 'medium',
|
|
"assignee_id" TEXT,
|
|
"customer_email" TEXT NOT NULL,
|
|
"customer_name" TEXT,
|
|
"customer_profile_id" TEXT,
|
|
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
"message_count" INTEGER NOT NULL DEFAULT 1,
|
|
"last_message_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"resolved_at" TIMESTAMP(3),
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "tickets_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "messages" (
|
|
"id" TEXT NOT NULL,
|
|
"ticket_id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"direction" TEXT NOT NULL,
|
|
"from_email" TEXT NOT NULL,
|
|
"from_name" TEXT,
|
|
"to_email" TEXT NOT NULL,
|
|
"subject" TEXT NOT NULL,
|
|
"body_text" TEXT NOT NULL,
|
|
"body_html" TEXT,
|
|
"message_id" TEXT,
|
|
"in_reply_to" TEXT,
|
|
"references" TEXT,
|
|
"headers" JSONB,
|
|
"sent_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "messages_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "attachments" (
|
|
"id" TEXT NOT NULL,
|
|
"message_id" TEXT NOT NULL,
|
|
"filename" TEXT NOT NULL,
|
|
"content_type" TEXT NOT NULL,
|
|
"size" INTEGER NOT NULL,
|
|
"storage_key" TEXT NOT NULL,
|
|
|
|
CONSTRAINT "attachments_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "ai_drafts" (
|
|
"id" TEXT NOT NULL,
|
|
"ticket_id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"message_id" TEXT NOT NULL,
|
|
"draft_body" TEXT NOT NULL,
|
|
"confidence" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
"model" TEXT NOT NULL DEFAULT 'gpt-4o',
|
|
"tokens_used" INTEGER NOT NULL DEFAULT 0,
|
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
|
"edited_body" TEXT,
|
|
"reviewed_by" TEXT,
|
|
"reviewed_at" TIMESTAMP(3),
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "ai_drafts_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "knowledge_base" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"title" TEXT NOT NULL,
|
|
"content" TEXT NOT NULL,
|
|
"category" TEXT NOT NULL DEFAULT 'general',
|
|
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "knowledge_base_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "audit_log" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"user_id" TEXT,
|
|
"action" TEXT NOT NULL,
|
|
"entity" TEXT NOT NULL,
|
|
"entity_id" TEXT,
|
|
"details" JSONB,
|
|
"ip_address" TEXT,
|
|
"user_agent" TEXT,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "audit_log_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "customer_profiles" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"email" TEXT NOT NULL,
|
|
"name" TEXT,
|
|
"phone" TEXT,
|
|
"company" TEXT,
|
|
"notes" TEXT,
|
|
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
|
"ticket_count" INTEGER NOT NULL DEFAULT 0,
|
|
"first_contact_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"last_contact_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "customer_profiles_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "canned_responses" (
|
|
"id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"title" TEXT NOT NULL,
|
|
"body" TEXT NOT NULL,
|
|
"category" TEXT NOT NULL DEFAULT 'general',
|
|
"shortcut" TEXT,
|
|
"created_by" TEXT NOT NULL,
|
|
"is_shared" BOOLEAN NOT NULL DEFAULT true,
|
|
"usage_count" INTEGER NOT NULL DEFAULT 0,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "canned_responses_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "notification_preferences" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"tenant_id" TEXT NOT NULL,
|
|
"new_ticket" BOOLEAN NOT NULL DEFAULT true,
|
|
"ticket_assigned" BOOLEAN NOT NULL DEFAULT true,
|
|
"ticket_reply" BOOLEAN NOT NULL DEFAULT true,
|
|
"ai_draft_ready" BOOLEAN NOT NULL DEFAULT true,
|
|
"daily_digest" BOOLEAN NOT NULL DEFAULT false,
|
|
"email_notifications" BOOLEAN NOT NULL DEFAULT true,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "plans_slug_key" ON "plans"("slug");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "users_tenant_id_idx" ON "users"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "users_tenant_id_email_key" ON "users"("tenant_id", "email");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "email_accounts_tenant_id_idx" ON "email_accounts"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "email_accounts_tenant_id_email_address_key" ON "email_accounts"("tenant_id", "email_address");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "tickets_tenant_id_status_idx" ON "tickets"("tenant_id", "status");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "tickets_tenant_id_assignee_id_idx" ON "tickets"("tenant_id", "assignee_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "tickets_tenant_id_customer_email_idx" ON "tickets"("tenant_id", "customer_email");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "tickets_tenant_id_created_at_idx" ON "tickets"("tenant_id", "created_at");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "tickets_tenant_id_ticket_number_key" ON "tickets"("tenant_id", "ticket_number");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "messages_ticket_id_idx" ON "messages"("ticket_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "messages_tenant_id_idx" ON "messages"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "messages_message_id_idx" ON "messages"("message_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "ai_drafts_ticket_id_idx" ON "ai_drafts"("ticket_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "ai_drafts_tenant_id_idx" ON "ai_drafts"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "knowledge_base_tenant_id_idx" ON "knowledge_base"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "audit_log_tenant_id_created_at_idx" ON "audit_log"("tenant_id", "created_at");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "audit_log_tenant_id_entity_idx" ON "audit_log"("tenant_id", "entity");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "customer_profiles_tenant_id_idx" ON "customer_profiles"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "customer_profiles_tenant_id_email_key" ON "customer_profiles"("tenant_id", "email");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "canned_responses_tenant_id_idx" ON "canned_responses"("tenant_id");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "notification_preferences_user_id_key" ON "notification_preferences"("user_id");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "notification_preferences_tenant_id_idx" ON "notification_preferences"("tenant_id");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "users" ADD CONSTRAINT "users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "email_accounts" ADD CONSTRAINT "email_accounts_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_email_account_id_fkey" FOREIGN KEY ("email_account_id") REFERENCES "email_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_customer_profile_id_fkey" FOREIGN KEY ("customer_profile_id") REFERENCES "customer_profiles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "messages" ADD CONSTRAINT "messages_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "messages" ADD CONSTRAINT "messages_ticket_id_fkey" FOREIGN KEY ("ticket_id") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "messages"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ai_drafts" ADD CONSTRAINT "ai_drafts_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ai_drafts" ADD CONSTRAINT "ai_drafts_ticket_id_fkey" FOREIGN KEY ("ticket_id") REFERENCES "tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ai_drafts" ADD CONSTRAINT "ai_drafts_message_id_fkey" FOREIGN KEY ("message_id") REFERENCES "messages"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "knowledge_base" ADD CONSTRAINT "knowledge_base_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "customer_profiles" ADD CONSTRAINT "customer_profiles_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "canned_responses" ADD CONSTRAINT "canned_responses_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "canned_responses" ADD CONSTRAINT "canned_responses_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|