-- Migration 026: item_frequency.weight column + admin-only RPCs (Fase 15.1) -- -- Adds a curation knob on top of the existing `use_count` suggestion ordering. -- Admins can promote (positive weight) or hide (negative weight) specific -- items in the per-collective suggestion dropdown. The client UI exposes a -- 3-way Hide / Normal / Boost control mapped to -50 / 0 / 50, but the column -- accepts any integer so future granularity (e.g. "strong boost") does not -- require a schema migration. -- -- Write path: same all-deny RLS as migration 006 — clients never UPDATE the -- column directly. The two SECURITY DEFINER RPCs below are the only entry -- points. Both gate on `collective_members.role = 'admin'`. ALTER TABLE public.item_frequency ADD COLUMN weight integer NOT NULL DEFAULT 0; -- Re-create the suggestion index to include weight first. The old index was a -- prefix of this one — drop it so we don't carry two overlapping indexes. DROP INDEX IF EXISTS public.item_frequency_collective_count_idx; CREATE INDEX item_frequency_collective_order_idx ON public.item_frequency (collective_id, weight DESC, use_count DESC, last_used_at DESC); -- ── RPC: admin upsert of weight ───────────────────────────────────────────── -- Upsert semantics: if the (collective_id, name) row doesn't exist yet, -- insert it with use_count=0 and the requested weight. Useful for seeding -- the catalog before any list actually mentions the item. CREATE OR REPLACE FUNCTION public.set_item_frequency_weight( p_collective_id uuid, p_name text, p_weight integer ) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, auth AS $$ DECLARE v_role text; v_norm text := lower(trim(p_name)); BEGIN IF auth.uid() IS NULL THEN RAISE EXCEPTION 'not authenticated' USING ERRCODE = 'P0001'; END IF; IF v_norm = '' THEN RAISE EXCEPTION 'name must not be empty' USING ERRCODE = 'P0001'; END IF; SELECT role INTO v_role FROM public.collective_members WHERE collective_id = p_collective_id AND user_id = auth.uid(); IF v_role IS DISTINCT FROM 'admin' THEN RAISE EXCEPTION 'only admins can manage common items' USING ERRCODE = 'P0002'; END IF; INSERT INTO public.item_frequency (collective_id, name, use_count, weight, last_used_at) VALUES (p_collective_id, v_norm, 0, p_weight, now()) ON CONFLICT (collective_id, name) DO UPDATE SET weight = EXCLUDED.weight; END; $$; GRANT EXECUTE ON FUNCTION public.set_item_frequency_weight(uuid, text, integer) TO authenticated; -- ── RPC: admin purge ──────────────────────────────────────────────────────── -- Removes the row from item_frequency only. Does NOT touch shopping_items -- (the existing items in lists stay; future inserts will recreate this row -- with weight=0 via the existing trigger). CREATE OR REPLACE FUNCTION public.purge_item_frequency( p_collective_id uuid, p_name text ) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, auth AS $$ DECLARE v_role text; v_norm text := lower(trim(p_name)); BEGIN IF auth.uid() IS NULL THEN RAISE EXCEPTION 'not authenticated' USING ERRCODE = 'P0001'; END IF; SELECT role INTO v_role FROM public.collective_members WHERE collective_id = p_collective_id AND user_id = auth.uid(); IF v_role IS DISTINCT FROM 'admin' THEN RAISE EXCEPTION 'only admins can purge common items' USING ERRCODE = 'P0002'; END IF; DELETE FROM public.item_frequency WHERE collective_id = p_collective_id AND name = v_norm; END; $$; GRANT EXECUTE ON FUNCTION public.purge_item_frequency(uuid, text) TO authenticated;