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_norm TEXT; BEGIN IF NEW.address IS NOT NULL AND btrim(NEW.address) <> '' THEN -- Aggressive normalization: lowercase, strip all whitespace and common punctuation v_norm := regexp_replace(lower(NEW.address), '[[:space:].,#\-/]+', '', 'g'); -- 1) Exact normalized match on units.address 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 regexp_replace(lower(coalesce(u.address,'')), '[[:space:].,#\-/]+', '', 'g') = v_norm ORDER BY u.created_at LIMIT 1; -- 2) Fallback: match by unit_number or account_number IF v_unit_id IS NULL THEN SELECT u.id INTO v_unit_id FROM public.units u WHERE u.association_id = NEW.association_id AND ( regexp_replace(lower(coalesce(u.unit_number,'')), '[[:space:].,#\-/]+', '', 'g') = v_norm OR regexp_replace(lower(coalesce(u.account_number,'')), '[[:space:].,#\-/]+', '', 'g') = v_norm ) ORDER BY u.created_at LIMIT 1; END IF; -- 3) Fallback: substring match on address IF v_unit_id IS NULL AND length(v_norm) >= 4 THEN SELECT u.id INTO v_unit_id FROM public.units u WHERE u.association_id = NEW.association_id AND regexp_replace(lower(coalesce(u.address,'')), '[[:space:].,#\-/]+', '', 'g') LIKE '%' || v_norm || '%' ORDER BY u.created_at LIMIT 1; END IF; IF v_unit_id IS NOT NULL THEN NEW.unit_id := v_unit_id; END IF; END IF; -- Owner: exact normalized match on property_address 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 regexp_replace(lower(coalesce(o.property_address,'')), '[[:space:].,#\-/]+', '', 'g') = v_norm ORDER BY (CASE WHEN o.status = 'active' THEN 0 ELSE 1 END), o.created_at LIMIT 1; -- Fallback: substring match IF v_owner_id IS NULL AND length(v_norm) >= 4 THEN SELECT o.id INTO v_owner_id FROM public.owners o WHERE o.association_id = NEW.association_id AND regexp_replace(lower(coalesce(o.property_address,'')), '[[:space:].,#\-/]+', '', 'g') LIKE '%' || v_norm || '%' ORDER BY (CASE WHEN o.status = 'active' THEN 0 ELSE 1 END), o.created_at LIMIT 1; END IF; IF v_owner_id IS NOT NULL THEN NEW.owner_id := v_owner_id; END IF; END IF; END IF; -- If unit known but owner unknown, derive owner from unit 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 known but unit unknown, derive 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; -- Backfill address from owner/unit if address is empty IF (NEW.address IS NULL OR btrim(NEW.address) = '') THEN IF NEW.owner_id IS NOT NULL THEN SELECT o.property_address INTO NEW.address FROM public.owners o WHERE o.id = NEW.owner_id; END IF; IF (NEW.address IS NULL OR btrim(NEW.address) = '') AND NEW.unit_id IS NOT NULL THEN SELECT u.address INTO NEW.address FROM public.units u WHERE u.id = NEW.unit_id; END IF; END IF; -- Auto-fill violation_type / title IF NEW.violation_type IS NULL OR btrim(NEW.violation_type) = '' THEN NEW.violation_type := NULLIF(btrim(COALESCE(NEW.category, NEW.title)), ''); END IF; 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(); -- Backfill existing violations: re-run the trigger logic via no-op update UPDATE public.violations SET updated_at = updated_at WHERE (owner_id IS NULL OR unit_id IS NULL) AND address IS NOT NULL AND btrim(address) <> '';