fix(billing): stop back-office edits from changing Payple renewal charges
This commit is contained in:
parent
957e136789
commit
f456d737c4
8 changed files with 639 additions and 3 deletions
|
|
@ -0,0 +1,200 @@
|
|||
\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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue