# Module — Licences & comptes clients (e-commerce)

> Phase 4, chantier **E1**. Couche **control plane** pilotée par l'**admin plateforme** : émettre
> et gérer les **licences** du connecteur e-commerce, rattachées aux **comptes boutiques**.
> Le connecteur (API + plugin WordPress) consomme ces licences (E1‑B / E1‑C).

## 1. Objectif

Donner à l'admin le contrôle de qui peut connecter un site WooCommerce à yima :
- **émettre** une licence pour une boutique, avec expiration et plan ;
- **suspendre / réactiver / révoquer / renouveler** une licence ;
- **révéler** la clé à la demande (elle n'est affichée en clair qu'à l'émission).

La licence gate **le canal e-commerce** (le connecteur), **pas** le POS. L'activation globale du
compte boutique reste les bascules `Company.isActive` / `User.isActive` (déjà pilotées par l'admin).

## 2. Décisions (ADR-008)

- **Compte du client = la boutique (manager)**, provisionnée/pilotée par l'admin ; les comptes
  acheteurs restent gérés par WooCommerce.
- **Modèle de synchro = le plugin tire depuis yima** (voir [connecteur WooCommerce](woocommerce-connector.md)) :
  le plugin s'authentifie avec la **clé de licence** ; licence invalide/expirée → synchro coupée.
- **Licence simple (v1)** : clé + statut + expiration + liaison boutique + URL du site.

## 3. Entités (backend `portail-manager`)

| Entité | Rôle |
|---|---|
| `License` (`ManyToOne` `Company`) | `keyHash` (SHA-256, indexé, validation), `keyEncrypted` (libsodium, révélation), `keyPrefix` (affichage), `status` (`LicenseStatus`), `plan`, `siteUrl`, `issuedAt`, `expiresAt` (null = perpétuelle), `activatedAt`, `lastSeenAt`. Helpers `isExpired()`, `isUsable()` |
| `LicenseEvent` (append-only) | Audit : `type` (issued/renewed/suspended/reactivated/revoked/activated/heartbeat), `note`, `actor`, `createdAt` |
| `LicenseStatus` (enum) | `active` / `suspended` / `expired` / `revoked` + `label()` + `color()` |

La clé n'est **jamais** stockée en clair : validation par hash, révélation par déchiffrement.

## 4. Service

`App\Service\License\LicenseService` :
- `generateKey()` → `YIMA-XXXX-XXXX-XXXX-XXXX` (base32) ;
- `issue(company, expiresAt?, plan?, actor?)` → `{license, plainKey}` (**clé en clair une seule fois**) ;
- `renew` / `suspend` / `reactivate` / `revoke` (journalisés) ;
- `revealKey(license)` (déchiffrement) ;
- `validate(key, siteUrl?)` → `{ok, license?, reason?}` (lookup hash, expiration paresseuse, contrôle du site) — utilisé par l'API connecteur (E1‑B).

## 5. Écrans admin (portail Symfony, Twig/Flowbite, `ROLE_ADMIN`)

`LicenseController` sous `/admin/licenses` (auto-gaté par la règle `^/admin`) :

| Route | Usage |
|---|---|
| `admin_licenses_index` (GET) | Liste filtrée (boutique/statut/recherche), pagination |
| `admin_licenses_new` (GET/POST) | Émettre → page affichant la clé en clair une fois |
| `admin_licenses_show` (GET, `?reveal=1`) | Détail + journal + révélation de clé |
| `admin_licenses_{renew,suspend,reactivate,revoke}` (POST, CSRF) | Actions de cycle de vie |

Intégration : item de menu **Licences** + **section Licence** sur la fiche boutique
(`admin/companies/_license_section.html.twig`, miroir de la section base de données).

## 6. Permissions

RBAC seedé : `integration.view` / `integration.manage` (catégorie `integration`, scope boutique) —
pour la visibilité manager du connecteur en E1‑B/‑C. Les écrans admin de E1‑A restent gatés
`ROLE_ADMIN` (pas de permission fine nécessaire).

## 7. Tests

- `tests/Functional/License/LicenseServiceTest.php` : émission (clé/hash/prefix/révélation), validation
  (actif/suspendu/révoqué/expiré + recalage paresseux), renouvellement, contrôle du site.
- `tests/Functional/Admin/LicenseAdminWebTest.php` : rendu réel des écrans admin (index → émission →
  clé affichée → suspension) ; refus non-admin.
- `tests/Functional/Api/ConnectorApiTest.php` (E1‑B) : activation → catalogue signé → commande
  idempotente + décrément de stock ; refus signature/en-têtes ; clé inconnue → 403.

## 8. API connecteur (E1‑B — livré)

Endpoints **publics** sous `/api/connector` (entrée `PUBLIC_ACCESS` avant le catch-all `^/api` ;
hors JWT). L'activation s'authentifie par la **clé de licence** ; les autres appels par **HMAC**.

| Méthode | Route | Auth | Rôle |
|---|---|---|---|
| POST | `/api/connector/activate` `{licenseKey, siteUrl}` | clé de licence | Lie le site + renvoie le **secret HMAC** (une seule fois) |
| GET | `/api/connector/catalog` | HMAC | Feed produits actifs + prix + stock (scopé boutique) |
| POST | `/api/connector/orders` `{externalId, lines:[{sku, quantity}]}` | HMAC | Commande → vente `channel=online` (prix recalculés serveur, stock −, **idempotente** par `externalId`) |
| GET | `/api/connector/ping` | HMAC | Heartbeat → `lastSeenAt` |

**Signature HMAC** (`ConnectorAuthService`) : en-têtes `X-Yima-Key` (clé), `X-Yima-Timestamp` (epoch,
anti-rejeu ±300 s), `X-Yima-Signature` = `HMAC-SHA256(secret, "timestamp\nMETHOD\nPATH\nbody")`
(comparaison `hash_equals`). Secret partagé stocké chiffré (`License.hmacSecretEncrypted`), remis au
plugin à l'activation. `ConnectorOrderService` apparie les lignes par **SKU** et réutilise `StockService`.

## 9. Plugin WordPress (E1‑C — livré)

Dossier **`yima-wc-connector/`** (à la racine du dépôt), plugin PHP autonome installé sur le site :

| Fichier | Rôle |
|---|---|
| `yima-wc-connector.php` | Bootstrap, WP‑Cron horaire (`yima_wc_sync_event`), hooks activation/désactivation |
| `includes/class-yima-wc-client.php` | Activation + appels **signés HMAC** (reproduit `ConnectorAuthService`) |
| `includes/class-yima-wc-sync.php` | Pull catalogue → **upsert produits WooCommerce par SKU** (nom, prix centimes XOF→francs, stock) |
| `includes/class-yima-wc-orders.php` | Hooks `processing`/`completed` → **push commande** vers yima (idempotent, méta `_yima_pushed`) |
| `includes/class-yima-wc-settings.php` | Écran **WooCommerce → Yima Connector** (URL + clé + Activer + statut + Synchroniser) |
| `uninstall.php`, `readme.txt` | Nettoyage des options ; documentation utilisateur |

La synchro se **coupe** si la licence est invalide/expirée (appels HMAC refusés côté yima).

## 10. Synchro produits bidirectionnelle (E1‑D — livré)

Harmonisation **par SKU**, **yima autoritaire**, **temps réel**, **SKU auto-généré** si absent.

| Sens | Mécanisme |
|---|---|
| **WooCommerce → yima** | Plugin `Yima_Wc_Products` (hooks `woocommerce_new_product`/`update_product`) → `POST /api/connector/products` (HMAC) → `ConnectorProductService::upsertFromWoo` : **crée** si SKU absent, sinon **yima prime** (aucun écrasement) |
| **yima → WooCommerce** (immédiat) | `ProductController::create/update` → `WooCommerceProductPusher` (HTTP signé HMAC vers `{site}/?rest_route=/yima/v1/product`, fail-soft) → plugin `Yima_Wc_Rest` (upsert WC par SKU) |

**Anti-boucle** : garde statique `Yima_Wc_Products::suppress()` activée pendant les écritures
programmatiques (pull catalogue, REST entrant) pour ne pas renvoyer le produit à l'expéditeur.
La règle « yima prime / créer-si-absent » casse aussi naturellement les échos.

**Stock temps réel (E1‑E)** : `StockSyncSubscriber` (listener Doctrine onFlush/postFlush) pousse
vers WooCommerce le stock de tout produit dont la quantité change dans yima (vente POS, réception,
inventaire, production, commande en ligne). La quantité est **absolue** (auto-correction de dérive),
fail-soft, no-op sans licence/site. Vérifié en réel : vente POS → stock WooCommerce mis à jour
instantanément.

**Rupture = masquage (E1‑F)** : côté plugin, stock ≤ 0 → produit `outofstock` + **`catalog_visibility=hidden`**
(masqué du catalogue, jamais supprimé) ; stock > 0 → `instock` + `visible`. Appliqué au pull et à la
route REST (`Yima_Wc_Sync::apply_stock`).

**Discussion WhatsApp (E1‑F)** : `Yima_Wc_Whatsapp` — bouton flottant site-wide **contextualisé au
produit** sur une fiche produit, bouton inline (hooks WooCommerce) + **shortcode `[yima_whatsapp]`**
(robuste avec Elementor). Numéro + messages personnalisables (`{produit}`/`{lien}`).

## 11. Boutique en ligne pilotée depuis la PWA (E1‑G)

Le **manager administre son site depuis l'app Ionic** ; l'**admin** garde l'activation/licence.
- Backend : `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`,
  `GET /stats` ventes en ligne). `GET /api/connector/config` (HMAC) fournit les réglages au plugin.
- Plugin : `Yima_Wc_Sync::pull_config` (cron + activation) applique les réglages yima ; la section
  WhatsApp du plugin devient **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 le catalogue**.

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é », stats et resync opérationnels.

## 12. Publication produit, stats, WhatsApp instantané (E1‑H)

- **Publication par produit** : `Product.publishedOnline` (toggle « Visible en ligne » dans la PWA)
  propagé au feed + pusher (`published`) ; le plugin masque un produit **en rupture OU dépublié**.
- **Stats en ligne / boutique / total** : `StoreController::stats` + affichage PWA (comparaison).
- **WhatsApp instantané** : `SiteConfigPusher` pousse les réglages au site (`/wp-json/yima/v1/config`,
  HMAC) dès l'enregistrement dans l'app ; le pull au cron reste le filet de sécurité.

Vérifié en réel : dépublier→masqué / republier→visible ; changement WhatsApp propagé sans pull.

## 13. Catégories dans la synchro (E2‑A)

- API catégories complétée : `GET/{uuid}`, `PUT/{uuid}`, `DELETE/{uuid}` (soft-delete + détache
  produits/sous-catégories). PWA : écran **Catégories** (créer/éditer/supprimer + hiérarchie).
- Sync : feed + `WooCommerceProductPusher` envoient `category {name, slug, parentName?}` ; plugin
  `Yima_Wc_Sync::apply_category` assure le terme `product_cat` (par slug, avec parent) et l'assigne.
- yima autoritaire. Vérifié en réel (terme WooCommerce créé + assigné au produit).

*Premier incrément de la **Phase E2** (catalogue e-commerce enrichi). Suite : SEO/descriptions,
produits à variations, offres 2 paliers, tableau de bord admin plateforme.*

**Vérifié en réel** (site amansa) : création produit yima → apparaît instantanément dans WooCommerce ;
création produit WooCommerce → remonte instantanément dans yima. **Chantier E1 complet.**

## 9. Notes / ADR liés

- **ADR-008** — Licences & connecteur *pull* (révise ADR-007). [ADR-003](../notes/decisions.md) (chiffrement des secrets).
- Réutilise `EncryptionService`, `EntityManagerService`, `FileLogger`, patron admin (`AdminUserController`).
