d3ro-voice/server/supabase/migrations/20260821000002_mobile_monetization_hardening.sql
2026-08-29 18:33:45 +09:00

121 lines
3.5 KiB
PL/PgSQL

-- ============================================================================
-- Mobile monetization hardening
-- Atomically revoke Google Play linked purchase tokens before applying a new
-- verified purchase. This prevents upgrade/downgrade and resubscribe chains
-- from retaining two entitlements.
-- ============================================================================
BEGIN;
CREATE OR REPLACE FUNCTION public.apply_verified_google_play_purchase(
p_user_id uuid,
p_platform text,
p_product_id text,
p_store_transaction_id text,
p_token_hash text,
p_linked_token_hash text,
p_purchase_token text,
p_purchase_state text,
p_purchase_at timestamptz,
p_expires_at timestamptz,
p_auto_renewing boolean,
p_acknowledged boolean,
p_tier text,
p_entitled boolean,
p_verification jsonb
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
v_linked_purchase_id uuid;
v_linked_user_id uuid;
v_linked_revoked boolean := false;
v_result jsonb;
BEGIN
IF p_platform <> 'google_play' THEN
RAISE EXCEPTION 'invalid_platform';
END IF;
IF p_linked_token_hash IS NOT NULL THEN
IF p_linked_token_hash !~ '^[0-9a-f]{64}$' OR p_linked_token_hash = p_token_hash THEN
RAISE EXCEPTION 'invalid_linked_purchase_token';
END IF;
SELECT id, user_id
INTO v_linked_purchase_id, v_linked_user_id
FROM public.iap_purchases
WHERE platform = 'google_play'
AND token_hash = p_linked_token_hash
FOR UPDATE;
IF v_linked_user_id IS NOT NULL AND v_linked_user_id <> p_user_id THEN
RAISE EXCEPTION 'linked_purchase_owned_by_other_user';
END IF;
IF v_linked_purchase_id IS NOT NULL THEN
UPDATE public.iap_purchases
SET purchase_state = 'expired',
expires_at = least(coalesce(expires_at, now()), now()),
auto_renewing = false,
verified_at = now(),
updated_at = now()
WHERE id = v_linked_purchase_id;
UPDATE public.subscriptions
SET tier = 'free',
status = 'expired',
current_period_end = now(),
cancel_at = now(),
provider = 'none',
payment_provider = 'none',
store_product_id = NULL,
store_purchase_id = NULL,
auto_renewing = false,
updated_at = now()
WHERE user_id = p_user_id
AND store_purchase_id = v_linked_purchase_id;
IF FOUND THEN
UPDATE public.profiles
SET tier = 'free',
updated_at = now()
WHERE id = p_user_id;
END IF;
v_linked_revoked := true;
END IF;
END IF;
v_result := public.apply_verified_store_purchase(
p_user_id,
p_platform,
p_product_id,
p_store_transaction_id,
p_token_hash,
p_purchase_token,
p_purchase_state,
p_purchase_at,
p_expires_at,
p_auto_renewing,
p_acknowledged,
p_tier,
p_entitled,
p_verification
);
RETURN v_result || jsonb_build_object('linked_purchase_revoked', v_linked_revoked);
END;
$$;
REVOKE ALL ON FUNCTION public.apply_verified_google_play_purchase(
uuid, text, text, text, text, text, text, text, timestamptz, timestamptz,
boolean, boolean, text, boolean, jsonb
) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.apply_verified_google_play_purchase(
uuid, text, text, text, text, text, text, text, timestamptz, timestamptz,
boolean, boolean, text, boolean, jsonb
) TO service_role;
COMMIT;