# Module — Caisse & paiements

> Sessions de caisse (ouverture avec fond, fermeture avec comptage et écarts), moyens de paiement (espèces, mobile money Wave/Orange Money, carte), Z de caisse. Écritures **append-only** (ADR-006). Fiche conforme au [canevas des modules](README.md).

## 0. État d'implémentation — incrément 1 (2026-07-24) 🚧

Sessions de caisse **livrées et testées** : ouverture (fond), rattachement automatique des
ventes, clôture Z (attendu vs compté → écart). Écarts assumés vs la cible :

| Livré ✅ | Cible non encore faite ⬜ |
|---|---|
| `CashSession` (append-only : fond, `openedAt`/`closedAt`, statut, compté/attendu/écart, caissier) | multi-caisses simultanées, reprise, X (lecture intermédiaire) |
| `Sale.cashSession` : la vente est rattachée à la session ouverte à l'encaissement | contrôle « caisse fermée ⇒ vente bloquée » (optionnel) |
| `POST /api/v1/cash-sessions/open` (fond), `GET /current` (totaux live), `POST /{uuid}/close` (compté ⇒ attendu = fond + espèces, écart), `GET /{uuid}/z-report` (**PDF dompdf** : fond, ventes, espèces/autres, attendu/compté/écart) | numérotation de session, historique |
| Moyens de paiement **sélectionnables à la PWA** : espèces / mobile money / carte (attendu caisse calculé sur les paiements `cash` uniquement) | intégration réelle mobile money (Wave/OM) & carte, ventilation Z par moyen, paiements multiples (split) |
| Gating : membre de la boutique (`CompanyVoter::VIEW`) | permission dédiée `pos.session.open/close` (bascule RBAC phase 2) |
| Écran **PWA Caisse** : ouverture (fond), session courante (ventes, espèces, attendu), clôture Z (prompt compté → écart coloré), **bouton Rapport Z (PDF)** | historique des sessions |
| Test `tests/Functional/Api/CashSessionApiTest.php` (6 tests : open/current/close, rattachement, écart) | tests multi-paiements |

**Note** : l'attendu ne couvre que les espèces (moyen `cash`) ; les autres moyens sont hors du
fond de caisse. La numérotation de session et le rapport Z formel restent à enrichir.
Fichiers : `src/Entity/CashSession.php`, `src/Controller/Api/V1/CashSessionController.php`,
`SaleRepository::sessionSummary()`, `yima-shop/src/app/features/cash/`.

La suite de la fiche décrit la **cible complète**.

## 1. Objectif

Encadrer l'argent physique et les encaissements d'un point de vente : ouvrir une caisse avec un fond, rattacher chaque paiement à une session, clôturer avec comptage et calcul d'écart, produire le Z de caisse.

- **Employé / caissier (PWA)** : ouvre sa session (`pos.session.open`), encaisse les paiements des ventes, clôture avec comptage (`pos.session.close`).
- **Manager (portail web)** : supervise les sessions de sa boutique, consulte les Z, détecte les écarts, contrôle les moyens de paiement (`report.view`).

## 2. Périmètre

**In (v1)**
- `CashSession` : ouverture (fond de caisse `openingFloat`), état `open`, fermeture (comptage `countedCash`, `closingExpected`, `cashDiscrepancy`).
- `Payment` : rattaché à une vente **et** à la session ouverte ; moyens `cash`, `mobile_money` (Wave, Orange Money), `card`.
- Paiements **multiples** par vente (split) ; total encaissé agrégé côté vente.
- Z de caisse : agrégats par moyen, nombre de ventes, total encaissé, écart espèces.
- Une vente n'est confirmée que si une session est **ouverte** pour le caissier.

**Out (v1) → évolutions**
- Intégration API temps réel des agrégateurs mobile money (v1 = saisie/référence manuelle du reçu, statut `captured` déclaratif).
- Terminaux de paiement carte (TPE) connectés.
- Multi-caisses/tiroirs simultanés par caissier.
- Rapprochement bancaire automatisé.

## 3. Entités

Rattachées à `Company`, `TimestampableEntity`, `uuid` public. **Pas de soft-delete** (append-only, ADR-006). Montants en **centimes XOF**.

| Entité | Champs clés | Relations |
|---|---|---|
| `CashSession` | `uuid`, `status` (`open`/`closed`), `openingFloat`, `openedAt`, `closedAt`, `expectedCash`, `countedCash`, `cashDiscrepancy`, `salesCount`, `totalsByMethod` (JSON agrégé) | `Company (1)`, `User` caissier (1), `Sale (N)`, `Payment (N)` |
| `Payment` | `uuid`, `method` (`cash`/`mobile_money`/`card`), `provider` (`wave`/`orange_money`/null), `amount`, `reference` (reçu/transaction), `status` (`captured`/`voided`), `paidAt` | `Sale (N)`, `CashSession (1)`, `Company (1)` |

- `CashSession (1) ── (N) Payment` ; `CashSession (1) ── (N) Sale`.
- `Payment (N) ── (1) Sale` ; `Payment (N) ── (1) CashSession`.
- Un seul `CashSession` `open` par `(User, Company)` à la fois.

## 4. Permissions

| Code | Action | Rôles v1 |
|---|---|---|
| `pos.session.open` *(existant)* | Ouvrir une session de caisse | Employé, Manager |
| `pos.session.close` *(existant)* | Clôturer + comptage | Employé, Manager |
| `pos.sell` *(existant)* | Enregistrer un paiement sur une vente | Employé, Manager |
| `report.view` *(existant)* | Consulter sessions / Z de caisse | Manager |

Gating : `denyAccessUnlessGranted('pos.session.open', $company)` ; templates `{% if is_granted('report.view', company) %}`.

## 5. API

Base `/api/v1`, Bearer, enveloppe standard, scope boutique imposé serveur. Créations : `Idempotency-Key`.

| Méthode | Route | Corps attendu | Permission |
|---|---|---|---|
| POST | `/api/v1/cash-sessions/open` | `{ uuid, openingFloat }` | `pos.session.open` |
| GET | `/api/v1/cash-sessions/current` | — (session ouverte du caissier) | `pos.sell` |
| POST | `/api/v1/cash-sessions/{uuid}/close` | `{ countedCash }` → écart calculé | `pos.session.close` |
| GET | `/api/v1/cash-sessions/{uuid}/z` | — → Z de caisse (agrégats) | `report.view` |
| POST | `/api/v1/sales/{uuid}/payments` | `{ uuid, method, provider?, amount, reference? }` | `pos.sell` |

- 409 si tentative d'ouverture alors qu'une session est déjà `open` ; 422 si paiement sur session `closed` ou montant ≤ 0.

## 6. Écrans

**Web (Twig — Manager)**
- Liste des sessions : caissier, ouverture/fermeture, total encaissé, **écart** (badge rouge si ≠ 0).
- Détail session + Z : agrégats par moyen (espèces / Wave / Orange Money / carte), liste des ventes et paiements.

**PWA (caissier)**
- Écran d'ouverture : saisie du fond de caisse.
- Bandeau « caisse ouverte » persistant durant l'encaissement.
- Pavé paiement (dans le flux de vente) : choix moyen, montant, référence mobile money, split.
- Écran de clôture : comptage espèces → affichage écart → confirmation → Z de caisse.

## 7. Règles métier

- **Une session ouverte par caissier/boutique** ; toute vente confirmée s'y rattache (sinon 422 côté [POS](pos-ventes.md)).
- **Fond de caisse** : `openingFloat` figé à l'ouverture.
- **Attendu espèces** : `expectedCash = openingFloat + Σ Payment(cash, captured)`.
- **Écart** : `cashDiscrepancy = countedCash - expectedCash` (peut être négatif) ; calculé à la fermeture, **immuable** ensuite.
- **Agrégats Z** : `totalsByMethod[method] = Σ amount` par moyen ; `salesCount` = ventes rattachées.
- **Transitions** : `open → closed` (irréversible) ; réouverture interdite → nouvelle session.
- **Paiements append-only** : une erreur se corrige par un `Payment` `voided` ou un avoir côté vente, jamais par suppression (ADR-006).
- **Mobile money v1** : statut `captured` déclaratif + `reference` obligatoire (traçabilité) ; capture temps réel = évolution.
- Montants en **centimes XOF**, fuseau `Africa/Dakar`.

## 8. Projection e-commerce

- **Non répliqué** : `CashSession`, `Payment` restent **centraux** (données financières sensibles, ADR-001) — jamais exposés à la base client ni au site.
- Les paiements en ligne du site e-commerce suivent un flux distinct (remonté au centre comme ventes canal `online`), hors périmètre v1.

## 9. Tests

**Unitaires**
- `expectedCash` avec paiements mixtes ; écart positif/négatif/nul.
- Agrégation `totalsByMethod` (espèces + Wave + Orange Money + carte).
- Invariant « une seule session ouverte » (ouverture concurrente rejetée).

**Fonctionnels**
- `open` puis `open` de nouveau → 409.
- `payments` sur session fermée → 422.
- `close` avec `countedCash` → écart correct + Z cohérent.
- Rejeu `Idempotency-Key` sur `open` → pas de doublon.

**Accès**
- Employé sans `pos.session.close` → 403 sur close.
- Manager boutique A consultant le Z d'une session boutique B → 403.

## 10. Dépendances

- [POS / Ventes](pos-ventes.md) — les paiements se rattachent aux ventes ; la confirmation de vente exige une session ouverte.
- Socle : RBAC/`PermissionVoter`, `EntityManagerService`, `FileLogger`, `ApiResponseFormatter`, JWT (firewall `api`), `EncryptionService` (si stockage futur de clés d'API paiement — jamais en clair).

## 11. Notes / ADR liés

- Hub : [../README.md](../README.md) · Modèle : [../02-modele-de-donnees.md](../02-modele-de-donnees.md) · Sécurité : [../03-securite.md](../03-securite.md) · API : [../04-api.md](../04-api.md) · Catalogue : [README.md](README.md).
- ADR : [../notes/decisions.md](../notes/decisions.md) — **ADR-004** (centimes XOF), **ADR-006** (append-only paiements), **ADR-002** (RBAC), **ADR-003** (chiffrement des futures clés paiement), **ADR-001** (central autoritaire).
