feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue