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;