feat(attendance): add geofence monitoring and policy controls

This commit is contained in:
zouantchaw
2026-03-16 15:31:13 +01:00
parent b455455a49
commit 5d8240ed51
22 changed files with 1667 additions and 162 deletions

View File

@@ -0,0 +1,155 @@
ALTER TABLE clock_points
ADD COLUMN IF NOT EXISTS default_clock_in_mode TEXT,
ADD COLUMN IF NOT EXISTS allow_clock_in_override BOOLEAN;
UPDATE clock_points
SET default_clock_in_mode = COALESCE(default_clock_in_mode, 'EITHER'),
allow_clock_in_override = COALESCE(allow_clock_in_override, TRUE)
WHERE default_clock_in_mode IS NULL
OR allow_clock_in_override IS NULL;
ALTER TABLE clock_points
ALTER COLUMN default_clock_in_mode SET DEFAULT 'EITHER',
ALTER COLUMN default_clock_in_mode SET NOT NULL,
ALTER COLUMN allow_clock_in_override SET DEFAULT TRUE,
ALTER COLUMN allow_clock_in_override SET NOT NULL;
ALTER TABLE clock_points
DROP CONSTRAINT IF EXISTS clock_points_default_clock_in_mode_check;
ALTER TABLE clock_points
ADD CONSTRAINT clock_points_default_clock_in_mode_check
CHECK (default_clock_in_mode IN ('NFC_REQUIRED', 'GEO_REQUIRED', 'EITHER'));
ALTER TABLE shifts
ADD COLUMN IF NOT EXISTS clock_in_mode TEXT,
ADD COLUMN IF NOT EXISTS allow_clock_in_override BOOLEAN;
ALTER TABLE shifts
DROP CONSTRAINT IF EXISTS shifts_clock_in_mode_check;
ALTER TABLE shifts
ADD CONSTRAINT shifts_clock_in_mode_check
CHECK (clock_in_mode IS NULL OR clock_in_mode IN ('NFC_REQUIRED', 'GEO_REQUIRED', 'EITHER'));
CREATE TABLE IF NOT EXISTS location_stream_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
business_id UUID REFERENCES businesses(id) ON DELETE SET NULL,
vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL,
shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE,
assignment_id UUID NOT NULL REFERENCES assignments(id) ON DELETE CASCADE,
staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT,
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
source_type TEXT NOT NULL DEFAULT 'GEO'
CHECK (source_type IN ('NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM')),
device_id TEXT,
object_uri TEXT,
point_count INTEGER NOT NULL DEFAULT 0 CHECK (point_count >= 0),
out_of_geofence_count INTEGER NOT NULL DEFAULT 0 CHECK (out_of_geofence_count >= 0),
missing_coordinate_count INTEGER NOT NULL DEFAULT 0 CHECK (missing_coordinate_count >= 0),
max_distance_to_clock_point_meters INTEGER CHECK (max_distance_to_clock_point_meters IS NULL OR max_distance_to_clock_point_meters >= 0),
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_location_stream_batches_assignment_received
ON location_stream_batches (assignment_id, received_at DESC);
CREATE INDEX IF NOT EXISTS idx_location_stream_batches_staff_received
ON location_stream_batches (staff_id, received_at DESC);
CREATE TABLE IF NOT EXISTS geofence_incidents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
business_id UUID REFERENCES businesses(id) ON DELETE SET NULL,
vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL,
shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE,
assignment_id UUID REFERENCES assignments(id) ON DELETE SET NULL,
staff_id UUID REFERENCES staffs(id) ON DELETE SET NULL,
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
location_stream_batch_id UUID REFERENCES location_stream_batches(id) ON DELETE SET NULL,
incident_type TEXT NOT NULL
CHECK (incident_type IN ('CLOCK_IN_OVERRIDE', 'OUTSIDE_GEOFENCE', 'LOCATION_UNAVAILABLE', 'NFC_MISMATCH', 'CLOCK_IN_REJECTED')),
severity TEXT NOT NULL DEFAULT 'WARNING'
CHECK (severity IN ('INFO', 'WARNING', 'CRITICAL')),
status TEXT NOT NULL DEFAULT 'OPEN'
CHECK (status IN ('OPEN', 'ACKNOWLEDGED', 'RESOLVED')),
effective_clock_in_mode TEXT
CHECK (effective_clock_in_mode IS NULL OR effective_clock_in_mode IN ('NFC_REQUIRED', 'GEO_REQUIRED', 'EITHER')),
source_type TEXT
CHECK (source_type IS NULL OR source_type IN ('NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM')),
nfc_tag_uid TEXT,
device_id TEXT,
latitude NUMERIC(9, 6),
longitude NUMERIC(9, 6),
accuracy_meters INTEGER CHECK (accuracy_meters IS NULL OR accuracy_meters >= 0),
distance_to_clock_point_meters INTEGER CHECK (distance_to_clock_point_meters IS NULL OR distance_to_clock_point_meters >= 0),
within_geofence BOOLEAN,
override_reason TEXT,
message TEXT,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_geofence_incidents_assignment_occurred
ON geofence_incidents (assignment_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_geofence_incidents_shift_occurred
ON geofence_incidents (shift_id, occurred_at DESC);
CREATE INDEX IF NOT EXISTS idx_geofence_incidents_staff_occurred
ON geofence_incidents (staff_id, occurred_at DESC);
CREATE TABLE IF NOT EXISTS notification_outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
business_id UUID REFERENCES businesses(id) ON DELETE SET NULL,
shift_id UUID REFERENCES shifts(id) ON DELETE SET NULL,
assignment_id UUID REFERENCES assignments(id) ON DELETE SET NULL,
related_incident_id UUID REFERENCES geofence_incidents(id) ON DELETE SET NULL,
audience_type TEXT NOT NULL DEFAULT 'USER'
CHECK (audience_type IN ('USER', 'STAFF', 'BUSINESS_MEMBERSHIP', 'SYSTEM')),
recipient_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
recipient_staff_id UUID REFERENCES staffs(id) ON DELETE SET NULL,
recipient_business_membership_id UUID REFERENCES business_memberships(id) ON DELETE SET NULL,
channel TEXT NOT NULL DEFAULT 'PUSH'
CHECK (channel IN ('PUSH', 'EMAIL', 'SMS', 'IN_APP', 'WEBHOOK')),
notification_type TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'NORMAL'
CHECK (priority IN ('LOW', 'NORMAL', 'HIGH', 'CRITICAL')),
dedupe_key TEXT,
subject TEXT,
body TEXT,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PROCESSING', 'SENT', 'FAILED', 'CANCELLED')),
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sent_at TIMESTAMPTZ,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_notification_outbox_recipient
CHECK (
recipient_user_id IS NOT NULL
OR recipient_staff_id IS NOT NULL
OR recipient_business_membership_id IS NOT NULL
OR audience_type = 'SYSTEM'
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_outbox_dedupe
ON notification_outbox (dedupe_key);
CREATE INDEX IF NOT EXISTS idx_notification_outbox_status_schedule
ON notification_outbox (status, scheduled_at ASC);
CREATE INDEX IF NOT EXISTS idx_notification_outbox_recipient_user
ON notification_outbox (recipient_user_id, created_at DESC)
WHERE recipient_user_id IS NOT NULL;

View File

@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_notification_outbox_dedupe;
CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_outbox_dedupe
ON notification_outbox (dedupe_key);