# 07 — Suivi & avancement

> Tableau de bord vivant. Mettre à jour à chaque incrément (statut, date, PR/commit). Statuts : ✅ livré · 🚧 en cours · ⬜ à faire · ⏸️ en attente.

## Fondations (transverses)

| Brique | Statut | Référence |
|---|---|---|
| RBAC par boutique (Role/Permission/Resolver/Voter) | ✅ | `src/Entity/Role.php`, `PermissionResolver` |
| Chiffrement au repos (libsodium) | ✅ | `EncryptionService` |
| Connexion DB par boutique + test | ✅ | `CompanyDatabase`, `TenantConnectionService` |
| Auth API (login/JWT/refresh/me) | ✅ | `AuthController` |
| Déduplication des alertes | ✅ | `AlertDeduplicationService` |
| Détection proactive de menaces | ✅ | `ProactiveThreatDetectionListener` |
| Client de test AmansaShop + tests auth | ✅ | `AppFixtures`, `AuthApiTest` |
| Rapport multi-tenant | ✅ | `portail-manager/docs/ARCHITECTURE_MULTITENANT.md` |
| Provisioning auto base client | ⬜ | phase 7 |
| Synchro central→client (Messenger) | ⬜ | phase 4 |
| Backups par tenant | ⬜ | phase 7 |
| CI (tests + lints) | ⬜ | phase 7 |

## Modules POS

| Module | Phase | Statut | Fiche |
|---|---|---|---|
| Catalogue produits | 2 | 🚧 | [catalogue-produits](modules/catalogue-produits.md) — API v1 (produits/catégories) + écran PWA livrés (incrément 1) |
| Stock & inventaire | 2 | 🚧 | [stock-inventaire](modules/stock-inventaire.md) — niveaux + sortie auto à la vente + mouvements manuels + stock au catalogue + **inventaire physique** (comptage → écarts → ajustements) (incrément 1) |
| POS / Ventes (dont anonymes) | 2 | 🚧 | [pos-ventes](modules/pos-ventes.md) — vente anonyme espèces de bout en bout (API v1 + écran PWA) livrée (incrément 1) |
| Caisse & paiements | 2 | 🚧 | [caisse-paiements](modules/caisse-paiements.md) — sessions de caisse (ouverture/Z, rattachement ventes, écart) + **rapport Z PDF** livrés ; espèces (incrément 1) |
| Clients / CRM | 3 | 🚧 | [clients-crm](modules/clients-crm.md) — clients + rattachement vente + **créances/vente à crédit** (grand-livre, recouvrement) livrés |
| Rapports & analytics | 3 | 🚧 | [rapports-analytics](modules/rapports-analytics.md) — résumé ventes + tableau de bord/journal PWA + **exports CSV/PDF** (incrément 1) |
| Achats / Fournisseurs | 3 | 🚧 | [fournisseurs-achats](modules/fournisseurs-achats.md) — fournisseurs + réceptions (réappro stock) + **dettes fournisseurs** (réception à crédit, règlements FIFO) livrés, API + PWA (incrément 1) |
| Production / transformation | 3 | 🚧 | [production-transformation](modules/production-transformation.md) — recettes (nomenclature) + fabrication (consomme composants, crédite le fini, roll-up coût) livrés, API + PWA (incrément 1) |
| Promotions & remises | 3 | 🚧 | [promotions](modules/promotions.md) — remise sur la vente (%/montant, recalcul serveur) + PWA (incrément 1) |
| Facturation & taxes | 3 | 🚧 | [facturation-taxes](modules/facturation-taxes.md) — facture PDF (NINEA/RCCM, TVA) depuis une vente, API + PWA (incrément 1) |
| Notifications | 3 | 🚧 | [notifications](modules/notifications.md) — alerte stock bas (in-app + e-mail) + centre de notifications PWA (API + écran) livrés (incrément 1) |
| Licences & comptes (e-commerce) | 4 | ✅ | [licences-comptes](modules/licences-comptes.md) — E1‑A (licences + écrans admin) + E1‑B (API connecteur licence+HMAC) + E1‑C (plugin WordPress) livrés |
| Projection e-commerce | 4 | ⬜ | [ecommerce-projection](modules/ecommerce-projection.md) (alternative non retenue pour WooCommerce) |
| Connecteur WooCommerce | 4 | ✅ | [woocommerce-connector](modules/woocommerce-connector.md) — plugin `yima-wc-connector/` (plugin tire, licence-gaté, ADR-008) livré |
| Design & templates des sites | 4 | ✅ | [design-templates](modules/design-templates.md) — plugin `yima-wc-design/` : E3‑A (socle jetons + en-tête/pied + widgets) + E3‑B (bibliothèque de kits, importeur idempotent, kit Mode) + E3‑C (kits Alimentation et Électronique) + E3‑D (vérification en réel, retrait de kit, cohabitation HFE) livrés |
| Administration boutiques | — | ✅/🚧 | [administration-boutiques](modules/administration-boutiques.md) — infos boutique + **gestion des employés/caissiers** (API + écran PWA) |

## API PWA

| Élément | Statut |
|---|---|
| Endpoints auth | ✅ |
| Endpoints métier v1 (catalogue/ventes/caisse/stock) | 🚧 Catalogue + Ventes + Caisse + Stock (`/api/v1/stock`) livrés (fondations phase 2) |
| Idempotence (`Idempotency-Key` sur ventes) | ✅ (rejeu → même vente) |
| Offline (file locale des ventes + rejeu auto) | ✅ côté PWA (`SalesSyncService`) |
| `/api/mobile/*` (device-bound, contrôleurs) | ⬜ |
| OpenAPI complet (`/api/doc`) | 🚧 |
| Idempotence / offline | ⬜ |

## Frontend PWA (`yima-shop/`, Ionic)

