Spaces:
Running
Running
| -- SLA Engine — Database schema for SLA breach detection and escalation | |
| -- Part of Issue #75: SLA Breach & Automated Multi-Channel Escalation Engine | |
| -- ========================================================================= | |
| -- 1. Extend the tickets table with SLA columns | |
| -- ========================================================================= | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_status text | |
| DEFAULT 'active' | |
| CHECK (sla_status IN ('active', 'warning', 'breached', 'met', 'paused')); | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_policy text | |
| DEFAULT 'medium' | |
| CHECK (sla_policy IN ('critical', 'high', 'medium', 'low')); | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_breach_at timestamptz; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_warning_at timestamptz; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_started_at timestamptz; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS sla_updated_at timestamptz; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS escalation_level integer | |
| DEFAULT 0 | |
| CHECK (escalation_level >= 0 AND escalation_level <= 3); | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS last_escalated_at timestamptz; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS remaining_seconds integer | |
| DEFAULT 0; | |
| ALTER TABLE public.tickets | |
| ADD COLUMN IF NOT EXISTS channel_notified text[] | |
| DEFAULT '{}'; | |
| -- ========================================================================= | |
| -- 2. Escalation Logs — audit trail for every SLA escalation event | |
| -- ========================================================================= | |
| CREATE TABLE IF NOT EXISTS public.escalation_logs ( | |
| id uuid DEFAULT gen_random_uuid() PRIMARY KEY, | |
| ticket_id uuid REFERENCES public.tickets(id) ON DELETE CASCADE, | |
| ticket_subject text NOT NULL DEFAULT '', | |
| priority text NOT NULL DEFAULT 'medium', | |
| sla_status text NOT NULL CHECK (sla_status IN ('active', 'warning', 'breached', 'met', 'paused')), | |
| escalation_level integer NOT NULL DEFAULT 0, | |
| remaining_seconds integer NOT NULL DEFAULT 0, | |
| assigned_team text NOT NULL DEFAULT '', | |
| notification_channels text[] DEFAULT '{}', | |
| triggered_at timestamptz NOT NULL DEFAULT now(), | |
| resolved_at timestamptz, | |
| notes text DEFAULT '' | |
| ); | |
| -- Index for fast dashboard queries | |
| CREATE INDEX IF NOT EXISTS idx_escalation_logs_ticket_id ON public.escalation_logs(ticket_id); | |
| CREATE INDEX IF NOT EXISTS idx_escalation_logs_triggered_at ON public.escalation_logs(triggered_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_escalation_logs_sla_status ON public.escalation_logs(sla_status); | |
| -- Enable RLS | |
| ALTER TABLE public.escalation_logs ENABLE ROW LEVEL SECURITY; | |
| -- Admins can read all escalation logs | |
| CREATE POLICY "Admins can read escalation logs" ON public.escalation_logs | |
| FOR SELECT | |
| USING ( | |
| EXISTS ( | |
| SELECT 1 FROM public.profiles | |
| WHERE profiles.id = auth.uid() | |
| AND (profiles.role = 'admin' OR profiles.role = 'master_admin') | |
| ) | |
| ); | |
| -- Service role and triggers can insert | |
| CREATE POLICY "Service role can insert escalation logs" ON public.escalation_logs | |
| FOR INSERT | |
| WITH CHECK (true); | |
| -- ========================================================================= | |
| -- 3. SLA Policies — configurable SLA definitions (optional override table) | |
| -- ========================================================================= | |
| CREATE TABLE IF NOT EXISTS public.sla_policies ( | |
| id uuid DEFAULT gen_random_uuid() PRIMARY KEY, | |
| priority text NOT NULL UNIQUE CHECK (priority IN ('critical', 'high', 'medium', 'low')), | |
| max_hours integer NOT NULL DEFAULT 8, | |
| warning_pct numeric(4,3) NOT NULL DEFAULT 0.750, | |
| auto_escalate boolean NOT NULL DEFAULT true, | |
| l2_after_minutes integer NOT NULL DEFAULT 0, | |
| l3_after_minutes integer NOT NULL DEFAULT 120, | |
| company_id uuid REFERENCES public.companies(id) ON DELETE CASCADE, | |
| is_custom boolean DEFAULT false, | |
| created_at timestamptz NOT NULL DEFAULT now(), | |
| updated_at timestamptz | |
| ); | |
| -- Insert default SLA policies | |
| INSERT INTO public.sla_policies (priority, max_hours, warning_pct, auto_escalate, l2_after_minutes, l3_after_minutes) | |
| VALUES | |
| ('critical', 2, 0.750, true, 0, 120), | |
| ('high', 4, 0.750, true, 30, 240), | |
| ('medium', 8, 0.750, true, 60, 480), | |
| ('low', 24, 0.750, false, 120, 1440) | |
| ON CONFLICT (priority) DO NOTHING; | |
| ALTER TABLE public.sla_policies ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY "Anyone can read sla_policies" ON public.sla_policies | |
| FOR SELECT USING (true); | |
| CREATE POLICY "Master admins can manage sla_policies" ON public.sla_policies | |
| FOR ALL | |
| USING ( | |
| EXISTS ( | |
| SELECT 1 FROM public.profiles | |
| WHERE profiles.id = auth.uid() | |
| AND profiles.role = 'master_admin' | |
| ) | |
| ); | |
| -- ========================================================================= | |
| -- 4. Indexes on tickets for SLA queries | |
| -- ========================================================================= | |
| CREATE INDEX IF NOT EXISTS idx_tickets_sla_status ON public.tickets(sla_status); | |
| CREATE INDEX IF NOT EXISTS idx_tickets_escalation_level ON public.tickets(escalation_level); | |
| CREATE INDEX IF NOT EXISTS idx_tickets_sla_breach_at ON public.tickets(sla_breach_at); | |
| CREATE INDEX IF NOT EXISTS idx_tickets_priority_sla ON public.tickets(priority, sla_status); | |
| -- ========================================================================= | |
| -- 5. Trigger: auto-set sla_policy when priority changes | |
| -- ========================================================================= | |
| CREATE OR REPLACE FUNCTION public.auto_set_sla_policy() | |
| RETURNS trigger AS $$ | |
| BEGIN | |
| NEW.sla_policy := LOWER(NEW.priority); | |
| -- If SLA hasn't started yet, start it now | |
| IF NEW.sla_started_at IS NULL THEN | |
| NEW.sla_started_at := now(); | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| DROP TRIGGER IF EXISTS trg_auto_set_sla_policy ON public.tickets; | |
| CREATE TRIGGER trg_auto_set_sla_policy | |
| BEFORE INSERT OR UPDATE OF priority ON public.tickets | |
| FOR EACH ROW | |
| EXECUTE FUNCTION public.auto_set_sla_policy(); | |
| -- ========================================================================= | |
| -- 6. Trigger: log when sla_status changes to breached | |
| -- ========================================================================= | |
| CREATE OR REPLACE FUNCTION public.log_sla_breach() | |
| RETURNS trigger AS $$ | |
| BEGIN | |
| IF NEW.sla_status = 'breached' AND (OLD.sla_status IS DISTINCT FROM 'breached') THEN | |
| INSERT INTO public.escalation_logs ( | |
| ticket_id, ticket_subject, priority, sla_status, | |
| escalation_level, remaining_seconds, assigned_team, | |
| notification_channels, triggered_at | |
| ) VALUES ( | |
| NEW.id, | |
| COALESCE(NEW.subject, NEW.summary, ''), | |
| NEW.priority, | |
| 'breached', | |
| COALESCE(NEW.escalation_level, 1), | |
| COALESCE(NEW.remaining_seconds, 0), | |
| COALESCE(NEW.assigned_team, ''), | |
| '{}', | |
| now() | |
| ); | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| DROP TRIGGER IF EXISTS trg_log_sla_breach ON public.tickets; | |
| CREATE TRIGGER trg_log_sla_breach | |
| AFTER UPDATE OF sla_status ON public.tickets | |
| FOR EACH ROW | |
| WHEN (NEW.sla_status = 'breached') | |
| EXECUTE FUNCTION public.log_sla_breach(); | |
| -- ========================================================================= | |
| -- 7. Grant permissions | |
| -- ========================================================================= | |
| GRANT ALL ON TABLE public.escalation_logs TO authenticated; | |
| GRANT ALL ON TABLE public.escalation_logs TO service_role; | |
| GRANT SELECT ON TABLE public.sla_policies TO authenticated; | |
| GRANT ALL ON TABLE public.sla_policies TO service_role; | |