# Cadrage backend — notifications push POS ciblées

## 1. Objet du document

Ce document sert de base de travail pour concevoir et implémenter un système de notifications push ciblées dans `TTM_API` pour le POS Android/WebView.

L'objectif est de définir une solution :

- cohérente avec l'architecture backend existante ;
- ciblée par `shopUser` et par appareil ;
- non intrusive côté UX ;
- compatible avec les flux temps réel déjà présents dans le POS ;
- maintenable et exploitable en production.

---

## 2. Besoin fonctionnel retenu

### Cas d'usage prioritaire

Le premier cas d'usage retenu est :

> notifier un caissier lorsqu'un produit de sa commande devient prêt, notamment lorsqu'une commande de caisse attend une production.

Exemple concret :

- un caissier A crée une commande ;
- un ou plusieurs produits partent en production ;
- un `orderItem` passe à l'état `READY` ;
- si la commande appartient au caissier A, alors le backend peut déclencher une notification push ciblée pour A.

### Règle UX importante

Les notifications push ne doivent **pas** être utilisées si l'utilisateur est déjà actif dans l'application au premier plan.

Autrement dit :

- **pas de push** si l'utilisateur est déjà connecté et actif dans l'app ;
- **push autorisé** si l'utilisateur est :
  - déconnecté ;
  - hors ligne côté app ;
  - ou si l'application est réduite / en arrière-plan.

Le push sert donc principalement à :

- réveiller l'utilisateur ;
- l'informer lorsqu'il n'est pas déjà en train de voir l'information via le temps réel normal ;
- déclencher ensuite une resynchronisation côté client.

Le push **n'est pas la source de vérité**.

---

## 3. Périmètre et non-objectifs

## Dans le périmètre

- persister les abonnements push des appareils Android ;
- cibler correctement un `shopUser` dans un `shop` donné ;
- gérer le changement d'utilisateur sur un même appareil ;
- éviter le spam si l'utilisateur est déjà actif dans l'app ;
- envoyer un payload minimal de type `data-only` ;
- nettoyer les tokens invalides.

## Hors périmètre initial

- remplacer les événements temps réel WebSocket existants ;
- envoyer des notifications génériques à tout le shop ;
- embarquer un état métier complet dans le payload push ;
- déduire la vérité métier depuis le push.

---

## 4. Constat sur l'existant backend

Les points suivants sont déjà présents et doivent être réutilisés.

### 4.1 Auth POS existante

Le backend POS repose déjà sur :

- `src/middlewares/auth.middleware.ts` avec `protect` ;
- `src/middlewares/shop.middleware.ts` avec `attachShop`.

Constat important :

- le JWT POS est **shop-agnostic** ;
- le `shopId` est résolu via le contexte de requête, notamment `X-Shop-Id` ;
- l'identité applicative côté POS est aujourd'hui `req.auth.sub`, qui correspond à un `ShopUser.id`.

### 4.2 Modèle métier existant à réutiliser

Le modèle `Order` expose déjà notamment :

- `shopId`
- `createdById`
- `stationId`

Le front POS exploite déjà `createdById` pour déterminer si un événement concerne le caissier courant.

### 4.3 Signal métier existant le plus naturel

Le point d'intégration le plus cohérent pour un MVP n'est pas un concept théorique séparé, mais le flux déjà présent autour de :

- `src/modules/orderItem/orderItem.service.ts`
- événements `order_item.status_changed`
- transition `OrderItemStatus.READY`

### 4.4 Scope temps réel déjà injecté dans les payloads

Les payloads temps réel existants embarquent déjà :

- `stationId`
- `cashSessionId`
- `createdById`

via les helpers existants de `orderItem.service.ts`.

Cela confirme que le backend a déjà la matière métier pour cibler un caissier donné.

---

## 5. Décision métier de ciblage

## Décision retenue pour le MVP

Le ciblage principal se base sur :

- `shopId`
- `Order.createdById`
- l'état réel `OrderItem.status === READY`

### Interprétation

Dans l'état actuel du codebase, la meilleure définition de “la commande appartient à un utilisateur” est :

> la commande appartient au caissier qui l'a créée.

Donc :

- lorsqu'un `orderItem` devient `READY` ;
- on retrouve la commande associée ;
- on lit `order.createdById` ;
- on cherche les abonnements push actifs de ce `shopUser` dans ce `shop` ;
- on applique ensuite la règle anti-spam liée à l'état actif / inactif de l'application.

### Pourquoi ce choix

Ce choix est cohérent avec :

- le backend existant ;
- les DTO déjà utilisés ;
- le comportement actuel du front POS pour les sons / événements caissier.

---

## 6. Règle anti-spam / présence utilisateur

## Principe général

Le backend ne doit envoyer une notification push que si l'utilisateur cible n'est pas déjà en train de recevoir l'information de manière active dans l'application.

### Règle retenue

