447 lines
18 KiB
PL/PgSQL
447 lines
18 KiB
PL/PgSQL
begin;
|
||
|
||
create unique index if not exists birth_time_rectification_cases_one_v3_import_per_legacy
|
||
on public.birth_time_rectification_cases (imported_from_case_id)
|
||
where imported_from_case_id is not null
|
||
and journey_protocol = 'conversational-evidence-v3';
|
||
|
||
create or replace function public.conversational_rectification_project_legacy_event_evidence(
|
||
p_life_events jsonb,
|
||
p_birth_date date,
|
||
p_as_of_date date
|
||
)
|
||
returns jsonb
|
||
language plpgsql
|
||
immutable
|
||
strict
|
||
set search_path = ''
|
||
as $$
|
||
declare
|
||
v_item jsonb;
|
||
v_id uuid;
|
||
v_domain text;
|
||
v_imported_domain text;
|
||
v_label text;
|
||
v_precision text;
|
||
v_date text;
|
||
v_day date;
|
||
v_seen uuid[] := '{}'::uuid[];
|
||
v_result jsonb := '[]'::jsonb;
|
||
begin
|
||
if pg_catalog.jsonb_typeof(p_life_events) is distinct from 'array' then
|
||
return '[]'::jsonb;
|
||
end if;
|
||
|
||
for v_item in
|
||
select item.value
|
||
from pg_catalog.jsonb_array_elements(p_life_events) with ordinality item(value, ordinality)
|
||
order by item.ordinality
|
||
loop
|
||
begin
|
||
if pg_catalog.jsonb_typeof(v_item) is distinct from 'object'
|
||
or not public.conversational_rectification_has_only_keys(
|
||
v_item, array['id', 'domain', 'precision', 'date']
|
||
)
|
||
or public.conversational_rectification_valid_uuid_text(v_item ->> 'id') is not true then
|
||
continue;
|
||
end if;
|
||
v_id := (v_item ->> 'id')::uuid;
|
||
if v_id = any(v_seen) then continue; end if;
|
||
v_domain := v_item ->> 'domain';
|
||
v_precision := v_item ->> 'precision';
|
||
v_date := v_item ->> 'date';
|
||
if v_domain not in (
|
||
'career', 'education', 'relocation', 'relationship', 'finance', 'health_pressure'
|
||
) or v_precision not in ('year', 'month', 'day') then
|
||
continue;
|
||
end if;
|
||
if v_precision = 'year' then
|
||
if v_date !~ '^(19|20)[0-9]{2}$'
|
||
or v_date < pg_catalog.to_char(p_birth_date, 'YYYY')
|
||
or v_date > pg_catalog.to_char(p_as_of_date, 'YYYY') then
|
||
continue;
|
||
end if;
|
||
elsif v_precision = 'month' then
|
||
if v_date !~ '^(19|20)[0-9]{2}-(0[1-9]|1[0-2])$'
|
||
or v_date < pg_catalog.to_char(p_birth_date, 'YYYY-MM')
|
||
or v_date > pg_catalog.to_char(p_as_of_date, 'YYYY-MM') then
|
||
continue;
|
||
end if;
|
||
else
|
||
if v_date !~ '^(19|20)[0-9]{2}-(0[1-9]|1[0-2])-([0-2][0-9]|3[01])$' then
|
||
continue;
|
||
end if;
|
||
v_day := v_date::date;
|
||
if pg_catalog.to_char(v_day, 'YYYY-MM-DD') is distinct from v_date
|
||
or v_day < p_birth_date or v_day > p_as_of_date then
|
||
continue;
|
||
end if;
|
||
end if;
|
||
|
||
v_imported_domain := case
|
||
when v_domain in ('finance', 'health_pressure') then 'other'
|
||
else v_domain
|
||
end;
|
||
v_label := case v_imported_domain
|
||
when 'career' then '事业'
|
||
when 'education' then '学业'
|
||
when 'relocation' then '迁居'
|
||
when 'relationship' then '关系'
|
||
when 'family' then '家庭'
|
||
else '其他'
|
||
end;
|
||
v_result := v_result || pg_catalog.jsonb_build_array(
|
||
pg_catalog.jsonb_build_object(
|
||
'id', v_id,
|
||
'rawText', '旧校时记录中的' || v_label || '事件(' || v_date || ')',
|
||
'domain', v_imported_domain,
|
||
'eventSummary', '旧校时记录中的' || v_label || '事件',
|
||
'dateValue', v_date,
|
||
'datePrecision', v_precision,
|
||
'extractionStatus', 'clear',
|
||
'scoreable', true,
|
||
'correctsEvidenceIds', '[]'::jsonb
|
||
)
|
||
);
|
||
v_seen := pg_catalog.array_append(v_seen, v_id);
|
||
if pg_catalog.jsonb_array_length(v_result) > 20 then
|
||
v_result := v_result - 0;
|
||
end if;
|
||
exception when others then
|
||
-- Old rows predate the strict v3 evidence contract. An invalid fragment
|
||
-- stays in the immutable source row instead of aborting or becoming fact.
|
||
continue;
|
||
end;
|
||
end loop;
|
||
return v_result;
|
||
end;
|
||
$$;
|
||
|
||
drop function if exists public.import_legacy_conversational_rectification_case(
|
||
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb
|
||
);
|
||
|
||
create or replace function public.import_legacy_conversational_rectification_case(
|
||
p_user_id uuid,
|
||
p_case_id uuid,
|
||
p_legacy_case_id uuid,
|
||
p_expected_version bigint,
|
||
p_action_id uuid,
|
||
p_price integer,
|
||
p_pending_consultation_question text,
|
||
p_declared_birth_input jsonb,
|
||
p_evidence jsonb,
|
||
p_first_turn jsonb,
|
||
p_validation_receipt jsonb,
|
||
p_private_candidate jsonb
|
||
)
|
||
returns jsonb
|
||
language plpgsql
|
||
security definer
|
||
set search_path = ''
|
||
as $$
|
||
declare
|
||
v_case public.birth_time_rectification_cases%rowtype;
|
||
v_legacy public.birth_time_rectification_cases%rowtype;
|
||
v_profile public.profiles%rowtype;
|
||
v_billing public.birth_time_rectification_billing%rowtype;
|
||
v_receipt public.birth_time_rectification_action_receipts%rowtype;
|
||
v_expected_declared jsonb;
|
||
v_expected_evidence jsonb;
|
||
v_expected_recap jsonb;
|
||
v_expected_range_start time without time zone;
|
||
v_expected_range_end time without time zone;
|
||
v_first_turn_id uuid;
|
||
v_response jsonb;
|
||
v_fingerprint text := public.conversational_rectification_fingerprint(
|
||
pg_catalog.jsonb_build_object(
|
||
'kind', 'import_legacy', 'userId', p_user_id, 'caseId', p_case_id,
|
||
'legacyCaseId', p_legacy_case_id, 'expectedVersion', p_expected_version,
|
||
'actionId', p_action_id, 'price', p_price,
|
||
'pendingConsultationQuestion', p_pending_consultation_question,
|
||
'declaredBirthInput', p_declared_birth_input, 'evidence', p_evidence,
|
||
'firstTurn', p_first_turn, 'validationReceipt', p_validation_receipt,
|
||
'privateCandidate', p_private_candidate
|
||
)
|
||
);
|
||
begin
|
||
if p_user_id is null or p_case_id is null or p_legacy_case_id is null
|
||
or p_action_id is null or p_case_id is distinct from p_action_id
|
||
or p_expected_version is null or p_expected_version < 0
|
||
or p_price is null or not (p_price between 1 and 1000000) then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
perform pg_catalog.pg_advisory_xact_lock(
|
||
pg_catalog.hashtextextended(
|
||
p_user_id::text || ':conversational-rectification-case', 0
|
||
)
|
||
);
|
||
perform pg_catalog.pg_advisory_xact_lock(
|
||
pg_catalog.hashtextextended(p_user_id::text || ':' || p_action_id::text, 0)
|
||
);
|
||
|
||
select c.* into v_case
|
||
from public.birth_time_rectification_cases c
|
||
where c.id = p_case_id
|
||
for update;
|
||
if found then
|
||
if v_case.user_id is distinct from p_user_id
|
||
or v_case.journey_protocol is distinct from 'conversational-evidence-v3'
|
||
or v_case.imported_from_case_id is distinct from p_legacy_case_id then
|
||
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
||
end if;
|
||
select r.* into v_receipt
|
||
from public.birth_time_rectification_action_receipts r
|
||
where r.case_id = p_case_id and r.action_id = p_action_id
|
||
for update;
|
||
if found
|
||
and v_receipt.user_id is not distinct from p_user_id
|
||
and v_receipt.action_kind is not distinct from 'import_legacy'
|
||
and v_receipt.expected_turn_version is not distinct from p_expected_version
|
||
and v_receipt.request_fingerprint is not distinct from v_fingerprint then
|
||
return v_receipt.response;
|
||
end if;
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
|
||
select legacy.* into v_legacy
|
||
from public.birth_time_rectification_cases legacy
|
||
where legacy.id = p_legacy_case_id and legacy.user_id = p_user_id
|
||
for update;
|
||
if not found
|
||
or v_legacy.journey_protocol not in ('legacy-guided-v1', 'dynamic-choice-v2')
|
||
or v_legacy.status not in ('assessing', 'rectifying', 'candidate', 'confirming') then
|
||
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
||
end if;
|
||
if v_legacy.turn_version is distinct from p_expected_version then
|
||
raise exception 'conversational_stale_turn' using errcode = 'P0001';
|
||
end if;
|
||
if exists (
|
||
select 1
|
||
from public.birth_time_rectification_cases imported
|
||
where imported.imported_from_case_id = p_legacy_case_id
|
||
and imported.journey_protocol = 'conversational-evidence-v3'
|
||
) then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
if exists (
|
||
select 1
|
||
from public.birth_time_rectification_cases active_case
|
||
where active_case.user_id = p_user_id
|
||
and active_case.id <> p_case_id
|
||
and active_case.id <> p_legacy_case_id
|
||
and active_case.journey_protocol = 'conversational-evidence-v3'
|
||
and active_case.status in ('starting', 'active', 'paused', 'confirming')
|
||
) then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
|
||
select profile.* into v_profile
|
||
from public.profiles profile
|
||
where profile.id = p_user_id
|
||
for update;
|
||
if not found then
|
||
raise exception 'conversational_case_not_found' using errcode = 'P0001';
|
||
end if;
|
||
if v_profile.rectification_case_id is distinct from p_legacy_case_id then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
v_profile.credits := public.recover_conversational_rectification_orphan_reservations(
|
||
p_user_id, null::uuid
|
||
);
|
||
v_expected_declared := pg_catalog.jsonb_build_object(
|
||
'birthDate', pg_catalog.to_char(v_legacy.reported_date, 'YYYY-MM-DD'),
|
||
'source', v_legacy.source,
|
||
'birthTimeClue', v_profile.birth_time_clue,
|
||
'birthplace', pg_catalog.jsonb_strip_nulls(pg_catalog.jsonb_build_object(
|
||
'countryCode', v_profile.country_code,
|
||
'provinceCode', v_profile.province_code,
|
||
'cityCode', v_profile.city_code,
|
||
'districtCode', v_profile.district_code,
|
||
'latitude', v_profile.latitude,
|
||
'longitude', v_profile.longitude,
|
||
'timezoneOffset', v_profile.timezone_offset
|
||
))
|
||
);
|
||
if v_legacy.reported_time is not null then
|
||
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
|
||
'reportedTime', pg_catalog.to_char(v_legacy.reported_time, 'HH24:MI')
|
||
);
|
||
end if;
|
||
if v_legacy.reported_period is not null then
|
||
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
|
||
'reportedPeriod', v_legacy.reported_period
|
||
);
|
||
end if;
|
||
if v_legacy.uncertainty_before_minutes is not null
|
||
or v_legacy.uncertainty_after_minutes is not null then
|
||
v_expected_declared := v_expected_declared || pg_catalog.jsonb_build_object(
|
||
'uncertaintyBeforeMinutes', v_legacy.uncertainty_before_minutes,
|
||
'uncertaintyAfterMinutes', v_legacy.uncertainty_after_minutes
|
||
);
|
||
end if;
|
||
v_expected_evidence := public.conversational_rectification_project_legacy_event_evidence(
|
||
v_legacy.life_events, v_legacy.reported_date, current_date
|
||
);
|
||
if v_legacy.journey_protocol = 'dynamic-choice-v2'
|
||
and v_legacy.turn_state #>> '{progress,currentRange,startTime}'
|
||
~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
|
||
and v_legacy.turn_state #>> '{progress,currentRange,endTime}'
|
||
~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
|
||
v_expected_range_start := (
|
||
v_legacy.turn_state #>> '{progress,currentRange,startTime}'
|
||
)::time;
|
||
v_expected_range_end := (
|
||
v_legacy.turn_state #>> '{progress,currentRange,endTime}'
|
||
)::time;
|
||
elsif v_legacy.candidate_start is not null and v_legacy.candidate_end is not null then
|
||
v_expected_range_start := v_legacy.candidate_start;
|
||
v_expected_range_end := v_legacy.candidate_end;
|
||
end if;
|
||
select coalesce(pg_catalog.jsonb_agg(
|
||
pg_catalog.jsonb_build_object(
|
||
'id', item.value ->> 'id',
|
||
'summary', item.value ->> 'eventSummary',
|
||
'dateLabel', item.value ->> 'dateValue'
|
||
) order by item.ordinality
|
||
), '[]'::jsonb) into v_expected_recap
|
||
from pg_catalog.jsonb_array_elements(v_expected_evidence)
|
||
with ordinality item(value, ordinality);
|
||
|
||
if p_declared_birth_input is distinct from v_expected_declared
|
||
or p_evidence is distinct from v_expected_evidence
|
||
or public.conversational_rectification_valid_declared_birth_input(
|
||
p_declared_birth_input
|
||
) is not true
|
||
or public.conversational_rectification_valid_life_event_evidence_array(
|
||
p_evidence
|
||
) is not true
|
||
or public.conversational_rectification_valid_public_turn(p_first_turn) is not true
|
||
or public.conversational_rectification_valid_validation_receipt(
|
||
p_validation_receipt
|
||
) is not true
|
||
or public.conversational_rectification_valid_private_candidate(
|
||
p_private_candidate
|
||
) is not true
|
||
or p_first_turn ->> 'caseId' is distinct from p_case_id::text
|
||
or p_first_turn ->> 'journeyProtocol' is distinct from 'conversational-evidence-v3'
|
||
or (p_first_turn ->> 'turnVersion')::bigint is distinct from 0
|
||
or p_first_turn ->> 'status' not in ('active', 'confirming')
|
||
or p_first_turn -> 'evidenceRecap' is distinct from v_expected_recap
|
||
or p_first_turn -> 'candidate' ->> 'rangeStart'
|
||
is distinct from p_private_candidate ->> 'rangeStart'
|
||
or p_first_turn -> 'candidate' ->> 'rangeEnd'
|
||
is distinct from p_private_candidate ->> 'rangeEnd'
|
||
or (v_expected_range_start is not null and (
|
||
nullif(p_private_candidate ->> 'rangeStart', '')::time
|
||
is distinct from v_expected_range_start
|
||
or nullif(p_private_candidate ->> 'rangeEnd', '')::time
|
||
is distinct from v_expected_range_end
|
||
))
|
||
or nullif(p_first_turn ->> 'pendingConsultationQuestion', '')
|
||
is distinct from nullif(p_pending_consultation_question, '')
|
||
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.candidateWeights')
|
||
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.candidateScores')
|
||
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.partitionId')
|
||
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.rawModelOutput')
|
||
or pg_catalog.jsonb_path_exists(p_first_turn, '$.**.systemPrompt') then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
if exists (
|
||
select 1 from public.birth_time_rectification_billing b
|
||
where b.user_id = p_user_id and b.state = 'reserved'
|
||
for update
|
||
) then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
|
||
insert into public.birth_time_rectification_cases (
|
||
id, user_id, journey_protocol, status, reported_date, reported_time,
|
||
reported_period, source, uncertainty_before_minutes,
|
||
uncertainty_after_minutes, declared_birth_input,
|
||
questionnaire, answers, life_events, candidate_scan,
|
||
journey_snapshot, turn_version, turn_state,
|
||
candidate_result, candidate_result_id, candidate_start, candidate_end,
|
||
event_scoring_version, imported_from_case_id, baseline_active_time,
|
||
pending_consultation_question, updated_at
|
||
) values (
|
||
p_case_id, p_user_id, 'conversational-evidence-v3',
|
||
p_first_turn ->> 'status', v_legacy.reported_date, v_legacy.reported_time,
|
||
v_legacy.reported_period, v_legacy.source, v_legacy.uncertainty_before_minutes,
|
||
v_legacy.uncertainty_after_minutes, p_declared_birth_input,
|
||
'{}'::jsonb, '{}'::jsonb, '[]'::jsonb, '{}'::jsonb,
|
||
p_first_turn, 0, p_first_turn, p_private_candidate,
|
||
nullif(p_private_candidate ->> 'resultId', '')::uuid,
|
||
nullif(p_private_candidate ->> 'rangeStart', '')::time,
|
||
nullif(p_private_candidate ->> 'rangeEnd', '')::time,
|
||
nullif(p_private_candidate ->> 'calculationVersion', ''),
|
||
p_legacy_case_id, v_profile.active_birth_time,
|
||
nullif(p_pending_consultation_question, ''), pg_catalog.now()
|
||
);
|
||
update public.profiles
|
||
set rectification_case_id = p_case_id,
|
||
updated_at = pg_catalog.now()
|
||
where id = p_user_id
|
||
and rectification_case_id = p_legacy_case_id;
|
||
if not found then
|
||
raise exception 'conversational_action_conflict' using errcode = 'P0001';
|
||
end if;
|
||
insert into public.birth_time_rectification_turns (
|
||
case_id, turn_version, narrative, candidate, technical_receipt,
|
||
evidence_request, evidence_recap, actions, output_validation_receipt
|
||
) values (
|
||
p_case_id, 0, p_first_turn ->> 'narrative', p_first_turn -> 'candidate',
|
||
p_first_turn -> 'technicalReceipt',
|
||
nullif(p_first_turn -> 'evidenceRequest', 'null'::jsonb),
|
||
p_first_turn -> 'evidenceRecap', p_first_turn -> 'actions', p_validation_receipt
|
||
) returning id into v_first_turn_id;
|
||
insert into public.birth_time_rectification_event_evidence (
|
||
id, case_id, source_turn_id, raw_text, domain, event_summary,
|
||
date_value, date_precision, extraction_status, scoreable,
|
||
corrects_evidence_ids
|
||
)
|
||
select (item ->> 'id')::uuid, p_case_id, v_first_turn_id,
|
||
item ->> 'rawText', item ->> 'domain', item ->> 'eventSummary',
|
||
nullif(item ->> 'dateValue', ''), item ->> 'datePrecision',
|
||
item ->> 'extractionStatus', (item ->> 'scoreable')::boolean,
|
||
array(select value::uuid from pg_catalog.jsonb_array_elements_text(
|
||
coalesce(item -> 'correctsEvidenceIds', '[]'::jsonb)
|
||
) correction(value))
|
||
from pg_catalog.jsonb_array_elements(p_evidence) evidence(item);
|
||
insert into public.birth_time_rectification_billing (
|
||
case_id, user_id, price, state, billing_receipt_id,
|
||
complete_action_id, balance_after
|
||
) values (
|
||
p_case_id, p_user_id, p_price, 'migration_waived', pg_catalog.gen_random_uuid(),
|
||
p_action_id, v_profile.credits
|
||
);
|
||
|
||
v_response := public.conversational_rectification_case_projection(p_user_id, p_case_id);
|
||
insert into public.birth_time_rectification_action_receipts (
|
||
case_id, action_id, user_id, action_kind, expected_turn_version,
|
||
result_turn_version, request_fingerprint, request, response
|
||
) values (
|
||
p_case_id, p_action_id, p_user_id, 'import_legacy', p_expected_version,
|
||
0, v_fingerprint,
|
||
public.conversational_rectification_action_request(
|
||
'import_legacy', p_user_id, p_case_id, p_expected_version,
|
||
p_action_id, v_fingerprint
|
||
),
|
||
v_response
|
||
);
|
||
return v_response;
|
||
end;
|
||
$$;
|
||
|
||
revoke all on function public.conversational_rectification_project_legacy_event_evidence(
|
||
jsonb, date, date
|
||
) from public, anon, authenticated, service_role;
|
||
revoke all on function public.import_legacy_conversational_rectification_case(
|
||
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb, jsonb, jsonb
|
||
) from public, anon, authenticated;
|
||
grant execute on function public.import_legacy_conversational_rectification_case(
|
||
uuid, uuid, uuid, bigint, uuid, integer, text, jsonb, jsonb, jsonb, jsonb, jsonb
|
||
) to service_role;
|
||
|
||
commit;
|