it0/packages/shared/database/migrations/011-user-referral-points.sql

78 lines
4.4 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Migration 011: User-level referral codes + personal circle + points system
-- Extends the existing tenant-level referral system (006) to support
-- individual user-to-user referrals and a unified points economy.
--
-- Design:
-- • Each user gets their own referral code (vs. per-tenant in 006)
-- • Referral relationships track user→user (personal circle, max 2 levels)
-- • Points replace raw USD credits as the reward currency (flexible redemption)
-- • Points ledger is append-only; balance is a denormalized cache
-- ── 1. User referral codes ──────────────────────────────────────────────────
-- One row per user. Created automatically on first registration.
CREATE TABLE IF NOT EXISTS public.user_referral_codes (
user_id UUID PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
click_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ── 2. User referral relationships (personal circle) ────────────────────────
-- Tracks who invited whom at the user level.
-- Each user can be referred at most once (UNIQUE referred_user_id).
-- level: 1 = direct invite, 2 = indirect (friend of friend).
-- status: PENDING → ACTIVE (first engagement) → REWARDED (first payment done)
-- EXPIRED (never activated after 90 days)
CREATE TABLE IF NOT EXISTS public.user_referral_relationships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
referrer_user_id UUID NOT NULL,
referred_user_id UUID NOT NULL UNIQUE,
referral_code VARCHAR(20) NOT NULL,
level SMALLINT NOT NULL DEFAULT 1 CHECK (level IN (1, 2)),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','ACTIVE','REWARDED','EXPIRED')),
activated_at TIMESTAMPTZ,
rewarded_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_urr_referrer ON public.user_referral_relationships(referrer_user_id);
CREATE INDEX IF NOT EXISTS idx_urr_status ON public.user_referral_relationships(status);
CREATE INDEX IF NOT EXISTS idx_urr_created ON public.user_referral_relationships(created_at);
-- ── 3. Points ledger (append-only) ──────────────────────────────────────────
-- Every point event is a row. delta > 0 = earned, delta < 0 = spent.
-- type values:
-- REFERRAL_FIRST_PAYMENT first subscription payment by referred user
-- REFERRAL_RECURRING recurring monthly payment reward (≤12 months)
-- REFERRAL_L2 level-2 indirect reward (≤6 months)
-- REFERRAL_WELCOME welcome bonus awarded to newly referred user
-- REDEMPTION_QUOTA redeemed for extra usage quota
-- REDEMPTION_UNLOCK redeemed to unlock a premium agent
-- ADMIN_GRANT platform admin manual grant/deduction
-- EXPIRY points expiry deduction
CREATE TABLE IF NOT EXISTS public.user_point_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
delta INT NOT NULL,
type VARCHAR(40) NOT NULL,
ref_id UUID, -- FK to user_referral_relationships.id or redemption id
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_upt_user_id ON public.user_point_transactions(user_id);
CREATE INDEX IF NOT EXISTS idx_upt_created ON public.user_point_transactions(created_at);
CREATE INDEX IF NOT EXISTS idx_upt_ref_id ON public.user_point_transactions(ref_id);
-- ── 4. Points balance (denormalized cache) ───────────────────────────────────
-- Maintained atomically alongside user_point_transactions.
-- balance = total_earned + total_spent (total_spent is always ≤ 0, so balance = earned - |spent|)
CREATE TABLE IF NOT EXISTS public.user_point_balances (
user_id UUID PRIMARY KEY,
balance INT NOT NULL DEFAULT 0,
total_earned INT NOT NULL DEFAULT 0,
total_spent INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);