feat: complete minute birth-time rectification flow
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
begin;
|
||||
|
||||
-- Follow-up targeting is part of the language-first rectification contract.
|
||||
-- Keep the legacy required keys stable, but allow the optional followUp object
|
||||
-- that the application persists to distinguish a new event from a date/detail
|
||||
-- clarification.
|
||||
create or replace function public.conversational_rectification_valid_evidence_request(
|
||||
p_value jsonb
|
||||
)
|
||||
returns boolean
|
||||
language sql
|
||||
immutable
|
||||
strict
|
||||
set search_path = ''
|
||||
as $$
|
||||
select pg_catalog.jsonb_typeof(p_value) = 'object'
|
||||
and pg_catalog.octet_length(p_value::text) <= 2048
|
||||
and public.conversational_rectification_numbers_are_stable(p_value)
|
||||
and public.conversational_rectification_has_only_keys(
|
||||
p_value,
|
||||
array['domains', 'datePrecision', 'freeTextAllowed', 'followUp']::text[]
|
||||
)
|
||||
and p_value ?& array['domains', 'datePrecision', 'freeTextAllowed']::text[]
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'domains') = 'array'
|
||||
and pg_catalog.jsonb_array_length(p_value -> 'domains') between 1 and 4
|
||||
and not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements(p_value -> 'domains') domain
|
||||
where pg_catalog.jsonb_typeof(domain) is distinct from 'string'
|
||||
or domain #>> '{}' not in (
|
||||
'career', 'education', 'finance', 'health_pressure', 'relocation',
|
||||
'relationship', 'family', 'other'
|
||||
)
|
||||
)
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'datePrecision') = 'string'
|
||||
and p_value ->> 'datePrecision' in ('month_preferred', 'year_accepted')
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'freeTextAllowed') = 'boolean'
|
||||
and p_value -> 'freeTextAllowed' = 'true'::jsonb
|
||||
and (
|
||||
not (p_value ? 'followUp')
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'followUp') = 'object'
|
||||
and public.conversational_rectification_has_only_keys(
|
||||
p_value -> 'followUp',
|
||||
array['kind', 'evidenceId']::text[]
|
||||
)
|
||||
and (p_value -> 'followUp') ?& array['kind', 'evidenceId']::text[]
|
||||
and p_value #>> '{followUp,kind}' in ('new_event', 'event_date', 'event_detail')
|
||||
and (
|
||||
p_value #>> '{followUp,evidenceId}' is null
|
||||
or public.conversational_rectification_valid_uuid_text(
|
||||
p_value #>> '{followUp,evidenceId}'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
commit;
|
||||
@@ -0,0 +1,39 @@
|
||||
begin;
|
||||
|
||||
alter table public.profiles
|
||||
add column if not exists birth_place_label text,
|
||||
add column if not exists birth_place_type text,
|
||||
add column if not exists birth_place_provider text,
|
||||
add column if not exists birth_place_provider_id text,
|
||||
add column if not exists timezone_id text,
|
||||
add column if not exists timezone_source text;
|
||||
|
||||
alter table public.profiles
|
||||
drop constraint if exists profiles_birth_place_provider_check,
|
||||
drop constraint if exists profiles_timezone_source_check;
|
||||
|
||||
alter table public.profiles
|
||||
add constraint profiles_birth_place_provider_check
|
||||
check (birth_place_provider is null or birth_place_provider in ('geoapify', 'china_locations', 'mapbox', 'geonames')),
|
||||
add constraint profiles_timezone_source_check
|
||||
check (timezone_source is null or timezone_source in ('iana_historical'));
|
||||
|
||||
grant update (
|
||||
birth_place_label,
|
||||
birth_place_type,
|
||||
birth_place_provider,
|
||||
birth_place_provider_id,
|
||||
timezone_id,
|
||||
timezone_source
|
||||
) on table public.profiles to authenticated;
|
||||
|
||||
grant select, insert, update (
|
||||
birth_place_label,
|
||||
birth_place_type,
|
||||
birth_place_provider,
|
||||
birth_place_provider_id,
|
||||
timezone_id,
|
||||
timezone_source
|
||||
) on table public.profiles to service_role;
|
||||
|
||||
commit;
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
begin;
|
||||
|
||||
-- Global birthplace intake stores provider identity and IANA timezone metadata
|
||||
-- in the immutable rectification declaration. Keep the database validator in
|
||||
-- lockstep with the strict TypeScript persistence contract so valid non-China
|
||||
-- profiles are not surfaced as conversational_action_conflict.
|
||||
create or replace function public.conversational_rectification_valid_declared_birth_input(
|
||||
p_value jsonb
|
||||
)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
immutable
|
||||
strict
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_place jsonb;
|
||||
v_source text;
|
||||
v_key text;
|
||||
v_before integer;
|
||||
v_after integer;
|
||||
begin
|
||||
if pg_catalog.jsonb_typeof(p_value) is distinct from 'object'
|
||||
or pg_catalog.octet_length(p_value::text) > 12000
|
||||
or public.conversational_rectification_numbers_are_stable(p_value) is not true
|
||||
or not public.conversational_rectification_has_only_keys(
|
||||
p_value,
|
||||
array[
|
||||
'birthDate', 'source', 'birthTimeClue', 'birthplace', 'reportedTime',
|
||||
'reportedPeriod', 'uncertaintyBeforeMinutes', 'uncertaintyAfterMinutes'
|
||||
]::text[]
|
||||
)
|
||||
or not (p_value ?& array['birthDate', 'source', 'birthTimeClue', 'birthplace']::text[])
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'birthDate') is distinct from 'string'
|
||||
or not public.conversational_rectification_valid_date_text(p_value ->> 'birthDate')
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'source') is distinct from 'string'
|
||||
or public.conversational_rectification_text_is_nonblank(
|
||||
p_value ->> 'source'
|
||||
) is not true
|
||||
or p_value -> 'birthTimeClue' <> 'null'::jsonb and (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'birthTimeClue') is distinct from 'string'
|
||||
or public.conversational_rectification_text_utf16_length(
|
||||
p_value ->> 'birthTimeClue'
|
||||
) not between 1 and 240
|
||||
or public.conversational_rectification_text_is_nonblank(
|
||||
p_value ->> 'birthTimeClue'
|
||||
) is not true
|
||||
) then return false; end if;
|
||||
|
||||
v_place := p_value -> 'birthplace';
|
||||
if pg_catalog.jsonb_typeof(v_place) is distinct from 'object'
|
||||
or pg_catalog.octet_length(v_place::text) > 4096
|
||||
or not public.conversational_rectification_has_only_keys(
|
||||
v_place,
|
||||
array[
|
||||
'city', 'placeId', 'placeType', 'provider', 'countryCode',
|
||||
'provinceCode', 'cityCode', 'districtCode', 'latitude', 'longitude',
|
||||
'timezoneId', 'timezoneSource', 'timezoneOffset'
|
||||
]::text[]
|
||||
)
|
||||
or not (v_place ? 'timezoneOffset')
|
||||
or not ((v_place ? 'city') or (v_place ? 'cityCode') or (v_place ? 'placeId'))
|
||||
or pg_catalog.jsonb_typeof(v_place -> 'timezoneOffset') is distinct from 'number'
|
||||
or (v_place ->> 'timezoneOffset')::numeric not between -12 and 14
|
||||
or (v_place ? 'latitude') <> (v_place ? 'longitude') then return false; end if;
|
||||
foreach v_key in array array[
|
||||
'city', 'placeId', 'placeType', 'provider', 'provinceCode', 'cityCode',
|
||||
'districtCode', 'timezoneId', 'timezoneSource'
|
||||
]::text[] loop
|
||||
if v_place ? v_key and (
|
||||
pg_catalog.jsonb_typeof(v_place -> v_key) is distinct from 'string'
|
||||
or public.conversational_rectification_text_utf16_length(
|
||||
v_place ->> v_key
|
||||
) not between 1 and case
|
||||
when v_key = 'city' then 120
|
||||
when v_key = 'placeId' then 240
|
||||
when v_key = 'timezoneId' then 120
|
||||
else 80
|
||||
end
|
||||
or public.conversational_rectification_text_is_nonblank(
|
||||
v_place ->> v_key
|
||||
) is not true
|
||||
) then return false; end if;
|
||||
end loop;
|
||||
if v_place ? 'countryCode' and (
|
||||
pg_catalog.jsonb_typeof(v_place -> 'countryCode') is distinct from 'string'
|
||||
or v_place ->> 'countryCode' !~ '^[A-Z0-9-]{1,8}$'
|
||||
) then return false; end if;
|
||||
if v_place ? 'latitude' and (
|
||||
pg_catalog.jsonb_typeof(v_place -> 'latitude') is distinct from 'number'
|
||||
or pg_catalog.jsonb_typeof(v_place -> 'longitude') is distinct from 'number'
|
||||
or (v_place ->> 'latitude')::numeric not between -90 and 90
|
||||
or (v_place ->> 'longitude')::numeric not between -180 and 180
|
||||
) then return false; end if;
|
||||
|
||||
v_source := p_value ->> 'source';
|
||||
if v_source not in (
|
||||
'hospital_record', 'family_exact', 'approximate', 'period_only', 'unknown', 'legacy_import'
|
||||
) then return false; end if;
|
||||
if p_value ? 'reportedTime' and (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'reportedTime') is distinct from 'string'
|
||||
or not public.conversational_rectification_valid_time_text(p_value ->> 'reportedTime')
|
||||
) then return false; end if;
|
||||
if p_value ? 'reportedPeriod' and (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'reportedPeriod') is distinct from 'string'
|
||||
or p_value ->> 'reportedPeriod' not in (
|
||||
'early_morning', 'morning', 'afternoon', 'evening', 'late_night'
|
||||
)
|
||||
) then return false; end if;
|
||||
if p_value ? 'uncertaintyBeforeMinutes' then
|
||||
if pg_catalog.jsonb_typeof(p_value -> 'uncertaintyBeforeMinutes') is distinct from 'number'
|
||||
or pg_catalog.jsonb_typeof(p_value -> 'uncertaintyAfterMinutes') is distinct from 'number'
|
||||
or p_value ->> 'uncertaintyBeforeMinutes' !~ '^[0-9]+$'
|
||||
or p_value ->> 'uncertaintyAfterMinutes' !~ '^[0-9]+$' then
|
||||
return false;
|
||||
end if;
|
||||
v_before := (p_value ->> 'uncertaintyBeforeMinutes')::integer;
|
||||
v_after := (p_value ->> 'uncertaintyAfterMinutes')::integer;
|
||||
elsif p_value ? 'uncertaintyAfterMinutes' then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
if v_source = 'hospital_record' then
|
||||
return p_value ? 'reportedTime'
|
||||
and not (p_value ? 'reportedPeriod')
|
||||
and v_before = 2 and v_after = 2;
|
||||
elsif v_source = 'family_exact' then
|
||||
return p_value ? 'reportedTime'
|
||||
and not (p_value ? 'reportedPeriod')
|
||||
and v_before = v_after and v_before in (5, 10, 15);
|
||||
elsif v_source = 'approximate' then
|
||||
return p_value ? 'reportedTime'
|
||||
and not (p_value ? 'reportedPeriod')
|
||||
and v_before = v_after and v_before in (15, 30, 60);
|
||||
elsif v_source = 'period_only' then
|
||||
return p_value ? 'reportedPeriod'
|
||||
and not (p_value ? 'reportedTime')
|
||||
and not (p_value ? 'uncertaintyBeforeMinutes')
|
||||
and not (p_value ? 'uncertaintyAfterMinutes');
|
||||
elsif v_source = 'unknown' then
|
||||
return not (p_value ? 'reportedPeriod')
|
||||
and not (p_value ? 'reportedTime')
|
||||
and not (p_value ? 'uncertaintyBeforeMinutes')
|
||||
and not (p_value ? 'uncertaintyAfterMinutes');
|
||||
end if;
|
||||
return not ((p_value ? 'reportedTime') and (p_value ? 'reportedPeriod'))
|
||||
and ((p_value ? 'reportedTime') or v_before is null)
|
||||
and (v_before is null or (v_before between 0 and 720 and v_after between 0 and 720));
|
||||
exception when others then
|
||||
return false;
|
||||
end;
|
||||
$$;
|
||||
|
||||
commit;
|
||||
Reference in New Issue
Block a user