INSIGHT-MVP/packages/crm-service/prisma/migrations/20260312_phase1_schema_expansion/migration.sql
Thomas Reitz 48df3c3144 feat(crm): Phase 1 backend schema expansion + frontend integration
Backend (CRM-Expert Phase 1):
- New enums: ContactSource, EntityStatus, CompanySize, OwnerRole,
  LostReason, EmailType, PhoneType
- Contact: add linkedinUrl, birthday, source, department, status
- Company: add vatId, taxId, tradeRegisterNumber, registerCourt,
  companySize, deliveryAddress, dataEnrichedAt/Source, status
- Deal: add lostReason + lostReasonText (required when status=LOST)
- Multi-value emails/phones tables (contact_emails, contact_phones)
- Owner m:n model (contact_owners, company_owners, deal_owners)
- Redis Pub/Sub CRM events (crm.contact.created, crm.deal.won, etc.)
- Activity due_soon scheduler (cron every 15 min)
- SQL migration with data migration for existing records

Frontend integration:
- types.ts: all new enums, interfaces, label maps
- api.ts: owner CRUD endpoints (add/remove for contacts/companies/deals)
- hooks.ts: 6 new owner mutation hooks
- ContactFormModal: LinkedIn, birthday, source, department, status fields
- ContactDetailPage: display new fields (LinkedIn, department, birthday,
  source, status badge)
- CompanyDetailPage: display vatId, taxId, trade register, company size,
  delivery address, data enrichment info
- DealFormModal: lost reason dropdown + text (shown when status=LOST)
- DealDetailPage: display lost reason with label
- CompaniesPage: EntityStatus-aware status dots (ACTIVE/INACTIVE/BLOCKED)
- ActivityType: add FOLLOWUP to all label maps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:56:41 +01:00

270 lines
10 KiB
SQL

-- ============================================================
-- Phase 1: Schema Expansion
-- Neue Enums, Felder, Multi-Value Tabellen, Owner-Tabellen
-- ============================================================
-- WICHTIG: ALTER TYPE ... ADD VALUE kann NICHT in einer Transaction laufen.
-- Diese Migration muss daher ausserhalb von BEGIN/COMMIT ausgefuehrt werden,
-- oder der ADD VALUE muss separat laufen.
-- --------------------------------------------------------
-- 1. Neue Enums erstellen
-- --------------------------------------------------------
CREATE TYPE "app_crm"."ContactSource" AS ENUM (
'TRADE_FAIR', 'REFERRAL', 'WEBSITE', 'COLD_CALL',
'IMPORT', 'BUSINESS_CARD', 'OTHER'
);
CREATE TYPE "app_crm"."EntityStatus" AS ENUM (
'ACTIVE', 'INACTIVE', 'BLOCKED'
);
CREATE TYPE "app_crm"."CompanySize" AS ENUM (
'SIZE_1_10', 'SIZE_11_50', 'SIZE_51_200', 'SIZE_201_500', 'SIZE_500_PLUS'
);
CREATE TYPE "app_crm"."OwnerRole" AS ENUM (
'OWNER', 'MEMBER', 'WATCHER'
);
CREATE TYPE "app_crm"."LostReason" AS ENUM (
'PRICE', 'TIMING', 'COMPETITOR', 'NO_NEED', 'OTHER'
);
CREATE TYPE "app_crm"."EmailType" AS ENUM (
'WORK', 'PERSONAL', 'OTHER'
);
CREATE TYPE "app_crm"."PhoneType" AS ENUM (
'OFFICE', 'MOBILE', 'FAX'
);
-- FOLLOWUP zum ActivityType Enum hinzufuegen (NICHT transaktionsfaehig!)
ALTER TYPE "app_crm"."ActivityType" ADD VALUE IF NOT EXISTS 'FOLLOWUP';
-- --------------------------------------------------------
-- 2. Neue Spalten auf contacts
-- --------------------------------------------------------
ALTER TABLE "app_crm"."contacts"
ADD COLUMN "linkedin_url" VARCHAR(500),
ADD COLUMN "birthday" TIMESTAMP(3),
ADD COLUMN "source" "app_crm"."ContactSource",
ADD COLUMN "department" VARCHAR(200),
ADD COLUMN "status" "app_crm"."EntityStatus" NOT NULL DEFAULT 'ACTIVE';
-- --------------------------------------------------------
-- 3. Neue Spalten auf companies
-- --------------------------------------------------------
ALTER TABLE "app_crm"."companies"
ADD COLUMN "vat_id" VARCHAR(50),
ADD COLUMN "tax_id" VARCHAR(50),
ADD COLUMN "trade_register_number" VARCHAR(100),
ADD COLUMN "register_court" VARCHAR(200),
ADD COLUMN "company_size" "app_crm"."CompanySize",
ADD COLUMN "delivery_street" VARCHAR(200),
ADD COLUMN "delivery_zip" VARCHAR(20),
ADD COLUMN "delivery_city" VARCHAR(100),
ADD COLUMN "delivery_country" VARCHAR(2),
ADD COLUMN "status" "app_crm"."EntityStatus" NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN "data_enriched_at" TIMESTAMP(3),
ADD COLUMN "data_enriched_source" VARCHAR(200);
-- --------------------------------------------------------
-- 4. Neue Spalten auf deals (Lost-Reason)
-- --------------------------------------------------------
ALTER TABLE "app_crm"."deals"
ADD COLUMN "lost_reason" "app_crm"."LostReason",
ADD COLUMN "lost_reason_text" TEXT;
-- --------------------------------------------------------
-- 5. Daten-Migration: isActive → status
-- --------------------------------------------------------
UPDATE "app_crm"."contacts"
SET "status" = 'INACTIVE'
WHERE "is_active" = false;
UPDATE "app_crm"."companies"
SET "status" = 'INACTIVE'
WHERE "is_active" = false;
-- --------------------------------------------------------
-- 6. Multi-Value: contact_emails Tabelle
-- --------------------------------------------------------
CREATE TABLE "app_crm"."contact_emails" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"contact_id" UUID,
"company_id" UUID,
"email" VARCHAR(255) NOT NULL,
"type" "app_crm"."EmailType" NOT NULL DEFAULT 'WORK',
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "contact_emails_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "contact_emails_contact_id_idx" ON "app_crm"."contact_emails"("contact_id");
CREATE INDEX "contact_emails_company_id_idx" ON "app_crm"."contact_emails"("company_id");
ALTER TABLE "app_crm"."contact_emails"
ADD CONSTRAINT "contact_emails_contact_id_fkey"
FOREIGN KEY ("contact_id") REFERENCES "app_crm"."contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "app_crm"."contact_emails"
ADD CONSTRAINT "contact_emails_company_id_fkey"
FOREIGN KEY ("company_id") REFERENCES "app_crm"."companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- --------------------------------------------------------
-- 7. Multi-Value: contact_phones Tabelle
-- --------------------------------------------------------
CREATE TABLE "app_crm"."contact_phones" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"contact_id" UUID,
"company_id" UUID,
"phone" VARCHAR(50) NOT NULL,
"type" "app_crm"."PhoneType" NOT NULL DEFAULT 'OFFICE',
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "contact_phones_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "contact_phones_contact_id_idx" ON "app_crm"."contact_phones"("contact_id");
CREATE INDEX "contact_phones_company_id_idx" ON "app_crm"."contact_phones"("company_id");
ALTER TABLE "app_crm"."contact_phones"
ADD CONSTRAINT "contact_phones_contact_id_fkey"
FOREIGN KEY ("contact_id") REFERENCES "app_crm"."contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "app_crm"."contact_phones"
ADD CONSTRAINT "contact_phones_company_id_fkey"
FOREIGN KEY ("company_id") REFERENCES "app_crm"."companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- --------------------------------------------------------
-- 8. Daten-Migration: Bestehende Emails/Phones migrieren
-- --------------------------------------------------------
-- Contact Emails
INSERT INTO "app_crm"."contact_emails" ("id", "contact_id", "email", "type", "is_primary")
SELECT gen_random_uuid(), "id", "email", 'WORK'::"app_crm"."EmailType", true
FROM "app_crm"."contacts"
WHERE "email" IS NOT NULL AND "email" != '';
-- Contact Phones (phone → OFFICE, mobile → MOBILE)
INSERT INTO "app_crm"."contact_phones" ("id", "contact_id", "phone", "type", "is_primary")
SELECT gen_random_uuid(), "id", "phone", 'OFFICE'::"app_crm"."PhoneType", true
FROM "app_crm"."contacts"
WHERE "phone" IS NOT NULL AND "phone" != '';
INSERT INTO "app_crm"."contact_phones" ("id", "contact_id", "phone", "type", "is_primary")
SELECT gen_random_uuid(), "id", "mobile", 'MOBILE'::"app_crm"."PhoneType", false
FROM "app_crm"."contacts"
WHERE "mobile" IS NOT NULL AND "mobile" != '';
-- Company Emails
INSERT INTO "app_crm"."contact_emails" ("id", "company_id", "email", "type", "is_primary")
SELECT gen_random_uuid(), "id", "email", 'WORK'::"app_crm"."EmailType", true
FROM "app_crm"."companies"
WHERE "email" IS NOT NULL AND "email" != '';
-- Company Phones
INSERT INTO "app_crm"."contact_phones" ("id", "company_id", "phone", "type", "is_primary")
SELECT gen_random_uuid(), "id", "phone", 'OFFICE'::"app_crm"."PhoneType", true
FROM "app_crm"."companies"
WHERE "phone" IS NOT NULL AND "phone" != '';
-- Hinweis: Legacy-Spalten email, phone, mobile bleiben bestehen (deprecated)
-- --------------------------------------------------------
-- 9. Owner-Tabellen: contact_owners
-- --------------------------------------------------------
CREATE TABLE "app_crm"."contact_owners" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"contact_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"role" "app_crm"."OwnerRole" NOT NULL DEFAULT 'OWNER',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "contact_owners_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "contact_owners_contact_id_user_id_key"
ON "app_crm"."contact_owners"("contact_id", "user_id");
CREATE INDEX "contact_owners_tenant_id_idx"
ON "app_crm"."contact_owners"("tenant_id");
CREATE INDEX "contact_owners_tenant_id_contact_id_idx"
ON "app_crm"."contact_owners"("tenant_id", "contact_id");
ALTER TABLE "app_crm"."contact_owners"
ADD CONSTRAINT "contact_owners_contact_id_fkey"
FOREIGN KEY ("contact_id") REFERENCES "app_crm"."contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- --------------------------------------------------------
-- 10. Owner-Tabellen: company_owners
-- --------------------------------------------------------
CREATE TABLE "app_crm"."company_owners" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"company_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"role" "app_crm"."OwnerRole" NOT NULL DEFAULT 'OWNER',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "company_owners_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "company_owners_company_id_user_id_key"
ON "app_crm"."company_owners"("company_id", "user_id");
CREATE INDEX "company_owners_tenant_id_idx"
ON "app_crm"."company_owners"("tenant_id");
CREATE INDEX "company_owners_tenant_id_company_id_idx"
ON "app_crm"."company_owners"("tenant_id", "company_id");
ALTER TABLE "app_crm"."company_owners"
ADD CONSTRAINT "company_owners_company_id_fkey"
FOREIGN KEY ("company_id") REFERENCES "app_crm"."companies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Bestehende Company.ownerId → company_owners migrieren
INSERT INTO "app_crm"."company_owners" ("id", "tenant_id", "company_id", "user_id", "role")
SELECT gen_random_uuid(), "tenant_id", "id", "owner_id", 'OWNER'::"app_crm"."OwnerRole"
FROM "app_crm"."companies"
WHERE "owner_id" IS NOT NULL;
-- --------------------------------------------------------
-- 11. Owner-Tabellen: deal_owners
-- --------------------------------------------------------
CREATE TABLE "app_crm"."deal_owners" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"tenant_id" UUID NOT NULL,
"deal_id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"role" "app_crm"."OwnerRole" NOT NULL DEFAULT 'OWNER',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "deal_owners_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "deal_owners_deal_id_user_id_key"
ON "app_crm"."deal_owners"("deal_id", "user_id");
CREATE INDEX "deal_owners_tenant_id_idx"
ON "app_crm"."deal_owners"("tenant_id");
CREATE INDEX "deal_owners_tenant_id_deal_id_idx"
ON "app_crm"."deal_owners"("tenant_id", "deal_id");
ALTER TABLE "app_crm"."deal_owners"
ADD CONSTRAINT "deal_owners_deal_id_fkey"
FOREIGN KEY ("deal_id") REFERENCES "app_crm"."deals"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- --------------------------------------------------------
-- 12. Zusaetzliche Indexes fuer status
-- --------------------------------------------------------
CREATE INDEX "contacts_tenant_id_status_idx"
ON "app_crm"."contacts"("tenant_id", "status");
CREATE INDEX "companies_tenant_id_status_idx"
ON "app_crm"."companies"("tenant_id", "status");