S'il existe au moins une installation active de l'utilisateur cible dans le shop courant et que cette installation est en **foreground**, alors :

> aucun push n'est envoyé.

Dans ce cas, le temps réel normal (WebSocket / refetch / son intégré à l'app) suffit.

### Push autorisé si

Le push reste autorisé si :

- l'utilisateur n'a aucune installation active connue ;
- l'application est en arrière-plan ;
- l'utilisateur n'est plus connecté ;
- l'appareil est connu mais n'est plus au premier plan.

### Stratégie recommandée

La bonne stratégie est **hybride** :

- la WebView / couche Android remonte l'état d'activité de l'application (`foreground` / `background`) ;
- le backend conserve cet état par installation ;
- la présence WebSocket reste un signal secondaire utile, mais pas la source de vérité du statut foreground.

### Conséquence produit

Si un utilisateur est connecté sur plusieurs appareils :

- si au moins un appareil est actif au premier plan, on supprime le push globalement pour cet utilisateur et ce shop ;
- sinon, le push peut partir vers les installations actives éligibles.

Cette règle évite les doublons et les vibrations inutiles quand l'utilisateur travaille déjà dans l'app.

---

## 7. Cycle de vie de l'abonnement push

## 7.1 À la connexion

Un utilisateur s'abonne aux notifications lorsqu'il se connecte.

Le flux attendu est :

1. login POS réussi ;
2. la couche Android / WebView obtient ou confirme le token FCM ;
3. l'app appelle un endpoint backend de type `register` ;
4. le backend associe cette installation à :
   - `shopUserId`
   - `shopId`
   - `terminalId` / identifiant d'installation
   - `fcmToken`

## 7.2 Pendant la session

L'app doit pouvoir remonter :

- le rafraîchissement du token FCM ;
- des heartbeats ;
- l'état `foreground` / `background`.

## 7.3 Au logout

Lors du logout :

- l'installation doit être désactivée pour l'utilisateur courant ;
- elle ne doit plus recevoir de push ciblé pour cet utilisateur.

## 7.4 Si un autre utilisateur se connecte sur le même appareil

C'est un cas explicitement important.

### Règle retenue

Si un autre utilisateur se connecte sur le même appareil, alors :

> le scope de notification doit immédiatement basculer vers le nouvel utilisateur.

Concrètement :

- l'installation existante est réaffectée au nouveau `shopUserId` ;
- l'ancien utilisateur perd immédiatement le droit de recevoir des push sur cet appareil ;
- l'appareil ne doit jamais rester lié simultanément à deux utilisateurs actifs pour le même scope POS.

---

## 8. Proposition de modèle de persistance

## Entité proposée

Créer une entité dédiée de type `PushInstallation` ou `PosPushInstallation`.

### Champs minimaux recommandés

- `id`
- `shopId`
- `shopUserId` nullable
- `terminalId` (identifiant stable de l'installation Android)
- `fcmToken`
- `platform` (`android`)
- `appVersion` nullable
- `isActive`
- `appState` (`foreground` / `background` / `unknown`)
- `lastSeenAt`
- `lastForegroundAt` nullable
- `createdAt`
- `updatedAt`

### Contraintes recommandées

- unicité sur `fcmToken`
- unicité sur le scope d'installation voulu (`terminalId` ou `(shopId, terminalId)` selon décision finale)
- index sur `(shopId, shopUserId, isActive)`
- index sur `lastSeenAt`

### Remarque importante

Le `terminalId` ici représente l'**installation de l'app Android / WebView POS**.

Il ne doit pas être confondu avec :

- `Station` (poste métier)
- `ProviderDevice` (terminal / reader de paiement)
- les `deviceId` des flux de paiement

---

## 9. Endpoints backend à prévoir

## Endpoints minimaux

- `POST /push/register`
- `POST /push/unregister`
- `POST /push/heartbeat`
- `POST /push/app-state` ou intégration équivalente dans `heartbeat`
- `GET /push/status` (optionnel mais utile)

## Règles de sécurité

Ces endpoints doivent être protégés par l'auth existante.

### Règle impérative

Le backend ne doit jamais faire confiance à un `userId` librement envoyé par le client si cette information peut être déduite depuis :

- `req.auth.sub`
- `req.shop.id`

Donc :

- `shopUserId` vient de `req.auth.sub`
- `shopId` vient du contexte de shop attaché

---

## 10. Décision d'envoi côté backend

## Déclencheur MVP retenu

Le déclencheur backend recommandé pour la première version est :

> transition d'un `orderItem` vers l'état `READY`.

### Raison

C'est le signal métier le plus proche du besoin utilisateur réel :

- “un produit est prêt”
- “la caisse / le caissier concerné doit être averti”

### Décision d'envoi

Pour chaque transition pertinente :

1. récupérer l'`orderItem`
2. récupérer la commande liée
3. lire `order.shopId`
4. lire `order.createdById`
5. charger les installations push actives du `shopUser` cible
6. vérifier s'il existe une installation `foreground`
7. si oui : ne pas envoyer de push
8. sinon : envoyer un push `data-only` aux installations éligibles

---

## 11. Payload push recommandé

Le payload doit rester minimal.

Exemple :

```json
{
  "event": "order_item_ready",
  "orderId": "...",
  "orderItemId": "...",
  "shopId": "...",
  "targetShopUserId": "...",
  "terminalId": "..."
}
```

### Règle

Le client doit ensuite refetch l'état réel.

Le payload push ne doit pas devenir un mini backend métier.

---

## 12. Matrice de décision anti-spam

| Situation | Push ? | Raison |
|---|---:|---|
| Utilisateur ciblé actif dans l'app au premier plan | Non | L'information est déjà visible via temps réel normal |
| Utilisateur connecté mais app en arrière-plan | Oui | Le push sert à le réveiller |
| Utilisateur non connecté | Oui si abonnement encore actif et cas d'usage validé | Réveil / reprise possible |
| Appareil réassigné à un autre utilisateur | Non pour l'ancien | Le scope doit suivre le nouvel utilisateur |
| Token FCM invalide | Non | L'installation doit être désactivée / nettoyée |
| Plusieurs appareils, aucun en foreground | Oui | Push vers les installations actives éligibles |
| Plusieurs appareils, au moins un en foreground | Non | Pas de spam |

---

## 13. Cas critiques à traiter explicitement

- changement d'utilisateur sur le même appareil ;
- logout puis login d'un autre utilisateur ;
- refresh du token FCM ;
- installation connue mais plus vue depuis longtemps ;
- token Firebase invalide ;
- commande ou item déjà modifié au moment de la réception côté client ;
- multi-appareils pour un même utilisateur.

---

## 14. Principes d'implémentation recommandés

## Architecture backend

Respecter l'architecture du projet :

- routes -> controllers -> services
- validation via Zod
- Prisma uniquement dans les services
- logique métier d'envoi dans un service dédié

## Service technique dédié

Créer un service de type `PushNotificationService` / `PushFcmService` chargé de :

- initialiser Firebase Admin si configuré ;
- envoyer à un ou plusieurs tokens ;
- gérer les erreurs ;
- nettoyer les tokens invalides ;
- fonctionner en mode dégradé si Firebase n'est pas configuré.

## Service métier dédié

Créer un service métier de type `posPushDispatchService` chargé de :

- appliquer les règles de ciblage ;
- appliquer la règle anti-spam ;
- choisir les installations destinataires ;
- préparer le payload minimal.

---

## 15. Position sur la source d'information "app active ou non"

## Décision recommandée

La source principale doit être un état remonté explicitement par l'application.

Par exemple :

- `foreground`
- `background`
- `unknown`

### Pourquoi ne pas se baser uniquement sur le WebSocket

La présence d'une socket ouverte ne signifie pas forcément :

- que l'utilisateur regarde réellement l'application ;
- que l'app est au premier plan ;
- qu'il faut supprimer la notification.

Donc le WebSocket peut être utilisé comme signal complémentaire, mais **pas comme seule règle anti-spam**.

---

## 16. Questions ouvertes à trancher avant implémentation complète

1. Le `terminalId` doit-il être unique globalement ou seulement dans un `shop` ?
2. Au logout, veut-on :
   - désactiver complètement l'installation,
   - ou garder l'installation active mais sans `shopUserId` ?
3. Souhaite-t-on permettre un push quand l'utilisateur est totalement déconnecté mais que le token du device est encore actif ?
4. Veut-on ajouter un TTL explicite sur les installations trop anciennes ?
5. Faut-il, à moyen terme, distinguer :
   - notifications “produit prêt”
   - notifications “commande totalement prête” ?

---

## 17. Décision produit / technique proposée pour le MVP

### MVP recommandé

- abonnement à la connexion ;
- désabonnement au logout ;
- réaffectation immédiate lors d'un switch d'utilisateur sur le même appareil ;
- push ciblé sur `orderItem -> READY` ;
- ciblage via `shopId + order.createdById` ;
- suppression du push si l'utilisateur a au moins une installation active en foreground ;
- envoi `data-only` avec refetch côté client.

### Résultat attendu

On obtient un système :

- précis ;
- peu spammy ;
- cohérent avec le temps réel déjà existant ;
- compatible avec l'UX Android souhaitée.

---

## 18. Synthèse exécutable

La règle fonctionnelle de référence est donc :

> Un utilisateur s'abonne aux notifications lorsqu'il se connecte. Si un autre utilisateur se connecte sur le même appareil, le scope de notification bascule immédiatement vers ce nouvel utilisateur. Lorsqu'un produit d'une commande du caissier devient prêt, le backend peut envoyer un push ciblé uniquement si ce caissier n'est pas déjà actif dans l'application au premier plan.

C'est cette phrase qui doit servir de règle directrice pour l'implémentation backend.

