Files
mdb/supabase/migrations/20260429180000_mdb_turbo_dossiers.sql
Bastien COIGNOUX ffc2e6b895 Initial commit
Generated by create-expo-app 3.5.3.
2026-04-29 19:48:53 +02:00

225 lines
8.4 KiB
PL/PgSQL

-- MDB-Turbo : cœur métier dossiers + checklist visite + base investisseurs
-- Exécuter après création du projet Supabase (auth.users existe déjà).
-- ---------------------------------------------------------------------------
-- Profil utilisateur (lien auth)
-- ---------------------------------------------------------------------------
create table if not exists public.profiles (
id uuid primary key references auth.users (id) on delete cascade,
full_name text,
company_name text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- ---------------------------------------------------------------------------
-- Dossier = opportunité / deal en cours
-- ---------------------------------------------------------------------------
create type public.dossier_status as enum (
'draft',
'sourcing',
'analysis',
'visit',
'offer',
'under_promise',
'resale',
'closed_won',
'closed_lost'
);
create table if not exists public.dossiers (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) on delete cascade,
title text not null default 'Nouveau dossier',
status public.dossier_status not null default 'draft',
-- Localisation
address_line text,
city text,
postal_code text,
insee_code text,
latitude double precision,
longitude double precision,
-- Physique
surface_m2 numeric(12, 2),
land_surface_m2 numeric(12, 2),
rooms_count smallint,
dpe_class text check (dpe_class is null or dpe_class ~ '^[ABCDEFG]$'),
-- Financier (saisie terrain / desk)
purchase_price_target numeric(14, 2),
resale_price_estimate numeric(14, 2),
dvf_reference_price_m2 numeric(14, 2),
works_estimate_total numeric(14, 2) not null default 0,
works_visit_adjustment numeric(14, 2) not null default 0,
notary_fee_rate numeric(6, 5) not null default 0.077,
sale_agency_fee_rate numeric(6, 5) not null default 0.05,
misc_acquisition_cost numeric(14, 2) not null default 0,
misc_sale_cost numeric(14, 2) not null default 0,
carrying_months smallint not null default 6,
carrying_annual_rate numeric(6, 5) not null default 0.05,
carrying_principal numeric(14, 2),
-- Urbanisme & stratégies "quick win"
plu_zone_code text,
plu_notes text,
parcel_subdivision_candidate boolean not null default false,
deficit_foncier_candidate boolean not null default false,
-- IA (agents) — JSON pour itérer vite sans migrations à chaque prompt
sourcing_agent_output jsonb,
tech_estimator_output jsonb,
financier_agent_output jsonb,
deal_maker_output jsonb,
under_promise_at timestamptz,
teaser_pdf_url text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists dossiers_user_id_idx on public.dossiers (user_id);
create index if not exists dossiers_status_idx on public.dossiers (status);
-- ---------------------------------------------------------------------------
-- Catalogue "points noirs" visite (anti-erreur)
-- ---------------------------------------------------------------------------
create table if not exists public.visit_finding_definitions (
code text primary key,
label text not null,
default_works_delta_eur numeric(14, 2) not null default 0,
severity smallint not null default 1,
sort_order int not null default 0
);
insert into public.visit_finding_definitions (code, label, default_works_delta_eur, severity, sort_order)
values
('structural_crack', 'Fissure structurelle / désordre porteur', 15000, 5, 10),
('roof_full_replace', 'Toiture à refaire (complète)', 35000, 5, 20),
('roof_partial', 'Toiture partielle / zinguerie lourde', 8000, 3, 30),
('humidity_basement', 'Infiltrations cave / vide sanitaire', 12000, 3, 40),
('electrical_rewire', 'Rénovation électrique complète', 15000, 4, 50),
('asbestos', 'Présence amiante / désamiantage à prévoir', 10000, 4, 60),
('septic_non_conform', 'Assainissement non conforme', 12000, 3, 70),
('facade_insulation', 'ITE / ravalement lourd', 25000, 3, 80),
('heat_pump_full', 'Chauffage à refaire (pompe à chaleur + réseau)', 18000, 2, 90)
on conflict (code) do nothing;
create table if not exists public.dossier_visit_findings (
id uuid primary key default gen_random_uuid(),
dossier_id uuid not null references public.dossiers (id) on delete cascade,
finding_code text not null references public.visit_finding_definitions (code),
checked boolean not null default false,
works_delta_override_eur numeric(14, 2),
checked_at timestamptz,
unique (dossier_id, finding_code)
);
create index if not exists dossier_visit_findings_dossier_idx
on public.dossier_visit_findings (dossier_id);
-- ---------------------------------------------------------------------------
-- Investisseurs (module "Flash" — critères en JSON)
-- ---------------------------------------------------------------------------
create table if not exists public.investisseurs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) on delete cascade,
display_name text not null,
email text,
phone text,
min_margin_pct numeric(5, 2) not null default 12,
max_ticket_eur numeric(14, 2),
zones jsonb,
strategies jsonb,
notes text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists investisseurs_user_id_idx on public.investisseurs (user_id);
-- ---------------------------------------------------------------------------
-- updated_at trigger
-- ---------------------------------------------------------------------------
create or replace function public.set_updated_at()
returns trigger language plpgsql as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists set_profiles_updated_at on public.profiles;
create trigger set_profiles_updated_at
before update on public.profiles
for each row execute function public.set_updated_at();
drop trigger if exists set_dossiers_updated_at on public.dossiers;
create trigger set_dossiers_updated_at
before update on public.dossiers
for each row execute function public.set_updated_at();
drop trigger if exists set_investisseurs_updated_at on public.investisseurs;
create trigger set_investisseurs_updated_at
before update on public.investisseurs
for each row execute function public.set_updated_at();
-- ---------------------------------------------------------------------------
-- RLS
-- ---------------------------------------------------------------------------
alter table public.profiles enable row level security;
alter table public.dossiers enable row level security;
alter table public.dossier_visit_findings enable row level security;
alter table public.investisseurs enable row level security;
-- lecture / écriture : uniquement son user_id
create policy "profiles_self" on public.profiles
for all using (auth.uid() = id) with check (auth.uid() = id);
create policy "dossiers_self" on public.dossiers
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "visit_findings_via_dossier" on public.dossier_visit_findings
for all using (
exists (
select 1 from public.dossiers d
where d.id = dossier_id and d.user_id = auth.uid()
)
)
with check (
exists (
select 1 from public.dossiers d
where d.id = dossier_id and d.user_id = auth.uid()
)
);
create policy "investisseurs_self" on public.investisseurs
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
-- Catalogue visites : lecture pour tous utilisateurs authentifiés
alter table public.visit_finding_definitions enable row level security;
create policy "visit_finding_definitions_read" on public.visit_finding_definitions
for select to authenticated using (true);
-- ---------------------------------------------------------------------------
-- Auto-création profil à l'inscription (optionnel)
-- ---------------------------------------------------------------------------
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
insert into public.profiles (id, full_name)
values (new.id, coalesce(new.raw_user_meta_data->>'full_name', ''))
on conflict (id) do nothing;
return new;
end;
$$;
drop trigger if exists on_auth_user_created on auth.users;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();