1d3ee69c6d
App Flutter de programme de fidélité : login staff, clients (nom/prénom), scan QR, factures (€→points), récompenses, réglages. Backend Supabase (schéma SQL + RLS anti-triche dans supabase/). Icône incluse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
227 lines
9.0 KiB
PL/PgSQL
227 lines
9.0 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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());
|