\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;