| Brique | Statut | Référence |
|---|---|---|
| Socle app (env, HttpClient, intercepteur JWT, guards, PWA/service worker) | ✅ | `src/app/core/`, `src/main.ts`, `ngsw-config.json` |
| Authentification (login + refresh + challenges 202 : appareil/pays/IP/2FA) | ✅ | `core/services/auth.service.ts`, `features/auth/` |
| Shell de navigation à onglets + profil/déconnexion | ✅ | `features/shell/` |
| Localisation FR / FCFA (centimes XOF), fuseau `Africa/Dakar` | ✅ | `shared/utils/money.util.ts`, `date.util.ts` |
| Écran **Produits** (liste API paginée, recherche, prix FCFA) | ✅ | `features/catalogue/`, `core/services/catalogue.service.ts` |
| Écran **Vente** (panier → encaissement espèces, idempotence, ticket) | ✅ | `features/sales/`, `core/services/sales.service.ts` |
| Écran **Caisse** (ouverture/fond, totaux live, clôture Z + écart) | ✅ | `features/cash/`, `core/services/cash-session.service.ts` |
| **Stock** affiché au catalogue et en vente (décrément à la vente) | ✅ | `features/catalogue/`, `features/sales/` |
| **File d'attente hors-ligne** des ventes (rejeu auto via Idempotency-Key) | ✅ | `core/services/sales-sync.service.ts` |
| Onglet **Journal** : tableau de bord (CA/jour, ticket moyen, par moyen) + historique + détail ticket | ✅ | `features/journal/` |
| **Gestion produits** (créer/éditer) via formulaire modal | ✅ | `features/catalogue/products.page` |
| **Sélection client** dans la vente (recherche/création) + client au ticket | ✅ | `features/sales/`, `core/services/customers.service.ts` |
| **Réapprovisionnement** produit (réception fournisseur → stock +) | ✅ | `features/catalogue/products.page`, `core/services/suppliers.service.ts` |
| **Remise** sur la vente (%/montant) dans le panier + ticket | ✅ | `features/sales/` |
| **Facture PDF** depuis un ticket (Journal) | ✅ | `features/journal/`, `core/services/invoices.service.ts` |
| **Avoir / remboursement** depuis un ticket — **total ou partiel** (quantité par ligne), CA net | ✅ | `features/journal/`, `CreditNoteController` |
| **Ma boutique** (Profil) : voir/éditer nom, NINEA/RCCM, adresse (entête facture) | ✅ | `features/shell/profile.page`, `BoutiqueController` |
| **Ajout au panier par code-barres/SKU** (scan + Entrée) dans la Vente | ✅ | `features/sales/sale.page` |
| **Inventaire physique** (liste, comptage avec écart live, application/annulation) | ✅ | `features/inventory/`, `core/services/inventory.service.ts` |
| **Alertes de stock bas** (seuil par produit, puce « Stock bas », écran d'alertes + badge Profil) | ✅ | `features/stock-alerts/`, `Product.lowStockThreshold`, `GET /api/v1/stock/alerts` |
| **Notification proactive stock bas** (in-app manager + e-mail au franchissement du seuil, dédupliquée, fail-soft) | ✅ | `LowStockAlertService`, `StockService::dispatchPendingLowStockAlerts` |
| **Centre de notifications PWA** (liste, non lues, marquage lu / tout lire, cloche Journal + entrée Profil avec badge) | ✅ | `features/notifications/`, `core/services/notifications.service.ts`, `GET/POST /api/v1/notifications` |
| **Exports rapports** (CSV des ventes + rapport de synthèse PDF, par période) depuis le Journal | ✅ | `ReportController` (`/sales/export`, `/sales/summary/pdf`), `features/journal/` |
| **Top produits / meilleures ventes** (classement par quantité, barres dans le tableau de bord) | ✅ | `GET /api/v1/reports/products/top`, `SaleLineRepository::topProductsByCompany`, `features/journal/` |
| **Rapport Z de caisse (PDF)** (fond, ventes, encaissements, attendu/compté/écart) depuis la Caisse | ✅ | `GET /api/v1/cash-sessions/{uuid}/z-report`, `features/cash/` |
| **Gestion des employés/caissiers** (liste, création de compte caissier, activation/désactivation) — responsable | ✅ | `EmployeeController`, `features/employees/`, Profil→Gestion |
| **Production / transformation** (recettes nomenclature + fabrication : consomme composants, crédite le fini, coût de revient) | ✅ | `features/production/`, `core/services/production.service.ts`, `Recipe`/`ProductionOrder` |

## Journal des livraisons (changelog)

| Date | Livraison |
|---|---|
| 2026-07-28 | **Paiements d'abonnement (admin + manager).** Nouvelle entité `SubscriptionPayment` (boutique, montant, moyen — mobile money/espèces/virement —, mois couverts, référence, `coversUntil`, `recordedBy`) + `SubscriptionPaymentService` (enregistre le paiement **et prolonge la licence** de la durée couverte, depuis la plus tardive entre aujourd'hui et l'expiration). **Admin** : écran `/admin/payments` (enregistrement + historique filtrable par boutique + total encaissé), entrée de menu « Paiements ». **API** : `GET /api/v1/subscription` (statut offre/licence/échéance + historique + total, gaté `report.view`). **Ionic** : onglet « Abonnement » (espace Réglages) — statut coloré selon l'échéance + historique lecture seule (`subscription.service`, `SubscriptionPage`), aide `reglages.abonnement`. **Admin enregistre seul, le manager consulte.** `SubscriptionPaymentServiceTest` + `SubscriptionApiTest`. **190 tests.** Aussi : objet d'email d'inscription unique (anti-regroupement Gmail). |
| 2026-07-28 | **PWA — identité visuelle YimaShop + refonte de l'écran de connexion.** Assets de marque intégrés à `yima-shop` : `src/assets/brand/` (logo PNG/SVG) et `src/assets/icon/` (favicon `.ico`/`.svg`/96px, apple-touch-icon 180, icônes PWA 192/512) ; `index.html` (titre **YimaShop**, jeu de favicons complet) et `manifest.webmanifest` (nom, icônes `any` + `maskable`) mis à jour. Nouveaux **jetons de marque** dans `theme/variables.scss` (`--yima-teal/navy/surface/ink/radius/shadow/gradient`, variantes thème sombre). Habillage d'auth partagé `features/auth/auth-shell.scss` (fond dégradé teal→marine avec halos + trame, carte flottante animée, plaque logo, champs personnalisés avec icône + anneau de focus, encart d'erreur, bouton dégradé, pied « Connexion sécurisée ») appliqué à **login** (labels empilés, œil à bascule, états d'erreur par champ) et à **challenge** (badge d'icône, champ code espacé, boutons fantômes). Icône `keypad-outline` enregistrée dans `main.ts`. Lint vert, vérifié dans le navigateur (:8110). |
| 2026-07-27 | **Rebrand 2AB → YimaShop + landing publique animée.** Remplacement de toutes les références de marque *2AB* (Portal/Group/CORP/PORTAIL, `SYSTEM_NAME`, emails, manifests, docs) par **YimaShop** ; infra prod (BASE_URL/DB) laissée telle quelle. Landing `/` : **suppression des modals d'application** (mise à jour/hors-ligne) via blocs surchargeables ; refonte UI **GSAP + ScrollTrigger** (entrée Encore `landing`) — hero avec mockup d'app animé, section « Comment ça marche », révélations au défilement, barre de progression, header réduit, smooth-scroll, `prefers-reduced-motion`. Intégration des **assets de marque** (favicons, logo dans la nav, manifest teal) versionnés (`.gitignore` adapté). Base dev réinitialisée (schéma + fixture admin unique + RBAC seedé : 5 rôles/26 permissions). |
| 2026-07-27 | **Client Flutter `yima_shop` — incrément 5 : espace Financier (trésorerie, créances, dettes).** **Trésorerie** : KPIs recettes / sorties / solde net (le net négatif étant coloré en erreur), ventilation des encaissements par moyen de paiement, dépenses par catégorie, liste des dépenses avec ajout (catégorie, montant, libellé, moyen — le formulaire rappelle que **seules les espèces impactent la caisse**) et suppression confirmée, rapport PDF gaté `report.export`. **Créances** : total dû, liste des débiteurs, **compte client** en feuille (relevé débit/crédit avec référence de vente et motif, solde, **recouvrement** — pré-rempli au solde et soldant les ventes les plus anciennes — et **créance manuelle**), **bannière de quota SMS** (utilisés/restants, illimité géré, bouton désactivé si le quota est épuisé ou si personne n'est joignable) et **envoi manuel des rappels** avec confirmation chiffrée puis relais du récapitulatif serveur, **import du carnet d'adresses** via `flutter_contacts` (permission `read`, `limited` iOS 18+ acceptée, message dédié si refus définitif, noms trop courts filtrés en amont, dédoublonnage par téléphone côté serveur). L'écran retombe sur `GET /customers/debtors` si l'endpoint de rappels est indisponible (droits, offre sans SMS). **Dettes fournisseurs** : total dû, créditeurs, **compte fournisseur** (historique des règlements avec réception rattachée, solde) et règlement pré-rempli au solde, gaté `supplier.manage`. Service `finance` porté ; `customers` et `suppliers` complétés. Permissions natives contacts (Android + clé d'usage iOS). **186 tests** verts (+14 : synthèse de trésorerie dont solde négatif, dépenses en centimes, quota SMS présent/absent/épuisé — le **429** remontant le message du serveur —, créance manuelle avec et sans vente rattachée, import de contacts, relevé lu depuis `meta`), `flutter analyze` propre, APK debug construit. *Suite : incrément 6 (Atelier + Site), dernier des écrans métiers.* |
| 2026-07-27 | **Client Flutter `yima_shop` — incrément 4 : espace Vente (POS, caisse, journal).** Le cœur du produit. **POS** : recherche débattue, ajout par saisie de code-barres/SKU (correspondance exacte d'abord, sinon 1er résultat) et **scan caméra** (`mobile_scanner`, avec repli explicite vers la saisie manuelle si la caméra manque ou est refusée), choix de variante pour les produits variables, panier à **clé composite produit+variante** valorisé au prix de la variante, remise (pourcentage ou montant, plafonnée au sous-total), sélecteur/création de client, encaissement par moyen de paiement, **vente à crédit** (montant partiel — le client devient obligatoire, le bouton étant désactivé sinon), partage du ticket, badge des ventes en attente avec synchro manuelle. **File hors-ligne** (`yima.sale_queue`) : `Idempotency-Key` généré à la mise en file et **rejoué tel quel**, FIFO strict, arrêt au premier échec réseau, **retrait d'une vente définitivement rejetée** (sinon un élément empoisonné bloquerait la file), verrou de ré-entrance, rejeu automatique au retour du réseau via `connectivity_plus`, tolérance au JSON corrompu. Les erreurs **serveur** (stock, validation) remontent à l'écran et **conservent le panier** ; seules les erreurs réseau mettent en file. **Caisse** : ouverture avec fond, totaux en direct (dont recouvrements, règlements fournisseurs et dépenses), clôture avec comptage et écart, **rapport Z en PDF**. **Journal** : KPIs par période, ventilation par moyen de paiement, meilleures ventes en barres, historique paginé, détail de ticket (partage, **facture PDF** — émission idempotente), **avoir par ligne** avec quantités remboursables et prorata, exports CSV et PDF gatés `report.export`. Nouveau `DocumentService` : les binaires sont écrits sur disque puis ouverts par l'application système (le navigateur les téléchargeait côté Ionic). **Blocage AGP 9 levé** : `dependency_overrides` sur `path_provider_android` en 2.2.x (les 2.3.x tirent `jni`) — cela rétablit `share_plus`, `path_provider` et `open_filex`, donc le **palier natif** de la cascade de partage, désormais conforme à Ionic (natif → WhatsApp → presse-papier). **172 tests** verts (+28 : 13 sur la file hors-ligne, 15 sur le panier et les remises), `flutter analyze` propre, APK debug construit. Correction au passage : la feuille de remise distingue désormais « retirer la remise » de « fermer sans rien changer », que le type de retour confondait. *Suite : incrément 5 (Financier).* |
| 2026-07-27 | **Client Flutter `yima_shop` — incrément 3 : espace Boutique (catalogue, stock, médias).** Six écrans portés. **Produits** : recherche débattue, pagination infinie, vignettes via le nouveau widget **`AuthImage`**, puces « stock bas » et « inactif », formulaire complet (prix et coût **saisis en francs, convertis en centimes** à la soumission, TVA, SKU, code-barres, catégorie, seuil d'alerte, actif, visible en ligne, section Publication/SEO repliable), **photos** (appareil, galerie, sélection dans la médiathèque sans re-téléversement, image principale, suppression), **réapprovisionnement** (quantité, coût unitaire, fournisseur avec création rapide, option « payer à crédit » qui rend le fournisseur obligatoire), accès aux **variantes**. Actions d'écriture gatées par `catalogue.manage`. **Catégories** : liste hiérarchique, formulaire (nom, description, parent — une catégorie ne peut être son propre parent —, actif), suppression confirmée rappelant que produits et sous-catégories sont seulement détachés. **Médiathèque** : grille paginée, **import multi-fichiers** en un appel, relais du **413 quota de stockage** avec le message du serveur, suppression. **Stock** : liste des inventaires avec statut et écart, création, **feuille de comptage** (saisie par ligne, écart en direct, enregistrer/appliquer/annuler — l'application enregistre d'abord les saisies en cours, le serveur recalant ensuite sur le stock réel), **alertes de stock** en lecture seule distinguant rupture et seuil franchi. **Variantes** : attributs (nom + valeurs séparées par virgules), génération du **produit cartésien** conservant les variantes déjà réglées (retrouvées par combinaison d'attributs, pas par position), édition prix/stock/SKU par ligne, retrait, enregistrement — sans variante le produit redevient `simple`. Services portés : `catalogue`, `product-images`, `mediatheque`, `variants`, `inventory`, `suppliers`. **Infrastructure** : `ApiClient.upload` (multipart) et **clone du `FormData` avant rejeu** dans l'intercepteur — sans quoi un upload rejoué après refresh repartirait avec un corps vide ; `image_picker` ajouté (vérifié compatible AGP 9, contrairement à `share_plus`) ; permissions caméra Android (caméra déclarée **facultative** pour ne pas exclure les tablettes) et clés d'usage iOS. **144 tests** verts (+39), `flutter analyze` propre, APK debug construit. **Bug corrigé par un test** : le calcul d'écart ne distinguait pas « pas de saisie » de « champ vidé » (les deux valant `null`) — vider un champ ne remettait donc pas la ligne à « non comptée » ; la présence de la clé fait désormais foi. *Suite : incrément 4 (Vente).* |
| 2026-07-27 | **Client Flutter `yima_shop` — incrément 2 : espace Réglages.** Quatre écrans portés, les premiers branchés sur l'API réelle. **Ma boutique** : bandeau d'identité (avatar à initiales, rôles), liste Compte (`/me`), informations légales avec feuille d'édition (nom, NINEA, RCCM, adresse, ville, téléphone, e-mail — un champ vidé est renvoyé à `null` pour effacer côté serveur), rappel si NINEA/RCCM manquent, **widget « Mon offre »** (barre de stockage colorée en erreur au-delà de 90 %, SMS du mois, gestion de l'illimité) et **carte Parrainage** avec bouton Partager — conformément au registre, ces deux blocs vivent sur « Ma boutique » et non sur « Site », donc restent visibles quelle que soit l'offre. Boutique et offre sont chargées **au mieux** : un employé sans droit dessus voit quand même son compte. **Employés** : liste avec bascule d'activation, création (feuille prénom/nom/e-mail/téléphone/mot de passe/rôle, mot de passe laissé vide = temporaire généré, affiché dans une boîte de dialogue bloquante car renvoyé une seule fois), changement de rôle par sélection radio. **Rôles** : rôles système et personnalisés, matrice de permissions groupée par catégorie avec **libellés lisibles** (port du `MODULE_LABELS`), création/édition/suppression confirmée, rôles système en lecture seule. **Alertes** : fil paginé (tirer-pour-rafraîchir + défilement infini), non lues teintées, marquage lu au tap puis navigation vers le lien, « tout marquer lu », compteur de non lues partagé via un `Notifier` Riverpod. Services portés : `boutique`, `employees`, `roles`, `notifications` (compteur lu dans `meta.unreadCount`), `store`, plus un `ShareService`. **105 tests** verts (+23 : faux serveur HTTP vérifiant chemins, corps, `meta` et messages d'erreur relayés ; groupement des permissions ; couverture des libellés de catégories), `flutter analyze` propre, APK debug construit. ⚠️ **Partage dégradé** : le palier « partage natif » de la cascade Ionic exige `share_plus`, toujours bloqué par l'AGP 9 — `ShareService` démarre à WhatsApp puis retombe sur le presse-papier ; le rétablir ne touchera aucun appelant. *Suite : incrément 3 (Boutique / Catalogue).* |
| 2026-07-27 | **Client Flutter `yima_shop` — incrément 1 : socle.** Démarrage du **portage** décrit par [08-portage-flutter.md](08-portage-flutter.md) (Ionic reste la source de vérité, backend et API v1 partagés). Livré : couche réseau **dio** (enveloppe `ApiResponse` + `meta`, `ApiException` typée relayant le message serveur — 413 stockage / 422 / 429 / réseau) et **intercepteur JWT** porté fidèlement (Bearer, URL publiques `/login`·`/version/check`, refresh **unique et dédoublonné** entre 401 concurrents, rejeu sur un second Dio **sans** intercepteur donc jamais re-rejoué, échec → purge + 401 d'origine) ; **20 modèles freezed + json_serializable** 1-pour-1 avec `core/models/*.model.ts` (centimes entiers, tolérants aux champs absents/inconnus, `explicit_to_json` pour que la future file hors-ligne soit re-lisible) ; `TokenStorage` sur les **mêmes clés** que Capacitor (`yima.token`/`yima.refresh_token`/`yima.user`) ; **authentification complète** (login + challenges appareil/pays/IP/2FA, renvoi de code, `/me`, refresh, restauration de session au démarrage avant le boot du routeur) ; état **Riverpod** avec `can()` reproduisant exactement la règle Angular (permissions absentes ≠ liste vide) ; **navigation par espaces** (registre `APP_MODULES` porté littéralement, `StatefulShellRoute` à 16 branches, barre d'onglets filtrée par permissions, sélecteur d'espace, redirections remplaçant les 3 guards) ; **aide contextuelle** `PAGE_INFO` (16 entrées reprises verbatim) ; thème M3 **teal `#0d9488`** clair/sombre ; formats **FCFA** (centimes, séparateur normalisé) et dates **Africa/Dakar** ; identifiants natifs `com.yima.shop`, permission INTERNET (absente du manifeste release), `queries` WhatsApp, clairtext limité au debug. Les 21 écrans métiers sont des espaces réservés, ce qui valide dès maintenant navigation, RBAC et header. **82 tests** verts (dédoublonnage du refresh sous concurrence réelle, règles d'URL, mapping des erreurs, filtrage des espaces par persona, `can()`, formats, stockage, tolérance JSON), `flutter analyze` propre, **APK debug construit**. ⚠️ **Blocage à lever avant l'incrément 4** : `share_plus`/`path_provider`/`open_filex` sont incompatibles avec l'AGP 9.0.1 du projet (`path_provider_android` → `jni` appelle `kotlin { }` sans le plugin Kotlin) ; les deux réglages s'excluent — `builtInKotlin=false` casse `jni`, `true` casse `connectivity_plus`. *Suite : incrément 2 (Réglages).* |
| 2026-07-27 | **PWA — déplacement des widgets « Mon offre » + Parrainage vers « Ma boutique ».** Les widgets usage (stockage/SMS) et la carte Parrainage passent de l'onglet « Site » (gaté `integration.view`, invisible aux boutiques App seule) vers l'onglet **« Ma boutique »** (espace Réglages, toujours accessible) → visibles quelle que soit l'offre. `ProfilePage` charge `GET /api/v1/store` (best-effort). |
| 2026-07-27 | **PWA — partage du code de parrainage.** `GET /api/v1/store` expose un bloc `referral` (code propre de la boutique via `ReferralService::generateForCompany`, `shareUrl` absolu vers `/register?ref=CODE`, usage). Page « Boutique en ligne » : carte Parrainage (code + jours offerts + usage) avec bouton « Partager » réutilisant `ReceiptService.share` (natif → WhatsApp → copie presse-papier). Assertion dans `StoreApiTest`. **186 tests.** |
| 2026-07-27 | **PWA — widget usage & quotas (offre).** Page « Boutique en ligne » : carte « Mon offre » (libellé de l'offre + barre de stockage utilisé/quota avec %, SMS de rappel du mois utilisés/restants), lue depuis `GET /api/v1/store` (`usage`), visible quelle que soit l'offre. `formatBytes`/`storagePercent`. Rappels de créance restent **manuels** (déclenchés par le manager, pas de cron). |
| 2026-07-27 | **Inscription — Incrément 5 : logo boutique + toggle mot de passe.** `RegistrationFormType` : champ `logo` (FileType optionnel, image ≤ 2 Mo) ; template register en `multipart`, upload via `MediaService::uploadCompanyLogo` (best-effort après flush). Toggle œil afficher/masquer sur les deux champs mot de passe (JS délégué, idempotent Turbo). Assets recompilés. **186 tests.** *Fonctionnalités demandées livrées : créances (ajout manuel + rappel SMS), quotas SMS/stockage par offre, 3ᵉ offre Premium, register (logo + toggle).* |
| 2026-07-27 | **Stockage — Incrément 4 : enforcement du quota à l'upload + affichage usage.** Blocage dur (HTTP 413, message clair) à l'upload médiathèque (`MediaLibraryController`) et image produit (`ProductImageController`) via `OfferQuotaService::storageWouldExceed`. `GET /api/v1/store` expose un bloc `usage` (stockage utilisé/quota + SMS quota) et l'offre réelle (pos/ecommerce/premium + `offerLabel`, fin du binaire). Fiche boutique admin : « Quota de l'offre : X Go (Y % utilisé) ». Test de blocage (quota 1 octet → 413). **186 tests.** *Suite : register (logo + toggle mot de passe web). Optionnel : widget usage côté PWA (l'API l'expose déjà).* |
| 2026-07-27 | **Créances — Incrément 3 : ajout manuel + rappel mensuel SMS.** API v1 : `POST /customers/{uuid}/debts` (créance manuelle, rattachable à une vente via `saleUuid`, `CustomerLedgerService::recordManualDebit`), `GET /customers/reminders` (débiteurs + quota SMS mensuel), `POST /customers/reminders/send` (envoi SMS aux débiteurs, respect du quota → blocage au-delà, `SmsService` avec `company`). PWA (page Créances) : bannière quota SMS + bouton « Envoyer les rappels », action « Ajouter une créance » (dans le compte client + via un sélecteur de client recherché), toggle œil déjà présent au login. Tests API (ajout créance, rattachement vente, quota meta, skip sans téléphone, blocage quota). **185 tests.** *Suite : enforcement stockage à l'upload + affichage usage ; register (logo + toggle mot de passe web).* |
| 2026-07-27 | **Offres & quotas — Incrément 1 : 3ᵉ offre Premium + quotas SMS/stockage.** `License::OFFER_PREMIUM` (illimité, **sans site**) + `OFFERS`/`OFFER_LABELS`/`getOfferLabel()`. Réglages : `offer.premium.price_cents`=30 000 FCFA, `offer.<offre>.sms_quota` (50/150/illimité, plafond mensuel de SMS de rappel) et `offer.<offre>.storage_bytes` (3/10 Go/illimité). Nouveau `OfferQuotaService` (résout offre via `LicenseRepository::findCurrentForCompany`, quotas, usage stockage via `MediaRepository::getCompanyMediaStatistics`, `storageWouldExceed`). Écran `/admin/settings` étendu (prix Premium + quotas, Go↔octets). Premium ajouté aux 2 formulaires d'offre + normalisation `RegistrationController` ; landing = 3 cartes avec quotas ; templates admin offre → `offerLabel`. `OfferQuotaServiceTest`. **178 tests.** *Suite : SMS par boutique + comptage quota, créances (rappel mensuel SMS + ajout manuel), enforcement stockage, register (logo + toggle mot de passe).* |
| 2026-07-27 | **Site public — Phase 4 : gestion admin des codes de parrainage.** `AdminReferralController` `/admin/referrals` (liste des codes campagne + codes propres des boutiques avec usage/offert/expiration/statut, création d'une campagne via `ReferralCampaignFormType`, activation/désactivation) ; `ReferralService::createCampaign(label, maxUses, freeDays, expiresAt)`. Entrée de menu « Parrainage ». `AdminReferralWebTest`. **175 tests.** *Le système de parrainage (E-4 site public) est complet : codes admin + codes boutiques, mois offerts, limites d'usage/expiration, application à la validation.* |
| 2026-07-27 | **Site public — Phase 2+3 : landing YimaShop + inscription manager avec validation admin.** Landing publique rebrandée (`home/index.html.twig` : hero, services, **2 offres avec prix lus des réglages** — 5 000 / 15 000 FCFA, CTA « Créer ma boutique » + « Ouvrir l'application ») ; `HomeController` rend la vitrine pour l'anonyme. Inscription publique `/register` (`RegistrationController` + `RegistrationFormType` : gérant + boutique + offre + code parrainage pré-rempli via `?ref=`) → crée `User(ROLE_MANAGER, inactif)` + `Company(inactive)` + `UserCompany(manager)` + `RegistrationRequest(PENDING)`, notifie les admins (cloche + email) et confirme au gérant ; page de succès. `AdminRegistrationController` `/admin/registrations` (liste + **badge menu en attente**, valider = activation + `LicenseService::issue` avec essai + **mois offerts** du parrainage + consommation du code + génération du code propre de la boutique + email ; refuser = statut REJECTED + email). **Verrou web** `AccountUserChecker` (câblé `user_checker` sur `main`) + `^/register` public. Emails `registration/{received,approved,rejected}`. `RegistrationWebTest` + `AccountUserCheckerTest`. **Assets recompilés** (Tailwind teal). **173 tests.** *Suite : Phase 4 — écran admin de gestion des codes de parrainage + affichage du code côté PWA. Suivi : la modale « Mise à jour disponible » (AppVersionListener) recouvre la landing au 1er passage anonyme.* |
| 2026-07-27 | **Site public — Phase 1 : paramètres plateforme + socle parrainage.** Nouveau store clé/valeur `PlatformSetting` + `PlatformSettingsService` (défauts : URL PWA, prix des offres POS/e‑commerce, durée d'essai, limites de parrainage) ; écran admin `/admin/settings` (`AdminSettingsController` + `PlatformSettingsFormType`, prix saisis en FCFA↔centimes) + entrée de menu. Entité `ReferralCode` (code unique, boutique propriétaire nullable = code admin, `maxUses`/`usedCount`/`freeDays`/`expiresAt`, `isUsable()`) + `ReferralService` (generateForCompany, validate, consume). Services publics **en test** via `when@test` (services.yaml). `PlatformSettingsAndReferralTest`. **169 tests.** *Suite : landing publique, inscription+validation, gestion parrainage.* |
| 2026-07-27 | **Supervision admin renforcée** (portail web `/admin`). **Tableau plateforme** (`/admin/platform`) enrichi : **créances clients & dettes fournisseurs consolidées**, **volume d'avoirs (30 j)**, et **table des connecteurs par boutique** (offre, statut, URL, dernier contact avec indicateur < 48 h) — le CA restait déjà net des avoirs. **Fiche boutique** (`/companies/{id}`) : nouveau **panneau supervision 30 j** (CA net, avoirs, créances, dettes, alertes stock, état de la caisse). **Offre licence** : champ `License.offer` (`pos`/`ecommerce`, défaut ecommerce → rétro-compatible), choisi à l'émission (`LicenseIssueFormType`), affiché sur les fiches, et **`isSiteEnabled` exige désormais l'offre e-commerce** (une licence `pos` n'ouvre pas le site). Repos : `Customer/Supplier::totalDebtByCompany`+`globalDebtTotal`, `StockItemRepository::countLowStockByCompany`, `CreditNoteRepository::refundedTotalByCompany`/`refundedGlobalTotal`, `LicenseRepository::findConnectorsOverview`. Tests : `AdminPlatformWebTest` (nouveaux blocs), `CompanyAdminWebTest` (panneau supervision), `LicenseServiceTest::testOfferControlsSiteEnabled`. **165 tests**. |
| 2026-07-27 | **Polish UX — FAB pour les actions + espace courant coloré.** Les actions de création/import passent en **boutons flottants (FAB)** cohérents : médiathèque (import), inventaire (nouvel inventaire), créances (importer contacts) — en plus des FAB existants (produits/catégories/recettes). `ModuleService.currentModuleId` (dérivé de l'URL via `NavigationEnd`) ; le sélecteur d'espace **surligne l'espace courant** (couleur primaire + coche) et le bouton du header affiche **l'icône de l'espace courant**. Vérifié en réel (:8110), 0 erreur console. ng lint/build verts. |
| 2026-07-27 | **Sync des images produit → WooCommerce** — Phase 4 (finale) de la refonte. Backend : `ProductWooSerializer::toPayload` ajoute un bloc **`images`** (`{uuid, isPrimary, position}`) au feed catalogue et au push produit ; nouvel endpoint connecteur **`GET /api/connector/media/{uuid}`** (vérif licence + HMAC) qui **sert le binaire** de l'image, scopé boutique via `ProductImageRepository::findOneByMediaUuidAndCompany`. Plugin `yima-wc-connector` **v1.6.0** : `Yima_Wc_Client::fetch_media($uuid)` (GET binaire signé HMAC) + `Yima_Wc_Sync::apply_images($pid, $images)` — **sideload** des images (image mise en avant + galerie) avec **dédoublonnage par post-meta** (`_yima_media_<uuid>` → attachment id, pas de re-téléchargement). `ConnectorApiTest::testCatalogImagesAndMediaEndpoint` (feed images + service binaire + refus sans signature) → **163 tests**. **Vérifié en réel** contre `127.0.0.1:81/amansa` : feed expose l'image de `EPIC-CAF25`, `fetch_media` renvoie le PNG (158 o), et `upsert` complet → **image mise en avant WooCommerce** (attachment `.../uploads/.../yima-*.png`). **🎉 Refonte terminée : navigation espaces · médiathèque · import contacts · sync images.** |
| 2026-07-27 | **Import des contacts téléphoniques → clients** — Phase 3 de la refonte. Backend : `POST /api/v1/customers/import` `{contacts:[{name,phone?}]}` → **dédoublonnage par téléphone** (dans le lot et en base, ignore les doublons), max 1000, gaté `customer.manage` ; `CustomerRepository::findOneByPhoneAndCompany` + index `idx_customer_company_phone`. `CustomerImportApiTest` (dédoublonnage lot + base, contacts sans tél, nom court, gating) → **162 tests**. PWA : `CustomersService.importContacts` ; bouton **« Importer des contacts »** (header Créances, gaté `customer.manage`) via le **Web Contact Picker API** (`navigator.contacts.select`, sans dépendance native, Android/Chrome) → mappe nom/tél → import → toast récap ; repli message si non supporté. **Vérifié en réel** (:8110) : bouton présent, espace Financier OK. ng lint/build verts. *Suite : sync images WooCommerce (Phase 4).* |
| 2026-07-26 | **Médiathèque (import massif + sélection produit)** — Phase 2 de la refonte. Backend : `Media.relatedCompany` renseigné aux uploads (scoping boutique) ; `MediaLibraryController` (`/api/v1/media`) — `GET` liste paginée des images (`media.view`), `POST` **multi-fichiers** (`media.upload`), `GET /{uuid}/raw` (service binaire), `DELETE /{uuid}` (si média inutilisé, `media.delete`) ; `MediaRepository::findLibraryByCompany`/`countLibraryByCompany`/`findLibraryOneByUuid` ; `ProductImageController::attach` (`POST /products/{uuid}/images/attach {mediaUuid}` — rattache un média **existant** sans re-téléverser, idempotent) + `relatedCompany` à l'upload produit. `MediaLibraryApiTest` (import massif, liste, attach idempotent, suppression refusée si utilisé) → **159 tests**. PWA : `MediathequeService` + écran **Médiathèque** (onglet Boutique, grille `appAuthImg`, **import multi-fichiers**, suppression) ; fiche produit → **« Choisir depuis la médiathèque »** (modal sélecteur → `attach`) + upload produit passé en **multi-fichiers**. Icônes ionicons manquantes enregistrées (`grid/settings/images/cloud-upload/file-tray`). **Vérifié en réel** (:8110) : import massif via l'API, grille (vraies images), boutons upload/suppression, onglet Médias. ng lint/build verts. *Suite : import contacts, sync images WooCommerce.* |
| 2026-07-26 | **Refonte navigation en « espaces » (modules) + bouton (i) + libellés RBAC** — Phase 1 de la refonte UX. L'app est réorganisée en **5 espaces** : **Vente** (Vente · Caisse · Journal), **Boutique** (Produits · Catégories · Stock · Site), **Atelier** (Production), **Financier** (Trésorerie · Créances · Dettes), **Réglages** (Ma boutique · Employés · Rôles · Alertes). Chaque espace a **sa propre barre d'onglets**, filtrée par les permissions ; on **change d'espace via le header** (sélecteur `grid` → popover). Registre `core/navigation/modules.config.ts` + `ModuleService` (`accessibleModules` selon `auth.can`) ; shell d'onglets **générique** `features/shell/module-tabs.page` (barre construite dynamiquement) ; routes regroupées par espace dans `app.routes.ts` (+ `landingGuard` → 1er espace accessible) ; détails plein écran (variants, inventaire détail, alertes stock) conservés avec back. **Header partagé** `shared/components/app-header` (sélecteur d'espace + **bouton (i)** ouvrant une aide contextuelle par page via `shared/help/page-info.ts` + slot `actions`/`toolbars`) ; ~15 pages migrées. Le fourre-tout de « Profil » est remplacé par les espaces. Éditeur de **rôles** : catégories affichées en **libellés de module** lisibles. **Vérifié en réel** (`:8110`) : switch d'espaces, bottom bar par espace, (i), aucune erreur console. PWA lint/build verts. *Suite : médiathèque, import contacts, sync images WooCommerce.* |
| 2026-07-26 | **Scan code-barres caméra au POS** — sans dépendance native, via l'API navigateur **`BarcodeDetector`** + flux `getUserMedia` (caméra arrière). Bouton **scan** à côté de la saisie code-barres (écran Vente) → modal caméra plein écran avec viseur ; au 1er code lu, le produit est ajouté au panier (réutilise `addByCode` → recherche par `barcode`/`sku`). Repli automatique : si `BarcodeDetector` absent (ex. iOS Safari) ou caméra refusée, message + **saisie manuelle** conservée. Nécessite un contexte sécurisé (app Capacitor / HTTPS / localhost). Libération propre de la caméra à la fermeture. PWA lint/build verts. |
| 2026-07-26 | **Reçu / ticket partageable (WhatsApp / partage natif)** — sans dépendance native. `receipt.util.buildReceiptText(sale, boutique)` compose un ticket texte (en-tête boutique, lignes, remise, total, paiements + réf., reste dû) ; `ReceiptService.share()` privilégie le **Web Share API** (→ WhatsApp/SMS/e-mail), sinon **WhatsApp Web** (`wa.me`), sinon **copie** presse-papier. Bouton **« Partager le ticket »** au détail du **Journal** et sur le **bandeau post-vente** (écran Vente). Boutique chargée/mise en cache à l'entrée de l'écran. PWA lint/build verts. |
| 2026-07-26 | **Paiement mobile money — confirmation + référence manuelle** (pas d'intégration d'API tierce, décision utilisateur). À l'encaissement, choisir « Mobile money » ouvre une confirmation « Transaction reçue » avec un champ **référence / n° de transaction** optionnel (Wave, Orange Money…), transmis via `payment.reference` (déjà stocké/sérialisé côté backend). La **référence s'affiche au ticket** (Journal → Paiements). `checkout()` accepte une `reference` optionnelle. PWA lint/build verts. |
| 2026-07-26 | **Cohésion caisse — mouvements d'espèces hors ventes rattachés à la session (Z juste)**. Jusqu'ici seules les ventes espèces entraient dans le Z ; les **recouvrements clients** (entrées) et **règlements fournisseurs / dépenses** (sorties) faussaient le fond de caisse théorique. Désormais : FK nullable `cashSession` sur `CustomerLedgerEntry`, `SupplierPayment`, `Expense` (SET NULL) ; chaque enregistrement rattache la **session ouverte** (`CashSessionRepository::findCurrentOpen`, même patron que la vente) — recouvrement (`CustomerLedgerService`), règlement + réception comptant (`SupplierLedgerService`), dépense (`FinanceController`). Nouveaux agrégats par session filtrés `method=cash` (`sessionCashCreditsCents`/`sessionCashTotalCents`). Le **Z** calcule `attendu = fond + ventes espèces + recouvrements − règlements − dépenses` (les 3 spots `close`/`buildZReportContext`/`serialize`), expose `recoveriesCents`/`supplierPaymentsCents`/`expensesCents`, et le **PDF Z** + l'écran **Caisse** (PWA) affichent le détail des mouvements. Seul `cash` touche la caisse (mobile money/carte ignorés). `CashSessionApiTest::testCashMovementsAdjustExpectedCash`. Backend **157 tests** verts, PWA lint/build verts. |
| 2026-07-26 | **RBAC‑fin (PWA) — UI sensible aux permissions** (ferme la boucle du gating fin). Le backend expose les **permissions effectives** de l'utilisateur : `PermissionResolver::getGrantedPermissions(user, company)` (rôle RBAC si assigné, sinon fallback ; admin = tout le catalogue) ajoutées à `POST /api/login`, au succès 2FA/appareil et à `GET /api/me` (`data.user.permissions[]`). PWA : `AuthService.permissions` + helper **`can(code)`** (un manager/admin sans liste chargée reste permis — confort d'UI, la sécurité réelle est côté backend) ; le modèle `CurrentUser` porte `permissions`. Les actions interdites sont **masquées ou désactivées** selon le code : encaisser (`pos.sell`), rembourser (`pos.refund`), créer/éditer produit + réappro (`catalogue.manage`/`purchase.manage`), catégories & variantes (`catalogue.manage`), inventaire (`stock.manage`), caisse ouvrir/fermer (`pos.session.open/close`), règlements fournisseur/client (`supplier.manage`/`customer.manage`), fabrication & recettes (`production.manage`). `RoleApiTest::testLoginExposesEffectivePermissions`. Backend **156 tests** verts, PWA `ng lint`/`ng build` verts. |
| 2026-07-26 | **RBAC‑fin — permissions par endpoint** (raffinement F1 : les codes fins déjà seedés sont enfin appliqués). Les endpoints **mutants** délèguent désormais à `PermissionVoter` → `PermissionResolver` → `boutiqueRole.hasPermission()` via des **codes fins** au lieu du `CompanyVoter::EDIT` grossier : `pos.sell` (encaissement), `pos.refund` (avoir), `stock.manage` (stock/inventaire), `catalogue.manage` (produits/catégories/variantes/images), `purchase.manage`, `supplier.manage`, `customer.manage`, `production.manage` (recettes/OF), `integration.manage` (boutique en ligne). Les **lectures** restent sur `boutique.view` (aucune régression). Fallback `PermissionResolver` (membre sans `boutiqueRole`) ajusté pour préserver la sémantique historique : tout membre actif peut **vendre** (`pos.sell`) et **lire** (`*.view`), le reste réservé au manager — dès qu'un `boutiqueRole` est assigné, les codes fins font autorité et **différencient réellement caissier / magasinier / manager** (ex. le caissier vend mais ne gère pas le catalogue ni les remboursements ; le magasinier gère le catalogue/stock mais ne vend pas). `RoleApiTest::testSeededRolesEnforceFineGrainedEndpoints` (rôles seedés vs endpoints). Suite verte (**155 tests**). *À suivre (PWA) : masquer/désactiver les actions selon les permissions effectives de l'utilisateur (exposer les permissions au login).* |
| 2026-07-26 | **E2‑G — Avoirs partiels (remboursement à la ligne / quantité)** : une vente peut désormais porter **plusieurs avoirs** (contrainte unique `sale_id` levée → index simple `idx_credit_note_sale`). `Sale.refundedCents` (cumul remboursé) + `getNetCents()` (CA net = total − remboursé) + statut **`partially_refunded`**. `SaleLine.refundedQuantity` + `getRefundableQuantity()`. `CreditNote.refunded_lines` (JSON, snapshot des lignes remboursées). `CreditNoteController::refund` accepte `lines:[{lineUuid,quantity}]` (absent = tout le restant) : prorata `lineTotalCents×qty/quantity`, restock **variante incluse**, statut `refunded` (plus rien à rembourser) ou `partially_refunded`, idempotent si déjà totalement remboursé. **Reporting CA** basé sur le **net** (`SUM(totalCents − refundedCents)`, statuts `paid`+`partially_refunded`) dans `globalSummary`/`summaryByCompany`/`onlineSummaryByCompany`/`topCompaniesByRevenue`. PWA **Journal** : modale d'avoir avec **stepper de quantité par ligne** (présélection du restant), montant d'avoir calculé en direct, motif ; affichage **Remboursé / CA net** sur le ticket ; statut « Remb. partiel ». `RefundApiTest::testPartialRefundThenComplete` (avoir partiel → net → avoir du reste → `refunded`). Suite verte (**154 tests**). Vérifié bout en bout via le test HTTP (statut/net/restock/quantités remboursables). |
| 2026-07-26 | **E2‑F — Vente d'une variante au POS** (dernier maillon des variations) : `SaleLine.variant` (FK nullable) — la ligne enregistre la variante vendue. `SaleController::create` accepte `variantUuid` par ligne (variante rattachée au produit → prix/SKU/libellé de la variante) ; `StockService::registerSaleMovements` décrémente le **stock de la variante** ; l'**avoir** (`CreditNoteController`) remet en stock la bonne variante ; ligne sérialisée avec `variantUuid`. PWA **Vente** : les produits variables affichent « Variantes › » et un **choix de variante** (action sheet : libellé + prix + stock) à l'ajout au panier ; panier à **clé composite** (produit + variante), total au prix variante ; `variantUuid` envoyé à l'encaissement. `SaleVariantApiTest`. Suite verte (**153 tests**). **Vérifié en réel** : vente POS variante `V-2026-000018` (T‑shirt — Taille M, 520 000) → stock variante 3→2 (+ resync WooCommerce automatique). |
| 2026-07-26 | **E2‑E — Tableau de bord admin plateforme** — *Phase E2 complète* : vue **consolidée multi-boutiques** (portail web admin, `ROLE_ADMIN`). `AdminPlatformController` `/admin/platform` (`admin_platform_overview`) : KPIs boutiques (actives/total), **licences par statut** (`LicenseRepository::statusCounts`), **connecteurs actifs** (`activeConnectorCount`, contact < 48 h), **CA & ventes agrégés toutes boutiques** + **ventes en ligne consolidées** (`SaleRepository::globalSummary`, cross-company sans scope), **top boutiques par CA** (`topCompaniesByRevenue`). `CompanyRepository::countBoutiques`. Template Flowbite + item de menu « Plateforme ». `AdminPlatformWebTest` (rendu réel admin + refus non-admin). Suite verte (**152 tests**). *(Vérif via test web — pas de login admin navigateur : compte admin `devyayaly@gmail.com`, mdp inconnu.)* **🎉 Phase E2 (catalogue e-commerce enrichi + offres + pilotage admin) terminée : E2‑A Catégories · E2‑B SEO · E2‑C Variations · E2‑D Offres · E2‑E Admin.** |
| 2026-07-26 | **E2‑D — Offres à 2 paliers + gating** : formalisation de l'offre — **« App seule »** (POS, `offer=pos`) vs **« App + Site »** (e-commerce, `offer=ecommerce`), le Site étant débloqué par une **licence utilisable** (`LicenseService::isSiteEnabled(Company)`). `StoreController` expose `connector.siteEnabled`/`connector.offer` et **gate les écritures** (WhatsApp → 403 si offre non activée ; resync déjà gaté). PWA : l'écran **Boutique en ligne** affiche un **voile/upsell** (« Offre App + Site non activée — contactez votre administrateur ») et masque gestion + stats si `!siteEnabled`. `StoreApiTest` (licence émise au setUp + assertion offre). Suite verte (**150 tests**). **Vérifié en réel** : licence active → offre `ecommerce` + WhatsApp OK ; licence suspendue → offre `pos` + PUT WhatsApp **403** ; restauration → `ecommerce`. Reste Phase E2 : E2‑E (tableau de bord admin plateforme). |
| 2026-07-26 | **E2‑C3 — Éditeur de variantes (PWA)** — *chantier E2‑C complet* : écran **Variantes** (`features/variants/`, route `/variants/:uuid`) — définir les **attributs** (nom + valeurs séparées par virgules), **« Générer les variantes »** (produit cartésien des attributs, conserve les variantes déjà réglées), régler **prix / stock / SKU / publication** par variante, supprimer. Service `variants.service` (`get`/`sync`). Accès depuis le **formulaire produit** (bouton « Variantes » en édition). `ng lint`/`ng build` verts (chunk `variants-page`). **E2‑C livré de bout en bout** (modèle + stock par variante + API + sync produit variable WooCommerce + éditeur PWA). Reste Phase E2 : E2‑D (offres 2 paliers) + E2‑E (tableau de bord admin plateforme). *(Vente d'une variante précise au POS = incrément ultérieur, touche `Sale`/`SaleLine`.)* |
| 2026-07-26 | **E3‑D — Vérification en réel du plugin design + corrections** (v1.3.0) : installé et exécuté sur le WordPress de test `127.0.0.1:81/amansa` — **WordPress 7.0.2, Astra 4.13.6, Elementor 4.2.0 (gratuit, Pro absent), WooCommerce, 42 produits, 9 catégories, connecteur yima actif**. Surface d'API Elementor 4.x vérifiée avant installation (`kits_manager`, `files_manager`, `frontend`, `widgets_manager->register`, les 5 hooks, `system_colors`/`system_typography`, `Icons_Manager`, sections/colonnes toujours présentes). **Résultats** : 7 widgets Yima enregistrés + catégorie « Yima » présente ; 20 variables CSS produites ; import du kit Mode **268 ms** (3 pages, 2 modèles, 1 menu, page d'accueil) ; **réimport = 0 créé / 5 mis à jour, aucun doublon** ; pages `/`, `/contact/`, `/a-propos/` en **HTTP 200 sans la moindre erreur PHP** ; en-tête d'Astra bien neutralisé par le hook natif (aucun repli CSS nécessaire) ; 10 vraies cartes produit et vraies catégories rendues ; `{{SHOP_NAME}}` → « Amansa ». **Six défauts trouvés et corrigés** : (1) **doublon de pied de page** avec *Header Footer Elementor* (présent sur le site) → détection HFE partie par partie, `renders_header()`/`renders_footer()`, adaptateurs de thème découpés, état affiché dans les réglages ; (2) le widget **Menu** prenait par défaut le **premier menu du site** au lieu de celui du kit ; (3) **page d'accueil non restaurée** au retrait d'un kit — WordPress remet `page_on_front` à zéro dès la mise à la corbeille (`_reset_front_page_settings_for_post`), l'identifiant est désormais lu **avant** ; (4) message WhatsApp du connecteur (contextualisé produit) rendu hors fiche produit en « Infos sur  ? » → message générique ; (5) tri des kits sensible aux accents ; (6) vignette de catégorie vide si la pièce jointe a disparu. **Ajout** : **retrait d'un kit** (bouton « Retirer ») — contenus à la corbeille, menu supprimé, réglage d'accueil d'origine restauré, rien qui ne porte la marque `_yima_design_kit` n'est touché ; **cycle import → retrait → état d'origine vérifié identique**. `php -l` OK (20 fichiers), banc d'essai vert (159 identifiants uniques). |
| 2026-07-26 | **E2‑C2 — Synchro des produits variables** (plugin `yima-wc-connector` v1.5.0) : service `ProductWooSerializer` (payload produit partagé pusher + feed ; pour un produit variable ajoute `attributes` + `variants` [sku/attributes/prix/stock/publication]). Plugin `Yima_Wc_Sync::upsert` (centralise simple **et** variable) + `apply_variations` (crée un **produit variable WooCommerce** : attributs de variation + variations SKU/prix/stock/visibilité, supprime les variations retirées, `WC_Product_Variable::sync`) ; pull + route REST refactorés dessus ; conversion de type simple↔variable gérée. `ConnectorOrderService` apparie d'abord une **variante par SKU** → décrémente le stock de la variante. `ProductVariantController::sync` **repousse** le produit après modification. Suite verte (**150 tests**). **Vérifié en réel** : produit variable yima → WooCommerce variable (2 variations TSHIRT‑S/M, prix/stock corrects) ; commande en ligne TSHIRT‑M ×2 → vente `V-2026-000017` + stock variante 5→3. Reste E2‑C3 (éditeur PWA). |
| 2026-07-26 | **E3‑C — Kits Alimentation et Électronique** (plugin `yima-wc-design` v1.2.0, **chantier E3 complet**) : deux kits sectoriels supplémentaires, chacun avec sa palette, sa typographie, ses formes et une mise en page pensée pour son commerce. **Alimentation & épicerie** (vert, angles très arrondis, boutons pilule, Poppins) : les **rayons passent avant les produits** sur l'accueil (grille 4 colonnes), puis meilleures ventes et promotions ; le panier de l'en-tête affiche le **total** ; réassurance livraison le jour même / fraîcheur / paiement à la livraison ; contact orienté « envoyez votre liste ». **Électronique & téléphonie** (bleu, angles nets, Montserrat) : sélection mise en avant d'abord, puis univers et nouveaux arrivages ; réassurance garantie / SAV / conseil ; la page « à propos » devient **Garantie & SAV** (diagnostic, devis avant réparation, pièces d'origine). Aucun code ajouté : les deux kits ne sont que des dossiers `kits/<slug>/` — c'est précisément ce que visait l'architecture d'E3‑B. `php -l` OK (**20 fichiers**), **18 JSON valides**, banc d'essai rejoué sur les **3 kits** : **159 identifiants générés, 159 uniques**, arbre conforme, aucun marqueur résiduel, aucun widget Elementor Pro, menus résolus, une page d'accueil par kit. ⚠️ **Vérification en réel encore à faire** (aucun WordPress accessible depuis l'environnement de dev) : import des 3 kits, réimport sans doublon, rendu sur Hello Elementor / Astra / OceanWP. |
| 2026-07-26 | **E3‑B — Bibliothèque de kits + importeur idempotent** (plugin `yima-wc-design` v1.1.0) : `Yima_Design_Kits` découvre les kits en lisant `kits/<slug>/kit.json` — **ajouter un secteur ne demande aucun code**, juste un dossier (palette, en-tête, pied, pages, menu). `Yima_Design_Importer::import()` applique la palette, crée l'en-tête et le pied comme **modèles Elementor** (`elementor_library`, type `section`), crée les pages (`_elementor_data` + `_wp_page_template=elementor_header_footer`), construit le menu et l'affecte à l'emplacement Yima, fixe la page d'accueil, puis vide le cache Elementor. **Idempotence** : état mémorisé dans `yima_design_imported` + méta `_yima_design_kit` — au réimport, seuls les objets **créés par le plugin** sont mis à jour ; une page homonyme du client n'est jamais reprise ni écrasée. Les identifiants Elementor sont **intégralement régénérés** à l'import (aucune collision entre gabarits) et les marqueurs `{{SHOP_NAME}}`/`{{WHATSAPP_LINK}}`/`{{URL:shop}}` sont substitués. Pages WooCommerce manquantes **signalées, jamais recréées**. Écran **Apparence → Kits Yima** (vignettes avec aperçu de palette généré, `manage_options`, nonce). Deux widgets ajoutés — **Menu Yima** et **Panier Yima** — car Elementor **gratuit n'en fournit aucun** (réservés à Pro) : sans eux un en-tête Elementor serait impossible ; le compteur du panier est déclaré en **fragment WooCommerce** (rafraîchi en AJAX). Bannière : **dégradé issu de la palette** à défaut d'image → **aucune photo de démonstration embarquée** dans le dépôt. Premier kit **Mode & accessoires** (en-tête, pied, accueil 6 sections, à propos, contact). `php -l` OK (**20 fichiers**), **6 JSON valides**, et **banc d'essai hors WordPress** rejouant `prepare()` sur les gabarits réels : arbre `section > column > widget` conforme, **53 identifiants générés, 53 uniques**, aucun marqueur résiduel, **aucun widget Elementor Pro**, entrées de menu toutes résolues, une seule page d'accueil. ⚠️ **Vérification en réel encore à faire**. Prochain : E3‑C (kits Alimentation et Électronique). |
| 2026-07-26 | **E2‑C1 — Produits à variations : fondations backend** : `Product.type` (`simple`/`variable`) + `variantAttributes` (JSON, ex. `[{name:"Taille",values:["S","M"]}]`). Nouvelle entité `ProductVariant` (product, `attributes` JSON `{Taille:"M"}`, `sku`, `priceCents`, `publishedOnline`, `isActive`, soft-delete). **Stock par variante** : `StockItem.variant`/`StockMovement.variant` (FK nullable ; unique `(product, variant)`), `StockService::applyMovement(..., ?ProductVariant $variant=null)` (rétro-compatible), `StockItemRepository::findOneByProductAndVariant`. `ProductVariantService::sync` (réconcilie attributs + variantes : créer/màj/soft-delete + réglage stock par delta). API `ProductVariantController` `/api/v1/products/{uuid}/variants` (`GET` + `PUT` remplacement). `ProductVariantApiTest` (2). Suite verte (**150 tests**). ⚠️ **Backfill requis** : la colonne `variant_attributes` (JSON NOT NULL) rejette la mise à jour des produits existants (valeur `''` invalide au CHECK MariaDB) → exécuter `UPDATE products SET variant_attributes='[]' WHERE JSON_VALID(variant_attributes)=0` (fait sur dev/test). Prochain : E2‑C2 (sync produit variable WooCommerce) + E2‑C3 (éditeur PWA). |
| 2026-07-26 | **E3‑A — Socle design des sites clients** (nouveau plugin `yima-wc-design/` v1.0.0, frère du connecteur, **indépendant** de lui) : `Yima_Design_Tokens` — jetons (couleurs, polices, `radius`, largeur, style de bouton, densité) publiés en **variables CSS** sur `:root` **et** recopiés dans les **couleurs/polices globales du kit actif Elementor** (`_elementor_page_settings`, `system_colors`/`system_typography`, puis `files_manager->clear_cache()`) → changer la palette re-skinne le site **sans rouvrir Elementor** ; 4 palettes prédéfinies. `Yima_Design_Layout` : en-tête/pied rendus sur `wp_body_open`/`wp_footer` depuis des **modèles Elementor** (`elementor_library`) éditables par le client, avec **rendu PHP par défaut** (marque, menu, panier, WhatsApp) tant qu'aucun kit n'est importé. `Yima_Design_Theme_Adapter` : neutralise l'en-tête/pied de **Hello Elementor, Astra, OceanWP, GeneratePress** (hooks testés avant usage) + repli **masquage CSS** paramétrable. `Yima_Design_Woo` : habillage WooCommerce **sans surcharge de gabarit** (styles adossés aux jetons, `loop_shop_columns`/`per_page`, badge promo en %) et `query_products()` en `visibility=catalog` → **un produit masqué par le POS ne réapparaît jamais** dans un widget. 5 widgets Elementor (catégorie « Yima ») : bannière, grille produits (réutilise le gabarit `content-product`, état global restauré), catégories, réassurance, contact. `Yima_Design_Bridge` : lecture seule des options du connecteur (n° WhatsApp) — **aucune dépendance**, et bouton flottant laissé au connecteur pour éviter le doublon. Écran **Apparence → Yima Design** (bandeau d'état Elementor/thème/connecteur, nonces, `edit_theme_options`). Cible **Elementor gratuit** (pas de Theme Builder) → **ADR‑009**. `php -l` OK sur les **15 fichiers**. ⚠️ **Vérification en réel encore à faire** (aucun WordPress accessible depuis l'environnement de dev) : palette → globales Elementor, en-tête/pied sur les 4 thèmes, cohabitation connecteur. Prochain : E3‑B (bibliothèque de kits + importeur idempotent + kit Mode). |
| 2026-07-26 | **E2‑B — Champs SEO / publication produit** (plugin v1.4.0) : `Product` + `shortDescription`/`longDescription` (TEXT), `metaTitle`/`metaDescription` (255), `onlineSlug` (slug de site personnalisable ; `getEffectiveOnlineSlug()` = onlineSlug sinon slug auto). `ProductController` accepte/expose. Feed + `WooCommerceProductPusher::seoPayload` transportent le bloc `seo`. Plugin : `Yima_Wc_Sync::apply_seo` (description/short_description/slug **avant** save) + `apply_seo_meta` (meta **Yoast** `_yoast_wpseo_*` + **Rank Math** `rank_math_*` **après** save), au pull et au push. PWA : section **« Publication / SEO »** au formulaire produit (descriptions, titre/méta SEO, slug). Suite verte (**148 tests**). **Vérifié en réel** : slug `cafe-touba-amansa-250`, descriptions, titre Yoast + méta Rank Math appliqués au produit WooCommerce. |
| 2026-07-26 | **E2‑A — Catégories dans la synchro** (phase e-commerce 2, plugin v1.3.0) : `CategoryController` complété (**`GET /{uuid}`, `PUT /{uuid}`, `DELETE /{uuid}`** — soft-delete + détache produits/sous-catégories ; refus parent = soi-même). Le **feed catalogue** et `WooCommerceProductPusher` transportent un bloc `category` `{name, slug, parentName?}` (`WooCommerceProductPusher::categoryPayload`). Plugin `Yima_Wc_Sync::apply_category` (+ `ensure_term`) : assure le terme `product_cat` (par slug, avec parent) et l'assigne au produit, au pull comme au push. PWA : écran **Catégories** (`features/categories/`, liste + créer/éditer/supprimer + parent) + service `createCategory/updateCategory/deleteCategory`, accès Profil→Gestion. `ProductCategoryRepository` : garde explicite `deletedAt IS NULL`. `CategoryCrudApiTest` (2). Suite verte (**148 tests**). **Vérifié en réel** : catégorie créée dans la PWA → assignée à un produit → terme WooCommerce créé (id 58) + assigné au produit. |
| 2026-07-26 | **E1‑H — Publication produit, stats en ligne/boutique, WhatsApp instantané** (plugin v1.2.0) : **(A)** `Product.publishedOnline` (bool) — le manager publie/dépublie un produit en ligne depuis la PWA (toggle « Visible en ligne ») ; propagé au feed catalogue + `WooCommerceProductPusher` (champ `published`) ; côté plugin `Yima_Wc_Sync::apply_stock($qty,$published)` masque le produit si **rupture OU dépublié**. **(B)** `StoreController::stats` renvoie **en ligne / boutique / total** (`SaleRepository::summaryByCompany` − online) ; PWA affiche les trois. **(C)** `SiteConfigPusher` pousse les réglages WhatsApp **immédiatement** au site (route REST plugin `/wp-json/yima/v1/config`, HMAC) dès `PUT /store/whatsapp` (le pull au cron reste le filet). Suite verte (**146 tests**). **Vérifié en réel** : dépublier→masqué / republier→visible (stock inchangé) ; n° WhatsApp changé dans l'app → option plugin mise à jour **sans pull** ; stats en ligne 1/300 000, boutique 13/9 890 000, total 14. |
| 2026-07-26 | **E1‑G — Boutique en ligne pilotée depuis la PWA** (le manager administre son site depuis l'app ; l'admin garde l'activation/licence) : entité `StoreSetting` (réglages WhatsApp par boutique, yima = source de vérité) + `StoreController` `/api/v1/store` (`GET` état+réglages+statut connecteur, `PUT /whatsapp`, `POST /resync` [repousse tout le catalogue via `WooCommerceProductPusher`], `GET /stats` [ventes en ligne, `SaleRepository::onlineSummaryByCompany`]). `GET /api/connector/config` (HMAC) → le **plugin tire les réglages WhatsApp depuis yima** (`Yima_Wc_Client::get_config` + `Yima_Wc_Sync::pull_config` au cron/activation) ; section WhatsApp du plugin passée en **lecture seule** (« géré depuis l'app yima »). PWA : écran **Boutique en ligne** (`features/store/`, statut connecteur + produits actifs + dernier contact, **stats ventes en ligne**, formulaire WhatsApp, bouton **Resynchroniser**), accès Profil→Gestion. `StoreApiTest` (2). Suite verte (**146 tests**). **Vérifié en réel** : réglage WhatsApp dans l'app → tiré par le plugin → bouton du site avec le n° yima + message contextualisé produit ; statut « connecté », 1 vente en ligne (300 000 c), resync 14 produits. |
| 2026-07-26 | **E1‑F — Rupture = masquage + WhatsApp** (plugin `yima-wc-connector` v1.1.0) : **(A) Rupture de stock → produit masqué** : `Yima_Wc_Sync::apply_stock()` (utilisé par le pull ET la route REST) met le stock **et** la visibilité — stock ≤ 0 → `outofstock` + `catalog_visibility=hidden` ; stock > 0 → `instock` + `visible` (on ne supprime jamais). **(B) Discussion WhatsApp** : `Yima_Wc_Whatsapp` — bouton flottant site-wide **contextualisé au produit** sur une fiche produit (message pré-rempli avec `{produit}`/`{lien}`), bouton inline via hooks WooCommerce + **shortcode `[yima_whatsapp]`** (robuste avec Elementor), **numéro + messages personnalisables par le manager** dans les réglages. `php -l` OK. **Vérifié en réel** (site amansa) : vente vidant le stock → produit masqué ; réappro → visible ; bouton WhatsApp `wa.me` rendu avec le produit. |
| 2026-07-26 | **E1‑E — Synchro stock temps réel** (yima → WooCommerce) : `StockSyncSubscriber` (listener Doctrine `#[AsDoctrineListener]` onFlush/postFlush) collecte les `StockItem` insérés/modifiés pendant le flush et pousse le stock de chaque produit concerné vers WooCommerce **après commit** (réutilise `WooCommerceProductPusher`, fail-soft, no-op sans licence/site). Couvre **tous** les mouvements (vente POS, réception, inventaire, production, commande en ligne) sans toucher les contrôleurs. La quantité poussée est **absolue** → auto-correction de toute dérive. Suite verte (**144 tests**). **Vérifié en réel** : vente POS yima (Café Touba 39→36) → stock WooCommerce mis à jour instantanément (44→36). |
| 2026-07-26 | **E1‑D — Synchro produits bidirectionnelle** (harmonisation par SKU, temps réel) : appariement **par SKU**, **yima autoritaire** (pas d'écrasement du catalogue central), **SKU auto-généré** si absent. **WooCommerce → yima** : `POST /api/connector/products` (HMAC) + `ConnectorProductService::upsertFromWoo` (création si SKU absent, sinon yima prime) ; plugin `Yima_Wc_Products` (hooks `woocommerce_new_product`/`update_product` → push). **yima → WooCommerce (immédiat)** : `WooCommerceProductPusher` (HTTP signé HMAC vers `/?rest_route=/yima/v1/product`, fail-soft) appelé depuis `ProductController::create/update` ; plugin `Yima_Wc_Rest` (route REST entrante, vérif HMAC, upsert WC par SKU). **Garde anti-boucle** `Yima_Wc_Products::suppress()` (écritures programmatiques du pull/REST non renvoyées). `ConnectorApiTest` +1 (yima prime). Suite verte (**144 tests**). **Vérifié en réel** (site amansa) : produit créé dans yima → apparaît instantanément dans WooCommerce ; produit créé dans WooCommerce → remonte instantanément dans yima. |
| 2026-07-26 | **E1‑C — Plugin WordPress/WooCommerce** (`yima-wc-connector/`, dernier incrément E1) : plugin PHP autonome que la boutique installe sur son site. `yima-wc-connector.php` (bootstrap, WP‑Cron horaire, hooks activation/désactivation), `Yima_Wc_Client` (activation + appels **signés HMAC** reproduisant `ConnectorAuthService` : `timestamp\nMETHOD\nPATH\nbody`), `Yima_Wc_Sync` (pull catalogue → **upsert produits WooCommerce par SKU** : nom/prix[centimes XOF→francs]/stock), `Yima_Wc_Orders` (hooks `processing`/`completed` → **push commande** vers yima, idempotent, méta `_yima_pushed`), `Yima_Wc_Settings` (écran WooCommerce → Yima Connector : URL yima + clé de licence + **Activer** + statut licence + **Synchroniser maintenant**, nonces/escaping). `uninstall.php`, `readme.txt`. Coupe la synchro si licence invalide (appels HMAC refusés). `php -l` OK sur tous les fichiers. **Vérifié en réel** contre un WordPress+WooCommerce local (`127.0.0.1:81/amansa`) : activation (licence→secret HMAC, site lié), **pull catalogue** (7 produits AmansaShop créés dans WooCommerce, prix/stock corrects), **push commande** (commande WC → vente yima `V-2026-000013` `channel=online`, stock 46→44), **idempotence** (re-push sans doublon). **🎉 Roadmap système complet F0→E1 terminée et connecteur validé de bout en bout.** |
| 2026-07-26 | **E1‑B — API connecteur e-commerce** (endpoints publics licence + HMAC) : `ConnectorController` sous `/api/connector` (hors JWT, entrée `PUBLIC_ACCESS` avant `^/api`). `POST /activate` (auth par **clé de licence** → lie le site + renvoie un **secret HMAC** une seule fois, `License.hmacSecretEncrypted`), `GET /catalog` (feed produits actifs + prix + stock, scopé boutique), `POST /orders` (→ vente `channel=online`, prix **recalculés serveur**, stock décrémenté, **idempotente** par `externalId`), `GET /ping` (heartbeat → `lastSeenAt`). Auth des appels signés via `ConnectorAuthService` (HMAC-SHA256 sur `timestamp\nMETHOD\nPATH\nbody`, anti-rejeu ±300 s, `hash_equals`). `LicenseService::activate/hmacSecret/touch`, `ConnectorOrderService` (appariement par SKU, réutilise `StockService`), `ProductRepository::findOneBySkuAndCompany`. En-têtes CORS `X-Yima-{Key,Timestamp,Signature}`. Suite verte (**143 tests**, `ConnectorApiTest`). Vérifié : tests bout en bout (activation → catalogue signé → commande idempotente + stock 20→17) + smoke live `:8010` (400/403/401). Prochain : E1‑C (plugin WordPress). |
| 2026-07-26 | **E1‑A — Licences & comptes clients** (control plane admin, 1er incrément du site e-commerce) : entité `License` (clé jamais en clair — `keyHash` SHA-256 indexé pour validation, `keyEncrypted` libsodium pour révélation, `keyPrefix` pour l'affichage ; statut `active`/`suspended`/`expired`/`revoked` via enum `LicenseStatus` ; `plan`, `siteUrl`, `expiresAt`, `activatedAt`, `lastSeenAt`) + journal append-only `LicenseEvent`. `LicenseService` (émission → clé en clair **une seule fois**, renouvellement, suspension/réactivation, révocation, `revealKey`, `validate` avec expiration paresseuse + contrôle du site). Écrans **admin** (portail Symfony, auto-`ROLE_ADMIN`) : `LicenseController` `/admin/licenses` (index filtré, émission, détail + journal + révélation, actions CSRF), item de menu « Licences », section **Licence** sur la fiche boutique. Seed RBAC `integration.view`/`integration.manage`. **Décision** : la licence gate le **canal e-commerce** (connecteur), pas le POS ; le compte = la boutique (pilotée par l'admin) ; modèle **plugin tire depuis yima** (révise ADR-007 → **ADR-008**). Suite verte (**138 tests**, `LicenseServiceTest` + `LicenseAdminWebTest`). Vérifié via test web (rendu réel des écrans admin : émission → clé affichée → suspension). Prochain : E1‑B (API connecteur licence-gatée). |
| 2026-07-26 | **B3 — Production / transformation** (recette à nomenclature) : `Product.costCents` (coût de revient) ; entités `Recipe`(produit fini, `yieldQuantity`/lot) + `RecipeComponent`(quantité/lot) + `ProductionOrder` **append-only** (`PRD-AAAA-000000`, instantané des composants consommés). `StockMovement` type **`production`**. `ProductionService::execute` : **contrôle de disponibilité** des composants (sinon 422 `InsufficientStockException`), consomme les composants (sorties `production`), crédite le produit fini (entrée), **roll-up du coût** des intrants → coût unitaire du fini (recalé sur `Product.costCents`). API `RecipeController` (`/api/v1/recipes` CRUD nomenclature, refus composant = produit fini) et `ProductionOrderController` (`/api/v1/production-orders` créer=exécuter + historique/détail). Gating VIEW/EDIT ; seed RBAC `production.view`/`production.manage` (manager + magasinier). PWA : écran **Production** (`features/production/`, onglets Recettes/Fabrications, formulaire recette avec composants, action **Fabriquer**, détail des composants consommés + coûts), champ **coût de revient** au formulaire produit, accès Profil→Gestion. Suite verte (**132 tests**, dont `ProductionApiTest`). Vérifié API bout en bout (`PRD-2026-000001` : 4 lots → 12 gâteaux, coût 2 800 FCFA / unitaire 233 ; farine 100→92, sucre 50→46). |
| 2026-07-25 | **B2 — Comptes & paiements fournisseurs** (dettes fournisseurs) : entité append-only `SupplierPayment` + `Supplier.balanceCents` + `PurchaseOrder.paidCents`/`getDueCents`. `SupplierLedgerService` (réception à crédit → dette ; règlement qui **solde les réceptions FIFO**). `PurchaseOrderController::create` accepte `amountPaidCents` (défaut = tout payé ; partiel → dette, fournisseur requis) ; `paidCents`/`dueCents` sérialisés. `SupplierController` : `GET /suppliers/creditors`, `GET /{uuid}/payments`, `POST /{uuid}/payments` (règlement, EDIT). **Trésorerie** : `FinanceController` intègre les paiements fournisseurs aux **sorties** (`sortiesCents`, `netCents` recalculé). PWA : écran **Dettes fournisseurs** (`features/suppliers/`, créditeurs + compte + règlement), option **réception à crédit** (montant payé) au réappro, carte « Sorties » du Finance. Suite verte (**128 tests**, dont `SupplierDebtApiTest`). Vérifié navigateur (Grossiste Sandaga, dû 5 000 FCFA). |
| 2026-07-25 | **B4 — Journal financier / trésorerie** (consignation, sans passerelle) : entité append-only `Expense` (dépenses hors achats, `DEP-AAAA-000000`) + `PaymentRepository::treasuryTotalsByMethod` (encaissements ventes+recouvrements par moyen, hors annulées/remboursées). `FinanceController` (`/api/v1/finance`) : `GET /summary?period=` (recettes/moyen, dépenses/catégorie, **solde net**), `GET /report/pdf` (rapport trésorerie **dompdf/Twig** `pdf/treasury.html.twig`), `GET/POST/DELETE /expenses`. Gating lecture VIEW, dépenses EDIT. PWA : écran **Finances** (`features/finance/`, KPIs recettes/dépenses/net par période, ventilation par moyen, dépenses list/ajout/suppression, rapport PDF), accès Profil→Gestion. Suite verte (**124 tests**, dont `FinanceApiTest`). Vérifié navigateur (recettes 11 500 / dépenses 15 000 / net −3 500 FCFA). |
| 2026-07-25 | **B1 — Dettes / créances clients** (vente à crédit) : entité append-only `CustomerLedgerEntry` (debit=créance / credit=paiement) + `Customer.balanceCents` dénormalisé ; `CustomerLedgerService` (débit à la vente à crédit ; recouvrement qui **solde les ventes en attente FIFO**). `SaleController` : paiement partiel (`payment.amountCents` < total) → vente `pending` + créance client (client obligatoire) ; `dueCents` sérialisé. `CustomerController` : `GET /customers/debtors` (débiteurs + total dû), `GET /{uuid}/ledger` (relevé + solde), `POST /{uuid}/payments` (recouvrement) ; `balanceCents` exposé. Gating VIEW (opération caissier). PWA : **vente à crédit** à l'encaissement (action sheet → montant payé), écran **Créances clients** (débiteurs + total, compte client : relevé + « Encaisser un paiement »), accès Profil→Gestion. Suite verte (**120 tests**, dont `CustomerDebtApiTest`). Vérifié navigateur (Fatou Ndiaye, dû 2 000 FCFA, relevé). |
| 2026-07-25 | **F2 — Médias & images produit** : entité `ProductImage`(company, product, media, position, isPrimary) **réutilisant `MediaService`** (stockage `var/media/products`). API `ProductImageController` (`/api/v1/products/{uuid}/images` : upload multipart, liste, **service binaire**, image principale, suppression) ; fiche produit expose `primaryImageUuid` (+ `images` au détail, lookup batch anti-N+1). Gating VIEW/EDIT. **Bug latent corrigé** : `MediaService::validateFileForType`/`getSubfolderForType` référençaient des constantes `Media::TYPE_MODULE_*` supprimées (→ `Error` sur tout upload non-profil) ; ajout de `Media::TYPE_PRODUCT` (validé image). PWA : **directive `appAuthImg`** (charge l'image via HttpClient+Bearer → object URL), **vignettes** dans la liste Produits + **section Photos** au formulaire (galerie, principale, ajout, suppression). Suite verte (**116 tests**, dont `ProductImageApiTest`). Vérifié navigateur (upload → vignette + galerie). |
| 2026-07-25 | **F1 — RBAC réel** (rôles par défaut + permissions personnalisées par boutique) : `PermissionResolver` **activé** — quand un membre a un `UserCompany.boutiqueRole`, ses permissions font autorité (sinon **fallback** rétrocompatible sur les rôles Symfony, rien cassé). `Role.company` (nullable) → **rôles personnalisés par boutique** (unicité `(company, code)`). Seed étendu : rôles **Caissier**/**Magasinier** + permissions `stock.*`/`purchase.*`/`customer.*`/`supplier.*` (`app:rbac:seed`). Nouvelle API `RoleController` (`/api/v1/roles` : liste rôles assignables + catalogue permissions, CRUD **rôles perso**), `EmployeeController` assigne `boutiqueRole` (défaut caissier) + `POST /{id}/role`. Gating `COMPANY_MANAGE_EMPLOYEES`. PWA : écran **Rôles & permissions** (`features/roles/`, création/édition avec cases par catégorie), sélecteur de rôle dans **Employés** (création + changement), accès Profil→Gestion (managers). Suite verte (**113 tests**, dont `RoleApiTest` : CRUD + **enforcement** — un rôle sans `boutique.edit` bloque la création de produit même pour un ROLE_MANAGER). Vérifié navigateur. |
| 2026-07-25 | **F0 — PDF → Twig** (refactor, plan « système complet ») : nouveau service `src/Service/Pdf/PdfRenderer.php` (encapsule dompdf : Options `isRemoteEnabled=false` + DejaVu Sans + A4 + `renderView`) ; les 3 HTML heredoc (facture, rapport de synthèse, rapport Z) déplacés en **templates Twig** `templates/pdf/{_base,_macros,invoice,sales_summary,z_report}.html.twig` (comme les e-mails). `InvoiceController`/`ReportController`/`CashSessionController` appellent `$this->pdfRenderer->render(tpl, data, filename)` et n'ont plus d'imports Dompdf. `lint:twig` OK, **107 tests verts** (parité vérifiée : PDF de tailles identiques). Prépare la réutilisation pour le rapport de trésorerie (chantier B4). |
| 2026-07 | Phase 0 : socle sécurité (RBAC, chiffrement, alertes dédupliquées, détection menaces) |
| 2026-07 | Phase 1 : `CompanyDatabase` + chiffrement + test de connexion |
| 2026-07 | Phase 1.5 : fixture AmansaShop + tests fonctionnels auth API |
| 2026-07 | Dossier de conception initialisé, déplacé au niveau parent (`shop/yima-pos/`) partagé Symfony + Ionic |
| 2026-07 | Ajout du canal e-commerce **WooCommerce** (ADR-007) : fiche module + roadmap |
| 2026-07-24 | **Frontend PWA `yima-shop` — incrément 1** : socle Ionic/Angular (env, HttpClient, intercepteur JWT + refresh, guards, PWA/service worker), **authentification complète** (login + challenges 202 appareil/pays/IP/2FA), shell à onglets + profil/déconnexion, localisation FR/FCFA. Lint + build prod verts. |
| 2026-07-24 | **Module Catalogue — incrément 1** (phase 2) : entités `Product`/`ProductCategory` (company-scopées, uuid, soft-delete), API v1 `products`/`categories` (pagination, recherche, scope boutique, RBAC `catalogue.view/manage`), fixtures de démo AmansaShop, **écran PWA Produits** branché sur l'API réelle. Suite de tests backend verte (33 tests, dont `CatalogueApiTest`). Clés JWT dev/test régénérées (passphrase alignée). |
| 2026-07-24 | **Module POS/Ventes — incrément 1** (phase 2) : entités append-only `Sale`/`SaleLine`/`Payment` (totaux HT/TVA/TTC centimes, référence séquentielle par boutique, ligne dénormalisée), API `POST/GET /api/v1/sales` avec **prix recalculés serveur** et **idempotence** (`Idempotency-Key`), **écran PWA Vente** (panier → encaissement espèces → ticket). CORS backend étendu (`Idempotency-Key`). Suite verte (39 tests, dont `SaleApiTest`). Vérifié bout en bout dans le navigateur. |
| 2026-07-24 | **Module Caisse — incrément 1** (phase 2) : entité `CashSession` (ouverture/fond, clôture Z, compté/attendu/écart), rattachement automatique des ventes à la session ouverte (`Sale.cashSession`), API `/api/v1/cash-sessions/{open,current,{uuid}/close}`, **écran PWA Caisse** (ouverture, totaux live, clôture Z avec écart). Suite verte (**45 tests**, dont `CashSessionApiTest`). Vérifié bout en bout dans le navigateur (ouverture → vente rattachée → Z, écart −200 FCFA). |
| 2026-07-24 | **Module Stock — incrément 1** (phase 2) : entités `StockItem`/`StockMovement` (append-only), `StockService` générant les **sorties automatiques à la vente**, API `GET /api/v1/stock` + `POST /api/v1/stock/movements` (in/out/adjustment), `stockQuantity` exposé au catalogue (lookup batch), **stock affiché** dans les écrans Produits/Vente. Suite verte (**51 tests**, dont `StockApiTest`). Vérifié bout en bout (vente → stock 40→38). **Jalon phase 2 atteint** : produit → panier → paiement → ticket → mouvement de stock → session de caisse. |
| 2026-07-24 | **Infra dev** : serveurs déplacés sur **backend `:8010`** (`symfony serve`) et **frontend `:8110`** (`ng serve`) ; `apiBaseUrl` + `BASE_URL` mis à jour. BDD `root`/`welcome`. **PWA — choix du moyen de paiement** à l'encaissement (action sheet Espèces / Mobile money / Carte ; l'attendu caisse ne compte que les espèces). Vérifié dans le navigateur (vente Mobile money). |
| 2026-07-24 | **PWA — file d'attente hors-ligne** (`SalesSyncService`, offline-first) : en cas d'échec réseau, la vente est **mise en file locale** (Preferences) puis **rejouée automatiquement** au retour en ligne / à l'ouverture de l'onglet, avec `Idempotency-Key` (aucun doublon). Indicateur « N en attente » + synchro manuelle dans l'écran Vente. **Vérifié bout en bout** dans le navigateur : backend coupé → vente en file (badge 1) → backend relancé → sync → vente `V-2026-000006` créée, stock décrémenté, badge à 0. |
| 2026-07-24 | **Reporting & Journal** (phase 3, incrément 1) : endpoint `GET /api/v1/reports/sales/summary` (nb ventes, CA, TVA, ticket moyen, ventilation par moyen de paiement) + **onglet PWA Journal** (tableau de bord par période + historique paginé + détail de ticket en modal). **Gestion des produits** dans la PWA (créer via FAB / éditer au tap, formulaire modal → `POST/PUT /products`). Suite verte (**53 tests**, dont `ReportApiTest`). Vérifié bout en bout (dashboard CA 19 400 FCFA, création « Thé Vert 100 g »). |
| 2026-07-24 | **Clients / CRM** (phase 3, incrément 1) : entité `Customer` (company-scopée, soft-delete) + API `/api/v1/customers` (liste/recherche/détail/CRUD), `Sale.customer` (vente **nominative** ou anonyme via `customerUuid`). PWA : **sélecteur de client** dans le panier (recherche / création rapide), client affiché sur le ticket. Suite verte (**58 tests**, dont `CustomerApiTest`). Vérifié bout en bout (vente `V-2026-000007` rattachée au client « Awa Ndiaye » créé à l'encaissement). |
| 2026-07-24 | **Achats / Fournisseurs** (phase 3, incrément 1) : entités `Supplier` + `PurchaseOrder`/`PurchaseLine`, API `/api/v1/suppliers` (CRUD) et `/api/v1/purchase-orders` (**réception → entrées de stock `in` automatiques** via `StockService`, référence `A-AAAA-000000`). PWA : **réapprovisionnement** depuis la fiche produit (quantité, coût, fournisseur optionnel + création rapide). Suite verte (**64 tests**, dont `PurchaseApiTest`). Vérifié bout en bout (réappro +15 → stock Café Touba 35→50). |
| 2026-07-24 | **Promotions / Remises** (phase 3, incrément 1) : `Sale.discountCents` + `POST /sales` accepte `discount { type: percent\|amount, value }` — **recalcul net serveur** avec allocation HT/TVA au prorata et plafonnement. Remise reflétée au reporting et au ticket. PWA : **bouton Remise** dans le panier (pourcentage/montant), net affiché. Suite verte (**66 tests**, dont remises). Vérifié bout en bout (remise 10 % → vente `V-2026-000009` net 1 350 FCFA, remise 150). |
| 2026-07-24 | **Facturation** (phase 3, incrément 1) : entité `Invoice` (une par vente, `FAC-AAAA-000000`, snapshot montants), `POST /api/v1/sales/{uuid}/invoice` (idempotent) + `GET /api/v1/invoices/{uuid}/pdf` (**dompdf** : entête boutique NINEA/RCCM, lignes, TVA, remise, total TTC). PWA : bouton **Facture PDF** dans le Journal. Suite verte (**70 tests**, dont `InvoiceApiTest`). Vérifié bout en bout (PDF `FAC-2026-000001` 22 Ko, ouvert dans le navigateur). |
| 2026-07-25 | **PWA — ajout par code-barres/SKU** dans l'écran Vente : champ scan (lecteur USB : code + Entrée) → recherche produit (match exact barcode/sku, sinon 1er résultat) et ajout au panier ; toast si introuvable. Vérifié dans le navigateur (code `6001234500035` → Café Touba au panier). Lint + build verts. |
| 2026-07-25 | **Git — consolidation monorepo** : tout le projet (`portail-manager` + `yima-shop` + `yima-pos`) regroupé dans un dépôt unique à la racine `shop/`, poussé sur `gitlab.com/xaliss/shopv1.git` branche **`dev`** (README + `.gitignore` racine ; dépendances exclues). |
| 2026-07-25 | **Gestion des employés/caissiers** (administration) : `EmployeeController` — `GET /api/v1/employees` (liste des rattachements boutique via `UserCompanyRepository::findByCompany`), `POST /employees` (crée un **compte caissier** `ROLE_EMPLOYER` + `UserCompany` role `cashier`, mot de passe fourni **ou temporaire généré** renvoyé une fois, `mustChangePassword`, e-mail de bienvenue fail-soft), `POST /{id}/{activate,deactivate}` (bascule `UserCompany.isActive`, protège soi-même et les ROLE_ADMIN/MANAGER). Gating `COMPANY_MANAGE_EMPLOYEES` (= membre actif **et** ROLE_MANAGER). PWA : écran **Employés** (`features/employees/`, liste + toggle actif + création via modal, mot de passe temporaire affiché en alerte) accès **Profil→Gestion (managers uniquement)**. Suite verte (**107 tests**, dont `EmployeeApiTest` : création, connexion caissier, unicité e-mail, désactivation, gating). Vérifié en direct (caissier « Mariama Sow » créé, se connecte) + navigateur. |
| 2026-07-25 | **Rapport Z de caisse (PDF)** (phase 2) : `GET /api/v1/cash-sessions/{uuid}/z-report` — **dompdf** (entête boutique NINEA/RCCM, caissier, ouverture/clôture, fond, nb ventes + CA TTC, encaissé espèces / autres, attendu, compté, **écart coloré**) via `SaleRepository::sessionSummary`. Fonctionne session ouverte (chiffres live) ou clôturée. PWA : bouton **« Rapport Z (PDF) »** dans la page Caisse (session ouverte et écran de clôture). Suite verte (**101 tests**, +1 dans `CashSessionApiTest`). Vérifié en direct (Z `rapport-z-*.pdf` 24 Ko) et bouton affiché dans le navigateur. |
| 2026-07-25 | **Top produits / analytics** (phase 3) : `GET /api/v1/reports/products/top?period=&limit=` — meilleures ventes agrégées par produit (nom dénormalisé, ventes payées), triées par quantité (`SaleLineRepository::topProductsByCompany`). PWA : section **« Meilleures ventes »** dans le tableau de bord Journal (barres proportionnelles quantité + CA par produit, `ReportsService.getTopProducts`). Suite verte (**100 tests**, +1 dans `ReportApiTest`). Vérifié dans le navigateur (Café Touba 19 vendus en tête). |
| 2026-07-25 | **Exports & rapport** (phase 3) : `GET /api/v1/reports/sales/export` (**CSV** des ventes de la période — BOM UTF-8, séparateur `;`, statut/paiement/montants FCFA en libellés FR ; `SaleRepository::findByCompanyForExport`) et `GET /api/v1/reports/sales/summary/pdf` (**rapport de synthèse dompdf** : entête boutique NINEA/RCCM, KPIs CA/nb/ticket moyen, HT/TVA/remise, ventilation par moyen de paiement). `ApiResponseFormatter::paginated` inchangé ; `resolvePeriod()` factorisé. PWA : boutons **CSV**/**Rapport PDF** dans le tableau de bord Journal (`ReportsService.exportSalesCsv/getSummaryPdf` en `responseType:'blob'`). Suite verte (**99 tests**, +3 dans `ReportApiTest`). Vérifié en direct (CSV `ventes-all-*.csv` avec entêtes + données, PDF 28 Ko `%PDF`) et boutons affichés dans le navigateur. |
| 2026-07-25 | **Centre de notifications PWA** (phase 3) : API `NotificationController` (`GET /api/v1/notifications` paginé + `meta.unreadCount`, `?unread=1`, `GET /unread-count`, `POST /{id}/read`, `POST /read-all`) scopée à l'utilisateur (`NotificationAppRepository`). PWA : écran **Notifications** (`features/notifications/`, non lues surlignées, marquage lu au tap + navigation lien, « tout lire », pagination), **cloche + badge** dans l'en-tête Journal et entrée **Profil→Gestion** avec badge (`NotificationsService.unreadCount` signal partagé). Les alertes de stock bas apparaissent désormais dans l'app. `ApiResponseFormatter::paginated` accepte un `$extraMeta`. Suite verte (**96 tests**, dont `NotificationApiTest`). Vérifié bout en bout (alerte « Stock faible : Café Touba » visible, « tout lire » → 0 non lue). |
| 2026-07-25 | **Notification proactive de stock bas** (phase 3) : `LowStockAlertService` (e-mail au manager via `EmailService`/template `emails/company/low_stock_alert.html.twig` + notification in-app `NotificationService`, dédupliqué `AlertDeduplicationService`, **fail-soft**). `StockService` détecte le **franchissement descendant** du seuil (tampon) et `dispatchPendingLowStockAlerts()` est appelé **après flush** par les contrôleurs Vente/Stock/Inventaire. Résolution destinataires via `UserCompanyRepository::findByCompanyAndRole($c,'manager')`. Suite verte (**90 tests**, dont `LowStockAlertApiTest`). Vérifié en direct (vente V-2026-000010 : Café Touba 45→39 sous seuil 40 → notification « Stock faible » créée). |
| 2026-07-25 | **Alertes de stock bas** (phase 2, incrément 1) : `Product.lowStockThreshold` (seuil par produit, nullable), `serializeProduct` expose `lowStockThreshold`+`lowStock`, endpoint `GET /api/v1/stock/alerts` (produits actifs avec stock ≤ seuil, triés) via `StockItemRepository::findLowStockByCompany`. PWA : champ **seuil d'alerte** au formulaire produit, puce **« Stock bas »** dans la liste Produits, écran **Alertes de stock** (Profil→Gestion) + **badge** compteur. Fixtures : seuils de démo. Suite verte (**88 tests**, +3 dans `StockApiTest`). Vérifié bout en bout (Dentifrice 25/seuil 30 + Jus bissap 30/30 → 2 alertes, badge « 2 »). |
| 2026-07-25 | **Inventaire physique** (phase 2, incrément 1) : entités `InventoryCount`/`InventoryCountLine` (session `INV-AAAA-000000`, cycle `draft`→`applied`/`cancelled`), `InventoryService` (application des écarts = ajustements de stock recalés sur le **stock réel au moment de l'application**), API `/api/v1/inventory-counts` (créer/lister/détail, `PUT /lines` comptage, `POST /apply` idempotent, `POST /cancel`). PWA : écran **Inventaire** (liste + détail comptage avec **écart live**, ENREGISTRER/APPLIQUER/ANNULER), accès depuis Profil → Gestion. Suite verte (**85 tests**, dont `InventoryApiTest` : recalage stock, idempotence, non-impact des lignes non comptées, annulation). Vérifié bout en bout dans le navigateur (INV-2026-000001 : comptage Café Touba 46→45, appliqué). |
| 2026-07-24 | **Boutique (infos légales)** : API `GET/PUT /api/v1/boutique` (nom, NINEA, RCCM, adresse, ville, tél, email, site) — **alimente l'entête des factures**. PWA : section **« Ma boutique »** dans Profil (voir/éditer + rappel NINEA/RCCM manquants). Suite verte (**78 tests**, dont `BoutiqueApiTest`). Vérifié via l'API (NINEA/RCCM renseignés sur AmansaShop). |
| 2026-07-24 | **Avoirs / Retours** (phase 3, incrément 1) : entité `CreditNote` (un avoir par vente, `AV-AAAA-000000`, append-only), `POST /api/v1/sales/{uuid}/refund` — émet l'avoir, passe la vente en **`refunded`** (sortie du CA) et **remet les articles en stock** (mouvements `in`). Idempotent, manager. PWA : bouton **Rembourser (avoir)** dans le Journal + statut « Remboursée ». Suite verte (**75 tests**, dont `RefundApiTest` : restock, idempotence, exclusion CA). Vérifié bout en bout via l'API (vente `V-2026-000009` → avoir `AV-2026-000001`, statut refunded, stock rétabli). |

## Comment mettre à jour ce suivi

1. Passer le statut du module (⬜→🚧→✅) et dater le changelog.
2. Mettre à jour la fiche module (endpoints, entités, tests réalisés).
3. Si décision structurante : ajouter un ADR dans [notes/decisions.md](notes/decisions.md).
