-- Homecare location-distance RPCs (precheck + submit) — Fixed for uuid types -- Date: 2026-06-03 (original), 2026-06-08 (fixed for uuid alignment) -- Purpose: move checkin distance flow to RPC-only for phase1 tables -- Fixed: p_org_id, p_team_id, p_operator_id changed from text to uuid -- to match hc_dispatch_assignments table schema create or replace function public.rpc_homecare_dispatch_assignment_upsert( p_work_order_id uuid, p_org_id uuid, -- ✅ fixed: was text, now uuid p_team_id uuid, -- ✅ fixed: was text, now uuid p_worker_id uuid, p_service_latitude numeric, p_service_longitude numeric, p_service_coordinate_type text default 'gcj02', p_operator_id uuid default null, -- ✅ fixed: was text, now uuid p_dispatch_reason text default null ) returns jsonb language plpgsql security definer set search_path = public, app as $$ declare v_prev_version integer := 0; v_prev_id text; begin if p_work_order_id is null then raise exception 'p_work_order_id is required'; end if; if p_org_id is null then raise exception 'p_org_id is required'; end if; if p_service_latitude is null or p_service_longitude is null then raise exception 'service coordinates are required'; end if; if to_regclass('public.hc_dispatch_assignments') is null then raise exception 'hc_dispatch_assignments table is required'; end if; select id, assign_version into v_prev_id, v_prev_version from public.hc_dispatch_assignments where work_order_id = p_work_order_id and is_current = true order by created_at desc limit 1; if found then update public.hc_dispatch_assignments set is_current = false, updated_at = now(), updated_by = coalesce(p_operator_id, updated_by) where id = v_prev_id; end if; insert into public.hc_dispatch_assignments( id, work_order_id, org_id, team_id, assign_version, worker_id, service_latitude, service_longitude, service_coordinate_type, dispatch_status, dispatch_reason, is_current, dispatched_at, created_by, updated_by, created_at, updated_at ) values ( 'dsp_' || gen_random_uuid()::text, p_work_order_id, p_org_id, p_team_id, v_prev_version + 1, p_worker_id, p_service_latitude, p_service_longitude, lower(coalesce(p_service_coordinate_type, 'gcj02')), 'PENDING', p_dispatch_reason, true, now(), p_operator_id, p_operator_id, now(), now() ); return jsonb_build_object('ok', true, 'assignVersion', v_prev_version + 1); end; $$; create or replace function public.rpc_homecare_checkin_precheck( p_work_order_id uuid, p_worker_id uuid, p_latitude numeric, p_longitude numeric, p_coordinate_type text default 'gcj02', p_accuracy numeric default null, p_reported_at timestamptz default null, p_location_scene text default 'CHECKIN_PRECHECK' ) returns jsonb language plpgsql security definer set search_path = public, app as $$ declare v_assignment public.hc_dispatch_assignments%rowtype; v_radius_meters numeric := 200; v_distance_meters numeric; v_can_checkin boolean := false; v_reason_code text := 'WORK_ORDER_NOT_ASSIGNABLE'; v_scene text := upper(coalesce(p_location_scene, 'CHECKIN_PRECHECK')); v_reported_at timestamptz := coalesce(p_reported_at, now()); v_worker_location_accepted boolean := false; v_service_location_ready boolean := false; v_insert_location boolean := false; v_lat1 double precision; v_lng1 double precision; v_lat2 double precision; v_lng2 double precision; v_a double precision; v_c double precision; begin if p_work_order_id is null then raise exception 'p_work_order_id is required'; end if; if p_worker_id is null then raise exception 'p_worker_id is required'; end if; if p_latitude is null or p_longitude is null then raise exception 'p_latitude and p_longitude are required'; end if; if v_scene not in ('CHECKIN_PRECHECK', 'CHECKIN') then raise exception 'invalid p_location_scene: %', v_scene; end if; if to_regclass('public.hc_dispatch_assignments') is null then raise exception 'hc_dispatch_assignments table is required'; end if; if to_regclass('public.hc_worker_locations') is null then raise exception 'hc_worker_locations table is required'; end if; if to_regclass('public.sys_sla_config') is null then raise exception 'sys_sla_config table is required'; end if; select * into v_assignment from public.hc_dispatch_assignments where work_order_id = p_work_order_id and is_current = true order by created_at desc limit 1; if not found then v_reason_code := 'SERVICE_LOCATION_MISSING'; return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', v_reason_code, 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; if coalesce(v_assignment.dispatch_status, '') not in ('PENDING', 'ACCEPTED') then v_reason_code := 'WORK_ORDER_NOT_ASSIGNABLE'; return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', v_reason_code, 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; if v_assignment.worker_id is not null and v_assignment.worker_id <> p_worker_id then v_reason_code := 'WORKER_NOT_MATCHED'; return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', v_reason_code, 'workerLocationAccepted', false, 'serviceLocationReady', true ); end if; if v_assignment.service_latitude is null or v_assignment.service_longitude is null then v_reason_code := 'SERVICE_LOCATION_MISSING'; return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', v_reason_code, 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; v_service_location_ready := true; -- SLA lookup priority: WORK_ORDER > ORG > TEAM > GLOBAL select config_value::numeric into v_radius_meters from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' and scope_type = 'WORK_ORDER' and scope_id = p_work_order_id::text and is_active = true limit 1; if v_radius_meters is null and v_assignment.org_id is not null then select config_value::numeric into v_radius_meters from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' and scope_type = 'ORG' and scope_id = v_assignment.org_id::text and is_active = true limit 1; end if; if v_radius_meters is null and v_assignment.team_id is not null then select config_value::numeric into v_radius_meters from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' and scope_type = 'TEAM' and scope_id = v_assignment.team_id::text and is_active = true limit 1; end if; if v_radius_meters is null then select config_value::numeric into v_radius_meters from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_RADIUS_METERS' and scope_type = 'GLOBAL' and scope_id is null and is_active = true limit 1; end if; v_radius_meters := coalesce(v_radius_meters, 200); -- Haversine distance v_lat1 := radians(p_latitude::double precision); v_lng1 := radians(p_longitude::double precision); v_lat2 := radians(v_assignment.service_latitude::double precision); v_lng2 := radians(v_assignment.service_longitude::double precision); v_a := power(sin((v_lat2 - v_lat1) / 2), 2) + cos(v_lat1) * cos(v_lat2) * power(sin((v_lng2 - v_lng1) / 2), 2); v_c := 2 * atan2(sqrt(v_a), sqrt(1 - v_a)); v_distance_meters := round((6371000 * v_c)::numeric, 2); v_can_checkin := (v_distance_meters <= v_radius_meters); v_reason_code := case when v_can_checkin then 'OK' else 'OUT_OF_RADIUS' end; v_insert_location := (v_scene = 'CHECKIN_PRECHECK') or (v_scene = 'CHECKIN'); if v_insert_location then insert into public.hc_worker_locations( id, work_order_id, worker_id, latitude, longitude, coordinate_type, accuracy, location_scene, reported_at, distance_meters, created_at ) values ( 'loc_' || gen_random_uuid()::text, p_work_order_id, p_worker_id, p_latitude, p_longitude, lower(coalesce(p_coordinate_type, 'gcj02')), p_accuracy, v_scene, v_reported_at, v_distance_meters, now() ); v_worker_location_accepted := true; end if; return jsonb_build_object( 'distanceMeters', v_distance_meters, 'allowedRadiusMeters', v_radius_meters, 'canCheckin', v_can_checkin, 'reasonCode', v_reason_code, 'workerLocationAccepted', v_worker_location_accepted, 'serviceLocationReady', v_service_location_ready ); end; $$; create or replace function public.rpc_homecare_checkin_submit( p_work_order_id uuid, p_worker_id uuid, p_latitude numeric, p_longitude numeric, p_coordinate_type text default 'gcj02', p_accuracy numeric default null, p_reported_at timestamptz default null, p_evidence_file_ids jsonb default '[]'::jsonb, p_signature_payload text default null, p_reason text default null ) returns jsonb language plpgsql security definer set search_path = public, app as $$ declare v_precheck jsonb; v_can_checkin boolean := false; v_assignment public.hc_dispatch_assignments%rowtype; v_assignment_found boolean := false; v_max_photo_count numeric := 3; v_requested_count integer := 0; v_existing_count integer := 0; v_mismatch_count integer := 0; v_not_owned_count integer := 0; v_not_ready_count integer := 0; begin if to_regclass('public.hc_work_order_confirmations') is null then raise exception 'hc_work_order_confirmations table is required'; end if; if p_signature_payload is null or length(trim(p_signature_payload)) = 0 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'SIGNATURE_REQUIRED', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; if length(trim(p_signature_payload)) < 8 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'SIGNATURE_INVALID', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; if p_evidence_file_ids is null or jsonb_typeof(p_evidence_file_ids) <> 'array' then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; v_requested_count := jsonb_array_length(p_evidence_file_ids); if v_requested_count < 1 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; select * into v_assignment from public.hc_dispatch_assignments where work_order_id = p_work_order_id and is_current = true order by created_at desc limit 1; v_assignment_found := found; select config_value::numeric into v_max_photo_count from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' and scope_type = 'WORK_ORDER' and scope_id = p_work_order_id::text and is_active = true limit 1; if v_max_photo_count is null and v_assignment_found and v_assignment.org_id is not null then select config_value::numeric into v_max_photo_count from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' and scope_type = 'ORG' and scope_id = v_assignment.org_id::text and is_active = true limit 1; end if; if v_max_photo_count is null and v_assignment_found and v_assignment.team_id is not null then select config_value::numeric into v_max_photo_count from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' and scope_type = 'TEAM' and scope_id = v_assignment.team_id::text and is_active = true limit 1; end if; if v_max_photo_count is null then select config_value::numeric into v_max_photo_count from public.sys_sla_config where config_key = 'HOMECARE_CHECKIN_MAX_PHOTO_COUNT' and scope_type = 'GLOBAL' and scope_id is null and is_active = true limit 1; end if; v_max_photo_count := coalesce(v_max_photo_count, 3); if v_requested_count > v_max_photo_count then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'PHOTO_COUNT_EXCEED_LIMIT', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; select count(*) into v_existing_count from public.hc_evidence_files e where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)); if v_existing_count <> v_requested_count then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_NOT_EXIST', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; select count(*) into v_mismatch_count from public.hc_evidence_files e where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) and e.work_order_id <> p_work_order_id; if v_mismatch_count > 0 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_WORK_ORDER_MISMATCH', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; select count(*) into v_not_owned_count from public.hc_evidence_files e where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) and e.work_order_id = p_work_order_id and coalesce(e.uploader_id, '') <> p_worker_id::text; if v_not_owned_count > 0 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_NOT_OWNED', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; select count(*) into v_not_ready_count from public.hc_evidence_files e where e.id in (select jsonb_array_elements_text(p_evidence_file_ids)) and e.work_order_id = p_work_order_id and (e.file_url is null or length(trim(e.file_url)) = 0); if v_not_ready_count > 0 then return jsonb_build_object( 'distanceMeters', null, 'allowedRadiusMeters', 0, 'canCheckin', false, 'reasonCode', 'EVIDENCE_FILE_NOT_READY', 'workerLocationAccepted', false, 'serviceLocationReady', false ); end if; v_precheck := public.rpc_homecare_checkin_precheck( p_work_order_id, p_worker_id, p_latitude, p_longitude, p_coordinate_type, p_accuracy, p_reported_at, 'CHECKIN' ); v_can_checkin := coalesce((v_precheck ->> 'canCheckin')::boolean, false); if not v_can_checkin then return v_precheck; end if; insert into public.hc_work_order_confirmations( id, work_order_id, confirmation_type, status, confirmed_by, confirmed_at, reason, payload, created_at, updated_at ) values ( 'woc_' || gen_random_uuid()::text, p_work_order_id, 'ARRIVAL', 'PENDING', p_worker_id, null, coalesce(p_reason, 'worker_checkin_submitted'), jsonb_build_object( 'distanceMeters', (v_precheck ->> 'distanceMeters')::numeric, 'allowedRadiusMeters', (v_precheck ->> 'allowedRadiusMeters')::numeric, 'coordinateType', coalesce(lower(p_coordinate_type), 'gcj02'), 'reportedAt', coalesce(p_reported_at, now()), 'signatureProvided', (p_signature_payload is not null and length(trim(p_signature_payload)) > 0), 'evidenceFileIds', coalesce(p_evidence_file_ids, '[]'::jsonb) ), now(), now() ); return v_precheck || jsonb_build_object('confirmationInserted', true); end; $$; -- ============================================================ -- Grants (updated for uuid types) -- ============================================================ revoke all on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) from public; grant execute on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) to service_role; revoke all on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) from public; grant execute on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) to service_role; revoke all on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) from public; grant execute on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) to service_role; -- ============================================================ -- Comments -- ============================================================ comment on function public.rpc_homecare_checkin_precheck(uuid, uuid, numeric, numeric, text, numeric, timestamptz, text) is 'Homecare checkin distance precheck RPC with SLA lookup and optional location snapshot insert'; comment on function public.rpc_homecare_dispatch_assignment_upsert(uuid, uuid, uuid, uuid, numeric, numeric, text, uuid, text) is 'Homecare dispatch assignment upsert RPC; rotates current assignment and snapshots service location for distance checkin (uuid types)'; comment on function public.rpc_homecare_checkin_submit(uuid, uuid, numeric, numeric, text, numeric, timestamptz, jsonb, text, text) is 'Homecare checkin submit RPC; re-runs distance precheck and writes confirmation snapshot when checkin is allowed';