feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -0,0 +1,206 @@
BEGIN;
-- Serialize rewards per user so distinct, concurrently verified callbacks
-- cannot both pass the cooldown/cap checks before either claim is visible.
CREATE OR REPLACE FUNCTION public.grant_verified_ad_reward(
p_user_id uuid,
p_network text,
p_placement text,
p_ad_unit_id text,
p_transaction_id text,
p_reward_tokens integer
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
v_claim_id uuid;
v_balance integer;
BEGIN
IF p_user_id IS NULL OR NOT EXISTS (SELECT 1 FROM auth.users WHERE id = p_user_id) THEN
RAISE EXCEPTION 'unknown_user';
END IF;
IF p_transaction_id IS NULL OR length(trim(p_transaction_id)) < 8
OR length(trim(p_transaction_id)) > 128 THEN
RAISE EXCEPTION 'invalid_transaction';
END IF;
IF p_reward_tokens <> 50 THEN
RAISE EXCEPTION 'invalid_reward_amount';
END IF;
IF length(trim(coalesce(p_network, ''))) NOT BETWEEN 1 AND 80
OR length(trim(coalesce(p_placement, ''))) NOT BETWEEN 1 AND 80
OR length(trim(coalesce(p_ad_unit_id, ''))) NOT BETWEEN 1 AND 80 THEN
RAISE EXCEPTION 'invalid_reward_metadata';
END IF;
PERFORM pg_advisory_xact_lock(
pg_catalog.hashtextextended('d3ro:ad-reward:' || p_user_id::text, 0)
);
IF NOT EXISTS (
SELECT 1
FROM public.subscriptions
WHERE user_id = p_user_id
AND tier = 'free'
AND coalesce(status, 'active') IN ('active', 'trialing')
) THEN
RETURN jsonb_build_object('granted', false, 'reason', 'ineligible_tier');
END IF;
IF EXISTS (
SELECT 1 FROM public.ad_reward_claims
WHERE transaction_id = trim(p_transaction_id)
) THEN
RETURN jsonb_build_object('granted', false, 'reason', 'duplicate');
END IF;
IF (
SELECT count(*)
FROM public.ad_reward_claims
WHERE user_id = p_user_id
AND verified_at >= date_trunc('day', now())
) >= 20 THEN
RETURN jsonb_build_object('granted', false, 'reason', 'daily_cap');
END IF;
IF EXISTS (
SELECT 1 FROM public.ad_reward_claims
WHERE user_id = p_user_id
AND verified_at > now() - interval '15 seconds'
) THEN
RETURN jsonb_build_object('granted', false, 'reason', 'cooldown');
END IF;
INSERT INTO public.ad_reward_claims (
user_id, network, placement, ad_unit_id, transaction_id, reward_tokens
) VALUES (
p_user_id,
trim(p_network),
trim(p_placement),
trim(p_ad_unit_id),
trim(p_transaction_id),
p_reward_tokens
)
ON CONFLICT (transaction_id) DO NOTHING
RETURNING id INTO v_claim_id;
IF v_claim_id IS NULL THEN
RETURN jsonb_build_object('granted', false, 'reason', 'duplicate');
END IF;
INSERT INTO public.subscriptions (user_id, tier, overage_credits, provider)
VALUES (p_user_id, 'free', p_reward_tokens, 'none')
ON CONFLICT (user_id) DO UPDATE
SET overage_credits = public.subscriptions.overage_credits + EXCLUDED.overage_credits,
updated_at = now()
RETURNING overage_credits INTO v_balance;
RETURN jsonb_build_object(
'granted', true,
'claim_id', v_claim_id,
'tokens_added', p_reward_tokens,
'balance', v_balance
);
END;
$$;
REVOKE ALL ON FUNCTION public.grant_verified_ad_reward(
uuid, text, text, text, text, integer
) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.grant_verified_ad_reward(
uuid, text, text, text, text, integer
) TO service_role;
-- Exact dashboard aggregates shared by mobile and web. SECURITY INVOKER plus
-- the explicit auth.uid predicate keeps this within the caller's RLS boundary.
CREATE OR REPLACE FUNCTION public.mobile_dashboard_stats()
RETURNS jsonb
LANGUAGE sql
STABLE
SECURITY INVOKER
SET search_path = public, pg_temp
AS $$
WITH activity AS (
SELECT id, title, original_text, duration, word_count, mode, created_at
FROM public.history
WHERE user_id = auth.uid()
AND status = 'completed'
),
activity_days AS (
SELECT DISTINCT (created_at AT TIME ZONE 'UTC')::date AS activity_day
FROM activity
),
streak_anchor AS (
SELECT CASE
WHEN EXISTS (
SELECT 1 FROM activity_days
WHERE activity_day = (now() AT TIME ZONE 'UTC')::date
) THEN (now() AT TIME ZONE 'UTC')::date
WHEN EXISTS (
SELECT 1 FROM activity_days
WHERE activity_day = (now() AT TIME ZONE 'UTC')::date - 1
) THEN (now() AT TIME ZONE 'UTC')::date - 1
ELSE NULL::date
END AS anchor_day
),
ordered_days AS (
SELECT activity_day,
row_number() OVER (ORDER BY activity_day DESC) AS position,
anchor_day
FROM activity_days
CROSS JOIN streak_anchor
WHERE anchor_day IS NOT NULL
AND activity_day <= anchor_day
),
recent_rows AS (
SELECT id, title, original_text, duration, mode, created_at
FROM activity
ORDER BY created_at DESC, id DESC
LIMIT 5
)
SELECT jsonb_build_object(
'total_sessions', (SELECT count(*) FROM activity),
'total_recording_seconds', coalesce((SELECT sum(greatest(duration, 0)) FROM activity), 0),
'total_word_count', coalesce((SELECT sum(greatest(word_count, 0)) FROM activity), 0),
'today_sessions', (
SELECT count(*) FROM activity
WHERE (created_at AT TIME ZONE 'UTC')::date = (now() AT TIME ZONE 'UTC')::date
),
'today_recording_seconds', coalesce((
SELECT sum(greatest(duration, 0)) FROM activity
WHERE (created_at AT TIME ZONE 'UTC')::date = (now() AT TIME ZONE 'UTC')::date
), 0),
'today_word_count', coalesce((
SELECT sum(greatest(word_count, 0)) FROM activity
WHERE (created_at AT TIME ZONE 'UTC')::date = (now() AT TIME ZONE 'UTC')::date
), 0),
'streak_days', (
SELECT count(*) FROM ordered_days
WHERE activity_day = anchor_day - ((position - 1)::integer)
),
'recent_history', coalesce((
SELECT jsonb_agg(jsonb_build_object(
'id', id,
'title', title,
'text', original_text,
'duration_seconds', duration,
'mode', mode,
'created_at', created_at
) ORDER BY created_at DESC, id DESC)
FROM recent_rows
), '[]'::jsonb),
'generated_at', now()
);
$$;
REVOKE ALL ON FUNCTION public.mobile_dashboard_stats() FROM PUBLIC, anon;
GRANT EXECUTE ON FUNCTION public.mobile_dashboard_stats() TO authenticated;
COMMENT ON FUNCTION public.mobile_dashboard_stats() IS
'Exact authenticated history aggregates for mobile/web dashboards; UTC day boundary.';
COMMIT;