225 lines
8.4 KiB
PL/PgSQL
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();
|