tasq/supabase/migrations/20260316120000_it_service_request_geofence_override.sql

194 lines
6.0 KiB
PL/PgSQL

-- Allow assigned IT staff to check in/out outside the geofence when an approved
-- IT Service Request has "outside_premise_allowed" enabled.
--
-- This enforces the same behavior server-side so clients cannot bypass geofence
-- checks by sending lat/lng values directly.
-- Recreate attendance_check_in to validate geofence and allow override.
CREATE OR REPLACE FUNCTION public.attendance_check_in(
p_duty_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_schedule duty_schedules%ROWTYPE;
v_log_id uuid;
v_now timestamptz := now();
v_status text;
v_geofence jsonb;
v_in_premise boolean := true;
v_polygon jsonb;
v_point_count int;
v_i int;
v_j int;
v_xi double precision;
v_yi double precision;
v_xj double precision;
v_yj double precision;
v_inside boolean := false;
v_override boolean := false;
BEGIN
SELECT * INTO v_schedule FROM duty_schedules WHERE id = p_duty_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Duty schedule not found';
END IF;
IF v_schedule.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your duty schedule';
END IF;
-- Validate geofence (unless overridden by a signed IT service request)
SELECT value INTO v_geofence FROM app_settings WHERE key = 'geofence';
IF v_geofence IS NOT NULL THEN
v_polygon := COALESCE(v_geofence->'polygon', v_geofence->'points');
IF v_polygon IS NOT NULL AND jsonb_array_length(v_polygon) > 2 THEN
-- Ray-casting point-in-polygon
v_point_count := jsonb_array_length(v_polygon);
v_j := v_point_count - 1;
FOR v_i IN 0..(v_point_count - 1) LOOP
v_xi := (v_polygon->v_i->>'lng')::double precision;
v_yi := (v_polygon->v_i->>'lat')::double precision;
v_xj := (v_polygon->v_j->>'lng')::double precision;
v_yj := (v_polygon->v_j->>'lat')::double precision;
IF ((v_yi > p_lat) != (v_yj > p_lat)) AND
(p_lng < (v_xj - v_xi) * (p_lat - v_yi) / (v_yj - v_yi) + v_xi) THEN
v_inside := NOT v_inside;
END IF;
v_j := v_i;
END LOOP;
v_in_premise := v_inside;
END IF;
END IF;
IF NOT v_in_premise THEN
SELECT EXISTS (
SELECT 1
FROM it_service_request_assignments a
JOIN it_service_requests r ON r.id = a.request_id
WHERE a.user_id = auth.uid()
AND r.outside_premise_allowed
AND r.status IN ('scheduled', 'in_progress_dry_run', 'in_progress')
) INTO v_override;
IF NOT v_override THEN
RAISE EXCEPTION 'Outside geofence';
END IF;
END IF;
-- Check 2-hour window
IF v_now < (v_schedule.start_time - interval '2 hours') THEN
RAISE EXCEPTION 'Too early to check in (2-hour window)';
END IF;
IF v_now > v_schedule.end_time THEN
RAISE EXCEPTION 'Duty has already ended';
END IF;
-- Determine status (used only when first check-in)
IF v_now <= v_schedule.start_time THEN
v_status := 'arrival';
ELSE
v_status := 'late';
END IF;
-- Insert a new attendance log row for each check-in
INSERT INTO attendance_logs (user_id, duty_schedule_id, check_in_at, check_in_lat, check_in_lng)
VALUES (auth.uid(), p_duty_id, v_now, p_lat, p_lng)
RETURNING id INTO v_log_id;
-- Only update duty schedule on first check-in (preserve original arrival status)
UPDATE duty_schedules
SET check_in_at = COALESCE(check_in_at, v_now),
check_in_location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
status = CASE
WHEN check_in_at IS NULL THEN v_status::duty_status
ELSE status
END
WHERE id = p_duty_id;
RETURN v_log_id;
END;
$$;
-- Recreate attendance_check_out to validate geofence and allow override.
CREATE OR REPLACE FUNCTION public.attendance_check_out(
p_attendance_id uuid,
p_lat double precision,
p_lng double precision
) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_log attendance_logs%ROWTYPE;
v_geofence jsonb;
v_in_premise boolean := true;
v_polygon jsonb;
v_point_count int;
v_i int;
v_j int;
v_xi double precision;
v_yi double precision;
v_xj double precision;
v_yj double precision;
v_inside boolean := false;
v_override boolean := false;
BEGIN
SELECT * INTO v_log FROM attendance_logs WHERE id = p_attendance_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Attendance log not found';
END IF;
IF v_log.user_id != auth.uid() THEN
RAISE EXCEPTION 'Not your attendance log';
END IF;
IF v_log.check_out_at IS NOT NULL THEN
RAISE EXCEPTION 'Already checked out';
END IF;
-- Validate geofence (unless overridden by a signed IT service request)
SELECT value INTO v_geofence FROM app_settings WHERE key = 'geofence';
IF v_geofence IS NOT NULL THEN
v_polygon := COALESCE(v_geofence->'polygon', v_geofence->'points');
IF v_polygon IS NOT NULL AND jsonb_array_length(v_polygon) > 2 THEN
-- Ray-casting point-in-polygon
v_point_count := jsonb_array_length(v_polygon);
v_j := v_point_count - 1;
FOR v_i IN 0..(v_point_count - 1) LOOP
v_xi := (v_polygon->v_i->>'lng')::double precision;
v_yi := (v_polygon->v_i->>'lat')::double precision;
v_xj := (v_polygon->v_j->>'lng')::double precision;
v_yj := (v_polygon->v_j->>'lat')::double precision;
IF ((v_yi > p_lat) != (v_yj > p_lat)) AND
(p_lng < (v_xj - v_xi) * (p_lat - v_yi) / (v_yj - v_yi) + v_xi) THEN
v_inside := NOT v_inside;
END IF;
v_j := v_i;
END LOOP;
v_in_premise := v_inside;
END IF;
END IF;
IF NOT v_in_premise THEN
SELECT EXISTS (
SELECT 1
FROM it_service_request_assignments a
JOIN it_service_requests r ON r.id = a.request_id
WHERE a.user_id = auth.uid()
AND r.outside_premise_allowed
AND r.status IN ('scheduled', 'in_progress_dry_run', 'in_progress')
) INTO v_override;
IF NOT v_override THEN
RAISE EXCEPTION 'Outside geofence';
END IF;
END IF;
UPDATE attendance_logs
SET check_out_at = now(),
check_out_lat = p_lat,
check_out_lng = p_lng
WHERE id = p_attendance_id;
END;
$$;