-- PolyWeather minimal commerce/auth schema (P0) -- Run in Supabase SQL editor. create extension if not exists pgcrypto; create table if not exists public.profiles ( id uuid primary key references auth.users(id) on delete cascade, email text not null default '', telegram_user_id bigint, telegram_username text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_profiles_email on public.profiles(email) include (id); create index if not exists idx_profiles_id_lookup on public.profiles(id) include (email, created_at); create table if not exists public.subscriptions ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, plan_code text not null, status text not null check (status in ('active', 'paused', 'expired', 'cancelled')), starts_at timestamptz not null default now(), expires_at timestamptz not null, source text not null default 'manual', created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_subscriptions_user_status_expiry on public.subscriptions(user_id, expires_at desc) include (id, starts_at, plan_code, source) where status = 'active'; create index if not exists idx_subscriptions_status_expiry on public.subscriptions(expires_at asc) include (user_id, starts_at, plan_code) where status = 'active'; create index if not exists idx_subscriptions_user_created on public.subscriptions(user_id, created_at desc) include (id, status, plan_code, source, starts_at, expires_at, updated_at); create table if not exists public.payments ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, amount numeric(18, 6) not null, currency text not null default 'USDC', chain text not null default 'polygon', tx_hash text unique, status text not null check (status in ('pending', 'confirmed', 'failed', 'refunded')), raw_payload jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_payments_created_at on public.payments(created_at desc) include (id, user_id, amount, currency, chain, tx_hash, status); create table if not exists public.entitlement_events ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, action text not null, reason text not null default '', actor text not null default 'system', payload jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create table if not exists public.user_wallets ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, chain_id integer not null default 137, address text not null, status text not null default 'active' check (status in ('active', 'revoked')), is_primary boolean not null default false, verified_at timestamptz, last_used_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique(chain_id, address) ); create index if not exists idx_user_wallets_user_chain on public.user_wallets(user_id, chain_id, is_primary desc, verified_at desc) include (id, address) where status = 'active'; create index if not exists idx_user_wallets_chain_address_owner on public.user_wallets(chain_id, address) include (user_id, status); create table if not exists public.trial_claims ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, email text not null default '', telegram_user_id bigint, primary_wallet_address text, metadata jsonb not null default '{}'::jsonb, claimed_at timestamptz not null default now(), created_at timestamptz not null default now() ); create unique index if not exists uq_trial_claims_user on public.trial_claims(user_id); create unique index if not exists uq_trial_claims_email on public.trial_claims(lower(email)) where email <> ''; create unique index if not exists uq_trial_claims_telegram on public.trial_claims(telegram_user_id) where telegram_user_id is not null; create table if not exists public.trial_claim_wallets ( id bigserial primary key, trial_claim_id bigint not null references public.trial_claims(id) on delete cascade, wallet_address text not null, created_at timestamptz not null default now() ); create unique index if not exists uq_trial_claim_wallets_address on public.trial_claim_wallets(lower(wallet_address)); create table if not exists public.referral_codes ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, code text not null, status text not null default 'active' check (status in ('active', 'revoked')), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create unique index if not exists uq_referral_codes_user on public.referral_codes(user_id); create unique index if not exists uq_referral_codes_code on public.referral_codes(upper(code)); create table if not exists public.referral_attributions ( id bigserial primary key, referrer_user_id uuid not null references auth.users(id) on delete cascade, referred_user_id uuid not null references auth.users(id) on delete cascade, code text not null, status text not null default 'pending' check (status in ('pending', 'converted', 'capped', 'cancelled')), converted_payment_intent_id text, converted_tx_hash text, converted_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), check (referrer_user_id <> referred_user_id) ); create unique index if not exists uq_referral_attributions_referred on public.referral_attributions(referred_user_id); create index if not exists idx_referral_attributions_pending on public.referral_attributions(referred_user_id, created_at desc) include (id, code, referrer_user_id) where status = 'pending'; create table if not exists public.referral_rewards ( id bigserial primary key, referral_attribution_id bigint not null references public.referral_attributions(id) on delete cascade, referrer_user_id uuid not null references auth.users(id) on delete cascade, referred_user_id uuid not null references auth.users(id) on delete cascade, payment_intent_id text not null, tx_hash text, reward_days integer not null default 3 check (reward_days > 0 and reward_days <= 30), created_at timestamptz not null default now() ); create unique index if not exists uq_referral_rewards_attribution on public.referral_rewards(referral_attribution_id); create index if not exists idx_referral_rewards_referrer_month on public.referral_rewards(referrer_user_id, created_at desc) include (id, reward_days); create table if not exists public.wallet_link_challenges ( id bigserial primary key, user_id uuid not null references auth.users(id) on delete cascade, chain_id integer not null default 137, address text not null, nonce text not null unique, message text not null, expires_at timestamptz not null, consumed_at timestamptz, created_at timestamptz not null default now() ); create table if not exists public.payment_intents ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, plan_code text not null, plan_id bigint not null, chain_id integer not null default 137, token_address text not null, receiver_address text not null, amount_units numeric(78,0) not null, payment_mode text not null default 'strict' check (payment_mode in ('strict', 'flex', 'direct')), allowed_wallet text, order_id_hex text not null unique, status text not null default 'created' check (status in ('created', 'submitted', 'confirmed', 'expired', 'failed', 'cancelled')), tx_hash text, expires_at timestamptz not null, confirmed_at timestamptz, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_payment_intents_status_updated on public.payment_intents(status, updated_at desc) include (user_id) where status in ('submitted', 'confirmed'); create index if not exists idx_payment_intents_user_status_updated on public.payment_intents(user_id, status, updated_at desc); create index if not exists idx_payment_intents_submitted_tx_updated on public.payment_intents(updated_at asc) include (id, user_id, tx_hash, chain_id) where status = 'submitted' and tx_hash is not null; create index if not exists idx_payment_intents_user_created on public.payment_intents(user_id, created_at desc); create index if not exists idx_payment_intents_tx_hash on public.payment_intents(tx_hash) include (id, user_id) where tx_hash is not null; create table if not exists public.payment_transactions ( id bigserial primary key, intent_id uuid not null references public.payment_intents(id) on delete cascade, tx_hash text not null unique, chain_id integer not null default 137, from_address text, to_address text not null, block_number bigint, payment_method text not null default 'wallet' check (payment_method in ('wallet', 'manual', 'direct')), status text not null default 'submitted' check (status in ('submitted', 'confirmed', 'failed', 'duplicate', 'refund_required')), raw_receipt jsonb not null default '{}'::jsonb, raw_tx jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists idx_payment_transactions_intent on public.payment_transactions(intent_id, created_at desc); create index if not exists idx_payment_transactions_tx_hash_intent on public.payment_transactions(tx_hash) include (intent_id); create or replace function public.sync_profile_from_auth() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, email) values (new.id, coalesce(new.email, '')) on conflict (id) do update set email = excluded.email, updated_at = now(); return new; end; $$; drop trigger if exists on_auth_user_created_polyweather on auth.users; create trigger on_auth_user_created_polyweather after insert on auth.users for each row execute function public.sync_profile_from_auth();