Carry explicit local date intervals instead of inferring the day from clock order. Cluster width, delivery, adoption, and reports keep the actual civil date; adopted date is stored separately from the reported birth_date. Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation thresholds, and Skill version are unchanged. Isolated Linux final-3 gates passed; four pre-existing Python failures remain. This is not a production release.
988 lines
47 KiB
PL/PgSQL
988 lines
47 KiB
PL/PgSQL
-- Nullable adoption outputs; declaration inputs and historical rows are untouched.
|
|
begin;
|
|
alter table public.profiles
|
|
add column if not exists active_birth_date date,
|
|
add column if not exists active_birth_timezone_offset double precision,
|
|
add column if not exists active_birth_provenance jsonb;
|
|
|
|
create or replace function public.guard_adopted_birth_date()
|
|
returns trigger language plpgsql set search_path = '' as $$
|
|
begin
|
|
if new.birth_time_status not in ('accepted', 'confirmed') or new.birth_time_status is null
|
|
or new.active_birth_time is null
|
|
or (tg_op = 'UPDATE' and (
|
|
new.birth_date is distinct from old.birth_date
|
|
or new.reported_birth_time is distinct from old.reported_birth_time
|
|
or new.birth_time_source is distinct from old.birth_time_source
|
|
or new.birth_time_period is distinct from old.birth_time_period
|
|
or new.declared_window_start is distinct from old.declared_window_start
|
|
or new.declared_window_end is distinct from old.declared_window_end
|
|
or new.uncertainty_before_minutes is distinct from old.uncertainty_before_minutes
|
|
or new.uncertainty_after_minutes is distinct from old.uncertainty_after_minutes
|
|
or new.latitude is distinct from old.latitude or new.longitude is distinct from old.longitude
|
|
or new.timezone_id is distinct from old.timezone_id
|
|
or new.timezone_offset is distinct from old.timezone_offset
|
|
or (new.active_birth_time is distinct from old.active_birth_time
|
|
and new.active_birth_provenance is not distinct from old.active_birth_provenance)
|
|
)) then
|
|
new.active_birth_date := null;
|
|
new.active_birth_timezone_offset := null;
|
|
new.active_birth_provenance := null;
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
drop trigger if exists zz_guard_adopted_birth_date on public.profiles;
|
|
create trigger zz_guard_adopted_birth_date before insert or update on public.profiles
|
|
for each row execute function public.guard_adopted_birth_date();
|
|
|
|
-- Internal, service-only validation shared by adoption and confirmation.
|
|
-- The candidate UUID identifies persisted facts; callers never supply a date.
|
|
create or replace function public.validate_dated_rectification_candidate(
|
|
p_case public.agentic_rectification_cases,
|
|
p_result public.agentic_rectification_results,
|
|
p_candidate public.agentic_rectification_candidates,
|
|
p_inference jsonb
|
|
) returns date language plpgsql security definer set search_path = '' as $$
|
|
declare
|
|
v_receipt jsonb := p_result.decision_receipt;
|
|
v_windows jsonb;
|
|
v_payload jsonb;
|
|
v_item jsonb;
|
|
v_interval jsonb;
|
|
v_window jsonb;
|
|
v_first timestamp;
|
|
v_at timestamp;
|
|
v_start timestamp;
|
|
v_end timestamp;
|
|
v_previous timestamp;
|
|
v_date date;
|
|
v_expected text;
|
|
v_count integer;
|
|
v_union pg_catalog.tsmultirange;
|
|
v_credible pg_catalog.tsmultirange;
|
|
v_expected_credible jsonb;
|
|
v_credible_ranges pg_catalog.tsrange[] := array[]::pg_catalog.tsrange[];
|
|
v_top text;
|
|
begin
|
|
if v_receipt ->> 'candidate_window_contract' is distinct from 'dated-v1' then
|
|
-- A partial new contract must never silently become a legacy same-day result.
|
|
if p_result.algorithm_version = 'rectification-v5-matrix-scoring-9'
|
|
or v_receipt ? 'candidate_window_contract' or v_receipt ? 'candidate_intervals'
|
|
or p_candidate.candidate_payload ? 'candidate_date'
|
|
or coalesce(p_inference ->> 'candidate_set_id', '') like 'dated-v1:%' then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
return null;
|
|
end if;
|
|
v_windows := v_receipt -> 'candidate_intervals';
|
|
if jsonb_typeof(v_windows) is distinct from 'array' or jsonb_array_length(v_windows) = 0
|
|
or v_windows is distinct from p_case.candidate_range -> 'candidate_intervals'
|
|
or jsonb_typeof(v_receipt -> 'candidate_timezone_offset') is distinct from 'number'
|
|
or (v_receipt ->> 'candidate_timezone_offset')::double precision is distinct from
|
|
(p_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision
|
|
or coalesce(v_receipt ->> 'candidate_timezone_id', '') is distinct from
|
|
coalesce(p_case.baseline_birth_snapshot ->> 'timezone_id', '')
|
|
or jsonb_typeof(p_inference -> 'candidates') is distinct from 'array'
|
|
or jsonb_typeof(p_inference -> 'credible_intervals') is distinct from 'array'
|
|
or p_inference ->> 'range_start' is distinct from p_case.candidate_range ->> 'start_time'
|
|
or p_inference ->> 'range_end' is distinct from p_case.candidate_range ->> 'end_time' then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
for v_window in select value from jsonb_array_elements(v_windows) loop
|
|
if coalesce(v_window ->> 'start_at', '') !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$'
|
|
or coalesce(v_window ->> 'end_at', '') !~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$' then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
v_start := (v_window ->> 'start_at')::timestamp;
|
|
v_end := (v_window ->> 'end_at')::timestamp;
|
|
if v_end < v_start or v_start <= v_previous then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
v_first := coalesce(v_first, v_start);
|
|
v_previous := v_end;
|
|
end loop;
|
|
select count(*), 'dated-v1:' || (p_case.candidate_range ->> 'start_time') || '-' ||
|
|
(p_case.candidate_range ->> 'end_time') || ':' || string_agg(
|
|
(candidate_payload ->> 'candidate_date') || 'T' || to_char(candidate_time, 'HH24:MI') || '@' ||
|
|
(candidate_payload ->> 'window_index') || '@' || (candidate_payload ->> 'window_offset_minutes') || '@' ||
|
|
(candidate_payload ->> 'segment_index'), ',' order by (candidate_payload ->> 'window_index')::integer)
|
|
into v_count, v_expected from public.agentic_rectification_candidates where result_id = p_result.id;
|
|
if v_count = 0 or jsonb_array_length(p_inference -> 'candidates') <> v_count
|
|
or p_inference ->> 'candidate_set_id' is distinct from v_expected
|
|
or (select count(distinct value ->> 'window_index') from jsonb_array_elements(p_inference -> 'candidates')) <> v_count then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
for v_item in select value from jsonb_array_elements(p_inference -> 'candidates') loop
|
|
select candidate_payload into v_payload from public.agentic_rectification_candidates
|
|
where result_id = p_result.id and to_char(candidate_time, 'HH24:MI') = v_item ->> 'time';
|
|
if not found or coalesce(v_item ->> 'candidate_date', '') !~ '^\d{4}-\d{2}-\d{2}$'
|
|
or coalesce(v_item ->> 'window_index', '') !~ '^\d+$'
|
|
or coalesce(v_item ->> 'window_offset_minutes', '') !~ '^\d+$'
|
|
or coalesce(v_item ->> 'segment_index', '') !~ '^\d+$'
|
|
or v_item -> 'candidate_date' is distinct from v_payload -> 'candidate_date'
|
|
or v_item -> 'window_index' is distinct from v_payload -> 'window_index'
|
|
or v_item -> 'window_offset_minutes' is distinct from v_payload -> 'window_offset_minutes'
|
|
or v_item -> 'segment_index' is distinct from v_payload -> 'segment_index'
|
|
or v_item -> 'cluster_intervals' is distinct from v_payload -> 'cluster_intervals'
|
|
or coalesce(v_item ->> 'status', '') not in ('active', 'equivalent', 'winner', 'eliminated')
|
|
or jsonb_typeof(v_item -> 'probability') is distinct from 'number'
|
|
or jsonb_typeof(v_item -> 'posterior_score') is distinct from 'number'
|
|
or jsonb_typeof(v_item -> 'cluster_intervals') is distinct from 'array'
|
|
or jsonb_array_length(v_item -> 'cluster_intervals') = 0 then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
v_at := ((v_item ->> 'candidate_date') || 'T' || (v_item ->> 'time'))::timestamp;
|
|
v_window := v_windows -> (v_item ->> 'segment_index')::integer;
|
|
if v_window is null or v_at < (v_window ->> 'start_at')::timestamp or v_at > (v_window ->> 'end_at')::timestamp
|
|
or extract(epoch from (v_at - v_first)) / 60 <> (v_item ->> 'window_offset_minutes')::integer then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
if not exists (select 1 from jsonb_array_elements(v_item -> 'cluster_intervals') x
|
|
where x -> 'segment_index' = v_item -> 'segment_index'
|
|
and (v_item ->> 'window_index')::integer between (x ->> 'start_index')::integer and (x ->> 'end_index')::integer
|
|
and v_at between (x ->> 'start_at')::timestamp and (x ->> 'end_at')::timestamp) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
for v_interval in select value from jsonb_array_elements(v_item -> 'cluster_intervals') loop
|
|
v_start := (v_interval ->> 'start_at')::timestamp;
|
|
v_end := (v_interval ->> 'end_at')::timestamp;
|
|
v_window := v_windows -> (v_interval ->> 'segment_index')::integer;
|
|
if v_start is null or v_end is null or v_window is null or v_end < v_start
|
|
or v_start < (v_window ->> 'start_at')::timestamp or v_end > (v_window ->> 'end_at')::timestamp
|
|
or (v_interval ->> 'start_offset_minutes')::integer is distinct from (extract(epoch from (v_start - v_first)) / 60)::integer
|
|
or (v_interval ->> 'end_offset_minutes')::integer is distinct from (extract(epoch from (v_end - v_first)) / 60)::integer
|
|
or coalesce(v_interval ->> 'start_index', '') !~ '^\d+$'
|
|
or coalesce(v_interval ->> 'end_index', '') !~ '^\d+$'
|
|
or (v_interval ->> 'end_index')::integer < (v_interval ->> 'start_index')::integer then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
end loop;
|
|
end loop;
|
|
-- Historical credible semantics envelope active clusters within each declared segment,
|
|
-- but never bridge separate declared segments.
|
|
select range_agg(tsrange(first_at, last_at + interval '1 minute', '[)')),
|
|
jsonb_agg(jsonb_build_object('segment_index', segment_index,
|
|
'start_index', first_index, 'end_index', last_index,
|
|
'start_offset_minutes', (extract(epoch from (first_at - v_first)) / 60)::integer,
|
|
'end_offset_minutes', (extract(epoch from (last_at - v_first)) / 60)::integer,
|
|
'start_at', to_char(first_at, 'YYYY-MM-DD"T"HH24:MI'),
|
|
'end_at', to_char(last_at, 'YYYY-MM-DD"T"HH24:MI')) order by first_index)
|
|
into v_union, v_expected_credible
|
|
from (
|
|
select (part ->> 'segment_index')::integer as segment_index,
|
|
min((part ->> 'start_index')::integer) as first_index,
|
|
max((part ->> 'end_index')::integer) as last_index,
|
|
min((part ->> 'start_at')::timestamp) as first_at,
|
|
max((part ->> 'end_at')::timestamp) as last_at
|
|
from jsonb_array_elements(p_inference -> 'candidates') candidate,
|
|
lateral jsonb_array_elements(candidate -> 'cluster_intervals') part
|
|
where candidate ->> 'status' <> 'eliminated'
|
|
and (select (value ->> 'posterior_score')::numeric from jsonb_array_elements(p_inference -> 'candidates')
|
|
where value ->> 'status' <> 'eliminated'
|
|
order by (value ->> 'probability')::numeric desc, (value ->> 'posterior_score')::numeric desc,
|
|
(value ->> 'window_index')::integer limit 1) - (candidate ->> 'posterior_score')::numeric < 8
|
|
group by (part ->> 'segment_index')::integer
|
|
) segments;
|
|
for v_interval in select value from jsonb_array_elements(p_inference -> 'credible_intervals') loop
|
|
v_start := (v_interval ->> 'start_at')::timestamp;
|
|
v_end := (v_interval ->> 'end_at')::timestamp;
|
|
if v_start is null or v_end is null or v_end < v_start then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
v_credible_ranges := array_append(v_credible_ranges, tsrange(v_start, v_end + interval '1 minute', '[)'));
|
|
end loop;
|
|
select range_agg(x) into v_credible from unnest(v_credible_ranges) x;
|
|
select value ->> 'time' into v_top from jsonb_array_elements(p_inference -> 'candidates')
|
|
where value ->> 'status' <> 'eliminated'
|
|
order by (value ->> 'probability')::numeric desc, (value ->> 'posterior_score')::numeric desc,
|
|
(value ->> 'window_index')::integer limit 1;
|
|
if v_union is null or v_credible is distinct from v_union
|
|
or p_inference -> 'credible_intervals' is distinct from v_expected_credible
|
|
or p_inference ->> 'representative_time' is distinct from v_top
|
|
or not exists (select 1 from jsonb_array_elements(p_inference -> 'candidates')
|
|
where value ->> 'time' = to_char(p_candidate.candidate_time, 'HH24:MI') and value ->> 'status' <> 'eliminated') then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent';
|
|
end if;
|
|
v_date := (p_candidate.candidate_payload ->> 'candidate_date')::date;
|
|
if v_date is null then raise exception 'agentic_rectification_candidate_state_inconsistent'; end if;
|
|
return v_date;
|
|
end;
|
|
$$;
|
|
revoke all on function public.validate_dated_rectification_candidate(public.agentic_rectification_cases, public.agentic_rectification_results, public.agentic_rectification_candidates, jsonb) from public, anon, authenticated, service_role;
|
|
|
|
create or replace function public.accept_agentic_rectification_candidate_for_case_v2(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_result_id uuid,
|
|
p_candidate_id uuid,
|
|
p_request_id uuid,
|
|
p_credible_range jsonb default null
|
|
)
|
|
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_candidate public.agentic_rectification_candidates%rowtype;
|
|
v_profile public.profiles%rowtype;
|
|
v_snapshot jsonb;
|
|
v_inference jsonb;
|
|
v_expected_candidate_set_id text;
|
|
v_persisted_candidate_count integer;
|
|
v_top_active_time text;
|
|
v_active_range_start text;
|
|
v_active_range_end text;
|
|
v_latest_transition public.agentic_rectification_inference_transitions%rowtype;
|
|
v_existing_decision public.agentic_rectification_candidate_decisions%rowtype;
|
|
v_response jsonb;
|
|
v_adopted jsonb;
|
|
v_adopted_start text;
|
|
v_adopted_end text;
|
|
v_adopted_rep text;
|
|
v_adopted_width integer;
|
|
v_saved_date date;
|
|
v_provenance jsonb;
|
|
v_saved_offset double precision;
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_result_id is null
|
|
or p_candidate_id is null or p_request_id is null then
|
|
raise exception 'agentic_rectification_candidate_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || p_request_id::text, 0)
|
|
);
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
select * into v_candidate
|
|
from public.agentic_rectification_candidates
|
|
where id = p_candidate_id
|
|
and result_id = p_result_id
|
|
and user_id = p_user_id
|
|
and case_id = p_case_id;
|
|
if not found then
|
|
raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_existing_decision
|
|
from public.agentic_rectification_candidate_decisions
|
|
where user_id = p_user_id and request_id = p_request_id
|
|
for update;
|
|
if found then
|
|
if v_existing_decision.decision_kind <> 'accept'
|
|
or v_existing_decision.case_id <> p_case_id
|
|
or v_existing_decision.result_id <> p_result_id
|
|
or v_existing_decision.candidate_id <> p_candidate_id then
|
|
raise exception 'agentic_rectification_candidate_request_conflict' using errcode = 'P0001';
|
|
end if;
|
|
return jsonb_set(v_existing_decision.response, '{idempotent}', 'true'::jsonb, true);
|
|
end if;
|
|
|
|
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
|
|
raise exception 'agentic_rectification_case_terminal' 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.display_allowed or not v_result.selection_allowed then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked' 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;
|
|
|
|
select * into v_profile
|
|
from public.profiles
|
|
where id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_snapshot := v_case.baseline_birth_snapshot;
|
|
if 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.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.declared_window_start is distinct from v_snapshot ->> 'declared_window_start'
|
|
or v_profile.declared_window_end is distinct from v_snapshot ->> 'declared_window_end'
|
|
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_id is distinct from v_snapshot ->> 'timezone_id'
|
|
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;
|
|
|
|
-- Adoption is a trust-boundary write. Structured answers append inference
|
|
-- transitions instead of mutating the engine result, so the latest transition
|
|
-- is authoritative when present; persisted display candidates remain a cache.
|
|
select * into v_latest_transition
|
|
from public.agentic_rectification_inference_transitions
|
|
where case_id = p_case_id
|
|
and result_id = v_result.id
|
|
order by revision desc
|
|
limit 1;
|
|
v_inference := case
|
|
when v_latest_transition.id is not null then v_latest_transition.inference_state
|
|
else v_result.decision_receipt -> 'inference_state'
|
|
end;
|
|
if v_inference is null
|
|
or jsonb_typeof(v_inference) <> 'object'
|
|
or jsonb_typeof(v_inference -> 'candidates') <> 'array'
|
|
or jsonb_array_length(v_inference -> 'candidates') = 0
|
|
or jsonb_typeof(v_inference -> 'revision') is distinct from 'number'
|
|
or coalesce((v_inference ->> 'revision') !~ '^[0-9]+$', true)
|
|
or length(btrim(coalesce(v_inference ->> 'candidate_set_id', ''))) = 0
|
|
or (v_inference ->> 'range_start') is distinct from v_case.candidate_range ->> 'start_time'
|
|
or (v_inference ->> 'range_end') is distinct from v_case.candidate_range ->> 'end_time' then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
if v_latest_transition.id is not null
|
|
and (
|
|
v_latest_transition.revision is distinct from (v_inference ->> 'revision')::integer
|
|
or v_latest_transition.candidate_set_id is distinct from v_inference ->> 'candidate_set_id'
|
|
) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_saved_date := public.validate_dated_rectification_candidate(v_case, v_result, v_candidate, v_inference);
|
|
if v_saved_date is null then
|
|
select count(*),
|
|
(v_case.candidate_range ->> 'start_time') || '-' ||
|
|
(v_case.candidate_range ->> 'end_time') || ':' ||
|
|
string_agg(pg_catalog.to_char(candidate_time, 'HH24:MI'), ',' order by candidate_time)
|
|
into v_persisted_candidate_count, v_expected_candidate_set_id
|
|
from public.agentic_rectification_candidates
|
|
where result_id = v_result.id
|
|
and user_id = p_user_id
|
|
and case_id = p_case_id;
|
|
|
|
if v_persisted_candidate_count = 0
|
|
or jsonb_array_length(v_inference -> 'candidates') <> v_persisted_candidate_count
|
|
or (v_inference ->> 'candidate_set_id') is distinct from v_expected_candidate_set_id
|
|
or exists (
|
|
select 1
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where jsonb_typeof(item.value) <> 'object'
|
|
) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where coalesce((item.value ->> 'time') !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
|
|
or coalesce(item.value ->> 'status', '') not in ('active', 'equivalent', 'winner', 'eliminated')
|
|
or jsonb_typeof(item.value -> 'probability') is distinct from 'number'
|
|
or jsonb_typeof(item.value -> 'posterior_score') is distinct from 'number'
|
|
or jsonb_typeof(item.value -> 'cluster_range') is distinct from 'array'
|
|
) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where jsonb_array_length(item.value -> 'cluster_range') is distinct from 2
|
|
or coalesce((item.value -> 'cluster_range' ->> 0) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
|
|
or coalesce((item.value -> 'cluster_range' ->> 1) !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$', true)
|
|
or item.value -> 'cluster_range' ->> 0 > item.value ->> 'time'
|
|
or item.value -> 'cluster_range' ->> 1 < item.value ->> 'time'
|
|
or not exists (
|
|
select 1
|
|
from public.agentic_rectification_candidates persisted
|
|
where persisted.result_id = v_result.id
|
|
and pg_catalog.to_char(persisted.candidate_time, 'HH24:MI') = item.value ->> 'time'
|
|
)
|
|
) or (
|
|
select count(distinct item.value ->> 'time')
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
) <> v_persisted_candidate_count then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select item.value ->> 'time'
|
|
into v_top_active_time
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where item.value ->> 'status' <> 'eliminated'
|
|
order by (item.value ->> 'probability')::numeric desc,
|
|
(item.value ->> 'posterior_score')::numeric desc,
|
|
item.value ->> 'time'
|
|
limit 1;
|
|
|
|
select min(item.value -> 'cluster_range' ->> 0),
|
|
max(item.value -> 'cluster_range' ->> 1)
|
|
into v_active_range_start, v_active_range_end
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where item.value ->> 'status' <> 'eliminated';
|
|
|
|
if v_top_active_time is null
|
|
or (v_inference ->> 'representative_time') is distinct from v_top_active_time
|
|
or jsonb_typeof(v_inference -> 'credible_range') <> 'array'
|
|
or jsonb_array_length(v_inference -> 'credible_range') <> 2
|
|
or (v_inference -> 'credible_range' ->> 0) is distinct from v_active_range_start
|
|
or (v_inference -> 'credible_range' ->> 1) is distinct from v_active_range_end
|
|
or not exists (
|
|
select 1
|
|
from pg_catalog.jsonb_array_elements(v_inference -> 'candidates') as item(value)
|
|
where item.value ->> 'time' = pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI')
|
|
and item.value ->> 'status' <> 'eliminated'
|
|
) then
|
|
raise exception 'agentic_rectification_candidate_state_inconsistent' using errcode = 'P0001';
|
|
end if;
|
|
|
|
else
|
|
v_top_active_time := v_inference ->> 'representative_time';
|
|
v_saved_offset := (v_result.decision_receipt ->> 'candidate_timezone_offset')::double precision;
|
|
v_provenance := jsonb_build_object('contract', 'dated-v1', 'case_id', p_case_id,
|
|
'result_id', p_result_id, 'candidate_id', p_candidate_id,
|
|
'declared_birth_date', v_profile.birth_date, 'candidate_date', v_saved_date,
|
|
'candidate_time', to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'timezone_offset', v_saved_offset, 'timezone_id', v_result.decision_receipt ->> 'candidate_timezone_id',
|
|
'source', 'persisted_candidate_calculation', 'adopted_at', pg_catalog.now());
|
|
end if;
|
|
|
|
if v_result.selected_candidate_id is not null then
|
|
if v_result.selected_candidate_id = p_candidate_id
|
|
and v_result.selected_time is not distinct from v_candidate.candidate_time
|
|
and v_case.accepted_time is not distinct from v_candidate.candidate_time
|
|
and v_result.selection_kind is not distinct from 'user_accepted'
|
|
and v_profile.active_birth_time is not distinct from v_candidate.candidate_time
|
|
and v_profile.birth_time_status is not distinct from 'accepted'
|
|
and v_profile.active_birth_date is not distinct from v_saved_date then
|
|
v_response := jsonb_build_object(
|
|
'success', true,
|
|
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'saved_date', coalesce(v_saved_date, v_profile.birth_date),
|
|
'day_offset', coalesce(v_saved_date - v_profile.birth_date, 0),
|
|
'date_changed', coalesce(v_saved_date <> v_profile.birth_date, false),
|
|
'saved_timezone_offset', coalesce(v_saved_offset, v_profile.timezone_offset),
|
|
'status', 'accepted',
|
|
'result_id', v_result.id,
|
|
'candidate_id', v_candidate.id,
|
|
'case_status', 'candidate_accepted',
|
|
'idempotent', true
|
|
);
|
|
insert into public.agentic_rectification_candidate_decisions (
|
|
user_id, case_id, result_id, candidate_id, request_id, decision_kind, response
|
|
) values (
|
|
p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'accept', v_response
|
|
);
|
|
return v_response;
|
|
end if;
|
|
|
|
if v_case.status is distinct from 'candidate_accepted'
|
|
or v_result.selection_kind is distinct from 'user_accepted'
|
|
or v_profile.active_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_selection_blocked' using errcode = 'P0001';
|
|
end if;
|
|
else
|
|
if v_result.selected_time is not null then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
|
|
end if;
|
|
end if;
|
|
|
|
update public.profiles
|
|
set active_birth_time = v_candidate.candidate_time,
|
|
active_birth_date = v_saved_date,
|
|
active_birth_timezone_offset = v_saved_offset,
|
|
active_birth_provenance = v_provenance,
|
|
birth_time = v_candidate.candidate_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_candidate_id = p_candidate_id,
|
|
selected_time = v_candidate.candidate_time,
|
|
selection_kind = 'user_accepted',
|
|
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;
|
|
|
|
v_adopted := null;
|
|
if p_credible_range is not null
|
|
and jsonb_typeof(p_credible_range) = 'object' then
|
|
v_adopted_start := nullif(btrim(coalesce(p_credible_range->>'start_time', '')), '');
|
|
v_adopted_end := nullif(btrim(coalesce(p_credible_range->>'end_time', '')), '');
|
|
v_adopted_rep := nullif(btrim(coalesce(p_credible_range->>'representative_time', '')), '');
|
|
begin
|
|
v_adopted_width := (p_credible_range->>'width_minutes')::integer;
|
|
exception when others then
|
|
v_adopted_width := null;
|
|
end;
|
|
end if;
|
|
if v_adopted_start is null or v_adopted_end is null
|
|
or v_adopted_start !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
or v_adopted_end !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
|
|
v_adopted_start := v_inference -> 'credible_range' ->> 0;
|
|
v_adopted_end := v_inference -> 'credible_range' ->> 1;
|
|
v_adopted_rep := v_inference ->> 'representative_time';
|
|
v_adopted_width := null;
|
|
end if;
|
|
if v_adopted_start is not null
|
|
and v_adopted_end is not null
|
|
and v_adopted_start ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
and v_adopted_end ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
|
|
v_adopted := jsonb_strip_nulls(jsonb_build_object(
|
|
'start_time', v_adopted_start,
|
|
'end_time', v_adopted_end,
|
|
'representative_time', coalesce(v_adopted_rep, v_top_active_time),
|
|
'width_minutes', v_adopted_width,
|
|
'source', 'inference_credible_range'
|
|
));
|
|
end if;
|
|
|
|
if v_saved_date is not null then
|
|
v_adopted := jsonb_build_object(
|
|
'candidate_window_contract', 'dated-v1',
|
|
'intervals', v_inference -> 'credible_intervals',
|
|
'representative_time', to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'representative_date', v_saved_date,
|
|
'source', 'inference_credible_range');
|
|
if jsonb_array_length(v_inference -> 'credible_intervals') = 1 then
|
|
v_adopted := v_adopted || jsonb_build_object(
|
|
'start_time', substring(v_inference -> 'credible_intervals' -> 0 ->> 'start_at' from 12 for 5),
|
|
'end_time', substring(v_inference -> 'credible_intervals' -> 0 ->> 'end_at' from 12 for 5));
|
|
end if;
|
|
end if;
|
|
|
|
update public.agentic_rectification_cases
|
|
set status = 'candidate_accepted',
|
|
accepted_time = v_candidate.candidate_time,
|
|
confirmed_time = null,
|
|
completed_at = null,
|
|
adopted_credible_range = coalesce(v_adopted, adopted_credible_range),
|
|
last_activity_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where id = v_case.id;
|
|
|
|
v_response := jsonb_build_object(
|
|
'success', true,
|
|
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'saved_date', coalesce(v_saved_date, v_profile.birth_date),
|
|
'day_offset', coalesce(v_saved_date - v_profile.birth_date, 0),
|
|
'date_changed', coalesce(v_saved_date <> v_profile.birth_date, false),
|
|
'saved_timezone_offset', coalesce(v_saved_offset, v_profile.timezone_offset),
|
|
'status', 'accepted',
|
|
'result_id', v_result.id,
|
|
'candidate_id', v_candidate.id,
|
|
'case_status', 'candidate_accepted',
|
|
'idempotent', false
|
|
);
|
|
|
|
insert into public.agentic_rectification_candidate_decisions (
|
|
user_id, case_id, result_id, candidate_id, request_id, decision_kind, response
|
|
) values (
|
|
p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'accept', v_response
|
|
);
|
|
|
|
return v_response;
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, jsonb)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, jsonb)
|
|
to service_role;
|
|
|
|
create or replace function public.confirm_agentic_rectification_candidate_for_case_v2(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_result_id uuid,
|
|
p_candidate_id uuid,
|
|
p_request_id uuid,
|
|
p_consent_quote text,
|
|
p_source_turn_id uuid
|
|
)
|
|
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_candidate public.agentic_rectification_candidates%rowtype;
|
|
v_profile public.profiles%rowtype;
|
|
v_turn public.agentic_rectification_turns%rowtype;
|
|
v_snapshot jsonb;
|
|
v_existing_decision public.agentic_rectification_candidate_decisions%rowtype;
|
|
v_response jsonb;
|
|
v_saved_date date;
|
|
v_saved_offset double precision;
|
|
v_provenance jsonb;
|
|
v_inference jsonb;
|
|
begin
|
|
if p_user_id is null or p_case_id is null or p_result_id is null
|
|
or p_candidate_id is null or p_request_id is null or p_source_turn_id is null
|
|
or length(btrim(coalesce(p_consent_quote, ''))) = 0 then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
|
|
perform pg_catalog.pg_advisory_xact_lock(
|
|
pg_catalog.hashtextextended(p_user_id::text || ':' || p_request_id::text, 0)
|
|
);
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
select * into v_candidate
|
|
from public.agentic_rectification_candidates
|
|
where id = p_candidate_id
|
|
and result_id = p_result_id
|
|
and user_id = p_user_id
|
|
and case_id = p_case_id;
|
|
if not found then
|
|
raise exception 'agentic_rectification_candidate_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_existing_decision
|
|
from public.agentic_rectification_candidate_decisions
|
|
where user_id = p_user_id and request_id = p_request_id
|
|
for update;
|
|
if found then
|
|
if v_existing_decision.decision_kind <> 'confirm'
|
|
or v_existing_decision.case_id <> p_case_id
|
|
or v_existing_decision.result_id <> p_result_id
|
|
or v_existing_decision.candidate_id <> p_candidate_id then
|
|
raise exception 'agentic_rectification_candidate_request_conflict' using errcode = 'P0001';
|
|
end if;
|
|
return jsonb_set(v_existing_decision.response, '{idempotent}', 'true'::jsonb, true);
|
|
end if;
|
|
|
|
select inference_state into v_inference from public.agentic_rectification_inference_transitions
|
|
where case_id = p_case_id and result_id = p_result_id order by revision desc limit 1;
|
|
v_inference := coalesce(v_inference, v_result.decision_receipt -> 'inference_state');
|
|
v_saved_date := public.validate_dated_rectification_candidate(v_case, v_result, v_candidate, v_inference);
|
|
if v_saved_date is not null then
|
|
v_saved_offset := (v_result.decision_receipt ->> 'candidate_timezone_offset')::double precision;
|
|
v_provenance := jsonb_build_object('contract', 'dated-v1', 'case_id', p_case_id,
|
|
'result_id', p_result_id, 'candidate_id', p_candidate_id,
|
|
'declared_birth_date', v_case.baseline_birth_snapshot ->> 'birth_date',
|
|
'candidate_date', v_saved_date, 'candidate_time', to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'timezone_offset', v_saved_offset, 'timezone_id', v_result.decision_receipt ->> 'candidate_timezone_id',
|
|
'source', 'persisted_candidate_calculation', 'adopted_at', pg_catalog.now());
|
|
end if;
|
|
|
|
if v_case.status = 'confirmed' then
|
|
if v_result.selected_candidate_id is distinct from p_candidate_id
|
|
or v_case.confirmed_time is distinct from v_candidate.candidate_time then
|
|
raise exception 'agentic_rectification_case_already_confirmed' using errcode = 'P0001';
|
|
end if;
|
|
v_response := jsonb_build_object(
|
|
'success', true,
|
|
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'saved_date', coalesce(v_saved_date, (v_case.baseline_birth_snapshot ->> 'birth_date')::date),
|
|
'day_offset', coalesce(v_saved_date - (v_case.baseline_birth_snapshot ->> 'birth_date')::date, 0),
|
|
'date_changed', coalesce(v_saved_date <> (v_case.baseline_birth_snapshot ->> 'birth_date')::date, false),
|
|
'saved_timezone_offset', coalesce(v_saved_offset, (v_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision),
|
|
'status', 'confirmed',
|
|
'result_id', v_result.id,
|
|
'candidate_id', v_candidate.id,
|
|
'case_status', 'confirmed',
|
|
'idempotent', true
|
|
);
|
|
insert into public.agentic_rectification_candidate_decisions (
|
|
user_id, case_id, result_id, candidate_id, request_id, decision_kind, response
|
|
) values (
|
|
p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'confirm', v_response
|
|
);
|
|
return v_response;
|
|
end if;
|
|
if v_case.status in ('closed', 'abandoned', 'superseded') then
|
|
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_turn
|
|
from public.agentic_rectification_turns
|
|
where id = p_source_turn_id and case_id = p_case_id;
|
|
if not found then
|
|
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
|
|
end if;
|
|
if v_turn.user_message is null
|
|
or position(
|
|
public.agentic_rectification_normalize_quote(p_consent_quote)
|
|
in public.agentic_rectification_normalize_quote(v_turn.user_message)
|
|
) = 0 then
|
|
raise exception 'agentic_rectification_consent_not_grounded' 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.confirmation_allowed then
|
|
raise exception 'agentic_rectification_confirmation_blocked' using errcode = 'P0001';
|
|
end if;
|
|
if not v_candidate.is_representative
|
|
or v_result.representative_time is distinct from v_candidate.candidate_time then
|
|
raise exception 'agentic_rectification_confirmation_exact_gate_blocked' 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;
|
|
|
|
select * into v_profile
|
|
from public.profiles
|
|
where id = p_user_id
|
|
for update;
|
|
if not found then
|
|
raise exception 'agentic_rectification_candidate_profile_changed' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_snapshot := v_case.baseline_birth_snapshot;
|
|
if 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.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.declared_window_start is distinct from v_snapshot ->> 'declared_window_start'
|
|
or v_profile.declared_window_end is distinct from v_snapshot ->> 'declared_window_end'
|
|
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_id is distinct from v_snapshot ->> 'timezone_id'
|
|
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 v_result.selected_candidate_id is null then
|
|
if v_result.selected_time is not null then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
|
|
end if;
|
|
v_snapshot := v_case.baseline_birth_snapshot;
|
|
if 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;
|
|
elsif v_result.selected_candidate_id <> p_candidate_id
|
|
or v_result.selected_time is distinct from v_candidate.candidate_time
|
|
or v_result.selection_kind is distinct from 'user_accepted'
|
|
or v_case.status is distinct from 'candidate_accepted'
|
|
or v_case.accepted_time is distinct from v_candidate.candidate_time
|
|
or v_profile.active_birth_time is distinct from v_candidate.candidate_time
|
|
or v_profile.birth_time_status is distinct from 'accepted' then
|
|
raise exception 'agentic_rectification_candidate_selection_blocked' using errcode = 'P0001';
|
|
end if;
|
|
|
|
update public.profiles
|
|
set active_birth_time = v_candidate.candidate_time,
|
|
active_birth_date = v_saved_date,
|
|
active_birth_timezone_offset = v_saved_offset,
|
|
active_birth_provenance = v_provenance,
|
|
birth_time = v_candidate.candidate_time,
|
|
birth_time_status = 'confirmed',
|
|
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_candidate_id = p_candidate_id,
|
|
selected_time = v_candidate.candidate_time,
|
|
selection_kind = 'engine_confirmed',
|
|
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 = 'confirmed',
|
|
accepted_time = v_candidate.candidate_time,
|
|
confirmed_time = v_candidate.candidate_time,
|
|
completed_at = pg_catalog.now(),
|
|
last_activity_at = pg_catalog.now(),
|
|
updated_at = pg_catalog.now()
|
|
where id = v_case.id;
|
|
|
|
v_response := jsonb_build_object(
|
|
'success', true,
|
|
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'saved_date', coalesce(v_saved_date, (v_case.baseline_birth_snapshot ->> 'birth_date')::date),
|
|
'day_offset', coalesce(v_saved_date - (v_case.baseline_birth_snapshot ->> 'birth_date')::date, 0),
|
|
'date_changed', coalesce(v_saved_date <> (v_case.baseline_birth_snapshot ->> 'birth_date')::date, false),
|
|
'saved_timezone_offset', coalesce(v_saved_offset, (v_case.baseline_birth_snapshot ->> 'timezone_offset')::double precision),
|
|
'status', 'confirmed',
|
|
'result_id', v_result.id,
|
|
'candidate_id', v_candidate.id,
|
|
'case_status', 'confirmed',
|
|
'idempotent', false
|
|
);
|
|
|
|
insert into public.agentic_rectification_candidate_decisions (
|
|
user_id, case_id, result_id, candidate_id, request_id, decision_kind, response
|
|
) values (
|
|
p_user_id, p_case_id, p_result_id, p_candidate_id, p_request_id, 'confirm', v_response
|
|
);
|
|
|
|
return v_response;
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.confirm_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, text, uuid)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.confirm_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid, text, uuid)
|
|
to service_role;
|
|
|
|
create or replace function public.read_report_candidate_range(
|
|
p_user_id uuid,
|
|
p_rectification_case_id uuid default null
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
stable
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_start text;
|
|
v_end text;
|
|
v_range jsonb;
|
|
begin
|
|
if p_user_id is null then
|
|
return null;
|
|
end if;
|
|
|
|
if p_rectification_case_id is not null then
|
|
select to_char(c.candidate_start, 'HH24:MI'), to_char(c.candidate_end, 'HH24:MI')
|
|
into v_start, v_end
|
|
from public.birth_time_rectification_cases as c
|
|
where c.id = p_rectification_case_id
|
|
and c.user_id = p_user_id
|
|
and c.status in ('confirmed', 'completed')
|
|
and c.candidate_start is not null
|
|
and c.candidate_end is not null;
|
|
if found then
|
|
return jsonb_build_object('start_time', v_start, 'end_time', v_end);
|
|
end if;
|
|
end if;
|
|
|
|
select case
|
|
when c.adopted_credible_range ->> 'candidate_window_contract' = 'dated-v1' then c.adopted_credible_range
|
|
when c.adopted_credible_range is not null
|
|
and nullif(btrim(coalesce(c.adopted_credible_range->>'start_time', '')), '') is not null
|
|
and nullif(btrim(coalesce(c.adopted_credible_range->>'end_time', '')), '') is not null
|
|
then c.adopted_credible_range
|
|
else c.candidate_range
|
|
end
|
|
into v_range
|
|
from public.agentic_rectification_cases as c
|
|
where c.user_id = p_user_id
|
|
and c.status = 'candidate_accepted'
|
|
order by c.updated_at desc
|
|
limit 1;
|
|
|
|
if v_range is null then
|
|
return null;
|
|
end if;
|
|
|
|
if v_range ->> 'candidate_window_contract' = 'dated-v1' then
|
|
return jsonb_build_object('candidate_intervals', v_range -> 'intervals');
|
|
end if;
|
|
|
|
v_start := nullif(btrim(coalesce(v_range->>'start_time', '')), '');
|
|
v_end := nullif(btrim(coalesce(v_range->>'end_time', '')), '');
|
|
if v_start is null or v_end is null then
|
|
return null;
|
|
end if;
|
|
|
|
return jsonb_build_object('start_time', v_start, 'end_time', v_end);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.read_report_candidate_range(uuid, uuid)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.read_report_candidate_range(uuid, uuid)
|
|
to service_role;
|
|
|
|
|
|
commit;
|