-- Flag rentals as owner-occupied (owner using their own lot) ALTER TABLE public.rv_boat_lot_rentals ADD COLUMN IF NOT EXISTS is_owner BOOLEAN NOT NULL DEFAULT false; -- Helpers for rv_owner role (owner_ids and association_ids the user has access to) CREATE OR REPLACE FUNCTION public.get_rv_owner_owner_ids(_user_id uuid DEFAULT auth.uid()) RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT DISTINCT r.owner_id FROM public.rv_boat_lot_rentals r WHERE r.user_id = _user_id AND r.is_owner = true AND r.owner_id IS NOT NULL AND r.status = 'active' $$; CREATE OR REPLACE FUNCTION public.get_rv_owner_association_ids(_user_id uuid DEFAULT auth.uid()) RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT DISTINCT r.association_id FROM public.rv_boat_lot_rentals r WHERE r.user_id = _user_id AND r.is_owner = true AND r.status = 'active' $$; -- RV owners can read their own rental row (covers the is_owner = true rows) CREATE POLICY "RV owners can view their own rental" ON public.rv_boat_lot_rentals FOR SELECT TO authenticated USING (user_id = auth.uid() AND is_owner = true); -- RV owners can read their lot CREATE POLICY "RV owners can view their lot" ON public.rv_boat_lots FOR SELECT TO authenticated USING ( EXISTS ( SELECT 1 FROM public.rv_boat_lot_rentals r WHERE r.lot_id = rv_boat_lots.id AND r.user_id = auth.uid() AND r.is_owner = true AND r.status = 'active' ) ); -- RV owners can read their association CREATE POLICY "RV owners can view their association" ON public.associations FOR SELECT TO authenticated USING (id IN (SELECT public.get_rv_owner_association_ids(auth.uid()))); -- RV owners can read the linked owner ledger CREATE POLICY "RV owners can view linked owner ledger" ON public.owner_ledger_entries FOR SELECT TO authenticated USING (owner_id IN (SELECT public.get_rv_owner_owner_ids(auth.uid()))); -- RV owners can read the linked owner record CREATE POLICY "RV owners can view linked owner" ON public.owners FOR SELECT TO authenticated USING (id IN (SELECT public.get_rv_owner_owner_ids(auth.uid()))); -- Intent to Vacate CREATE TABLE IF NOT EXISTS public.rv_intent_to_vacate ( id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, rental_id UUID NOT NULL REFERENCES public.rv_boat_lot_rentals(id) ON DELETE CASCADE, association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE, lot_id UUID REFERENCES public.rv_boat_lots(id) ON DELETE SET NULL, owner_id UUID REFERENCES public.owners(id) ON DELETE SET NULL, submitted_by_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, vacate_date DATE NOT NULL, notes TEXT, status TEXT NOT NULL DEFAULT 'pending', acknowledged_at TIMESTAMP WITH TIME ZONE, acknowledged_by_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS rv_itv_rental_idx ON public.rv_intent_to_vacate(rental_id); CREATE INDEX IF NOT EXISTS rv_itv_assoc_idx ON public.rv_intent_to_vacate(association_id); CREATE INDEX IF NOT EXISTS rv_itv_user_idx ON public.rv_intent_to_vacate(submitted_by_user_id); ALTER TABLE public.rv_intent_to_vacate ENABLE ROW LEVEL SECURITY; CREATE TRIGGER update_rv_intent_to_vacate_updated_at BEFORE UPDATE ON public.rv_intent_to_vacate FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); -- RLS: RV owners manage their own intents CREATE POLICY "RV owners can view their intents" ON public.rv_intent_to_vacate FOR SELECT TO authenticated USING (submitted_by_user_id = auth.uid()); CREATE POLICY "RV owners can submit intents for their rental" ON public.rv_intent_to_vacate FOR INSERT TO authenticated WITH CHECK ( submitted_by_user_id = auth.uid() AND EXISTS ( SELECT 1 FROM public.rv_boat_lot_rentals r WHERE r.id = rv_intent_to_vacate.rental_id AND r.user_id = auth.uid() AND r.is_owner = true AND r.status = 'active' ) ); CREATE POLICY "RV owners can cancel their pending intents" ON public.rv_intent_to_vacate FOR UPDATE TO authenticated USING (submitted_by_user_id = auth.uid() AND status = 'pending') WITH CHECK (submitted_by_user_id = auth.uid()); -- Staff can manage all CREATE POLICY "Staff can view all intents" ON public.rv_intent_to_vacate FOR SELECT TO authenticated USING ( public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role) ); CREATE POLICY "Staff can update all intents" ON public.rv_intent_to_vacate FOR UPDATE TO authenticated USING ( public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role) ) WITH CHECK ( public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role) ); CREATE POLICY "Staff can delete intents" ON public.rv_intent_to_vacate FOR DELETE TO authenticated USING ( public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role) ); -- Notify staff when an Intent to Vacate is submitted CREATE OR REPLACE FUNCTION public.notify_staff_on_rv_intent_to_vacate() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE staff_user record; v_renter text; v_message text; BEGIN SELECT r.renter_name INTO v_renter FROM public.rv_boat_lot_rentals r WHERE r.id = NEW.rental_id; v_message := COALESCE(v_renter, 'An RV/Boat lot owner') || ' submitted an Intent to Vacate effective ' || to_char(NEW.vacate_date, 'Mon DD, YYYY'); FOR staff_user IN SELECT DISTINCT user_id FROM public.user_roles WHERE role IN ('admin'::public.app_role, 'manager'::public.app_role) AND user_id IS NOT NULL LOOP INSERT INTO public.in_app_notifications (user_id, type, title, message, related_item_id, related_item_type, link) VALUES ( staff_user.user_id, 'rv_intent_to_vacate', 'RV/Boat Lot — Intent to Vacate', v_message, NEW.id, 'rv_intent_to_vacate', '/dashboard/rv-boat-lots' ); END LOOP; RETURN NEW; END; $$; CREATE TRIGGER trg_notify_staff_on_rv_intent_to_vacate AFTER INSERT ON public.rv_intent_to_vacate FOR EACH ROW EXECUTE FUNCTION public.notify_staff_on_rv_intent_to_vacate();