1130 lines
46 KiB
PL/PgSQL
1130 lines
46 KiB
PL/PgSQL
-- PR-4: server-owned event/decision contract persistence and candidate UUID boundary.
|
|
-- Forward-only: historical candidate migrations and RPC definitions remain unchanged,
|
|
-- while service_role is moved to explicit V2 entry points that never accept HH:MM
|
|
-- for candidate acceptance or confirmation.
|
|
|
|
begin;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 1. Durable decision contract fields on candidate results
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists event_contract_version text;
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists decision_policy_version text;
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists decision_receipt jsonb;
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists execution_ledger jsonb;
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists display_allowed boolean not null default false;
|
|
alter table public.agentic_rectification_results
|
|
add column if not exists selected_candidate_id uuid;
|
|
|
|
alter table public.agentic_rectification_results
|
|
drop constraint if exists agentic_rectification_results_event_contract_version_check;
|
|
alter table public.agentic_rectification_results
|
|
add constraint agentic_rectification_results_event_contract_version_check check (
|
|
event_contract_version is null
|
|
or length(btrim(event_contract_version)) between 1 and 120
|
|
);
|
|
|
|
alter table public.agentic_rectification_results
|
|
drop constraint if exists agentic_rectification_results_decision_policy_version_check;
|
|
alter table public.agentic_rectification_results
|
|
add constraint agentic_rectification_results_decision_policy_version_check check (
|
|
decision_policy_version is null
|
|
or length(btrim(decision_policy_version)) between 1 and 120
|
|
);
|
|
|
|
alter table public.agentic_rectification_results
|
|
drop constraint if exists agentic_rectification_results_decision_receipt_check;
|
|
alter table public.agentic_rectification_results
|
|
add constraint agentic_rectification_results_decision_receipt_check check (
|
|
decision_receipt is null
|
|
or case
|
|
when jsonb_typeof(decision_receipt) = 'object'
|
|
and jsonb_typeof(decision_receipt -> 'display_allowed') = 'boolean'
|
|
and jsonb_typeof(decision_receipt -> 'accept_allowed') = 'boolean'
|
|
and jsonb_typeof(decision_receipt -> 'confirm_allowed') = 'boolean'
|
|
then display_allowed = (decision_receipt ->> 'display_allowed')::boolean
|
|
and selection_allowed = (decision_receipt ->> 'accept_allowed')::boolean
|
|
and confirmation_allowed = (decision_receipt ->> 'confirm_allowed')::boolean
|
|
else false
|
|
end
|
|
);
|
|
|
|
alter table public.agentic_rectification_results
|
|
drop constraint if exists agentic_rectification_results_execution_ledger_check;
|
|
alter table public.agentic_rectification_results
|
|
add constraint agentic_rectification_results_execution_ledger_check check (
|
|
execution_ledger is null or jsonb_typeof(execution_ledger) = 'array'
|
|
);
|
|
|
|
create unique index if not exists agentic_rectification_results_candidate_owner_unique
|
|
on public.agentic_rectification_results (id, user_id, case_id);
|
|
|
|
create index if not exists agentic_rectification_results_v2_cache_idx
|
|
on public.agentic_rectification_results (
|
|
case_id,
|
|
evidence_ledger_fingerprint,
|
|
candidate_range_fingerprint,
|
|
skill_version,
|
|
algorithm_version,
|
|
event_contract_version,
|
|
decision_policy_version,
|
|
created_at desc
|
|
)
|
|
where invalidated_at is null and event_contract_version is not null;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 2. Server-issued candidate UUIDs and request-id decision ledger
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create table if not exists public.agentic_rectification_candidates (
|
|
id uuid primary key default gen_random_uuid(),
|
|
result_id uuid not null,
|
|
user_id uuid not null,
|
|
case_id uuid not null,
|
|
ordinal integer not null check (ordinal > 0),
|
|
candidate_time time without time zone not null,
|
|
candidate_payload jsonb not null check (jsonb_typeof(candidate_payload) = 'object'),
|
|
is_representative boolean not null default false,
|
|
created_at timestamptz not null default pg_catalog.now(),
|
|
foreign key (result_id, user_id, case_id)
|
|
references public.agentic_rectification_results (id, user_id, case_id)
|
|
on delete cascade,
|
|
unique (result_id, ordinal),
|
|
unique (result_id, candidate_time)
|
|
);
|
|
|
|
create unique index if not exists agentic_rectification_candidates_owner_unique
|
|
on public.agentic_rectification_candidates (id, result_id, user_id, case_id);
|
|
create index if not exists agentic_rectification_candidates_case_result_idx
|
|
on public.agentic_rectification_candidates (case_id, result_id, ordinal);
|
|
create unique index if not exists agentic_rectification_candidates_one_representative_idx
|
|
on public.agentic_rectification_candidates (result_id)
|
|
where is_representative;
|
|
|
|
alter table public.agentic_rectification_results
|
|
drop constraint if exists agentic_rectification_results_selected_candidate_fk;
|
|
alter table public.agentic_rectification_results
|
|
add constraint agentic_rectification_results_selected_candidate_fk
|
|
foreign key (selected_candidate_id)
|
|
references public.agentic_rectification_candidates(id)
|
|
on delete set null;
|
|
|
|
create table if not exists public.agentic_rectification_candidate_decisions (
|
|
id uuid primary key default gen_random_uuid(),
|
|
user_id uuid not null,
|
|
case_id uuid not null,
|
|
result_id uuid not null,
|
|
candidate_id uuid not null,
|
|
request_id uuid not null,
|
|
decision_kind text not null check (decision_kind in ('accept', 'confirm')),
|
|
response jsonb not null check (jsonb_typeof(response) = 'object'),
|
|
created_at timestamptz not null default pg_catalog.now(),
|
|
foreign key (candidate_id, result_id, user_id, case_id)
|
|
references public.agentic_rectification_candidates (id, result_id, user_id, case_id),
|
|
unique (user_id, request_id)
|
|
);
|
|
|
|
create index if not exists agentic_rectification_candidate_decisions_case_idx
|
|
on public.agentic_rectification_candidate_decisions (case_id, created_at desc);
|
|
|
|
alter table public.agentic_rectification_candidates enable row level security;
|
|
alter table public.agentic_rectification_candidate_decisions enable row level security;
|
|
|
|
revoke all on table public.agentic_rectification_candidates
|
|
from public, anon, authenticated, service_role;
|
|
revoke all on table public.agentic_rectification_candidate_decisions
|
|
from public, anon, authenticated, service_role;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 3. V2 candidate persistence
|
|
--
|
|
-- Booleans and presentation/decision fields are derived from decision_receipt.
|
|
-- The caller cannot provide them as independent function arguments. Candidate
|
|
-- UUIDs are generated here and caller-provided candidate_id fields are removed.
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create or replace function public.persist_agentic_rectification_candidate_v2(
|
|
p_user_id uuid,
|
|
p_case_id uuid,
|
|
p_engine_result_id text,
|
|
p_evidence_ledger_fingerprint text,
|
|
p_candidate_range_fingerprint text,
|
|
p_skill_version text,
|
|
p_algorithm_version text,
|
|
p_event_contract_version text,
|
|
p_decision_policy_version text,
|
|
p_candidate_range jsonb,
|
|
p_candidates jsonb,
|
|
p_decision_receipt jsonb,
|
|
p_execution_ledger jsonb
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_cached public.agentic_rectification_results%rowtype;
|
|
v_snapshot jsonb;
|
|
v_result_id uuid;
|
|
v_candidate jsonb;
|
|
v_candidate_id uuid;
|
|
v_candidate_time time without time zone;
|
|
v_candidate_time_text text;
|
|
v_candidate_ordinal integer;
|
|
v_saved_candidate jsonb;
|
|
v_saved_candidates jsonb := '[]'::jsonb;
|
|
v_seen_times text[] := array[]::text[];
|
|
v_display_allowed boolean;
|
|
v_selection_allowed boolean;
|
|
v_confirmation_allowed boolean;
|
|
v_overall_confidence text;
|
|
v_margin_percent numeric;
|
|
v_representative_time_text text;
|
|
v_representative_time time without time zone;
|
|
v_representative_candidate_id uuid;
|
|
v_representative_count integer := 0;
|
|
v_saved_decision_receipt jsonb;
|
|
begin
|
|
if p_user_id is null or p_case_id is null
|
|
or length(btrim(coalesce(p_engine_result_id, ''))) = 0
|
|
or length(btrim(coalesce(p_evidence_ledger_fingerprint, ''))) = 0
|
|
or length(btrim(coalesce(p_candidate_range_fingerprint, ''))) = 0
|
|
or length(btrim(coalesce(p_skill_version, ''))) = 0
|
|
or length(btrim(coalesce(p_algorithm_version, ''))) = 0
|
|
or length(btrim(coalesce(p_event_contract_version, ''))) = 0
|
|
or length(btrim(coalesce(p_decision_policy_version, ''))) = 0 then
|
|
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
|
end if;
|
|
if p_candidate_range is null or jsonb_typeof(p_candidate_range) <> 'object' then
|
|
raise exception 'agentic_rectification_invalid_candidate_range' using errcode = 'P0001';
|
|
end if;
|
|
if p_candidates is null or jsonb_typeof(p_candidates) <> 'array'
|
|
or jsonb_array_length(p_candidates) = 0 then
|
|
raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
|
|
end if;
|
|
if p_decision_receipt is null or jsonb_typeof(p_decision_receipt) <> 'object'
|
|
or jsonb_typeof(p_decision_receipt -> 'display_allowed') is distinct from 'boolean'
|
|
or jsonb_typeof(p_decision_receipt -> 'accept_allowed') is distinct from 'boolean'
|
|
or jsonb_typeof(p_decision_receipt -> 'confirm_allowed') is distinct from 'boolean'
|
|
or jsonb_typeof(p_decision_receipt -> 'overall_confidence') is distinct from 'string' then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
if p_execution_ledger is null or jsonb_typeof(p_execution_ledger) <> 'array' then
|
|
raise exception 'agentic_rectification_invalid_execution_ledger' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_display_allowed := (p_decision_receipt ->> 'display_allowed')::boolean;
|
|
v_selection_allowed := (p_decision_receipt ->> 'accept_allowed')::boolean;
|
|
v_confirmation_allowed := (p_decision_receipt ->> 'confirm_allowed')::boolean;
|
|
v_overall_confidence := p_decision_receipt ->> 'overall_confidence';
|
|
v_saved_decision_receipt := p_decision_receipt - 'representative_candidate_id';
|
|
if v_overall_confidence not in ('low', 'medium', 'high') then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
if (p_decision_receipt ? 'margin_percent')
|
|
and jsonb_typeof(p_decision_receipt -> 'margin_percent') not in ('number', 'null') then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
v_margin_percent := case
|
|
when jsonb_typeof(p_decision_receipt -> 'margin_percent') = 'number'
|
|
then (p_decision_receipt ->> 'margin_percent')::numeric
|
|
else null
|
|
end;
|
|
if v_selection_allowed and not v_display_allowed then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
if v_confirmation_allowed and not v_selection_allowed then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
|
|
v_representative_time_text := nullif(btrim(coalesce(p_decision_receipt ->> 'representative_time', '')), '');
|
|
if v_representative_time_text is not null then
|
|
if v_representative_time_text !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
v_representative_time := v_representative_time_text::time without time zone;
|
|
end if;
|
|
if v_confirmation_allowed and v_representative_time is null then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' 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;
|
|
if v_case.skill_version is distinct from p_skill_version then
|
|
raise exception 'agentic_rectification_skill_version_mismatch' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select * into v_cached
|
|
from public.agentic_rectification_results
|
|
where case_id = p_case_id
|
|
and invalidated_at is null
|
|
and evidence_ledger_fingerprint = p_evidence_ledger_fingerprint
|
|
and candidate_range_fingerprint = p_candidate_range_fingerprint
|
|
and skill_version = p_skill_version
|
|
and algorithm_version = p_algorithm_version
|
|
and event_contract_version = p_event_contract_version
|
|
and decision_policy_version = p_decision_policy_version
|
|
order by created_at desc
|
|
limit 1;
|
|
|
|
if found then
|
|
return jsonb_build_object(
|
|
'result_id', v_cached.id,
|
|
'cached', true,
|
|
'candidates', v_cached.candidates,
|
|
'overall_confidence', v_cached.overall_confidence,
|
|
'margin_percent', v_cached.margin_percent,
|
|
'display_allowed', v_cached.display_allowed,
|
|
'selection_allowed', v_cached.selection_allowed,
|
|
'confirmation_allowed', v_cached.confirmation_allowed,
|
|
'representative_time', v_cached.representative_time,
|
|
'algorithm_version', v_cached.algorithm_version,
|
|
'event_contract_version', v_cached.event_contract_version,
|
|
'decision_policy_version', v_cached.decision_policy_version,
|
|
'decision_receipt', v_cached.decision_receipt,
|
|
'execution_ledger', v_cached.execution_ledger
|
|
);
|
|
end if;
|
|
|
|
v_snapshot := v_case.baseline_birth_snapshot;
|
|
|
|
insert into public.agentic_rectification_results (
|
|
user_id, session_id, case_id,
|
|
engine_result_id, canonical_input_hash, algorithm_version,
|
|
evidence_ledger_fingerprint, candidate_range_fingerprint, skill_version,
|
|
event_contract_version, decision_policy_version,
|
|
candidate_range, candidates,
|
|
overall_confidence, margin_percent,
|
|
display_allowed, selection_allowed, confirmation_allowed, representative_time,
|
|
decision_receipt, execution_ledger,
|
|
baseline_birth_date, baseline_reported_birth_time, baseline_active_birth_time,
|
|
baseline_birth_time_source, baseline_birth_time_period,
|
|
baseline_uncertainty_before_minutes, baseline_uncertainty_after_minutes,
|
|
baseline_latitude, baseline_longitude, baseline_timezone_offset
|
|
) values (
|
|
v_case.user_id, v_case.session_id, v_case.id,
|
|
p_engine_result_id, p_evidence_ledger_fingerprint, p_algorithm_version,
|
|
p_evidence_ledger_fingerprint, p_candidate_range_fingerprint, p_skill_version,
|
|
p_event_contract_version, p_decision_policy_version,
|
|
p_candidate_range, '[]'::jsonb,
|
|
v_overall_confidence, v_margin_percent,
|
|
v_display_allowed, v_selection_allowed, v_confirmation_allowed, v_representative_time,
|
|
v_saved_decision_receipt, p_execution_ledger,
|
|
(v_snapshot ->> 'birth_date')::date,
|
|
(v_snapshot ->> 'reported_birth_time')::time without time zone,
|
|
(v_snapshot ->> 'active_birth_time')::time without time zone,
|
|
v_snapshot ->> 'birth_time_source',
|
|
v_snapshot ->> 'birth_time_period',
|
|
(v_snapshot ->> 'uncertainty_before_minutes')::integer,
|
|
(v_snapshot ->> 'uncertainty_after_minutes')::integer,
|
|
(v_snapshot ->> 'latitude')::double precision,
|
|
(v_snapshot ->> 'longitude')::double precision,
|
|
(v_snapshot ->> 'timezone_offset')::double precision
|
|
) returning id into v_result_id;
|
|
|
|
for v_candidate, v_candidate_ordinal in
|
|
select item.value, item.ordinality::integer
|
|
from pg_catalog.jsonb_array_elements(p_candidates) with ordinality as item(value, ordinality)
|
|
loop
|
|
if jsonb_typeof(v_candidate) <> 'object' then
|
|
raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
|
|
end if;
|
|
v_candidate_time_text := nullif(btrim(coalesce(v_candidate ->> 'time', '')), '');
|
|
if v_candidate_time_text is null
|
|
or v_candidate_time_text !~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
|
or v_candidate_time_text = any(v_seen_times) then
|
|
raise exception 'agentic_rectification_invalid_candidates' using errcode = 'P0001';
|
|
end if;
|
|
v_seen_times := pg_catalog.array_append(v_seen_times, v_candidate_time_text);
|
|
v_candidate_time := v_candidate_time_text::time without time zone;
|
|
v_candidate_id := gen_random_uuid();
|
|
if v_representative_time is not null
|
|
and v_candidate_time is not distinct from v_representative_time then
|
|
v_representative_count := v_representative_count + 1;
|
|
v_representative_candidate_id := v_candidate_id;
|
|
end if;
|
|
|
|
v_saved_candidate := jsonb_set(
|
|
v_candidate - 'candidate_id',
|
|
'{candidate_id}',
|
|
to_jsonb(v_candidate_id::text),
|
|
true
|
|
);
|
|
|
|
insert into public.agentic_rectification_candidates (
|
|
id, result_id, user_id, case_id, ordinal,
|
|
candidate_time, candidate_payload, is_representative
|
|
) values (
|
|
v_candidate_id, v_result_id, p_user_id, p_case_id, v_candidate_ordinal,
|
|
v_candidate_time, v_saved_candidate,
|
|
v_representative_time is not null and v_candidate_time is not distinct from v_representative_time
|
|
);
|
|
v_saved_candidates := v_saved_candidates || jsonb_build_array(v_saved_candidate);
|
|
end loop;
|
|
|
|
if v_representative_time is not null and v_representative_count <> 1 then
|
|
raise exception 'agentic_rectification_invalid_decision_receipt' using errcode = 'P0001';
|
|
end if;
|
|
if v_representative_candidate_id is not null then
|
|
v_saved_decision_receipt := jsonb_set(
|
|
v_saved_decision_receipt,
|
|
'{representative_candidate_id}',
|
|
to_jsonb(v_representative_candidate_id::text),
|
|
true
|
|
);
|
|
end if;
|
|
|
|
update public.agentic_rectification_results
|
|
set candidates = v_saved_candidates,
|
|
decision_receipt = v_saved_decision_receipt,
|
|
updated_at = pg_catalog.now()
|
|
where id = v_result_id;
|
|
|
|
return jsonb_build_object(
|
|
'result_id', v_result_id,
|
|
'cached', false,
|
|
'candidates', v_saved_candidates,
|
|
'overall_confidence', v_overall_confidence,
|
|
'margin_percent', v_margin_percent,
|
|
'display_allowed', v_display_allowed,
|
|
'selection_allowed', v_selection_allowed,
|
|
'confirmation_allowed', v_confirmation_allowed,
|
|
'representative_time', v_representative_time,
|
|
'algorithm_version', p_algorithm_version,
|
|
'event_contract_version', p_event_contract_version,
|
|
'decision_policy_version', p_decision_policy_version,
|
|
'decision_receipt', v_saved_decision_receipt,
|
|
'execution_ledger', p_execution_ledger
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.persist_agentic_rectification_candidate_v2(uuid, uuid, text, text, text, text, text, text, text, jsonb, jsonb, jsonb, jsonb)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.persist_agentic_rectification_candidate_v2(uuid, uuid, text, text, text, text, text, text, text, jsonb, jsonb, jsonb, jsonb)
|
|
to service_role;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 4. V2 recovery projections: preserve existing shape and expose decision data
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
create or replace function public.get_agentic_rectification_case(
|
|
p_user_id uuid,
|
|
p_case_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_evidence_count bigint;
|
|
v_turn_count bigint;
|
|
begin
|
|
if p_user_id is null or p_case_id is null then
|
|
raise exception 'agentic_rectification_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;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select count(*) into v_evidence_count
|
|
from public.agentic_rectification_evidence
|
|
where case_id = v_case.id;
|
|
|
|
select count(*) into v_turn_count
|
|
from public.agentic_rectification_turns
|
|
where case_id = v_case.id;
|
|
|
|
select * into v_result
|
|
from public.agentic_rectification_results
|
|
where case_id = v_case.id
|
|
and invalidated_at is null
|
|
order by created_at desc
|
|
limit 1;
|
|
|
|
return jsonb_build_object(
|
|
'case_id', v_case.id,
|
|
'session_id', v_case.session_id,
|
|
'status', v_case.status,
|
|
'skill_name', v_case.skill_name,
|
|
'skill_version', v_case.skill_version,
|
|
'candidate_range', v_case.candidate_range,
|
|
'accepted_time', v_case.accepted_time,
|
|
'confirmed_time', v_case.confirmed_time,
|
|
'created_at', v_case.created_at,
|
|
'last_activity_at', v_case.last_activity_at,
|
|
'completed_at', v_case.completed_at,
|
|
'closed_reason', v_case.closed_reason,
|
|
'evidence_count', v_evidence_count,
|
|
'turn_count', v_turn_count,
|
|
'latest_result', case
|
|
when v_result.id is null then null
|
|
else jsonb_build_object(
|
|
'result_id', v_result.id,
|
|
'candidates', v_result.candidates,
|
|
'overall_confidence', v_result.overall_confidence,
|
|
'display_allowed', v_result.display_allowed,
|
|
'selection_allowed', v_result.selection_allowed,
|
|
'confirmation_allowed', v_result.confirmation_allowed,
|
|
'representative_time', v_result.representative_time,
|
|
'selected_candidate_id', v_result.selected_candidate_id,
|
|
'selected_time', v_result.selected_time,
|
|
'selection_kind', v_result.selection_kind,
|
|
'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
|
|
'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
|
|
'skill_version', v_result.skill_version,
|
|
'algorithm_version', v_result.algorithm_version,
|
|
'event_contract_version', v_result.event_contract_version,
|
|
'decision_policy_version', v_result.decision_policy_version,
|
|
'decision_receipt', v_result.decision_receipt,
|
|
'execution_ledger', v_result.execution_ledger,
|
|
'created_at', v_result.created_at,
|
|
'invalidated_at', v_result.invalidated_at
|
|
)
|
|
end
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.get_agentic_rectification_case(uuid, uuid)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.get_agentic_rectification_case(uuid, uuid)
|
|
to service_role;
|
|
|
|
create or replace function public.get_agentic_rectification_case_dossier(
|
|
p_user_id uuid,
|
|
p_case_id uuid
|
|
)
|
|
returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = ''
|
|
as $$
|
|
declare
|
|
v_case public.agentic_rectification_cases%rowtype;
|
|
v_turns jsonb;
|
|
v_evidence jsonb;
|
|
v_summary public.agentic_rectification_case_conversation_summaries%rowtype;
|
|
v_result public.agentic_rectification_results%rowtype;
|
|
v_evidence_count bigint;
|
|
v_turn_count bigint;
|
|
begin
|
|
if p_user_id is null or p_case_id is null then
|
|
raise exception 'agentic_rectification_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;
|
|
if not found then
|
|
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
|
|
end if;
|
|
|
|
select coalesce(jsonb_agg(jsonb_build_object(
|
|
'id', recent.id,
|
|
'role', message.role,
|
|
'text', message.text,
|
|
'status', recent.status,
|
|
'created_at', recent.created_at,
|
|
'completed_at', recent.completed_at
|
|
) order by recent.created_at, recent.id, message.ordinal), '[]'::jsonb)
|
|
into v_turns
|
|
from (
|
|
select t.*
|
|
from public.agentic_rectification_turns t
|
|
where t.case_id = v_case.id
|
|
order by t.created_at desc, t.id desc
|
|
limit 50
|
|
) recent
|
|
cross join lateral (
|
|
values
|
|
(1, 'user'::text, recent.user_message),
|
|
(2, 'assistant'::text, recent.assistant_message)
|
|
) as message(ordinal, role, text)
|
|
where message.text is not null;
|
|
|
|
select coalesce(jsonb_agg(jsonb_build_object(
|
|
'id', e.id,
|
|
'source_turn_id', e.source_turn_id,
|
|
'subject', e.subject,
|
|
'event_kind', e.event_kind,
|
|
'domain', e.domain,
|
|
'occurred_from', e.occurred_from,
|
|
'occurred_to', e.occurred_to,
|
|
'date_precision', e.date_precision,
|
|
'summary', e.summary,
|
|
'status', e.status,
|
|
'supersedes_evidence_id', e.supersedes_evidence_id,
|
|
'created_at', e.created_at
|
|
) order by e.created_at, e.id), '[]'::jsonb)
|
|
into v_evidence
|
|
from public.agentic_rectification_evidence e
|
|
where e.case_id = v_case.id;
|
|
|
|
select count(*) into v_evidence_count
|
|
from public.agentic_rectification_evidence where case_id = v_case.id;
|
|
select count(*) into v_turn_count
|
|
from public.agentic_rectification_turns where case_id = v_case.id;
|
|
|
|
select * into v_summary
|
|
from public.agentic_rectification_case_conversation_summaries
|
|
where case_id = v_case.id;
|
|
if not found then
|
|
perform public.refresh_agentic_rectification_case_conversation_summary(v_case.id);
|
|
select * into v_summary
|
|
from public.agentic_rectification_case_conversation_summaries
|
|
where case_id = v_case.id;
|
|
end if;
|
|
|
|
select * into v_result
|
|
from public.agentic_rectification_results
|
|
where case_id = v_case.id and invalidated_at is null
|
|
order by created_at desc, id desc
|
|
limit 1;
|
|
|
|
return jsonb_build_object(
|
|
'case', jsonb_build_object(
|
|
'case_id', v_case.id,
|
|
'session_id', v_case.session_id,
|
|
'status', v_case.status,
|
|
'skill_name', v_case.skill_name,
|
|
'skill_version', v_case.skill_version,
|
|
'candidate_range', v_case.candidate_range,
|
|
'accepted_time', v_case.accepted_time,
|
|
'confirmed_time', v_case.confirmed_time,
|
|
'completed_at', v_case.completed_at,
|
|
'closed_reason', v_case.closed_reason,
|
|
'last_activity_at', v_case.last_activity_at,
|
|
'evidence_count', v_evidence_count,
|
|
'turn_count', v_turn_count
|
|
),
|
|
'turns', v_turns,
|
|
'evidence', v_evidence,
|
|
'conversation_summary', jsonb_build_object(
|
|
'confirmed_evidence_summary', v_summary.confirmed_evidence_summary,
|
|
'pending_revisions', v_summary.pending_revisions,
|
|
'active_focus', v_summary.active_focus,
|
|
'declined_skipped_topics', v_summary.declined_skipped_topics,
|
|
'candidate_divergence_summary', v_summary.candidate_divergence_summary,
|
|
'missing_evidence_categories', v_summary.missing_evidence_categories,
|
|
'last_result_policy', v_summary.last_result_policy,
|
|
'summary_version', v_summary.summary_version,
|
|
'updated_at', v_summary.updated_at
|
|
),
|
|
'latest_result', case when v_result.id is null then null else jsonb_build_object(
|
|
'result_id', v_result.id,
|
|
'candidates', v_result.candidates,
|
|
'overall_confidence', v_result.overall_confidence,
|
|
'display_allowed', v_result.display_allowed,
|
|
'selection_allowed', v_result.selection_allowed,
|
|
'confirmation_allowed', v_result.confirmation_allowed,
|
|
'representative_time', v_result.representative_time,
|
|
'selected_candidate_id', v_result.selected_candidate_id,
|
|
'selected_time', v_result.selected_time,
|
|
'selection_kind', v_result.selection_kind,
|
|
'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
|
|
'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
|
|
'skill_version', v_result.skill_version,
|
|
'algorithm_version', v_result.algorithm_version,
|
|
'event_contract_version', v_result.event_contract_version,
|
|
'decision_policy_version', v_result.decision_policy_version,
|
|
'decision_receipt', v_result.decision_receipt,
|
|
'execution_ledger', v_result.execution_ledger,
|
|
'created_at', v_result.created_at,
|
|
'invalidated_at', v_result.invalidated_at
|
|
) end
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
revoke all on function public.get_agentic_rectification_case_dossier(uuid, uuid)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.get_agentic_rectification_case_dossier(uuid, uuid)
|
|
to service_role;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 5. V2 acceptance: UUID lookup, request idempotency, accepted-only state
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
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
|
|
)
|
|
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_existing_decision public.agentic_rectification_candidate_decisions%rowtype;
|
|
v_response 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 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;
|
|
|
|
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' then
|
|
v_response := jsonb_build_object(
|
|
'success', true,
|
|
'saved_time', pg_catalog.to_char(v_candidate.candidate_time, 'HH24:MI'),
|
|
'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;
|
|
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;
|
|
end if;
|
|
|
|
update public.profiles
|
|
set active_birth_time = v_candidate.candidate_time,
|
|
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;
|
|
|
|
update public.agentic_rectification_cases
|
|
set status = 'candidate_accepted',
|
|
accepted_time = v_candidate.candidate_time,
|
|
confirmed_time = null,
|
|
completed_at = null,
|
|
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'),
|
|
'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)
|
|
from public, anon, authenticated;
|
|
grant execute on function public.accept_agentic_rectification_candidate_for_case_v2(uuid, uuid, uuid, uuid, uuid)
|
|
to service_role;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 5. V2 confirmation: exact representative gate plus grounded user consent
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
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;
|
|
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;
|
|
|
|
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'),
|
|
'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;
|
|
|
|
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,
|
|
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'),
|
|
'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;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 7. Retire every legacy service-role entry that accepts caller-provided time
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
revoke execute on function public.persist_agentic_rectification_candidate(uuid, uuid, text, text, text, text, text, jsonb, jsonb, text, numeric, boolean, boolean, time without time zone) from service_role;
|
|
revoke execute on function public.accept_agentic_rectification_candidate(uuid, uuid, uuid, time without time zone) from service_role;
|
|
revoke execute on function public.accept_agentic_rectification_candidate_for_case(uuid, uuid, uuid, time without time zone) from service_role;
|
|
revoke execute on function public.confirm_agentic_rectification_birth_time(uuid, uuid, uuid, time without time zone, text, uuid) from service_role;
|
|
|
|
commit;
|