This commit is contained in:
Timo Knuth
2026-07-06 00:38:26 +02:00
parent 91bd940edc
commit 192c186027

View File

@@ -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();