# 08 — Portage Flutter (référence de synchronisation)

> **Statut du document :** vivant. Le portage est **en cours** dans `../yima_shop/` (incrément 1 — socle —
> livré le 2026-07-27, voir [§11.1](#111-feuille-de-route-du-portage-flutter)). Ce document décrit **quoi**
> réimplémenter et sert de **contrat de synchronisation** entre les deux clients.
>
> **Règle d'or :** le client **Ionic/Angular (`yima-shop`) est la source de vérité**. Le client Flutter
> n'est qu'une **mise à jour/portage** ultérieure. Toute évolution fonctionnelle est d'abord livrée sur
> Ionic, puis **consignée ici** (section [11. Registre de synchronisation](#11-registre-de-synchronisation))
> pour être répercutée sur Flutter. Aucune divergence de comportement n'est admise sans être notée ici.
>
> Le **backend Symfony (`portail-manager`) et son API v1 sont partagés** par les deux clients : Flutter ne
> réécrit **aucune** logique métier, il consomme la même API. Ce document ne duplique donc pas les règles
> métier (voir `02-modele-de-donnees.md`, `03-securite.md`, `04-api.md`, `modules/`) — il fige le **contrat
> client** que Flutter doit respecter à l'identique.

---

## 1. Objet & principe

- **Un seul backend, deux clients.** L'API REST v1 (`/api/v1`, JWT) est le point de contact unique. Le portage
  Flutter est un **client de plus**, iso-fonctionnel à Ionic.
- **Parité, pas réinvention.** Chaque écran, permission, format et comportement d'Ionic a un équivalent Flutter.
  En cas de doute, **le comportement Ionic fait foi**.
- **Aucune règle métier côté client.** Prix, TVA, remises, soldes, quotas, licences : tout est (re)calculé et
  contrôlé **côté serveur**. Le client n'affiche/soumet que des données ; il ne « décide » de rien de sensible.
- **Sécurité = serveur.** Les permissions RBAC côté client ne servent qu'au **confort d'UI** (masquer/afficher).
  Le backend refait toujours l'autorisation. Ne jamais s'appuyer sur le client pour la sécurité.

## 2. Vue d'ensemble produit

YimaShop : SaaS de **point de vente (POS)** pour le petit commerce ouest-africain (Sénégal en pilote).
Langue **français**, devise **FCFA/XOF** (montants en **centimes**, entiers), fuseau **Africa/Dakar**, identifiants
légaux NINEA/RCCM. Fonctionne **hors-ligne** pour la vente. Multi-boutiques (une boutique = `Company`), chaque
utilisateur est rattaché à une boutique « primaire » qui **scope** toutes les données.

Modules fonctionnels : Vente/Caisse, Catalogue & Stock, Médiathèque, Clients & Créances, Fournisseurs & Dettes,
Atelier/Production, Boutique en ligne (connecteur WooCommerce), Rôles/Employés, Rapports, Réglages.

## 3. Architecture cible Flutter (recommandée)

| Préoccupation | Ionic/Angular (référence) | Équivalent Flutter recommandé |
|---|---|---|
| Langage/UI | Angular 20 standalone + Ionic 8 | Flutter 3.x + Material 3 (thème teal) |
| État réactif | Angular **signals** (`signal`, `computed`) | **Riverpod** (`StateNotifier`/`Notifier`) ou signals |
| Navigation | Angular Router + shell d'onglets par « espace » | **go_router** + `StatefulShellRoute` (une branche par espace) |
| HTTP | `HttpClient` + intercepteur | **dio** + intercepteur (Bearer + refresh 401) |
| Stockage clé/valeur | `@capacitor/preferences` | **shared_preferences** (jetons) / `flutter_secure_storage` si durci |
| Modèles | interfaces TS | classes Dart immuables (**freezed** + `json_serializable`) |
| Partage natif | Web Share API (`navigator.share`) | **share_plus** |
| Contacts (import) | Web Contact Picker | **flutter_contacts** |
| Scan code-barres | caméra Web / `BarcodeDetector` | **mobile_scanner** |
| Notifications/haptique | `@capacitor/haptics` | **haptic_feedback** / plugin natif |
| i18n | textes FR en dur | **flutter_localizations** (locale `fr`), tout FR |

> Le **découplage services ↔ écrans** d'Ionic (un service par domaine dans `core/services/`, des modèles dans
> `core/models/`) doit être reproduit : un **repository/dépôt Dart par domaine** consommant l'API, des écrans qui
> n'appellent que ces dépôts. Cela garde la parité 1-pour-1 avec Ionic et facilite la synchro.

## 4. Contrat backend (API v1)

### 4.1 Base & enveloppe de réponse
- Base : `\<API_BASE\>/v1` (dev Ionic : `http://localhost:8010/api`). Configurable par environnement.
- **Toutes** les réponses suivent l'enveloppe `ApiResponse<T>` :
  ```json
  { "success": true, "message": "…", "data": <T|null>, "meta": { … }, "errors": null }
  ```
  - Listes paginées : `meta.pagination = { page, limit, total, totalPages?, hasMore }`.
  - Agrégats/quotas : dans `meta` (ex. `meta.totalDueCents`, `meta.sms`).
- Codes usuels : `200` OK, `201` créé, `202` **challenge d'auth** (voir 4.2), `400` requête invalide,
  `401` non authentifié, `403` interdit (permission/scope), `404` introuvable, `413` **quota de stockage
  dépassé**, `422` validation, `429` throttling.

### 4.2 Authentification (JWT, flux à challenges)
- `POST /login` `{ email, password }` → `200 { token, refresh_token, user }` **ou** `202` avec un challenge à
  enchaîner **dans l'ordre** : appareil → pays → IP → 2FA.
- Vérifs de challenge : `POST /login/country/verify`, `/login/ip/verify`, `/login/2fa/verify` `{ email, code }`
  (même enveloppe, peut renvoyer un 202 suivant). `POST /login/2fa/resend` `{ email }`.
- `GET /me` → profil complet (`CurrentUser`, dont `permissions[]` et `roles[]`).
- `POST /token/refresh` `{ refresh_token }` → nouveau `token` (+ `refresh_token`).
- `POST /logout` (best-effort ; purge locale quoi qu'il arrive).
- **Stockage session** : `token`, `refresh_token`, `user` (JSON) sous les clés `Preferences` — Flutter : mêmes
  clés logiques via `shared_preferences`.
- **Intercepteur** : ajoute `Authorization: Bearer <token>` ; sur `401`, tente **un** refresh puis rejoue la
  requête ; si le refresh échoue, purge la session et renvoie au login.
- **Restauration au démarrage** : lit le stockage → état optimiste → valide via `/me` ; purge si invalide.
- Un **compte inactif** (inscription en attente de validation admin) est **rejeté au login** (401). Ne pas
  contourner côté client.

### 4.3 RBAC — permissions fines (confort d'UI)
`user.permissions` (depuis `/me`) pilote l'affichage. Helper `can(code)` : si `permissions` absent, ne pas
bloquer un `ROLE_MANAGER`/`ROLE_ADMIN` (session ancienne). Catalogue des codes :

```
boutique.view  boutique.create  boutique.edit  boutique.delete
boutique.manager  boutique.cashier  boutique.employee  boutique.employee.manage
pos.sell  pos.refund  pos.session.open  pos.session.close
catalogue.view  catalogue.manage  stock.view  stock.manage
media.view  media.upload  media.delete
customer.manage  supplier.manage  purchase.manage
production.view  production.manage
integration.view  integration.manage
report.view  report.export
system.admin  system.admin.access
```

### 4.4 Cartographie des endpoints (par domaine)
Base `/api/v1`. (Détails/champs : voir `04-api.md`.)

| Domaine | Endpoints clés |
|---|---|
| Boutique (infos légales) | `GET/PUT /boutique` |
| Catalogue | `GET/POST /products`, `GET/PUT/DELETE /products/{uuid}`, catégories `/categories`, variantes `/products/{uuid}/variants` |
| Images produit | `GET/POST /products/{uuid}/images`, `/attach`, `DELETE …` |
| Médiathèque | `GET/POST /media` (upload massif), `GET /media/{uuid}/raw`, `DELETE /media/{uuid}` |
| Stock | `GET /stock`, ajustements, alertes ; inventaires `/inventory-counts` |
| Ventes (POS) | `GET/POST /sales`, `GET /sales/{uuid}` ; **`Idempotency-Key` obligatoire** en création |
| Caisse | `/cash-sessions` (ouverture, clôture Z, résumé) |
| Avoirs/Remboursements | `/credit-notes` (`pos.refund`) |
| Clients & Créances | `/customers`, `/customers/{uuid}/ledger`, `/customers/debtors`, `/customers/{uuid}/payments`, **`POST /customers/{uuid}/debts`** (créance manuelle), **`GET /customers/reminders`**, **`POST /customers/reminders/send`**, `POST /customers/import` |
| Fournisseurs & Dettes | `/suppliers`, dettes, achats `/purchase-orders` |
| Production | `/production-orders`, recettes `/recipes` |
| Boutique en ligne | `GET /store` (settings + connector + **`usage`** + **`referral`**), `PUT /store/whatsapp`, `POST /store/resync`, `GET /store/stats` |
| Employés & Rôles | `/employees`, `/roles` |
| Rapports | `/reports` (`report.view` / `report.export`) |
| Facturation | `/invoices` |
| Notifications | `/notifications` |

## 5. Modèles de données (conventions client)

- **Argent = `int` centimes** (jamais de flottant). Formatage FCFA : séparateur de milliers **espace**, sans
  décimale (ex. `1 500 000` → « 1 500 000 FCFA »). `toCents(francs)` = `francs*100` à la saisie.
- **Identité API = `uuid`** (UUID v7) pour toutes les ressources exposées ; jamais l'`id` interne.
- **Dates** en ISO 8601 (`DATE_ATOM`) ; affichage localisé `fr` / Africa/Dakar.
- **Booléens d'état** explicites (`isActive`, `siteEnabled`, `unlimited`).
- Reproduire fidèlement les modèles de `yima-shop/src/app/core/models/*.model.ts` (un `*.dart` par fichier).
  Points sensibles à ne pas déformer :
  - `Customer.balanceCents` (> 0 = créance), `CustomerLedgerEntry.type` ∈ {`debit`,`credit`}, `.saleReference`.
  - `Sale`: `status` ∈ {`paid`,`pending`,`cancelled`,`refunded`,`partially_refunded`}, `totalCents`, `dueCents`.
  - `StoreOverview`: `{ settings, connector, usage, referral }` — `usage.storage{usedBytes,quotaBytes,unlimited}`,
    `usage.sms{quota,used,remaining,unlimited}`, `referral{code,shareUrl,usedCount,maxUses,freeDays}`,
    `connector.offer` ∈ {`pos`,`ecommerce`,`premium`} + `offerLabel`.

## 6. Navigation — « espaces » et onglets

Une **barre d'onglets par espace**, construite **dynamiquement** depuis un registre filtré par permissions
(`APP_MODULES` dans `core/navigation/modules.config.ts`). Un **sélecteur d'espace** dans le header permet de
basculer d'espace ; l'espace courant est **coloré** (état actif). Registre à reproduire tel quel :

| Espace (route) | Visible si (une des perms) | Onglets (`route` · perm) |
|---|---|---|
| **Vente** (`vente`) | `pos.sell`, `pos.session.open`, `report.view` | Vente (`pos`·`pos.sell`) · Caisse (`caisse`·`pos.session.open`) · Journal (`journal`·`report.view`) |
| **Boutique** (`boutique`) | `catalogue.view`, `stock.view`, `media.view`, `integration.view` | Produits (`produits`·`catalogue.view`) · Catégories (`categories`·`catalogue.view`) · Stock (`stock`·`stock.view`) · Médias (`mediatheque`·`media.view`) · Site (`site`·`integration.view`) |
| **Atelier** (`atelier`) | `production.view` | Production (`production`·`production.view`) |
| **Financier** (`financier`) | `report.view`, `customer.manage`, `supplier.manage` | Trésorerie (`tresorerie`·`report.view`) · Créances (`creances`·`customer.manage`) · Dettes (`dettes`·`supplier.manage`) |
| **Réglages** (`reglages`) | *(toujours)* | Ma boutique (`boutique`) · Employés (`employes`·`boutique.employee.manage`) · Rôles (`roles`·`boutique.employee.manage`) · Alertes (`notifications`) |

Règles : un **espace** est masqué si aucune de ses perms n'est détenue ; un **onglet** sans perm est toujours
visible, sinon gaté. Chaque page a un **bouton (i)** ouvrant une **modale d'aide** contextuelle (clé `infoKey`).
Actions principales en **FAB** (boutons flottants).

## 7. Comportements transverses (à répliquer à l'identique)

1. **Offline-first des ventes.** Une vente créée hors-ligne (ou sur échec réseau, `status 0`) est **mise en file
   locale** (`Preferences`, clé `yima.sale_queue`) avec sa **`Idempotency-Key`** ; un compteur `pending` est
   exposé ; à la reconnexion, **flush FIFO** rejouant chaque vente (le backend déduplique via l'idempotency key).
   Flutter : file persistée (`shared_preferences`/SQLite) + rejeu FIFO ; **même en-tête d'idempotence**.
2. **Chargement d'images authentifié (blob).** Les binaires (`/media/{uuid}/raw`, images produit) exigent le
   Bearer → charger en **blob avec le token** puis afficher (directive `appAuthImg` côté Ionic). Flutter : widget
   image avec en-têtes d'auth (`Image.network` + `headers`, ou `dio` → mémoire).
3. **Partage.** Réutiliser un service de partage unique : **partage natif → WhatsApp (`wa.me`) → copie
   presse-papier** (tickets de caisse, **code de parrainage**). Flutter : `share_plus` + repli `wa.me` + clipboard.
4. **Quotas & blocages (affichage).** Le serveur bloque (SMS de rappel au-delà du plafond mensuel → message ;
   upload média au-delà du stockage → **413**). Le client **affiche** l'usage (widget « Mon offre » : barre de
   stockage utilisé/quota, SMS du mois utilisés/restants) et **relaie les messages d'erreur** sans les réinventer.
5. **Rappels de créance = manuels.** Pas de tâche planifiée : le manager déclenche l'envoi depuis l'écran
   Créances (« Envoyer les rappels »), soumis au quota. À répliquer tel quel (pas de cron côté client).
6. **Import de contacts** (Clients) : sélecteur natif → dédoublonnage **par téléphone côté serveur**.
7. **Scan code-barres** à l'encaissement (caméra) pour retrouver un produit.
8. **Thème** : marque **teal** (`#0d9488`), clair/sombre.

## 8. Boutique en ligne & offres (rappel client)

- Offres : `pos` (App seule), `ecommerce` (App + Site), `premium` (illimité, **sans site**). L'offre effective
  vient de `GET /store` (`connector.offer` + `offerLabel`) ; ne **jamais** la déduire côté client.
- L'écran « Site » n'active ses actions (WhatsApp, resync) que si `connector.siteEnabled` (offre e-commerce).
- **Widget « Mon offre »** (usage stockage + SMS) et **carte Parrainage** (code + bouton Partager) : lus depuis
  `GET /store` (`usage`, `referral.shareUrl` = lien absolu `/register?ref=CODE`). **Placement : onglet
  « Ma boutique » (espace Réglages, toujours accessible)** — surtout pas l'onglet « Site » qui est gaté par
  `integration.view` et invisible aux boutiques App seule.

## 9. Ce qui reste côté portail web (hors app cliente)

Non porté dans le client Flutter (ce sont des écrans **web Symfony**, admin ou public) : landing publique,
inscription manager + validation admin, réglages plateforme, gestion des licences/parrainage/inscriptions,
supervision admin. Le client (Ionic **ou** Flutter) ne fait que **consommer** les licences/offres activées.

## 10. Procédure de synchronisation (Ionic ⇄ Flutter)

1. Toute évolution est **d'abord** conçue et livrée sur **Ionic** (source de vérité) + son API.
2. La livraison est notée dans `07-suivi.md` **et** une ligne est ajoutée au **registre ci-dessous** (§11) avec :
   date, résumé, **impact client** (nouveau champ API ? nouvel écran ? nouveau comportement ?), et l'état Flutter
   (`à porter` / `porté` / `n/a`).
3. Au moment du (re)développement Flutter, ce registre est la **liste de travail** : tout `à porter` doit être
   traité pour atteindre la parité, puis passé à `porté`.
4. Un changement **d'API partagée** (nouveau champ/endpoint) est rétro-compatible : Ionic et Flutter tolèrent les
   champs inconnus/absents. Ne jamais casser l'enveloppe `ApiResponse`.

## 11. Registre de synchronisation

> Ajouter une ligne **à chaque évolution cliente**. `Impact client` décrit ce que Flutter devra reproduire.
> `Flutter` : `à porter` (défaut au moment de l'écriture) · `porté` · `n/a` (web/admin uniquement).

| Date | Évolution (Ionic / API) | Impact client (à répliquer) | Flutter |
|---|---|---|---|
| ≤ 2026-07 | **Socle technique** : enveloppe `ApiResponse`, intercepteur JWT (Bearer + refresh dédoublonné), auth à challenges (appareil/pays/IP/2FA), restauration de session, RBAC `can()`, espaces/onglets dynamiques par permission, aide contextuelle `PAGE_INFO`, thème teal, formats FCFA / Africa/Dakar | §4.2, §4.3, §5, §6, §7.8 | **porté** (inc. 1) |
| ≤ 2026-07 | **Écrans Réglages** : ma boutique (compte + infos légales), employés, rôles & permissions, notifications | 4 écrans de l'espace `reglages` | **porté** (inc. 2) |
| ≤ 2026-07 | **Écrans Boutique** : catalogue (+ photos, réappro), catégories, variantes, stock/inventaire, alertes de stock, médiathèque (import massif) | 6 écrans + chargement authentifié des binaires (`AuthImage`) | **porté** (inc. 3) |
| ≤ 2026-07 | **Écrans Vente** : POS, caisse (Z), journal (avoirs, factures, exports) | 3 écrans + leurs feuilles (variante, remise, client, encaissement, avoir) | **porté** (inc. 4) |
| ≤ 2026-07 | **Écrans Financier** : trésorerie (dépenses, rapport PDF), créances clients, dettes fournisseurs | 3 écrans + comptes client/fournisseur | **porté** (inc. 5) |
| ≤ 2026-07 | **Écrans métiers restants** : production (recettes, ordres de fabrication), boutique en ligne (WhatsApp/resync/stats) | Les 2 écrans restants et leurs modales (voir §6) | à porter |
| ≤ 2026-07 | **Offline-first ventes** (file `yima.sale_queue` + Idempotency-Key + flush FIFO) | Répliquer la file locale + rejeu idempotent | **porté** (inc. 4) |
| ≤ 2026-07 | **Scan code-barres**, **partage tickets** (natif→WhatsApp→copie) | mobile_scanner / share_plus | **porté** (inc. 4) |
| ≤ 2026-07 | **Import contacts** | flutter_contacts | **porté** (inc. 5) |
| 2026-07-27 | **Créances** : `POST /customers/{uuid}/debts` (créance manuelle, `saleUuid`), `GET /customers/reminders`, `POST /customers/reminders/send` | Écran Créances : ajout manuel (sélecteur client) + bannière quota SMS + « Envoyer les rappels » (quota) | **porté** (inc. 5) |
| 2026-07-27 | **Quotas par offre** + 3ᵉ offre `premium` : `GET /store` renvoie `usage{storage,sms}` et `connector.offer/offerLabel` | Widget « Mon offre » (barre stockage, SMS mois) ; gérer 3 offres ; relayer le **413** stockage et le message quota SMS | à porter |
| 2026-07-27 | **Partage parrainage** : `GET /store` renvoie `referral{code,shareUrl,…}` | Carte Parrainage + bouton Partager (service de partage) | à porter |
| 2026-07-27 | **Placement usage/parrainage** : widgets « Mon offre » + Parrainage déplacés de l'onglet « Site » vers **« Ma boutique »** (Réglages, toujours accessible) | Placer ces widgets sur l'écran « Ma boutique », pas sur « Site » | **porté** (inc. 2) |
| 2026-07-28 | **Abonnement (lecture seule)** : `GET /api/v1/subscription` (statut offre/licence/échéance + historique paiements + total, gaté `report.view`) ; paiements enregistrés côté admin uniquement | Onglet **« Abonnement »** (espace Réglages) : statut coloré selon l'échéance (danger ≤ 7 j) + historique lecture seule ; aucune saisie côté manager | à porter |

### 11.1 Feuille de route du portage Flutter

Le client Flutter vit dans `../yima_shop/`. Il est construit par incréments ; chaque
incrément solde une partie du registre ci-dessus.

| # | Incrément | État |
|---|---|---|
| 1 | **Socle** — réseau + intercepteur JWT, 20 modèles, auth à challenges, navigation par espaces, thème, formats | ✅ livré (2026-07-27) |
| 2 | **Réglages** — Ma boutique (+ widgets « Mon offre » et Parrainage), Employés, Rôles, Alertes | ✅ livré (2026-07-27) |
| 3 | **Boutique** — Produits, Catégories, Stock/Inventaire, Alertes de stock, Médiathèque, Variantes | ✅ livré (2026-07-27) |
| 4 | **Vente** — POS, file hors-ligne, scan, Caisse (Z), Journal (avoirs, factures, exports) | ✅ livré (2026-07-27) |
| 5 | **Financier** — Trésorerie, Créances (créance manuelle, rappels SMS, import contacts), Dettes | ✅ livré (2026-07-27) |
| 6 | **Atelier & Site** — Production, boutique en ligne | ⬜ |
| 7 | **Finitions & livraison** — mode sombre, erreurs, icônes, signature, tests | ⬜ |

**Contrainte de build (résolue, à surveiller).** `path_provider_android` 2.3.x
dépend de `jni`, dont le `build.gradle` appelle `kotlin { }` sans appliquer le
plugin Kotlin : la build échoue avec l'AGP 9.0.1 du projet Flutter, et basculer
`android.builtInKotlin` à `true` ne fait que déplacer l'échec sur
`connectivity_plus`. Le projet épingle donc `path_provider_android` en 2.2.x via
`dependency_overrides` (incrément 4), ce qui rétablit `share_plus`,
`path_provider` et `open_filex` — et avec eux le palier **natif** de la cascade
de partage. **Retirer cet épinglage quand `jni` sera compatible**, pas avant.

---

*Fichiers de référence à ouvrir côté Ionic lors du portage :* `src/environments/`, `core/services/*.service.ts`,
`core/models/*.model.ts`, `core/navigation/modules.config.ts`, `core/interceptors/auth.interceptor.ts`,
`core/directives/auth-image.directive.ts`, `core/services/sales-sync.service.ts`, `features/*`.
