fix(rectification): allow accepted candidate reselection
This commit is contained in:
+251
@@ -0,0 +1,251 @@
|
||||
begin;
|
||||
|
||||
-- Candidate cards explicitly allow the user to change an already accepted
|
||||
-- minute. Keep same-time retries idempotent, but permit a different minute
|
||||
-- from the same live result only while the Case is still candidate_accepted
|
||||
-- and the profile still matches the previous user-accepted selection.
|
||||
create or replace function public.accept_agentic_rectification_candidate_for_case(
|
||||
p_user_id uuid,
|
||||
p_case_id uuid,
|
||||
p_result_id uuid,
|
||||
p_time time without time zone
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_case public.agentic_rectification_cases%rowtype;
|
||||
v_result public.agentic_rectification_results%rowtype;
|
||||
v_profile public.profiles%rowtype;
|
||||
v_snapshot jsonb;
|
||||
v_selection_kind text;
|
||||
v_status text;
|
||||
begin
|
||||
if p_user_id is null or p_case_id is null or p_result_id is null or p_time is null
|
||||
or extract(second from p_time) is distinct from 0 then
|
||||
raise exception 'agentic_rectification_candidate_invalid_input' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
select * into v_case
|
||||
from public.agentic_rectification_cases
|
||||
where id = p_case_id and user_id = p_user_id
|
||||
for update;
|
||||
if not found then
|
||||
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
|
||||
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
select * into v_result
|
||||
from public.agentic_rectification_results
|
||||
where id = p_result_id
|
||||
and user_id = p_user_id
|
||||
and case_id = p_case_id
|
||||
for update;
|
||||
if not found then
|
||||
raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
if v_result.invalidated_at is not null or v_result.expires_at <= pg_catalog.now() then
|
||||
raise exception 'agentic_rectification_candidate_expired' using errcode = 'P0001';
|
||||
end if;
|
||||
if not v_result.selection_allowed then
|
||||
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
|
||||
end if;
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements(v_result.candidates) candidate
|
||||
where candidate ->> 'time' = pg_catalog.to_char(p_time, 'HH24:MI')
|
||||
) then
|
||||
raise exception 'agentic_rectification_candidate_time_not_allowed' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
if v_result.selected_time is not null then
|
||||
select * into v_profile
|
||||
from public.profiles
|
||||
where id = p_user_id
|
||||
for update;
|
||||
|
||||
-- Same candidate and minute is a retry/double-click, not a new choice.
|
||||
if v_result.selected_time is not distinct from p_time
|
||||
and v_case.accepted_time is not distinct from p_time then
|
||||
if not found
|
||||
or v_profile.active_birth_time is distinct from v_result.selected_time
|
||||
or v_profile.birth_time is distinct from v_result.selected_time
|
||||
or v_profile.birth_time_status is distinct from (
|
||||
case when v_result.selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end
|
||||
) then
|
||||
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
|
||||
end if;
|
||||
return jsonb_build_object(
|
||||
'success', true,
|
||||
'saved_time', pg_catalog.to_char(p_time, 'HH24:MI'),
|
||||
'status', case when v_result.selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end,
|
||||
'result_id', v_result.id,
|
||||
'case_status', v_case.status,
|
||||
'idempotent', true
|
||||
);
|
||||
end if;
|
||||
|
||||
-- A card-driven switch is legal only for an unconfirmed user acceptance.
|
||||
-- It must never demote a confirmed result or mutate a non-selection Case.
|
||||
if v_case.status is distinct from 'candidate_accepted'
|
||||
or v_case.accepted_time is null
|
||||
or v_case.accepted_time is distinct from v_result.selected_time
|
||||
or v_case.confirmed_time is not null
|
||||
or v_result.selection_kind is distinct from 'user_accepted' then
|
||||
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
-- The profile must still reflect the old accepted minute. Any independent
|
||||
-- profile edit invalidates the switch instead of silently overwriting it.
|
||||
if not found
|
||||
or v_profile.active_birth_time is distinct from v_result.selected_time
|
||||
or v_profile.birth_time is distinct from v_result.selected_time
|
||||
or v_profile.birth_time_status is distinct from 'accepted' then
|
||||
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from public.agentic_rectification_results newer
|
||||
where newer.user_id = p_user_id
|
||||
and newer.case_id = p_case_id
|
||||
and newer.invalidated_at is null
|
||||
and newer.created_at > v_result.created_at
|
||||
) then
|
||||
raise exception 'agentic_rectification_candidate_superseded' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
update public.profiles
|
||||
set active_birth_time = p_time,
|
||||
birth_time = p_time,
|
||||
birth_time_status = 'accepted',
|
||||
rectification_confidence = case
|
||||
when v_result.overall_confidence = 'high' then 100
|
||||
when v_result.overall_confidence = 'medium' then 70
|
||||
else 40
|
||||
end,
|
||||
updated_at = pg_catalog.now()
|
||||
where id = p_user_id;
|
||||
|
||||
update public.agentic_rectification_results
|
||||
set selected_time = p_time,
|
||||
selection_kind = 'user_accepted',
|
||||
selected_at = pg_catalog.now(),
|
||||
updated_at = pg_catalog.now()
|
||||
where id = v_result.id;
|
||||
|
||||
-- The profile trigger temporarily marks resumable cases needs_rebaseline;
|
||||
-- this atomic candidate switch is the intended accepted state and wins.
|
||||
update public.agentic_rectification_cases
|
||||
set status = 'candidate_accepted',
|
||||
accepted_time = p_time,
|
||||
last_activity_at = pg_catalog.now(),
|
||||
updated_at = pg_catalog.now()
|
||||
where id = v_case.id;
|
||||
|
||||
return jsonb_build_object(
|
||||
'success', true,
|
||||
'saved_time', pg_catalog.to_char(p_time, 'HH24:MI'),
|
||||
'status', 'accepted',
|
||||
'result_id', v_result.id,
|
||||
'case_status', 'candidate_accepted',
|
||||
'idempotent', false
|
||||
);
|
||||
end if;
|
||||
|
||||
-- Fresh acceptance still requires the profile to match the Case baseline.
|
||||
v_snapshot := v_case.baseline_birth_snapshot;
|
||||
select * into v_profile
|
||||
from public.profiles
|
||||
where id = p_user_id
|
||||
for update;
|
||||
|
||||
if not found
|
||||
or v_profile.birth_date is distinct from (v_snapshot ->> 'birth_date')::date
|
||||
or v_profile.reported_birth_time is distinct from (v_snapshot ->> 'reported_birth_time')::time without time zone
|
||||
or v_profile.active_birth_time is distinct from (v_snapshot ->> 'active_birth_time')::time without time zone
|
||||
or v_profile.birth_time_source is distinct from v_snapshot ->> 'birth_time_source'
|
||||
or v_profile.birth_time_period is distinct from v_snapshot ->> 'birth_time_period'
|
||||
or v_profile.uncertainty_before_minutes is distinct from (v_snapshot ->> 'uncertainty_before_minutes')::integer
|
||||
or v_profile.uncertainty_after_minutes is distinct from (v_snapshot ->> 'uncertainty_after_minutes')::integer
|
||||
or v_profile.latitude is distinct from (v_snapshot ->> 'latitude')::double precision
|
||||
or v_profile.longitude is distinct from (v_snapshot ->> 'longitude')::double precision
|
||||
or v_profile.timezone_offset is distinct from (v_snapshot ->> 'timezone_offset')::double precision then
|
||||
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
if exists (
|
||||
select 1
|
||||
from public.agentic_rectification_results newer
|
||||
where newer.user_id = p_user_id
|
||||
and newer.case_id = p_case_id
|
||||
and newer.invalidated_at is null
|
||||
and newer.created_at > v_result.created_at
|
||||
) then
|
||||
raise exception 'agentic_rectification_candidate_superseded' using errcode = 'P0001';
|
||||
end if;
|
||||
|
||||
v_selection_kind := case
|
||||
when v_result.confirmation_allowed
|
||||
and v_result.representative_time is not distinct from p_time
|
||||
then 'engine_confirmed'
|
||||
else 'user_accepted'
|
||||
end;
|
||||
v_status := case when v_selection_kind = 'engine_confirmed' then 'confirmed' else 'accepted' end;
|
||||
|
||||
update public.profiles
|
||||
set active_birth_time = p_time,
|
||||
birth_time = p_time,
|
||||
birth_time_status = v_status,
|
||||
rectification_confidence = case
|
||||
when v_result.overall_confidence = 'high' then 100
|
||||
when v_result.overall_confidence = 'medium' then 70
|
||||
else 40
|
||||
end,
|
||||
updated_at = pg_catalog.now()
|
||||
where id = p_user_id;
|
||||
|
||||
update public.agentic_rectification_results
|
||||
set selected_time = p_time,
|
||||
selection_kind = v_selection_kind,
|
||||
selected_at = pg_catalog.now(),
|
||||
updated_at = pg_catalog.now()
|
||||
where id = v_result.id;
|
||||
|
||||
update public.agentic_rectification_results
|
||||
set invalidated_at = pg_catalog.now(),
|
||||
updated_at = pg_catalog.now()
|
||||
where user_id = p_user_id
|
||||
and case_id = p_case_id
|
||||
and id <> v_result.id
|
||||
and invalidated_at is null
|
||||
and selected_time is null;
|
||||
|
||||
update public.agentic_rectification_cases
|
||||
set status = 'candidate_accepted',
|
||||
accepted_time = p_time,
|
||||
last_activity_at = pg_catalog.now(),
|
||||
updated_at = pg_catalog.now()
|
||||
where id = v_case.id;
|
||||
|
||||
return jsonb_build_object(
|
||||
'success', true,
|
||||
'saved_time', pg_catalog.to_char(p_time, 'HH24:MI'),
|
||||
'status', v_status,
|
||||
'result_id', v_result.id,
|
||||
'case_status', 'candidate_accepted',
|
||||
'idempotent', false
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.accept_agentic_rectification_candidate_for_case(uuid, uuid, uuid, time without time zone)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.accept_agentic_rectification_candidate_for_case(uuid, uuid, uuid, time without time zone)
|
||||
to service_role;
|
||||
|
||||
commit;
|
||||
Reference in New Issue
Block a user