From 192c186027b21d285c4f642cdd5be35c078ab58a Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 6 Jul 2026 00:38:26 +0200 Subject: [PATCH] sql --- manual-billing-sql.txt | 223 ++++++++++++++--------------------------- 1 file changed, 75 insertions(+), 148 deletions(-) diff --git a/manual-billing-sql.txt b/manual-billing-sql.txt index fdb7673..35e6c0c 100644 --- a/manual-billing-sql.txt +++ b/manual-billing-sql.txt @@ -1,163 +1,90 @@ -Manual billing SQL for QR Master -================================ +QR Master automatic billing alarm SQL +===================================== -Use these statements for manual plan changes in the database. -Replace USER_EMAIL_HERE before running. +Run this SQL once in the database. -Do not use a naked UPDATE like: -UPDATE "User" SET "plan" = 'PRO' WHERE email = '...'; +After it is installed, every database update of "User"."plan" automatically +creates a "UserLifecycleLog" entry. -That changes the plan but creates no billing log. Always use the matching full -transaction below so UserLifecycleLog gets the alert entry. +That means: +- FREE -> PRO or BUSINESS creates reason = subscription_created +- PRO/BUSINESS -> FREE creates reason = subscription_deleted +- PRO -> BUSINESS or BUSINESS -> PRO creates reason = subscription_updated -Upgrade user to PRO -=================== +No customer email needs to be typed into this file. +No manual log insert is needed after this is installed. -BEGIN; -WITH old_user AS ( - SELECT * - FROM "User" - WHERE email = 'USER_EMAIL_HERE' - FOR UPDATE -), -updated_user AS ( - UPDATE "User" - SET - "plan" = 'PRO', - "lifecycleStage" = 'paid', - "lastScoredAt" = NOW(), - "lastQualifiedAt" = NOW(), - "updatedAt" = NOW() - WHERE "id" = (SELECT "id" FROM old_user) - RETURNING * -) -INSERT INTO "UserLifecycleLog" ( - "id", - "userId", - "fromStage", - "toStage", - "fitScore", - "intentScore", - "leadScore", - "reason", - "createdAt" -) -SELECT - CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)), - updated_user."id", - old_user."lifecycleStage", - 'paid', - updated_user."fitScore", - updated_user."intentScore", - updated_user."leadScore", - 'subscription_created', - NOW() -FROM updated_user -JOIN old_user ON old_user."id" = updated_user."id"; +CREATE OR REPLACE FUNCTION qrmaster_log_plan_change() +RETURNS trigger AS $$ +DECLARE + next_stage TEXT; + log_reason TEXT; +BEGIN + IF NEW."plan" IS NOT DISTINCT FROM OLD."plan" THEN + RETURN NEW; + END IF; -COMMIT; + IF NEW."plan"::TEXT IN ('PRO', 'BUSINESS') THEN + next_stage := 'paid'; + ELSIF COALESCE(NEW."leadScore", 0) >= 70 THEN + next_stage := 'upgrade_candidate'; + ELSIF COALESCE(NEW."leadScore", 0) >= 55 THEN + next_stage := 'hot'; + ELSIF COALESCE(NEW."leadScore", 0) >= 30 THEN + next_stage := 'warm'; + ELSIF NEW."activationAt" IS NOT NULL THEN + next_stage := 'activated'; + ELSE + next_stage := 'cold'; + END IF; -Upgrade user to BUSINESS -======================== + IF OLD."plan"::TEXT = 'FREE' AND NEW."plan"::TEXT IN ('PRO', 'BUSINESS') THEN + log_reason := 'subscription_created'; + ELSIF OLD."plan"::TEXT IN ('PRO', 'BUSINESS') AND NEW."plan"::TEXT = 'FREE' THEN + log_reason := 'subscription_deleted'; + ELSE + log_reason := 'subscription_updated'; + END IF; -BEGIN; + NEW."lifecycleStage" := next_stage; + NEW."lastScoredAt" := NOW(); -WITH old_user AS ( - SELECT * - FROM "User" - WHERE email = 'USER_EMAIL_HERE' - FOR UPDATE -), -updated_user AS ( - UPDATE "User" - SET - "plan" = 'BUSINESS', - "lifecycleStage" = 'paid', - "lastScoredAt" = NOW(), - "lastQualifiedAt" = NOW(), - "updatedAt" = NOW() - WHERE "id" = (SELECT "id" FROM old_user) - RETURNING * -) -INSERT INTO "UserLifecycleLog" ( - "id", - "userId", - "fromStage", - "toStage", - "fitScore", - "intentScore", - "leadScore", - "reason", - "createdAt" -) -SELECT - CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)), - updated_user."id", - old_user."lifecycleStage", - 'paid', - updated_user."fitScore", - updated_user."intentScore", - updated_user."leadScore", - 'subscription_created', - NOW() -FROM updated_user -JOIN old_user ON old_user."id" = updated_user."id"; + IF next_stage IN ('paid', 'hot', 'upgrade_candidate') THEN + NEW."lastQualifiedAt" := NOW(); + END IF; -COMMIT; + INSERT INTO "UserLifecycleLog" ( + "id", + "userId", + "fromStage", + "toStage", + "fitScore", + "intentScore", + "leadScore", + "reason", + "createdAt" + ) + VALUES ( + CONCAT('planlog_', MD5(NEW."id" || clock_timestamp()::TEXT || random()::TEXT)), + NEW."id", + OLD."lifecycleStage", + next_stage, + COALESCE(NEW."fitScore", 0), + COALESCE(NEW."intentScore", 0), + COALESCE(NEW."leadScore", 0), + log_reason, + NOW() + ); -Set user to FREE after deabo / subscription ended -================================================= + RETURN NEW; +END; +$$ LANGUAGE plpgsql; -BEGIN; -WITH old_user AS ( - SELECT * - FROM "User" - WHERE email = 'USER_EMAIL_HERE' - FOR UPDATE -), -updated_user AS ( - UPDATE "User" - SET - "plan" = 'FREE', - "stripeSubscriptionId" = NULL, - "stripePriceId" = NULL, - "stripeCurrentPeriodEnd" = NULL, - "lifecycleStage" = CASE - WHEN COALESCE("leadScore", 0) >= 70 THEN 'upgrade_candidate' - WHEN COALESCE("leadScore", 0) >= 55 THEN 'hot' - WHEN COALESCE("leadScore", 0) >= 30 THEN 'warm' - WHEN "activationAt" IS NOT NULL THEN 'activated' - ELSE 'cold' - END, - "lastScoredAt" = NOW(), - "updatedAt" = NOW() - WHERE "id" = (SELECT "id" FROM old_user) - RETURNING * -) -INSERT INTO "UserLifecycleLog" ( - "id", - "userId", - "fromStage", - "toStage", - "fitScore", - "intentScore", - "leadScore", - "reason", - "createdAt" -) -SELECT - CONCAT('manual_', MD5(updated_user."id" || clock_timestamp()::TEXT || random()::TEXT)), - updated_user."id", - old_user."lifecycleStage", - updated_user."lifecycleStage", - updated_user."fitScore", - updated_user."intentScore", - updated_user."leadScore", - 'subscription_deleted', - NOW() -FROM updated_user -JOIN old_user ON old_user."id" = updated_user."id"; +DROP TRIGGER IF EXISTS qrmaster_user_plan_change_alarm ON "User"; -COMMIT; +CREATE TRIGGER qrmaster_user_plan_change_alarm +BEFORE UPDATE OF "plan" ON "User" +FOR EACH ROW +EXECUTE FUNCTION qrmaster_log_plan_change();