# Environnement de test — reset, comptes et diagnostic RBAC

Document de référence pour remettre à zéro l'environnement de développement
(`portail-manager` + PWA `yima-shop`) et repartir sur un jeu de données propre.

Dernière mise à jour : 2026-09-01

---

## 1. Topologie locale

| Composant | URL | Notes |
|-----------|-----|-------|
| Backend Symfony (portail + API) | `https://127.0.0.1:8000` | `symfony serve`, certificat local |
| PWA Ionic (`yima-shop`) | `http://localhost:8100` | `ng serve` |
| Base principale | MySQL `yima-shop` @ `127.0.0.1:3306` | `DATABASE_URL` dans `.env.local` |
| Base cliente de démo | MySQL `amansa` | référencée par `CompanyDatabase` (multi-tenant) |

`environment.ts` de la PWA pointe sur `https://127.0.0.1:8000/api`. Le CORS dev
autorise `localhost` / `127.0.0.1` sur n'importe quel port (`CORS_ALLOW_ORIGIN` dans `.env.dev`).

---

## 2. Procédure de reset complet

**L'ordre compte.** Les trois commandes doivent être enchaînées :

```bash
cd portail-manager

# 1. Aligner le schéma sur les entités (pas de migrations versionnées en dev)
php bin/console doctrine:schema:update --force

# 2. Purger toutes les tables + recharger les données de démo
php bin/console doctrine:fixtures:load -n

# 3. OBLIGATOIRE — repeupler le RBAC, que l'étape 2 vient de purger
php bin/console app:rbac:seed

# 4. Optionnel
php bin/console cache:clear
```

Côté PWA, vider la session stockée dans le navigateur (origine `http://localhost:8100`) :

```js
// Console DevTools
localStorage.clear(); sessionStorage.clear();
(await navigator.serviceWorker.getRegistrations()).forEach(r => r.unregister());
(await caches.keys()).forEach(k => caches.delete(k));
```

Les clés de session sont gérées par `@capacitor/preferences`, qui les préfixe en web :
`CapacitorStorage.yima.token`, `CapacitorStorage.yima.refresh_token`, `CapacitorStorage.yima.user`
(cf. `yima-shop/src/app/core/services/token-storage.service.ts`).

### Vérification post-reset

```bash
php bin/console doctrine:query:sql "SELECT COUNT(*) FROM permission"   # doit valoir 26
php bin/console doctrine:query:sql "SELECT COUNT(*) FROM role"         # doit valoir 5
```

---

## 3. Comptes de démo

Créés par `src/DataFixtures/AppFixtures.php`.

| Compte | Mot de passe | Rôles | Contexte |
|--------|--------------|-------|----------|
| `devyayaly@gmail.com` | `Welcome123@` | `ROLE_ADMIN`, `ROLE_USER` | Super-admin du portail, rattaché à aucune boutique |
| `yalya96@gmail.com` | `Welcome123@` | `ROLE_MANAGER`, `ROLE_USER` | Manager de la boutique **AmansaShop**, compte de test de la PWA |

Le manager est créé avec les garde-fous de sécurité désactivés pour permettre un
login API en une seule étape : 2FA off, pas d'approbation d'appareil, pas de
validation IP ni pays, `mustChangePassword` à `false`.

### Deux versions de la fixture

Le dossier `src/DataFixtures/` contient deux fois la même classe :

- `AppFixtures.php` — la version **active**, chargée par Doctrine.
- `AppFixtures.txt` — la version **complète**, parquée hors du chargement.

La version complète est la seule à créer AmansaShop, son manager, sa
`CompanyDatabase` et un catalogue de démo (3 catégories, 6 produits avec stock).
La version réduite ne crée que le super-admin et une configuration SMTP —
donc **aucun manager, aucune boutique**, et la PWA se retrouve sans contexte.

> Le 2026-09-01, `AppFixtures.php` a été remplacé par le contenu de `AppFixtures.txt`
> pour restaurer le compte de test. La version réduite reste récupérable via
> `git show HEAD:portail-manager/src/DataFixtures/AppFixtures.php`.

---

## 4. Diagnostic — « le manager se connecte mais ne voit que Réglages »

Symptôme observé après un reset : le manager s'authentifie correctement, mais la
PWA n'affiche plus qu'un seul espace (**Réglages**). Vente, Boutique, Atelier et
Financier ont disparu du sélecteur d'espaces.

### Cause racine

`doctrine:fixtures:load` purge **toutes** les tables, y compris `permission` et
`role`. Or le catalogue RBAC n'est pas dans `AppFixtures` : il est produit par la
commande `app:rbac:seed`. Si on ne la relance pas, la table `permission` reste vide.

Enchaînement :

1. **`PermissionResolver::getGrantedPermissions()`** construit sa réponse à partir de
   `permissionRepository->findAll()`. Table vide ⇒ `$allCodes = []`, et le `array_filter`
   du fallback rétrocompatible retourne `[]` — même pour un `ROLE_MANAGER`
   membre actif d'une boutique active.
   → `portail-manager/src/Service/Security/PermissionResolver.php`

