merge: sync latest staging into rectification v9

# Conflicts:
#	docs/BUG_HISTORY.md
This commit is contained in:
Jesse
2026-08-11 18:08:42 +08:00
29 changed files with 1893 additions and 310 deletions
@@ -0,0 +1,227 @@
begin;
create or replace function public.admin_reset_customer_account(
p_actor_user_id uuid,
p_target_user_id uuid,
p_reason text,
p_request_id text
)
returns table (
user_id uuid,
email text,
credits integer,
chat_sessions_deleted integer,
chart_profiles_deleted integer,
synastry_reports_deleted integer
)
language plpgsql
security definer
set search_path = ''
as $$
declare
v_actor_email text;
v_target_email text;
v_profile_email text;
v_credits integer;
v_identity_accounts bigint;
v_identity_sessions bigint;
v_admin_user_roles bigint;
v_credit_transactions bigint;
v_credit_cancellations bigint;
v_consultation_requests bigint;
v_rectification_billing bigint;
v_action_receipts bigint;
v_redeemed_codes bigint;
v_admin_audit_logs bigint;
v_chat_sessions_deleted integer;
v_chart_profiles_deleted integer;
v_synastry_reports_deleted integer;
begin
if not public.admin_has_permission(p_actor_user_id, 'admin.users.manage_roles') then
raise exception 'admin_permission_denied' using errcode = '42501';
end if;
if p_target_user_id is null then
raise exception 'admin_customer_scope_invalid' using errcode = '22023';
end if;
if char_length(btrim(coalesce(p_reason, ''))) not between 1 and 500 then
raise exception 'admin_reason_required' using errcode = '22023';
end if;
if char_length(btrim(coalesce(p_request_id, ''))) not between 1 and 200 then
raise exception 'admin_request_id_invalid' using errcode = '22023';
end if;
select lower(btrim(value.email))
into v_actor_email
from identity.users value
where value.id = p_actor_user_id;
if v_actor_email is null then
raise exception 'admin_user_not_found' using errcode = '22023';
end if;
select
identity_user.email,
profile.email,
profile.credits,
(select count(*) from identity.accounts value where value.user_id = identity_user.id),
(select count(*) from identity.sessions value where value.user_id = identity_user.id),
(select count(*) from public.admin_user_roles value where value.admin_user_id = identity_user.id),
(select count(*) from public.credit_transactions value where value.user_id = identity_user.id),
(select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id),
(select count(*) from public.consultation_requests value where value.user_id = identity_user.id),
(select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id),
(select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id),
(select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id),
(select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id)
into
v_target_email,
v_profile_email,
v_credits,
v_identity_accounts,
v_identity_sessions,
v_admin_user_roles,
v_credit_transactions,
v_credit_cancellations,
v_consultation_requests,
v_rectification_billing,
v_action_receipts,
v_redeemed_codes,
v_admin_audit_logs
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
join public.profiles profile on profile.id = identity_user.id
where identity_user.id = p_target_user_id
and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email))
for update of identity_user, auth_user, profile;
if not found then
raise exception 'admin_customer_not_found_or_identity_bridge_mismatch' using errcode = 'P0002';
end if;
update public.profiles
set name = null,
birth_date = null,
birth_time = null,
country_code = null,
province_code = null,
city_code = null,
district_code = null,
onboarding_payload = null,
onboarding_version = null,
onboarding_generated_at = null,
latitude = null,
longitude = null,
timezone_offset = null,
reported_birth_time = null,
active_birth_time = null,
birth_time_source = null,
birth_time_period = null,
birth_time_clue = null,
uncertainty_before_minutes = null,
uncertainty_after_minutes = null,
birth_time_status = null,
rectification_confidence = null,
rectification_case_id = null,
birth_place_label = null,
birth_place_type = null,
birth_place_provider = null,
birth_place_provider_id = null,
timezone_id = null,
timezone_source = null,
updated_at = pg_catalog.now()
where id = p_target_user_id;
delete from public.chat_sessions value where value.user_id = p_target_user_id;
get diagnostics v_chat_sessions_deleted = row_count;
delete from public.chart_profiles value where value.user_id = p_target_user_id;
get diagnostics v_chart_profiles_deleted = row_count;
delete from public.synastry_reports value where value.user_id = p_target_user_id;
get diagnostics v_synastry_reports_deleted = row_count;
if exists (
select 1
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
join public.profiles profile on profile.id = identity_user.id
where identity_user.id = p_target_user_id
and (
identity_user.email is distinct from v_target_email
or auth_user.email is distinct from v_target_email
or profile.email is distinct from v_profile_email
or profile.credits is distinct from v_credits
or (select count(*) from identity.accounts value where value.user_id = identity_user.id) <> v_identity_accounts
or (select count(*) from identity.sessions value where value.user_id = identity_user.id) <> v_identity_sessions
or (select count(*) from public.admin_user_roles value where value.admin_user_id = identity_user.id) <> v_admin_user_roles
or (select count(*) from public.credit_transactions value where value.user_id = identity_user.id) <> v_credit_transactions
or (select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) <> v_credit_cancellations
or (select count(*) from public.consultation_requests value where value.user_id = identity_user.id) <> v_consultation_requests
or (select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) <> v_rectification_billing
or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) <> v_action_receipts
or (select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) <> v_redeemed_codes
or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) <> v_admin_audit_logs
)
) then
raise exception 'admin_customer_preserved_state_changed' using errcode = 'P0001';
end if;
if exists (select 1 from public.chat_sessions value where value.user_id = p_target_user_id)
or exists (select 1 from public.chart_profiles value where value.user_id = p_target_user_id)
or exists (select 1 from public.synastry_reports value where value.user_id = p_target_user_id)
or exists (
select 1
from public.profiles profile
where profile.id = p_target_user_id
and (
profile.name is not null or profile.birth_date is not null or profile.birth_time is not null
or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null
or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null
or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null
or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null
or profile.birth_time_period is not null or profile.birth_time_clue is not null
or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null
or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null
or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null
or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null
)
) then
raise exception 'admin_customer_reset_state_not_empty' using errcode = 'P0001';
end if;
insert into audit.admin_audit_logs (
actor_user_id, actor_email, actor_role, action, target_type, target_id,
after_value, request_id, permission_used, reason
) values (
p_actor_user_id, v_actor_email, 'admin', 'admin.customer.account.reset',
'customer', p_target_user_id,
jsonb_build_object(
'email', lower(btrim(v_target_email)),
'creditsPreserved', v_credits,
'chatSessionsDeleted', v_chat_sessions_deleted,
'chartProfilesDeleted', v_chart_profiles_deleted,
'synastryReportsDeleted', v_synastry_reports_deleted
),
btrim(p_request_id), 'admin.users.manage_roles', btrim(p_reason)
) on conflict do nothing;
return query select
p_target_user_id,
lower(btrim(v_target_email)),
v_credits,
v_chat_sessions_deleted,
v_chart_profiles_deleted,
v_synastry_reports_deleted;
end;
$$;
revoke all on function public.admin_reset_customer_account(uuid, uuid, text, text)
from public, anon, authenticated, service_role;
do $$
begin
if exists (select 1 from pg_roles where rolname = 'admin_runtime') then
grant execute on function public.admin_reset_customer_account(uuid, uuid, text, text)
to admin_runtime;
end if;
end;
$$;
commit;