Files
QR-master/sql/revops_onboarding_v1.sql
2026-04-22 22:29:36 +02:00

264 lines
9.2 KiB
PL/PgSQL

BEGIN;
-- 1 user columns
ALTER TABLE "User"
ADD COLUMN IF NOT EXISTS "signupSource" TEXT,
ADD COLUMN IF NOT EXISTS "signupSourceSelfReported" TEXT,
ADD COLUMN IF NOT EXISTS "signupMedium" TEXT,
ADD COLUMN IF NOT EXISTS "signupCampaign" TEXT,
ADD COLUMN IF NOT EXISTS "signupContent" TEXT,
ADD COLUMN IF NOT EXISTS "signupTerm" TEXT,
ADD COLUMN IF NOT EXISTS "signupReferrer" TEXT,
ADD COLUMN IF NOT EXISTS "signupLandingPath" TEXT,
ADD COLUMN IF NOT EXISTS "signupFirstSeenAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "emailDomain" TEXT,
ADD COLUMN IF NOT EXISTS "primaryUseCase" TEXT,
ADD COLUMN IF NOT EXISTS "primaryGoal" TEXT,
ADD COLUMN IF NOT EXISTS "jobRole" TEXT,
ADD COLUMN IF NOT EXISTS "companyName" TEXT,
ADD COLUMN IF NOT EXISTS "companyWebsite" TEXT,
ADD COLUMN IF NOT EXISTS "teamSizeBucket" TEXT,
ADD COLUMN IF NOT EXISTS "onboardingStartedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "sourceConfirmedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "useCaseSelectedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "goalSelectedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "profileCompletedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstQrCreatedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstDynamicQrAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstStaticQrAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "firstScanAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "activationAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "onboardingCompletedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "fitScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "intentScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "leadScore" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "lifecycleStage" TEXT NOT NULL DEFAULT 'cold',
ADD COLUMN IF NOT EXISTS "lastQualifiedAt" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "lastScoredAt" TIMESTAMP(3);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'User_lifecycleStage_check'
) THEN
ALTER TABLE "User"
ADD CONSTRAINT "User_lifecycleStage_check"
CHECK ("lifecycleStage" IN (
'cold',
'activated',
'warm',
'hot',
'upgrade_candidate',
'paid'
));
END IF;
END $$;
-- 2 lifecycle log table
CREATE TABLE IF NOT EXISTS "UserLifecycleLog" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL,
"fromStage" TEXT,
"toStage" TEXT NOT NULL,
"fitScore" INTEGER NOT NULL DEFAULT 0,
"intentScore" INTEGER NOT NULL DEFAULT 0,
"leadScore" INTEGER NOT NULL DEFAULT 0,
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UserLifecycleLog_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id")
ON DELETE CASCADE ON UPDATE CASCADE
);
-- 3 indexes
CREATE INDEX IF NOT EXISTS "User_signupSource_idx" ON "User" ("signupSource");
CREATE INDEX IF NOT EXISTS "User_signupSourceSelfReported_idx" ON "User" ("signupSourceSelfReported");
CREATE INDEX IF NOT EXISTS "User_signupCampaign_idx" ON "User" ("signupCampaign");
CREATE INDEX IF NOT EXISTS "User_signupLandingPath_idx" ON "User" ("signupLandingPath");
CREATE INDEX IF NOT EXISTS "User_emailDomain_idx" ON "User" ("emailDomain");
CREATE INDEX IF NOT EXISTS "User_primaryUseCase_idx" ON "User" ("primaryUseCase");
CREATE INDEX IF NOT EXISTS "User_primaryGoal_idx" ON "User" ("primaryGoal");
CREATE INDEX IF NOT EXISTS "User_jobRole_idx" ON "User" ("jobRole");
CREATE INDEX IF NOT EXISTS "User_teamSizeBucket_idx" ON "User" ("teamSizeBucket");
CREATE INDEX IF NOT EXISTS "User_lifecycleStage_idx" ON "User" ("lifecycleStage");
CREATE INDEX IF NOT EXISTS "User_leadScore_idx" ON "User" ("leadScore" DESC);
CREATE INDEX IF NOT EXISTS "User_activationAt_idx" ON "User" ("activationAt");
CREATE INDEX IF NOT EXISTS "User_firstScanAt_idx" ON "User" ("firstScanAt");
CREATE INDEX IF NOT EXISTS "User_firstQrCreatedAt_idx" ON "User" ("firstQrCreatedAt");
CREATE INDEX IF NOT EXISTS "User_lastQualifiedAt_idx" ON "User" ("lastQualifiedAt");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_userId_idx" ON "UserLifecycleLog" ("userId");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_toStage_idx" ON "UserLifecycleLog" ("toStage");
CREATE INDEX IF NOT EXISTS "UserLifecycleLog_createdAt_idx" ON "UserLifecycleLog" ("createdAt" DESC);
-- 4 backfill
UPDATE "User"
SET "emailDomain" = lower(split_part("email", '@', 2))
WHERE "email" IS NOT NULL
AND ("emailDomain" IS NULL OR "emailDomain" = '');
UPDATE "User" u
SET
"firstQrCreatedAt" = q."firstQrAt",
"firstDynamicQrAt" = q."firstDynamicQrAt",
"firstStaticQrAt" = q."firstStaticQrAt",
"onboardingCompletedAt" = COALESCE(u."onboardingCompletedAt", q."firstQrAt")
FROM (
SELECT
"userId",
MIN("createdAt") AS "firstQrAt",
MIN("createdAt") FILTER (WHERE "type" = 'DYNAMIC') AS "firstDynamicQrAt",
MIN("createdAt") FILTER (WHERE "type" = 'STATIC') AS "firstStaticQrAt"
FROM "QRCode"
GROUP BY "userId"
) q
WHERE u."id" = q."userId";
UPDATE "User" u
SET
"firstScanAt" = s."firstScanAt",
"activationAt" = COALESCE(u."activationAt", s."firstScanAt")
FROM (
SELECT
q."userId",
MIN(s."ts") AS "firstScanAt"
FROM "QRCode" q
INNER JOIN "QRScan" s ON s."qrId" = q."id"
GROUP BY q."userId"
) s
WHERE u."id" = s."userId";
-- 5 scoring
WITH qr_stats AS (
SELECT
q."userId",
COUNT(*) AS qr_count,
COUNT(*) FILTER (WHERE q."type" = 'DYNAMIC') AS dynamic_count,
COUNT(DISTINCT q."contentType") AS content_type_count,
COUNT(*) FILTER (WHERE q."contentType" IN ('BARCODE','PDF','VCARD','COUPON','FEEDBACK')) AS businessish_type_count
FROM "QRCode" q
GROUP BY q."userId"
),
scan_stats AS (
SELECT
q."userId",
COUNT(s."id") AS scan_count
FROM "QRCode" q
LEFT JOIN "QRScan" s ON s."qrId" = q."id"
GROUP BY q."userId"
),
scored AS (
SELECT
u."id",
COALESCE(qs.qr_count, 0) AS qr_count,
COALESCE(qs.dynamic_count, 0) AS dynamic_count,
COALESCE(qs.content_type_count, 0) AS content_type_count,
COALESCE(qs.businessish_type_count, 0) AS businessish_type_count,
COALESCE(ss.scan_count, 0) AS scan_count
FROM "User" u
LEFT JOIN qr_stats qs ON qs."userId" = u."id"
LEFT JOIN scan_stats ss ON ss."userId" = u."id"
)
UPDATE "User" u
SET
"fitScore" =
(CASE
WHEN lower(split_part(u."email", '@', 2)) IN ('gmail.com','yahoo.com','hotmail.com','outlook.com','icloud.com') THEN -15
WHEN u."email" IS NOT NULL THEN 20
ELSE 0
END)
+
(CASE
WHEN u."primaryUseCase" IN ('marketing_campaign','bulk_qr','menu_pdf','barcode') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."primaryGoal" IN ('track_printed_campaigns','generate_leads','manage_multiple_qr_codes') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."jobRole" IN ('founder_owner','marketing_manager','agency_freelancer','operations') THEN 10
ELSE 0
END)
+
(CASE
WHEN u."companyName" IS NOT NULL AND u."companyName" <> '' THEN 5
ELSE 0
END)
+
(CASE
WHEN u."teamSizeBucket" IN ('6_20','21_100','100_plus') THEN 10
ELSE 0
END),
"intentScore" =
(CASE WHEN u."firstQrCreatedAt" IS NOT NULL THEN 20 ELSE -10 END)
+
(CASE WHEN u."firstDynamicQrAt" IS NOT NULL THEN 20 ELSE 0 END)
+
(CASE WHEN COALESCE(s.qr_count, 0) >= 3 THEN 15 ELSE 0 END)
+
(CASE WHEN COALESCE(s.scan_count, 0) > 0 THEN 10 ELSE 0 END)
+
(CASE WHEN COALESCE(s.businessish_type_count, 0) > 0 THEN 10 ELSE 0 END)
+
(CASE WHEN COALESCE(s.content_type_count, 0) >= 2 THEN 10 ELSE 0 END),
"lastScoredAt" = CURRENT_TIMESTAMP
FROM scored s
WHERE u."id" = s."id";
UPDATE "User"
SET "leadScore" = COALESCE("fitScore", 0) + COALESCE("intentScore", 0);
UPDATE "User"
SET
"lifecycleStage" = CASE
WHEN "plan" IN ('PRO','BUSINESS') THEN 'paid'
WHEN "leadScore" >= 70 THEN 'upgrade_candidate'
WHEN "leadScore" >= 55 THEN 'hot'
WHEN "leadScore" >= 30 THEN 'warm'
WHEN "activationAt" IS NOT NULL THEN 'activated'
ELSE 'cold'
END,
"lastQualifiedAt" = CASE
WHEN "leadScore" >= 55 OR "plan" IN ('PRO','BUSINESS') THEN CURRENT_TIMESTAMP
ELSE "lastQualifiedAt"
END;
-- 6 reporting queries
-- Acquisition overview
SELECT
COALESCE("signupSource", 'unknown') AS source,
COUNT(*) AS signups,
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr,
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'hot') AS hot,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'upgrade_candidate') AS upgrade_candidates,
COUNT(*) FILTER (WHERE "lifecycleStage" = 'paid') AS paid
FROM "User"
GROUP BY 1
ORDER BY signups DESC;
-- Onboarding funnel
SELECT
COUNT(*) AS signup,
COUNT(*) FILTER (WHERE "sourceConfirmedAt" IS NOT NULL) AS source_confirmed,
COUNT(*) FILTER (WHERE "useCaseSelectedAt" IS NOT NULL) AS use_case_selected,
COUNT(*) FILTER (WHERE "goalSelectedAt" IS NOT NULL) AS goal_selected,
COUNT(*) FILTER (WHERE "profileCompletedAt" IS NOT NULL) AS profile_completed,
COUNT(*) FILTER (WHERE "firstQrCreatedAt" IS NOT NULL) AS first_qr_created,
COUNT(*) FILTER (WHERE "firstDynamicQrAt" IS NOT NULL) AS first_dynamic_qr_created,
COUNT(*) FILTER (WHERE "activationAt" IS NOT NULL) AS activated
FROM "User";
-- Lifecycle summary
SELECT
"lifecycleStage",
COUNT(*) AS users
FROM "User"
GROUP BY 1
ORDER BY users DESC;
COMMIT;