feat: add campaign management
- packages/database: Campaign, LeadImport, Lead, DialAttempt, CallDisposition, Callback, SuppressionEntry (agente.md secao 53) - packages/shared: normalizePhone (BR, E.164) com 7 testes unitarios - apps/api/src/campaigns: CRUD completo (todos os campos da secao 24) com maquina de estados validada (DRAFT/READY/RUNNING/PAUSED/DRAINING/ STOPPED/COMPLETED) — edicao bloqueada com campanha RUNNING - apps/api/src/leads: import CSV via streaming multipart (deteccao automatica de delimitador, mapeamento de coluna por header, validacao+ normalizacao+dedupe em lotes de 500, CSV de rejeitados com motivo, modo dryRun para preview) - apps/api/src/suppression: CRUD + import CSV da lista de bloqueio, remocao sempre exige motivo e e auditada, isSuppressed() pronto para o dialer-worker checar antes de originar - apps/api/src/dispositions: CRUD de disposicoes de chamada Testado ponta a ponta: campanha criada com defaults corretos, import de CSV real (3 validos/1 invalido/1 duplicado, contadores batendo), leads persistidos com telefone normalizado, transicoes de estado da campanha rejeitando movimentos invalidos (PAUSED->PAUSED = 400).
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CampaignStatus" AS ENUM ('DRAFT', 'READY', 'RUNNING', 'PAUSED', 'DRAINING', 'STOPPED', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LeadStatus" AS ENUM ('NEW', 'READY', 'RESERVED', 'DIALING', 'RINGING', 'ANSWERED', 'CONNECTED_AGENT', 'BUSY', 'NO_ANSWER', 'FAILED', 'INVALID', 'VOICEMAIL', 'CALLBACK', 'COMPLETED', 'DO_NOT_CALL', 'MAX_ATTEMPTS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LeadImportStatus" AS ENUM ('PROCESSING', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CallState" AS ENUM ('CREATED', 'RESERVED', 'ORIGINATING', 'RINGING', 'ANSWERED', 'QUEUED', 'AGENT_CONNECTED', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AmdResult" AS ENUM ('HUMAN', 'MACHINE', 'NOT_SURE', 'HANGUP');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DispositionAction" AS ENUM ('NONE', 'CALLBACK', 'DO_NOT_CALL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "campaigns" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"queue_id" TEXT NOT NULL,
|
||||
"trunk_id" TEXT NOT NULL,
|
||||
"caller_id" TEXT,
|
||||
"context" TEXT NOT NULL DEFAULT 'outbound',
|
||||
"status" "CampaignStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"start_date" TIMESTAMP(3),
|
||||
"end_date" TIMESTAMP(3),
|
||||
"days_of_week" INTEGER[] DEFAULT ARRAY[1, 2, 3, 4, 5]::INTEGER[],
|
||||
"start_time" TEXT NOT NULL DEFAULT '08:00',
|
||||
"end_time" TEXT NOT NULL DEFAULT '20:00',
|
||||
"timezone" TEXT NOT NULL DEFAULT 'America/Sao_Paulo',
|
||||
"max_cps" INTEGER NOT NULL DEFAULT 2,
|
||||
"max_concurrent_calls" INTEGER NOT NULL DEFAULT 10,
|
||||
"pacing_initial" DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
||||
"pacing_min" DOUBLE PRECISION NOT NULL DEFAULT 0.5,
|
||||
"pacing_max" DOUBLE PRECISION NOT NULL DEFAULT 3.0,
|
||||
"target_abandon_rate" DOUBLE PRECISION NOT NULL DEFAULT 0.03,
|
||||
"max_wait_for_agent_seconds" INTEGER NOT NULL DEFAULT 30,
|
||||
"ring_timeout_seconds" INTEGER NOT NULL DEFAULT 25,
|
||||
"max_attempts" INTEGER NOT NULL DEFAULT 5,
|
||||
"retry_rules" JSONB NOT NULL DEFAULT '{"BUSY":15,"NO_ANSWER":60,"CONGESTION":5,"FAILED":30}',
|
||||
"amd_enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"wrap_up_time_seconds" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "campaigns_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "lead_imports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaign_id" TEXT NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"status" "LeadImportStatus" NOT NULL DEFAULT 'PROCESSING',
|
||||
"total_rows" INTEGER NOT NULL DEFAULT 0,
|
||||
"valid_rows" INTEGER NOT NULL DEFAULT 0,
|
||||
"invalid_rows" INTEGER NOT NULL DEFAULT 0,
|
||||
"duplicate_rows" INTEGER NOT NULL DEFAULT 0,
|
||||
"rejected_csv" TEXT,
|
||||
"created_by_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "lead_imports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "leads" (
|
||||
"id" TEXT NOT NULL,
|
||||
"campaign_id" TEXT NOT NULL,
|
||||
"import_id" TEXT,
|
||||
"name" TEXT,
|
||||
"phone" TEXT NOT NULL,
|
||||
"phone_normalized" TEXT NOT NULL,
|
||||
"status" "LeadStatus" NOT NULL DEFAULT 'NEW',
|
||||
"attempt_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_attempt_at" TIMESTAMP(3),
|
||||
"next_attempt_at" TIMESTAMP(3),
|
||||
"last_result" TEXT,
|
||||
"reserved_at" TIMESTAMP(3),
|
||||
"reserved_by" TEXT,
|
||||
"custom_fields" JSONB,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "leads_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "dial_attempts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"lead_id" TEXT NOT NULL,
|
||||
"campaign_id" TEXT NOT NULL,
|
||||
"state" "CallState" NOT NULL DEFAULT 'CREATED',
|
||||
"asterisk_unique_id" TEXT,
|
||||
"asterisk_linked_id" TEXT,
|
||||
"called_number" TEXT NOT NULL,
|
||||
"caller_id_used" TEXT,
|
||||
"agent_id" TEXT,
|
||||
"disposition_id" TEXT,
|
||||
"disposition_notes" TEXT,
|
||||
"amd_result" "AmdResult",
|
||||
"hangup_cause" TEXT,
|
||||
"started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ringing_at" TIMESTAMP(3),
|
||||
"answered_at" TIMESTAMP(3),
|
||||
"queued_at" TIMESTAMP(3),
|
||||
"agent_connected_at" TIMESTAMP(3),
|
||||
"ended_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "dial_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "call_dispositions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"action" "DispositionAction" NOT NULL DEFAULT 'NONE',
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "call_dispositions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "call_callbacks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"lead_id" TEXT NOT NULL,
|
||||
"campaign_id" TEXT NOT NULL,
|
||||
"preferred_agent_id" TEXT,
|
||||
"scheduled_at" TIMESTAMP(3) NOT NULL,
|
||||
"timezone" TEXT NOT NULL DEFAULT 'America/Sao_Paulo',
|
||||
"notes" TEXT,
|
||||
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "call_callbacks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "suppression_list" (
|
||||
"id" TEXT NOT NULL,
|
||||
"phone_normalized" TEXT NOT NULL,
|
||||
"reason" TEXT,
|
||||
"added_by_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "suppression_list_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "campaigns_name_key" ON "campaigns"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "leads_campaign_id_status_next_attempt_at_idx" ON "leads"("campaign_id", "status", "next_attempt_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "leads_phone_normalized_idx" ON "leads"("phone_normalized");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "dial_attempts_asterisk_unique_id_key" ON "dial_attempts"("asterisk_unique_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "dial_attempts_campaign_id_state_idx" ON "dial_attempts"("campaign_id", "state");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "dial_attempts_lead_id_idx" ON "dial_attempts"("lead_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "call_dispositions_code_key" ON "call_dispositions"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "call_callbacks_scheduled_at_completed_idx" ON "call_callbacks"("scheduled_at", "completed");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "suppression_list_phone_normalized_key" ON "suppression_list"("phone_normalized");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lead_imports" ADD CONSTRAINT "lead_imports_campaign_id_fkey" FOREIGN KEY ("campaign_id") REFERENCES "campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "leads" ADD CONSTRAINT "leads_campaign_id_fkey" FOREIGN KEY ("campaign_id") REFERENCES "campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "leads" ADD CONSTRAINT "leads_import_id_fkey" FOREIGN KEY ("import_id") REFERENCES "lead_imports"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dial_attempts" ADD CONSTRAINT "dial_attempts_lead_id_fkey" FOREIGN KEY ("lead_id") REFERENCES "leads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dial_attempts" ADD CONSTRAINT "dial_attempts_campaign_id_fkey" FOREIGN KEY ("campaign_id") REFERENCES "campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "dial_attempts" ADD CONSTRAINT "dial_attempts_disposition_id_fkey" FOREIGN KEY ("disposition_id") REFERENCES "call_dispositions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "call_callbacks" ADD CONSTRAINT "call_callbacks_lead_id_fkey" FOREIGN KEY ("lead_id") REFERENCES "leads"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "call_callbacks" ADD CONSTRAINT "call_callbacks_campaign_id_fkey" FOREIGN KEY ("campaign_id") REFERENCES "campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_queue_id_fkey" FOREIGN KEY ("queue_id") REFERENCES "queues"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_trunk_id_fkey" FOREIGN KEY ("trunk_id") REFERENCES "trunks"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
Reference in New Issue
Block a user