206 lines
6.3 KiB
PL/PgSQL
206 lines
6.3 KiB
PL/PgSQL
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;
|