feat: implement atomic signup trial claims and referral discount logic with Supabase database schema updates

This commit is contained in:
2569718930@qq.com
2026-05-30 16:02:02 +08:00
parent c0b1edaed0
commit 44638760b7
8 changed files with 685 additions and 1 deletions
+220
View File
@@ -46,6 +46,11 @@ 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 unique index if not exists uq_subscriptions_signup_trial_user
on public.subscriptions(user_id)
where plan_code = 'signup_trial_3d'
and source = 'signup_trial';
create table if not exists public.payments (
id bigserial primary key,
user_id uuid not null references auth.users(id) on delete cascade,
@@ -128,6 +133,221 @@ create table if not exists public.trial_claim_wallets (
create unique index if not exists uq_trial_claim_wallets_address
on public.trial_claim_wallets(lower(wallet_address));
create or replace function public.claim_signup_trial(
p_user_id uuid,
p_email text default '',
p_telegram_user_id bigint default null,
p_wallet_addresses text[] default array[]::text[]
)
returns jsonb
language plpgsql
security definer
set search_path = public, auth
as $$
declare
v_email text := lower(trim(coalesce(p_email, '')));
v_wallets text[] := (
select coalesce(array_agg(distinct lower(trim(input_wallet.value))), array[]::text[])
from unnest(coalesce(p_wallet_addresses, array[]::text[])) as input_wallet(value)
where trim(input_wallet.value) <> ''
);
v_now timestamptz := now();
v_expires timestamptz := v_now + interval '3 days';
v_claim public.trial_claims%rowtype;
v_claim_id bigint;
v_rows integer := 0;
begin
if p_user_id is null then
return jsonb_build_object('created', false, 'reason', 'missing_user_id');
end if;
select tc.*
into v_claim
from public.trial_claims tc
where tc.user_id = p_user_id
or (v_email <> '' and lower(tc.email) = v_email)
or (p_telegram_user_id is not null and tc.telegram_user_id = p_telegram_user_id)
or exists (
select 1
from public.trial_claim_wallets tcw
where tcw.trial_claim_id = tc.id
and lower(tcw.wallet_address) = any(v_wallets)
)
order by
case when tc.user_id = p_user_id then 0 else 1 end,
tc.created_at asc,
tc.id asc
limit 1;
if found then
if v_claim.user_id <> p_user_id then
return jsonb_build_object('created', false, 'reason', 'already_claimed');
end if;
insert into public.subscriptions (
user_id,
plan_code,
status,
starts_at,
expires_at,
source,
created_at,
updated_at
)
values (
p_user_id,
'signup_trial_3d',
'active',
coalesce(v_claim.claimed_at, v_now),
coalesce(v_claim.claimed_at, v_now) + interval '3 days',
'signup_trial',
v_now,
v_now
)
on conflict (user_id)
where plan_code = 'signup_trial_3d'
and source = 'signup_trial'
do nothing;
get diagnostics v_rows = row_count;
if v_rows > 0 then
insert into public.entitlement_events (
user_id,
action,
reason,
actor,
payload,
created_at
)
values (
p_user_id,
'signup_trial_granted',
'claim_repaired',
'supabase_auth',
jsonb_build_object(
'plan_code', 'signup_trial_3d',
'expires_at', coalesce(v_claim.claimed_at, v_now) + interval '3 days'
),
v_now
);
return jsonb_build_object(
'created', true,
'repaired', true,
'plan_code', 'signup_trial_3d',
'expires_at', coalesce(v_claim.claimed_at, v_now) + interval '3 days'
);
end if;
return jsonb_build_object('created', false, 'reason', 'already_claimed');
end if;
insert into public.trial_claims (
user_id,
email,
telegram_user_id,
primary_wallet_address,
metadata,
claimed_at,
created_at
)
values (
p_user_id,
v_email,
p_telegram_user_id,
nullif(v_wallets[1], ''),
jsonb_build_object('wallet_addresses', v_wallets),
v_now,
v_now
)
returning id into v_claim_id;
insert into public.trial_claim_wallets (trial_claim_id, wallet_address, created_at)
select v_claim_id, wallet.value, v_now
from unnest(v_wallets) as wallet(value)
on conflict do nothing;
insert into public.subscriptions (
user_id,
plan_code,
status,
starts_at,
expires_at,
source,
created_at,
updated_at
)
values (
p_user_id,
'signup_trial_3d',
'active',
v_now,
v_expires,
'signup_trial',
v_now,
v_now
)
on conflict (user_id)
where plan_code = 'signup_trial_3d'
and source = 'signup_trial'
do nothing;
get diagnostics v_rows = row_count;
insert into public.entitlement_events (
user_id,
action,
reason,
actor,
payload,
created_at
)
values
(
p_user_id,
'signup_trial_claimed',
'trial_dedupe',
'supabase_auth',
jsonb_build_object(
'user_id', p_user_id,
'email', v_email,
'telegram_user_id', p_telegram_user_id,
'wallet_addresses', v_wallets,
'claimed_at', v_now,
'storage', 'trial_claims'
),
v_now
),
(
p_user_id,
'signup_trial_granted',
'first_auth',
'supabase_auth',
jsonb_build_object(
'plan_code', 'signup_trial_3d',
'expires_at', v_expires
),
v_now
);
if v_rows = 0 then
return jsonb_build_object('created', false, 'reason', 'already_claimed');
end if;
return jsonb_build_object(
'created', true,
'plan_code', 'signup_trial_3d',
'expires_at', v_expires
);
exception
when unique_violation then
return jsonb_build_object('created', false, 'reason', 'already_claimed');
end;
$$;
revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from public;
revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from anon;
revoke all on function public.claim_signup_trial(uuid, text, bigint, text[]) from authenticated;
grant execute on function public.claim_signup_trial(uuid, text, bigint, text[]) to service_role;
create table if not exists public.referral_codes (
id bigserial primary key,
user_id uuid not null references auth.users(id) on delete cascade,