# Spec — Import des membres

> Statut : `done`

## Intention

Créer les comptes d'une équipe de vendeurs en une fois, depuis le fichier que l'association a déjà — la liste des élèves inscrits au stand, exportée du tableur ou du logiciel de vie scolaire. Saisir trente comptes un par un dans le back-office est un travail de rentrée qui décourage l'usage de l'outil avant même la première vente.

## Contraintes

- Réservé aux admins, et les comptes créés rejoignent **l'association de l'admin** qui importe. Un super-admin n'a pas d'association : l'action lui est refusée avec un message qui le dit.
- L'unicité de l'e-mail est vérifiée **hors du filtre multi-tenant** : une adresse prise par une autre association, ou par le super-admin, est invisible en DQL ordinaire mais refusée par la base. Sans ça, l'import casserait au milieu sur une contrainte d'intégrité.
- **Aucun message n'est envoyé, et ce n'est plus une limite technique.** Depuis [mail.md](mail.md) l'application sait écrire, mais un mot de passe ne part jamais par e-mail : il resterait des années dans une boîte, lisible par qui l'ouvre. Les identifiants se transmettent à la main ; qui perd le sien passe par « mot de passe oublié », qui n'envoie qu'un lien à usage unique.
- CSV et XLSX, jusqu'à 500 lignes. Au-delà, ce n'est plus une classe, c'est un annuaire.

## Le format

Quatre colonnes, dans cet ordre : **prénom, nom, e-mail, rôle**.

| Prénom | Nom | E-mail | Rôle |
|--------|-----|--------|------|
| Camille | Durand | camille.durand@exemple.fr | vendeur |
| Sam | Bertin | sam.bertin@exemple.fr | admin |
| Alex | Roy | alex.roy@exemple.fr | _(vide)_ |

- Le rôle accepte `vendeur` ou `admin`, en majuscules ou non, avec ou sans le préfixe `ROLE_`.
- **Une colonne rôle vide vaut vendeur.** C'est de très loin le cas le plus fréquent ; obliger à la remplir serait faire saisir la même valeur trente fois.
- Une ligne d'en-têtes est reconnue et ignorée si sa première cellule vaut « prénom ». Un tableur en exporte presque toujours une, et la rejeter avec un message incompréhensible serait le premier obstacle rencontré.
- Une ligne entièrement vide n'est pas une erreur : les tableurs en sèment en fin de fichier.

## Pourquoi deux temps

L'import **analyse d'abord, écrit ensuite**, et ce n'est pas du confort. Un import de masse qui s'exécute directement est un import qu'on n'ose plus relancer : au moindre doute sur ce qu'il a fait, on préfère tout refaire à la main — et l'outil n'a servi à rien.

L'aperçu montre, sans rien créer : les lignes retenues, les lignes refusées, et **pourquoi** chacune l'est. L'analyse voyage en session entre les deux écrans ; redemander le fichier pour confirmer serait absurde, et le garder sur le disque serait pire.

Les lignes refusées **ne bloquent pas** l'import du reste. Une adresse mal tapée sur trente ne doit pas condamner les vingt-neuf autres.

## Les mots de passe

Ils sont **générés**, affichés **une seule fois** sur l'écran de résultat, et ne partent en base que sous forme d'empreinte. La page est faite pour être imprimée puis découpée ; passé ce moment, il faut les réinitialiser un par un. Les deux écrans qui précèdent le disent.

L'alphabet exclut `O`, `0`, `l`, `1` et `I`. Ces comptes se transmettent sur un bout de papier, dans un couloir, à des élèves qui les tapent sur un téléphone.

## Comportement attendu

1. **Membres → « Importer une liste »** : l'écran de dépôt décrit le format et prévient de ce qui va se passer.
2. **Aperçu** : ce qui sera créé, ce qui sera laissé de côté et pourquoi. Rien n'est enregistré.
3. **Confirmation** : les comptes sont créés **tout ou rien**, dans une transaction, et les mots de passe s'affichent.

Cas limites :

- Un fichier qui n'est ni CSV ni XLSX est refusé d'emblée. PhpSpreadsheet lit n'importe quel fichier texte comme un CSV : sans ce garde-fou, un PDF déposé par erreur rendrait un aperçu de charabia refusé pour une raison incompréhensible.
- Un fichier illisible renvoie un message, jamais une page blanche.
- Une adresse en double **dans le fichier** est signalée à la seconde occurrence.
- Les deux formulaires portent un jeton CSRF.

## Critères de « fait »

- [x] `ImportMembresService` : `analyser()` n'écrit rien, `importer()` est transactionnel.
- [x] CSV et XLSX, en-tête reconnue, lignes vides ignorées, plafond à 500 lignes.
- [x] Rôle vide ⇒ vendeur ; rôle inconnu refusé, avec son libellé dans le message.
- [x] E-mail déjà pris ici ou ailleurs sur le serveur : refusé, ligne par ligne.
- [x] Aperçu avant écriture, confirmation explicite, jeton CSRF sur les deux étapes.
- [x] Mots de passe générés, affichés une fois, stockés en empreinte, sans caractères ambigus.
- [x] Écran de résultat imprimable.
- [x] Couvert par des tests de service et de contrôleur.

## Hors périmètre

- **L'envoi des identifiants par e-mail.** Il faudrait d'abord un service d'e-mail, et un formulaire qui perd les messages est pire que pas de formulaire. Voir `docs/chantiers.md`.
- **La mise à jour de comptes existants.** Un import qui écrase des comptes est un import dangereux ; ici, une adresse déjà prise est refusée, point.
- **L'import des produits.** Même mécanique, mais le catalogue se saisit une fois et bouge peu — le besoin n'existe pas encore.
