d3ro-voice/server/supabase/tests/admin-subscription-provider-guard.integration.sql

200 lines
7.7 KiB
PL/PgSQL

\set ON_ERROR_STOP on
-- Regression: back-office subscription edits must not rewrite the billing terms
-- of a provider-owned (Payple / store) subscription, and admin delete must
-- release provider ownership so the customer can purchase again.
-- Before 20260928190000 a manager PATCH of tier='pro_plus' on an auto-renewing
-- Payple Pro row was accepted, and payple-renew then charged the pro_plus price.
BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF condition IS NOT TRUE THEN
RAISE EXCEPTION 'assertion_failed: %', message;
END IF;
END;
$$;
INSERT INTO auth.users (
id, aud, role, email, encrypted_password, email_confirmed_at,
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
) VALUES
(
'41900000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
'guard-manager@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'41900000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
'guard-admin@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'41900000-0000-4000-8000-000000000003', 'authenticated', 'authenticated',
'guard-payple-customer@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
),
(
'41900000-0000-4000-8000-000000000004', 'authenticated', 'authenticated',
'guard-comp-customer@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
);
UPDATE public.profiles SET role = 'manager' WHERE id = '41900000-0000-4000-8000-000000000001';
UPDATE public.profiles SET role = 'admin' WHERE id = '41900000-0000-4000-8000-000000000002';
INSERT INTO public.subscriptions (user_id, tier, status, provider, payment_provider)
VALUES
('41900000-0000-4000-8000-000000000003', 'free', 'active', 'none', 'none'),
('41900000-0000-4000-8000-000000000004', 'free', 'active', 'none', 'none')
ON CONFLICT (user_id) DO NOTHING;
UPDATE public.subscriptions
SET tier = 'pro',
status = 'active',
provider = 'payple',
provider_resource_id = 'payple-guard-fixture-resource',
payple_payer_id = 'payple-guard-fixture-payer',
auto_renewing = true,
current_period_start = '2026-09-15T03:12:45Z',
current_period_end = '2026-10-15T03:12:45Z'
WHERE user_id = '41900000-0000-4000-8000-000000000003';
UPDATE public.subscriptions
SET tier = 'free', status = 'active', provider = 'none', auto_renewing = NULL
WHERE user_id = '41900000-0000-4000-8000-000000000004';
-- A rejected mutation must raise provider_managed_subscription and change nothing.
CREATE OR REPLACE FUNCTION pg_temp.expect_provider_guard(
p_label text,
p_tier text,
p_status text,
p_period_end timestamptz
)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
v_rejected boolean := false;
v_row public.subscriptions%ROWTYPE;
BEGIN
BEGIN
PERFORM public.admin_mutate_subscription_v1(
'guard-manager@example.invalid', gen_random_uuid(), 'update',
'41900000-0000-4000-8000-000000000003',
p_tier, p_status, p_period_end, NULL, NULL, 'guard regression ' || p_label
);
EXCEPTION
WHEN check_violation THEN
PERFORM pg_temp.assert_true(
SQLERRM = 'provider_managed_subscription',
format('%s rejected with provider_managed_subscription, got %s', p_label, SQLERRM)
);
v_rejected := true;
END;
PERFORM pg_temp.assert_true(v_rejected, format('%s must be rejected on a Payple-owned row', p_label));
SELECT * INTO v_row FROM public.subscriptions
WHERE user_id = '41900000-0000-4000-8000-000000000003';
PERFORM pg_temp.assert_true(
v_row.tier = 'pro'
AND v_row.status = 'active'
AND v_row.provider = 'payple'
AND v_row.auto_renewing
AND v_row.current_period_end = '2026-10-15T03:12:45Z'::timestamptz,
format('%s left the Payple row unchanged: %s/%s/%s/%s', p_label,
v_row.tier, v_row.status, v_row.provider, v_row.current_period_end)
);
END;
$$;
SELECT pg_temp.expect_provider_guard('manager tier comp', 'pro_plus', NULL, NULL);
SELECT pg_temp.expect_provider_guard('manager tier downgrade', 'free', NULL, NULL);
SELECT pg_temp.expect_provider_guard('manager status change', NULL, 'expired', NULL);
SELECT pg_temp.expect_provider_guard('manager period shift', NULL, NULL, '2026-10-01T00:00:00Z');
SELECT pg_temp.expect_provider_guard('form resend with new tier', 'pro_plus', 'active', '2026-10-15T00:00:00Z');
-- The admin form resends tier/status and a day-granular period end on every
-- save. Unchanged values must pass so notes and credits stay editable, and the
-- stored period-end instant must not be truncated to midnight.
DO $$
DECLARE
v_result jsonb;
v_row public.subscriptions%ROWTYPE;
BEGIN
v_result := public.admin_mutate_subscription_v1(
'guard-manager@example.invalid', gen_random_uuid(), 'update',
'41900000-0000-4000-8000-000000000003',
'pro', 'active', '2026-10-15T00:00:00Z', 250, 'support note', 'note-only edit'
);
PERFORM pg_temp.assert_true((v_result->>'success')::boolean, 'note-only edit succeeds');
SELECT * INTO v_row FROM public.subscriptions
WHERE user_id = '41900000-0000-4000-8000-000000000003';
PERFORM pg_temp.assert_true(
v_row.tier = 'pro'
AND v_row.provider = 'payple'
AND v_row.current_period_end = '2026-10-15T03:12:45Z'::timestamptz
AND v_row.overage_credits = 250
AND v_row.admin_note = 'support note',
format('note-only edit keeps billing terms: %s/%s/%s/%s/%s',
v_row.tier, v_row.provider, v_row.current_period_end, v_row.overage_credits, v_row.admin_note)
);
END;
$$;
-- Admin-managed rows keep the existing manager edit behavior.
DO $$
DECLARE
v_row public.subscriptions%ROWTYPE;
BEGIN
PERFORM public.admin_mutate_subscription_v1(
'guard-manager@example.invalid', gen_random_uuid(), 'update',
'41900000-0000-4000-8000-000000000004',
'pro_plus', 'active', '2026-12-01T00:00:00Z', NULL, NULL, 'comp grant'
);
SELECT * INTO v_row FROM public.subscriptions
WHERE user_id = '41900000-0000-4000-8000-000000000004';
PERFORM pg_temp.assert_true(
v_row.tier = 'pro_plus'
AND v_row.provider = 'none'
AND v_row.current_period_end = '2026-12-01T00:00:00Z'::timestamptz,
format('comp row accepts tier/period edits: %s/%s/%s', v_row.tier, v_row.provider, v_row.current_period_end)
);
END;
$$;
-- Admin delete releases provider ownership: renewals stop and a new purchase is allowed.
DO $$
DECLARE
v_row public.subscriptions%ROWTYPE;
BEGIN
PERFORM public.admin_mutate_subscription_v1(
'guard-admin@example.invalid', gen_random_uuid(), 'delete',
'41900000-0000-4000-8000-000000000003',
NULL, NULL, NULL, NULL, NULL, 'refund and revoke'
);
SELECT * INTO v_row FROM public.subscriptions
WHERE user_id = '41900000-0000-4000-8000-000000000003';
PERFORM pg_temp.assert_true(
v_row.tier = 'free'
AND v_row.status = 'expired'
AND v_row.provider = 'none'
AND v_row.payment_provider = 'none'
AND v_row.provider_resource_id IS NULL
AND v_row.auto_renewing IS FALSE,
format('delete releases provider ownership: %s/%s/%s/%s/%s/%s', v_row.tier, v_row.status,
v_row.provider, v_row.payment_provider, v_row.provider_resource_id, v_row.auto_renewing)
);
PERFORM pg_temp.assert_true(
(SELECT tier FROM public.profiles WHERE id = '41900000-0000-4000-8000-000000000003') = 'free',
'profile tier follows delete'
);
END;
$$;
ROLLBACK;