# Module — Clients / CRM

> Fiche de conception. Voir le [catalogue des modules](README.md), les [conventions](../notes/conventions.md) et les [décisions (ADR)](../notes/decisions.md).

## 0. État d'implémentation — incrément 1 (2026-07-24) 🚧

CRM léger **livré et testé**, intégré au flux de vente (client optionnel, vente anonyme par défaut).

| Livré ✅ | Cible non encore faite ⬜ |
|---|---|
| `Customer` (company-scopée, uuid, soft-delete : nom, téléphone, email, note, `loyaltyPoints`) | fidélité (cumul/échange de points), segments, historique client agrégé |
| `Sale.customer` nullable : la vente peut être **nominative** (rattachée via `customerUuid`) ou **anonyme** | fidélité (usage des `loyaltyPoints`) |
| **Créances / vente à crédit** (B1) : `CustomerLedgerEntry` (débit/crédit, append-only) + `Customer.balanceCents` ; `payment.amountCents < total` → vente `pending` + créance ; `POST /customers/{uuid}/payments` (recouvrement, solde FIFO), `GET /{uuid}/ledger`, `GET /debtors`. PWA : vente à crédit à l'encaissement + écran **Créances** (relevé, recouvrement) | rattachement du recouvrement à la caisse (journal B4) |
| API `GET /api/v1/customers` (paginé, recherche), `GET/{uuid}`, `POST`, `PUT` | fusion de doublons, import/export |
| Gating : lecture + **création** par tout membre (le caissier crée un client à l'encaissement) ; modification réservée aux managers (`COMPANY_EDIT`) | permission dédiée `customer.*` (bascule RBAC phase 2) |
| PWA : **sélecteur de client** dans le panier (recherche / sélection / **création rapide**), client affiché sur le ticket (Journal) | fiche client dédiée, historique d'achats |
| Tests `tests/Functional/Api/CustomerApiTest.php` + `CustomerDebtApiTest.php` (vente à crédit, relevé, recouvrement) | tests fidélité |

Fichiers : `src/Entity/Customer.php`, `src/Controller/Api/V1/CustomerController.php`,
`Sale.customer`, `yima-shop/src/app/features/sales/` (modal client), `core/services/customers.service.ts`.

## Objectif

Permettre à une boutique de gérer un **répertoire de clients** optionnel et de rattacher, quand c'est utile, une vente à un client identifié pour consulter son **historique d'achats** et préparer une **fidélité** (points).

- **Manager** : gère le répertoire (créer/éditer/supprimer), consulte l'historique et pilote la fidélité (activation, paramètres).
- **Employé** : recherche/sélectionne un client au moment de l'encaissement, le crée à la volée, consulte son historique.

La vente **anonyme reste la norme** ([ADR-005](../notes/decisions.md)) : le client est un enrichissement, jamais un prérequis.

## Périmètre

**In (v1)**
- CRUD `Customer` (nom, téléphone, email — tous optionnels) scoped `Company`.
- Recherche rapide (nom / téléphone) pour l'encaissement.
- Rattachement optionnel d'une vente à un client (`Sale.customer`).
- Historique d'achats du client (agrégé depuis `Sale`).
- Base fidélité **désactivée par défaut** : compteur de points par client, à activer par boutique.

**Out (v1)**
- Segmentation marketing, campagnes, RGPD avancé (consentements fins).
- Barème de fidélité complexe (paliers, cash-back) → itération ultérieure.
- Fusion de doublons automatique (prévu manuel plus tard).
- Comptes clients du **site e-commerce** (gérés par le site, cf. §8).

## Entités

Toutes rattachées à `Company` ; `TimestampableEntity` ; **soft-delete Gedmo autorisé** (client effaçable) avec index `idx_customer_deleted` ; **UUID public** (exposée API).

- **`Customer`**
  - `id`, `uuid`, `company` (FK, requis), `fullName?`, `phone?`, `email?`, `notes?`.
  - `loyaltyEnabled` (bool, hérite du réglage boutique), `loyaltyPoints` (int, défaut 0).
  - `deletedAt` (soft-delete), timestamps.
  - Contrainte : au moins un champ d'identification (nom **ou** téléphone) non vide.
  - Index : `(company, phone)`, `(company, fullName)`.
- **Relation vente** : `Sale.customer` **nullable** (FK, `ON DELETE SET NULL` logique via soft-delete) — porté par le module [POS / Ventes](pos-ventes.md), pas de FK inverse lourde côté `Customer` (historique lu par requête).
- **Réglage boutique** (léger) : `Company.loyaltyEnabled` (ou table de config module) pour activer la fidélité.

## Permissions

Codes RBAC (seed idempotent, cf. [sécurité](../03-securite.md)) :

| Code | Description | Rôles par défaut |
|---|---|---|
| `customer.view` | Consulter clients + historique | Manager, Employé |
| `customer.create` | Créer un client | Manager, Employé |
| `customer.edit` | Modifier un client | Manager |
| `customer.delete` | Supprimer (soft-delete) un client | Manager |

Gating : `denyAccessUnlessGranted('customer.view', $company)` (contrôleur), `is_granted('customer.edit', company)` (Twig). Scope `Company` **imposé serveur**.

## API

Base `/api/v1`, enveloppe `ApiResponseFormatter`, scope boutique résolu serveur.

| Méthode | Route | Corps | Permission |
|---|---|---|---|
| GET | `/api/v1/customers?filter[q]=&page=&limit=` | — | `customer.view` |
| POST | `/api/v1/customers` | `{ fullName?, phone?, email?, notes? }` | `customer.create` |
| GET | `/api/v1/customers/{uuid}` | — | `customer.view` |
| PUT | `/api/v1/customers/{uuid}` | `{ fullName?, phone?, email?, notes? }` | `customer.edit` |
| DELETE | `/api/v1/customers/{uuid}` | — | `customer.delete` |
| GET | `/api/v1/customers/{uuid}/purchases?page=&limit=` | — | `customer.view` |
| GET | `/api/v1/customers/{uuid}/loyalty` | — | `customer.view` |

- Validation → 422. Recherche `filter[q]` sur nom + téléphone.
- Historique = ventes (`Sale`) du client, montants en **centimes XOF**, en lecture seule (source POS).

## Écrans

**Web (Twig, espace Manager)**
- Liste clients (recherche, pagination, badges fidélité si activée).
- Fiche client : coordonnées, historique d'achats (dates, tickets, montants), solde de points.
- Formulaire création/édition ; suppression avec confirmation.
- Réglage boutique : activer/désactiver la fidélité.

**PWA (encaissement)**
- Champ « Client » optionnel dans le panier : recherche instantanée (nom/téléphone), création à la volée (formulaire minimal).
- Affichage discret des points si fidélité active ; jamais bloquant.

## Règles métier

- Un `Customer` appartient à **une** boutique ; jamais partagé entre boutiques.
- Identification minimale requise (nom **ou** téléphone) pour éviter les fiches vides.
- Rattacher un client à une vente est **facultatif** ; l'absence de client n'altère aucun parcours.
- **Fidélité** : si `loyaltyEnabled`, l'attribution de points se fait à la **validation** d'une vente rattachée (barème simple v1 : X points par tranche de montant). Les ventes étant append-only ([ADR-006](../notes/decisions.md)), un **remboursement/avoir** génère un ajustement inverse de points, jamais une réécriture.
- Suppression client = soft-delete : les ventes passées conservent leur trace (le lien devient anonyme), l'historique agrégé n'est plus rattaché.
- Montants d'historique **jamais recalculés** ici : lecture depuis les ventes.

## Projection e-commerce

**Non répliqué** vers la base client (cf. [ADR-001](../notes/decisions.md), [projection e-commerce](ecommerce-projection.md)).

Frontière explicite : les **comptes clients du site e-commerce sont distincts**. Le site gère ses propres inscriptions/identités (email + mot de passe, panier en ligne). Le `Customer` POS est un référentiel **interne de caisse**. Un éventuel rapprochement (même téléphone/email) est une évolution future, opt-in, et ne crée aucune dépendance v1.

## Tests

- **Unitaires** : contrainte d'identification minimale ; calcul/ajustement de points (attribution, avoir) ; agrégation d'historique.
- **Fonctionnels** : CRUD `/customers`, recherche, pagination ; historique ; rattachement d'un client à une vente.
- **Accès** : `customer.*` refusée hors permission (403) ; **isolation boutique** (un Manager ne voit pas les clients d'une autre `Company`) ; soft-delete masque le client des listes.

## Dépendances

- Socle : RBAC/permissions, `EntityManagerService`, `FileLogger`, `ApiResponseFormatter`, scope `Company`.
- [POS / Ventes](pos-ventes.md) : porte `Sale.customer` et déclenche l'attribution de points.
- [Caisse & paiements](caisse-paiements.md) : contexte d'encaissement (indirect).
- Fournit des données à [Rapports & analytics](rapports-analytics.md) (ventes par client).

## Notes / ADR liés

- [ADR-004](../notes/decisions.md) — Montants en centimes XOF.
- [ADR-005](../notes/decisions.md) — Vente anonyme par défaut (`Sale.customer` nullable).
- [ADR-006](../notes/decisions.md) — Ventes append-only (impacte les ajustements de points).
- [ADR-001](../notes/decisions.md) — Central autoritaire ; comptes site distincts.
- Voir aussi : [modèle de données](../02-modele-de-donnees.md), [sécurité](../03-securite.md), [API](../04-api.md), [README projet](../README.md).
