create table if not exists public.referral_codes ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, user_id uuid not null references public.platform_users(id) on delete cascade, code citext not null, status text not null default 'active' check (status in ('active', 'disabled')), channel text, landing_path text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, user_id), unique (tenant_id, code) ); comment on table public.referral_codes is 'Tenant-scoped referral codes for sales, agents, teachers, operators, and other permitted referrers.'; create table if not exists public.referral_leads ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, student_user_id uuid not null references public.platform_users(id) on delete cascade, referrer_user_id uuid references public.platform_users(id) on delete set null, ref_code citext, source text, first_track_id uuid references public.referral_tracks(id) on delete set null, bind_type text not null default 'first_touch' check (bind_type in ('first_touch', 'manual', 'imported')), status text not null default 'protected' check (status in ('protected', 'invalid', 'released')), protected_until timestamptz, metadata jsonb not null default '{}'::jsonb, bound_at timestamptz not null default now(), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, student_user_id) ); comment on table public.referral_leads is 'First-binding lead ownership. Once a student is bound to a referrer, normal scan/share events cannot rebind the lead.'; create table if not exists public.referral_team_edges ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, member_user_id uuid not null references public.platform_users(id) on delete cascade, leader_user_id uuid references public.platform_users(id) on delete set null, relation_type text not null default 'sales_team' check (relation_type in ('sales_team', 'agent_network', 'teacher_class')), status text not null default 'active' check (status in ('active', 'disabled')), metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, member_user_id, relation_type) ); create table if not exists public.referral_qrcodes ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, user_id uuid references public.platform_users(id) on delete set null, ref_code citext not null, scene text not null, page text not null default 'pages/index/index', provider text not null default 'wechat-miniapp', qrcode_url text, status text not null default 'pending' check (status in ('pending', 'ready', 'failed', 'disabled')), error_message text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, provider, scene, page) ); alter table public.referral_tracks add column if not exists metadata jsonb not null default '{}'::jsonb, add column if not exists lead_id uuid references public.referral_leads(id) on delete set null; alter table public.crm_webhook_queue add column if not exists source text, add column if not exists payload jsonb not null default '{}'::jsonb, add column if not exists idempotency_key text, add column if not exists target_url text; alter table public.crm_webhook_log add column if not exists request_payload jsonb not null default '{}'::jsonb; create unique index if not exists idx_crm_queue_tenant_record_id on public.crm_webhook_queue(tenant_id, record_id) where record_id is not null; create unique index if not exists idx_crm_queue_tenant_idempotency on public.crm_webhook_queue(tenant_id, idempotency_key) where idempotency_key is not null; create index if not exists idx_referral_codes_tenant_code on public.referral_codes(tenant_id, code, status); create index if not exists idx_referral_leads_referrer on public.referral_leads(tenant_id, referrer_user_id, bound_at desc); create index if not exists idx_referral_team_leader on public.referral_team_edges(tenant_id, leader_user_id, status); create index if not exists idx_referral_tracks_lead on public.referral_tracks(tenant_id, lead_id, created_at desc); alter table public.referral_codes enable row level security; alter table public.referral_leads enable row level security; alter table public.referral_team_edges enable row level security; alter table public.referral_qrcodes enable row level security; drop policy if exists tenant_isolation on public.referral_codes; create policy tenant_isolation on public.referral_codes for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin()); drop policy if exists tenant_isolation on public.referral_leads; create policy tenant_isolation on public.referral_leads for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin()); drop policy if exists tenant_isolation on public.referral_team_edges; create policy tenant_isolation on public.referral_team_edges for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin()); drop policy if exists tenant_isolation on public.referral_qrcodes; create policy tenant_isolation on public.referral_qrcodes for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin()); drop trigger if exists set_updated_at on public.referral_codes; create trigger set_updated_at before update on public.referral_codes for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.referral_leads; create trigger set_updated_at before update on public.referral_leads for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.referral_team_edges; create trigger set_updated_at before update on public.referral_team_edges for each row execute function app.touch_updated_at(); drop trigger if exists set_updated_at on public.referral_qrcodes; create trigger set_updated_at before update on public.referral_qrcodes for each row execute function app.touch_updated_at();