CREATE OR REPLACE FUNCTION public.auto_link_violation_owner() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$ DECLARE v_owner_id UUID; v_unit_id UUID; v_normalized_address TEXT; BEGIN -- Normalize the violation address once IF NEW.address IS NOT NULL AND NEW.address != '' THEN v_normalized_address := LOWER(TRIM(REPLACE(REPLACE(REPLACE(REPLACE(NEW.address, '.', ''), ',', ''), '#', ''), '-', ''))); -- Auto-link unit_id if not already set IF NEW.unit_id IS NULL THEN SELECT u.id INTO v_unit_id FROM public.units u WHERE u.association_id = NEW.association_id AND LOWER(TRIM(REPLACE(REPLACE(REPLACE(REPLACE(u.address, '.', ''), ',', ''), '#', ''), '-', ''))) = v_normalized_address ORDER BY u.created_at LIMIT 1; IF v_unit_id IS NOT NULL THEN NEW.unit_id := v_unit_id; END IF; END IF; -- Auto-link owner_id if not already set IF NEW.owner_id IS NULL THEN SELECT o.id INTO v_owner_id FROM public.owners o WHERE o.association_id = NEW.association_id AND LOWER(TRIM(REPLACE(REPLACE(REPLACE(REPLACE(o.property_address, '.', ''), ',', ''), '#', ''), '-', ''))) = v_normalized_address ORDER BY (CASE WHEN o.status = 'active' THEN 0 ELSE 1 END), o.created_at LIMIT 1; IF v_owner_id IS NOT NULL THEN NEW.owner_id := v_owner_id; END IF; END IF; END IF; -- If unit found but owner still null, try linking owner via unit_id IF NEW.owner_id IS NULL AND NEW.unit_id IS NOT NULL THEN SELECT o.id INTO v_owner_id FROM public.owners o WHERE o.association_id = NEW.association_id AND o.unit_id = NEW.unit_id ORDER BY (CASE WHEN o.status = 'active' THEN 0 ELSE 1 END), o.created_at LIMIT 1; IF v_owner_id IS NOT NULL THEN NEW.owner_id := v_owner_id; END IF; END IF; -- If owner is set but unit isn't, copy unit from owner IF NEW.unit_id IS NULL AND NEW.owner_id IS NOT NULL THEN SELECT o.unit_id INTO v_unit_id FROM public.owners o WHERE o.id = NEW.owner_id; IF v_unit_id IS NOT NULL THEN NEW.unit_id := v_unit_id; END IF; END IF; -- Auto-fill violation_type from category or title if missing IF NEW.violation_type IS NULL OR btrim(NEW.violation_type) = '' THEN NEW.violation_type := NULLIF(btrim(COALESCE(NEW.category, NEW.title)), ''); END IF; -- Mirror back to title if title is missing IF NEW.title IS NULL OR btrim(NEW.title) = '' THEN NEW.title := COALESCE(NEW.violation_type, 'Violation'); END IF; RETURN NEW; END; $function$; DROP TRIGGER IF EXISTS auto_link_violation_owner_trigger ON public.violations; CREATE TRIGGER auto_link_violation_owner_trigger BEFORE INSERT OR UPDATE ON public.violations FOR EACH ROW EXECUTE FUNCTION public.auto_link_violation_owner();