-- ============================================================================ -- FIDÉLITÉ — Schéma Supabase (Postgres) -- À coller dans Supabase → SQL Editor → New query → Run. -- Idempotent : on peut le relancer sans casser l'existant. -- ============================================================================ -- ---------------------------------------------------------------------------- -- 1. Fonction de génération de code (utilisée en défaut de la table clients, -- donc définie AVANT la table). -- ---------------------------------------------------------------------------- -- Génère un code client unique du type « FID-3F9K2A » (sans I, O, 0, 1). create or replace function public.generer_code_client() returns text language plpgsql set search_path = public as $$ declare chars text := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; nouveau_code text; i int; begin loop nouveau_code := 'FID-'; for i in 1..6 loop nouveau_code := nouveau_code || substr(chars, floor(random() * length(chars))::int + 1, 1); end loop; -- variable renommée pour éviter l'ambiguïté avec la colonne clients.code exit when not exists (select 1 from public.clients c where c.code = nouveau_code); end loop; return nouveau_code; end; $$; -- ---------------------------------------------------------------------------- -- 2. Tables -- ---------------------------------------------------------------------------- -- Personnel du magasin (droits d'écriture). On y ajoute l'ID d'un compte auth -- pour qu'il devienne « staff » (voir SETUP.md). create table if not exists public.staff ( user_id uuid primary key references auth.users(id) on delete cascade, created_at timestamptz not null default now() ); -- Un client fidélité. Lié à un compte de connexion (auth.users) quand le client -- s'inscrit lui-même depuis l'app client. user_id peut être null si le staff -- crée un client « au comptoir » sans compte. create table if not exists public.clients ( id uuid primary key default gen_random_uuid(), user_id uuid unique references auth.users(id) on delete set null, code text unique not null default public.generer_code_client(), nom text not null, prenom text, telephone text, points numeric not null default 0, created_at timestamptz not null default now() ); -- Ajout de la colonne prénom si la table existait déjà (migration). alter table public.clients add column if not exists prenom text; -- Historique : gain (facture) / dépense (récompense) / ajustement. create table if not exists public.mouvements ( id uuid primary key default gen_random_uuid(), client_id uuid not null references public.clients(id) on delete cascade, type text not null check (type in ('facture','recompense','ajustement')), montant_euros numeric, points numeric not null default 0, libelle text not null default '', created_at timestamptz not null default now() ); create index if not exists idx_mouvements_client on public.mouvements(client_id); -- Catalogue des récompenses. create table if not exists public.recompenses ( id uuid primary key default gen_random_uuid(), nom text not null, cout_points numeric not null default 0, actif boolean not null default true, created_at timestamptz not null default now() ); -- Réglages globaux du magasin (une seule ligne, id = 1). create table if not exists public.reglages ( id int primary key default 1 check (id = 1), euros_par_point numeric not null default 10, nom_magasin text not null default '' ); insert into public.reglages (id) values (1) on conflict (id) do nothing; -- ---------------------------------------------------------------------------- -- 3. Fonctions & triggers de logique métier -- ---------------------------------------------------------------------------- -- Vrai si l'utilisateur connecté fait partie du personnel. -- SECURITY DEFINER pour pouvoir lire la table staff malgré la RLS. create or replace function public.est_staff() returns boolean language sql security definer stable set search_path = public as $$ select exists (select 1 from public.staff s where s.user_id = auth.uid()); $$; -- Tient à jour le solde de points du client à chaque mouvement, pour que le -- solde reste fiable quelle que soit l'app qui écrit. create or replace function public.maj_points() returns trigger language plpgsql security definer set search_path = public as $$ begin if (tg_op = 'INSERT') then update public.clients set points = points + new.points where id = new.client_id; elsif (tg_op = 'UPDATE') then update public.clients set points = points - old.points + new.points where id = new.client_id; elsif (tg_op = 'DELETE') then update public.clients set points = points - old.points where id = old.client_id; end if; return null; end; $$; drop trigger if exists trg_maj_points on public.mouvements; create trigger trg_maj_points after insert or update or delete on public.mouvements for each row execute function public.maj_points(); -- Empêche un client (non-staff) de modifier son solde, son code ou son user_id. create or replace function public.proteger_client() returns trigger language plpgsql security definer set search_path = public as $$ begin if not public.est_staff() then new.points := old.points; new.code := old.code; new.user_id := old.user_id; end if; return new; end; $$; drop trigger if exists trg_proteger_client on public.clients; create trigger trg_proteger_client before update on public.clients for each row execute function public.proteger_client(); -- ---------------------------------------------------------------------------- -- 4. Sécurité (Row Level Security) -- Client = lit uniquement SES données. Staff = accès total. -- ---------------------------------------------------------------------------- alter table public.staff enable row level security; alter table public.clients enable row level security; alter table public.mouvements enable row level security; alter table public.recompenses enable row level security; alter table public.reglages enable row level security; -- staff : seul le staff peut lire la liste (personne ne s'auto-déclare staff). drop policy if exists staff_select on public.staff; create policy staff_select on public.staff for select using (public.est_staff()); -- clients drop policy if exists clients_select on public.clients; create policy clients_select on public.clients for select using (public.est_staff() or user_id = auth.uid()); drop policy if exists clients_insert on public.clients; create policy clients_insert on public.clients for insert with check (public.est_staff() or user_id = auth.uid()); drop policy if exists clients_update on public.clients; create policy clients_update on public.clients for update using (public.est_staff() or user_id = auth.uid()); drop policy if exists clients_delete on public.clients; create policy clients_delete on public.clients for delete using (public.est_staff()); -- mouvements : le client lit les siens ; seul le staff écrit (anti-triche). drop policy if exists mouvements_select on public.mouvements; create policy mouvements_select on public.mouvements for select using ( public.est_staff() or client_id in (select id from public.clients where user_id = auth.uid()) ); drop policy if exists mouvements_insert on public.mouvements; create policy mouvements_insert on public.mouvements for insert with check (public.est_staff()); drop policy if exists mouvements_update on public.mouvements; create policy mouvements_update on public.mouvements for update using (public.est_staff()) with check (public.est_staff()); drop policy if exists mouvements_delete on public.mouvements; create policy mouvements_delete on public.mouvements for delete using (public.est_staff()); -- recompenses : tout compte connecté lit ; seul le staff modifie. drop policy if exists recompenses_select on public.recompenses; create policy recompenses_select on public.recompenses for select using (auth.role() = 'authenticated'); drop policy if exists recompenses_write on public.recompenses; create policy recompenses_write on public.recompenses for all using (public.est_staff()) with check (public.est_staff()); -- reglages : tout compte connecté lit ; seul le staff modifie. drop policy if exists reglages_select on public.reglages; create policy reglages_select on public.reglages for select using (auth.role() = 'authenticated'); drop policy if exists reglages_update on public.reglages; create policy reglages_update on public.reglages for update using (public.est_staff()) with check (public.est_staff());