-- Tenant info request flow (mirrors vendor_profile_requests pattern) CREATE TABLE public.tenant_info_requests ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid NOT NULL REFERENCES public.owners(id) ON DELETE CASCADE, unit_id uuid REFERENCES public.units(id) ON DELETE SET NULL, association_id uuid NOT NULL, token text NOT NULL UNIQUE DEFAULT encode(extensions.gen_random_bytes(24), 'hex'), sent_to_email text, custom_message text, sent_at timestamptz NOT NULL DEFAULT now(), submitted_at timestamptz, expires_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'), created_by uuid, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_tenant_info_requests_token ON public.tenant_info_requests(token); CREATE INDEX idx_tenant_info_requests_owner ON public.tenant_info_requests(owner_id); CREATE INDEX idx_tenant_info_requests_unit ON public.tenant_info_requests(unit_id); ALTER TABLE public.tenant_info_requests ENABLE ROW LEVEL SECURITY; CREATE POLICY "Staff manage tenant info requests" ON public.tenant_info_requests TO authenticated USING (has_role(auth.uid(),'admin'::app_role) OR has_role(auth.uid(),'manager'::app_role)) WITH CHECK (has_role(auth.uid(),'admin'::app_role) OR has_role(auth.uid(),'manager'::app_role)); -- Submission rows store full tenant data per request CREATE TABLE public.tenant_info_submissions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), request_id uuid NOT NULL REFERENCES public.tenant_info_requests(id) ON DELETE CASCADE, owner_id uuid NOT NULL, unit_id uuid, association_id uuid NOT NULL, -- Basic tenant_first_name text NOT NULL, tenant_last_name text NOT NULL, occupants jsonb DEFAULT '[]'::jsonb, -- Contact email text, phone text, emergency_contact_name text, emergency_contact_phone text, -- Lease lease_start date, lease_end date, lease_length_months integer, lease_document_url text, -- Vehicles & pets (free-form arrays) vehicles jsonb DEFAULT '[]'::jsonb, pets jsonb DEFAULT '[]'::jsonb, notes text, submitted_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_tenant_info_submissions_owner ON public.tenant_info_submissions(owner_id); CREATE INDEX idx_tenant_info_submissions_unit ON public.tenant_info_submissions(unit_id); ALTER TABLE public.tenant_info_submissions ENABLE ROW LEVEL SECURITY; CREATE POLICY "Staff manage tenant info submissions" ON public.tenant_info_submissions TO authenticated USING (has_role(auth.uid(),'admin'::app_role) OR has_role(auth.uid(),'manager'::app_role)) WITH CHECK (has_role(auth.uid(),'admin'::app_role) OR has_role(auth.uid(),'manager'::app_role)); -- Lookup RPC (public, security definer — token is the secret) CREATE OR REPLACE FUNCTION public.lookup_tenant_info_request(p_token text) RETURNS TABLE ( request_id uuid, owner_id uuid, unit_id uuid, association_id uuid, owner_name text, property_address text, association_name text, custom_message text, expires_at timestamptz, submitted_at timestamptz ) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT r.id, r.owner_id, r.unit_id, r.association_id, trim(coalesce(o.first_name,'') || ' ' || coalesce(o.last_name,'')) AS owner_name, coalesce(u.address, o.property_address) AS property_address, a.name AS association_name, r.custom_message, r.expires_at, r.submitted_at FROM public.tenant_info_requests r JOIN public.owners o ON o.id = r.owner_id LEFT JOIN public.units u ON u.id = r.unit_id LEFT JOIN public.associations a ON a.id = r.association_id WHERE r.token = p_token LIMIT 1; $$; GRANT EXECUTE ON FUNCTION public.lookup_tenant_info_request(text) TO anon, authenticated; -- Submit RPC CREATE OR REPLACE FUNCTION public.submit_tenant_info( p_token text, p_tenant_first_name text, p_tenant_last_name text, p_email text DEFAULT NULL, p_phone text DEFAULT NULL, p_emergency_contact_name text DEFAULT NULL, p_emergency_contact_phone text DEFAULT NULL, p_lease_start date DEFAULT NULL, p_lease_end date DEFAULT NULL, p_lease_length_months integer DEFAULT NULL, p_lease_document_url text DEFAULT NULL, p_vehicles jsonb DEFAULT '[]'::jsonb, p_pets jsonb DEFAULT '[]'::jsonb, p_occupants jsonb DEFAULT '[]'::jsonb, p_notes text DEFAULT NULL ) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE r record; BEGIN SELECT * INTO r FROM public.tenant_info_requests WHERE token = p_token; IF r IS NULL THEN RETURN false; END IF; IF r.submitted_at IS NOT NULL THEN RETURN false; END IF; IF r.expires_at < now() THEN RETURN false; END IF; INSERT INTO public.tenant_info_submissions ( request_id, owner_id, unit_id, association_id, tenant_first_name, tenant_last_name, occupants, email, phone, emergency_contact_name, emergency_contact_phone, lease_start, lease_end, lease_length_months, lease_document_url, vehicles, pets, notes ) VALUES ( r.id, r.owner_id, r.unit_id, r.association_id, p_tenant_first_name, p_tenant_last_name, coalesce(p_occupants,'[]'::jsonb), p_email, p_phone, p_emergency_contact_name, p_emergency_contact_phone, p_lease_start, p_lease_end, p_lease_length_months, p_lease_document_url, coalesce(p_vehicles,'[]'::jsonb), coalesce(p_pets,'[]'::jsonb), p_notes ); -- Upsert a basic record into unit_tenants for roster visibility IF r.unit_id IS NOT NULL THEN INSERT INTO public.unit_tenants (unit_id, name, email, phone, lease_start, lease_end, status) VALUES ( r.unit_id, trim(p_tenant_first_name || ' ' || p_tenant_last_name), p_email, p_phone, p_lease_start, p_lease_end, 'active' ); END IF; UPDATE public.tenant_info_requests SET submitted_at = now() WHERE id = r.id; RETURN true; END; $$; GRANT EXECUTE ON FUNCTION public.submit_tenant_info( text,text,text,text,text,text,text,date,date,integer,text,jsonb,jsonb,jsonb,text ) TO anon, authenticated;