2. **L'API** renvoie donc `permissions: []` dans la charge utile de `/api/login` et `/api/me`.

3. **`AuthService.can()`** côté PWA n'applique son repli « manager = tout autorisé »
   que si `permissions` vaut `undefined`. Un tableau **vide** passe dans la branche
   `perms.includes(code)`, qui est fausse pour tout code.
   → `yima-shop/src/app/core/services/auth.service.ts`

4. **`ModuleService.accessibleModules`** filtre alors chaque espace sur `anyPerm`,
   puis supprime les espaces dont plus aucun onglet ne passe. Seul **Réglages**
   survit : il n'a pas de `anyPerm`, et ses onglets « Ma boutique » et « Alertes »
   n'ont pas de `perm`.
   → `yima-shop/src/app/core/navigation/module.service.ts` + `modules.config.ts`

### Réflexe de diagnostic

Devant ce symptôme, vérifier **d'abord** :

```bash
php bin/console doctrine:query:sql "SELECT COUNT(*) FROM permission"
```

Si le compte est à 0, il n'y a rien à chercher ailleurs : relancer `app:rbac:seed`.

### Correctifs durables envisagés (non implémentés)

| Piste | Effet |
|-------|-------|
| Appeler le seed RBAC depuis `AppFixtures::load()` | La fixture devient auto-suffisante, l'ordre des commandes n'a plus d'importance |
| Dans `can()`, appliquer aussi le repli manager quand `permissions` est un tableau vide | Défense en profondeur : une table RBAC vide ne vide plus l'UI |

---

## 5. Matrice permissions → modules de la PWA

Les 26 codes du catalogue RBAC (`app:rbac:seed`) et leur effet sur la navigation.
Un manager sans rôle RBAC de boutique (`user_company.boutique_role_id` à `NULL`)
reçoit la totalité des codes via le fallback rétrocompatible.

| Espace | Visible si (`anyPerm`) | Onglets → permission requise |
|--------|------------------------|------------------------------|
| **Vente** | `pos.sell`, `pos.session.open`, `report.view` | Vente → `pos.sell` · Caisse → `pos.session.open` · Journal → `report.view` |
| **Boutique** | `catalogue.view`, `stock.view`, `media.view`, `integration.view` | Produits/Catégories → `catalogue.view` · Stock → `stock.view` · Médias → `media.view` · Site → `integration.view` |
| **Atelier** | `production.view` | Production → `production.view` |
| **Financier** | `report.view`, `customer.manage`, `supplier.manage` | Trésorerie → `report.view` · Créances → `customer.manage` · Dettes → `supplier.manage` |
| **Réglages** | *(toujours visible)* | Ma boutique → *(aucune)* · Abonnement → `report.view` · Employés/Rôles → `boutique.employee.manage` · Alertes → *(aucune)* |

---

## 6. Campagne de test fonctionnelle — 2026-09-01

Environnement : backend `symfony serve` (dev, HTTPS), PWA `ng serve` (dev, Vite),
compte `yalya96@gmail.com` (manager AmansaShop), après reset complet + `app:rbac:seed`.

### 6.1 Couverture API — 28/28 endpoints GET en 200

Balayage de tous les endpoints GET référencés par les services de `yima-shop/src/app/core/services/`.

| Endpoint | Statut | Latence |
|----------|--------|---------|
| `/api/me` | 200 | 1834 ms |
| `/api/v1/boutique` | 200 | 1450 ms |
| `/api/v1/cash-sessions/current` | 200 | 2068 ms |
| `/api/v1/categories` | 200 | 1805 ms |
| `/api/v1/customers` | 200 | 1138 ms |
| `/api/v1/customers/debtors` | 200 | 1885 ms |
| `/api/v1/customers/reminders` | 200 | 1720 ms |
| `/api/v1/employees` | 200 | 1354 ms |
| `/api/v1/finance/expenses` | 200 | 1811 ms |
| `/api/v1/finance/summary` | 200 | 1458 ms |
| `/api/v1/inventory-counts` | 200 | 1163 ms |
| `/api/v1/media` | 200 | 1915 ms |
| `/api/v1/notifications` | 200 | 1379 ms |
| `/api/v1/notifications/unread-count` | 200 | 1151 ms |
| `/api/v1/production-orders` | 200 | 1322 ms |
| `/api/v1/products` | 200 | 1607 ms |
| `/api/v1/purchase-orders` | 200 | 1261 ms |
| `/api/v1/recipes` | 200 | 1624 ms |
| `/api/v1/reports/sales/summary?period=today` | 200 | 1534 ms |
| `/api/v1/reports/products/top?period=today&limit=5` | 200 | 1211 ms |
| `/api/v1/roles` | 200 | 1904 ms |
| `/api/v1/sales` | 200 | 1646 ms |
| `/api/v1/stock/alerts` | 200 | 1797 ms |
| `/api/v1/store` | 200 | 2009 ms |
| `/api/v1/store/stats` | 200 | 1676 ms |
| `/api/v1/subscription` | 200 | 1387 ms |
| `/api/v1/suppliers` | 200 | 1386 ms |
| `/api/v1/suppliers/creditors` | 200 | 1527 ms |

