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