-- Flux opportunités aspirées + agent Scout (batch JSON côté Postgres) create table if not exists public.deals_sources ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, title text not null, description text, source_url text, source_name text, price_eur numeric(14, 2), surface_m2 numeric(12, 2) not null, price_per_m2_eur numeric(14, 2) not null, dvf_avg_m2_simulated numeric(14, 2) not null default 3500, distress_keywords text[] not null default '{}', opportunity_score numeric(6, 2) not null, grade text not null check (grade in ('A', 'B', 'C')), raw_payload jsonb, created_at timestamptz not null default now() ); create index if not exists deals_sources_user_score_idx on public.deals_sources (user_id, opportunity_score desc); create index if not exists deals_sources_user_created_idx on public.deals_sources (user_id, created_at desc); alter table public.profiles add column if not exists expo_push_token text; -- Activer Realtime sur cette table : Dashboard Supabase → Realtime → ajouter public.deals_sources alter table public.deals_sources enable row level security; create policy deals_sources_select_own on public.deals_sources for select to authenticated using (auth.uid() = user_id); -- Pas d’INSERT direct : uniquement via la fonction SECURITY DEFINER ci-dessous. create or replace function public.scout_process_batch(p_listings jsonb) returns jsonb language plpgsql security definer set search_path = public as $$ declare uid uuid := auth.uid(); el jsonb; txt text; price numeric; surf numeric; pm2 numeric; avg_m2 constant numeric := 3500; keywords text[] := array['succession', 'urgent', 'travaux important']; k text; matched text[]; ok_kw boolean; ok_pm2 boolean; score numeric; grade text; n int := 0; na int := 0; begin if uid is null then raise exception 'scout_process_batch: non authentifié'; end if; for el in select * from jsonb_array_elements(coalesce(p_listings, '[]'::jsonb)) loop txt := lower( coalesce(el ->> 'description', '') || ' ' || coalesce(el ->> 'title', '') ); price := nullif(trim(el ->> 'price_eur'), '')::numeric; surf := nullif(trim(el ->> 'surface_m2'), '')::numeric; if price is null or surf is null or surf <= 0 then continue; end if; pm2 := price / surf; matched := array[]::text[]; ok_kw := false; foreach k in array keywords loop if strpos(txt, k) > 0 then ok_kw := true; matched := array_append(matched, k); end if; end loop; ok_pm2 := pm2 < avg_m2; if not (ok_kw and ok_pm2) then continue; end if; score := 40::numeric + greatest(0::numeric, (avg_m2 - pm2) / nullif(avg_m2, 0) * 50) + coalesce(array_length(matched, 1), 0) * 10; grade := case when score >= 80 then 'A' when score >= 55 then 'B' else 'C' end; insert into public.deals_sources ( user_id, title, description, source_url, source_name, price_eur, surface_m2, price_per_m2_eur, dvf_avg_m2_simulated, distress_keywords, opportunity_score, grade, raw_payload ) values ( uid, coalesce(nullif(trim(el ->> 'title'), ''), 'Sans titre'), el ->> 'description', nullif(trim(el ->> 'url'), ''), nullif(trim(el ->> 'source'), ''), price, surf, pm2, avg_m2, matched, score, grade, el ); n := n + 1; if grade = 'A' then na := na + 1; end if; end loop; return jsonb_build_object( 'inserted_count', n, 'grade_a_count', na, 'simulated_dvf_avg_m2', avg_m2 ); end; $$; grant execute on function public.scout_process_batch(jsonb) to authenticated; comment on function public.scout_process_batch(jsonb) is 'Filtre un lot JSON d''annonces (mots-clés détresse + prix/m² < moyenne simulée) et insère dans deals_sources.';