`/api/v1/store/whatsapp` n'apparaît pas dans ce tableau : la route est exposée en **PUT
uniquement** (`StoreService.updateWhatsapp()`), un GET y répond 405.

### 6.2 Couverture écrans — 18/18 écrans fonctionnels

| Espace | Écran | Résultat observé |
|--------|-------|------------------|
| Vente | Vente (POS) | Catalogue chargé, recherche produit, champ code-barres, onglets Produits/Panier |
| Vente | Caisse | Session ouverte affichée, fond de caisse, ventes, dépenses espèces, attendu en caisse, clôture Z + rapport PDF |
| Vente | Journal | CA 2 900 FCFA · 1 vente · ticket moyen · TVA 442 FCFA, meilleures ventes, exports CSV + PDF, filtres Aujourd'hui / 7 jours / Tout |
| Boutique | Produits | 6 produits avec catégorie, SKU, stock, prix ; badges « Stock bas » |
| Boutique | Catégories | 3 catégories (Boissons, Épicerie, Hygiène) |
| Boutique | Stock | Inventaire `INV-2026-000001` (0/7 comptés, statut Annulé) |
| Boutique | Médias | État vide correct + bouton d'import |
| Boutique | Site | « Non connecté », 7 produits actifs, gating licence : offre « App + Site » non activée |
| Atelier | Recettes | Recette « Fondé » → Riz parfumé 5 kg ×1 / lot · 2 composants, action Fabriquer |
| Atelier | Fabrications | `PRD-2026-000001` Riz parfumé 5 kg ×20 |
| Financier | Trésorerie | Recettes 2 900 · Sorties 52 000 · Solde net −49 100 FCFA, dépense « loyer » listée |
| Financier | Créances | État vide « Tous les comptes sont soldés » |
| Financier | Dettes | État vide « Aucune dette fournisseur en cours » |
| Réglages | Ma boutique | Profil Amansa Manager, badges ROLE_MANAGER / ROLE_USER, e-mail, fuseau Africa/Dakar |
| Réglages | Abonnement | Offre « App seule », aucune licence active, total réglé 0 FCFA, historique des paiements vide |
| Réglages | Employés | 1 employé (Amansa Manager, badge Manager) |
| Réglages | Rôles | 4 rôles système : Caissier (7 perms), Employé de boutique (4), Magasinier (13), Manager de boutique (23) |
| Réglages | Alertes | 1 notification « Nouveau appareil détecté » |

Le sélecteur d'espaces expose bien les 5 espaces (Vente, Boutique, Atelier, Financier,
Réglages) et la redirection par défaut après connexion pointe sur `/vente/pos`.

### 6.3 Anomalies relevées

| # | Sévérité | Constat |
|---|----------|---------|
| 1 | Mineure | **Icône `scan-outline` non enregistrée.** `sale.page.html:37` utilise `<ion-icon name="scan-outline">`, mais ce nom est absent du `addIcons({...})` de `yima-shop/src/main.ts`. À l'exécution : `[Ionicons Warning] Could not load icon with name "scan-outline"` + `TypeError: Failed to construct 'URL': Invalid base URL`, répété à chaque rendu du POS. L'icône du bouton de scan ne s'affiche pas. Correctif : importer `scanOutline` depuis `ionicons/icons` et l'ajouter au registre. |
| 2 | Environnement | **Lenteur du serveur de dev.** Chaque appel API coûte 1,2–2,1 s et `symfony serve` sérialise les requêtes. Un écran qui déclenche 3 à 5 appels parallèles met 10 à 20 s à s'afficher complètement (spinners persistants). Ce n'est pas un défaut applicatif — les mêmes endpoints répondent en ~1,5 s en appel unitaire — mais cela rend le test manuel pénible. Piste : augmenter le nombre de workers PHP de `symfony serve`. |

Aucune erreur applicative en console hors le point 1. Les `Failed to fetch dynamically
imported module` observés proviennent de rechargements Vite après redémarrage du serveur
de dev, pas du code applicatif.

### 6.4 Points d'attention pour les prochains tests

- **La PWA et les scripts partagent le même stockage navigateur.** Les clés
  `CapacitorStorage.yima.*` sont liées à l'origine `http://localhost:8100` : tous les
  onglets la partagent. Écraser ces clés dans un onglet déconnecte les autres.
- **Une nouvelle connexion révoque le token précédent.** Se reconnecter au même compte
  depuis un autre client (script, second navigateur) invalide la session en cours : au
  rechargement suivant, `restoreSession()` échoue sur `/me` et purge la session.
- **La base `amansa` n'est pas purgée** par `doctrine:fixtures:load`, qui n'agit que sur
  la base principale `yima-shop`. Les données POS de démo vues ici vivent bien dans la
  base principale (`products`, `sales`, `cash_sessions`…), mais le `CompanyDatabase`
  pointant sur `amansa` reste à considérer lors d'un reset « vraiment » complet.
