# Patch Spec Frontend ESTAIR — Selling GDS Amadeus

> **Document** : Patch à intégrer dans `ESTAIR_FRONTEND_SPEC_COMPLET.md`
> **Périmètre** : Modules PNR, Presales (vue contextuelle), États de Vente, Accounts (rappel)
> **Version** : 1.1 — 10/05/2026
> **Auteur** : Nicephore / ESTAIR Connect
>
> **Changelog v1.1** :
> - Statuts PNR alignés sur les valeurs métier existantes (En attente, À émettre, En cours, Émission partielle, Émis)
> - Mise à jour temps réel via **WebSocket** (et non polling)
> - Permissions des CTAs (Void, Refund, Execute) **gérées dynamiquement** via Admin CHAPS et metadata, non figées dans le frontend

---

## Sommaire

1. [Module PNR](#1-module-pnr)
   - 1.1 [Vue Liste](#11-vue-liste)
   - 1.2 [Vue Détail](#12-vue-détail)
   - 1.3 [Vue contextuelle Presales (dans détail PNR)](#13-vue-contextuelle-presales)
   - 1.4 [Triggers automatiques](#14-triggers-automatiques)
2. [Module États de Vente](#2-module-états-de-vente)
3. [Module Accounts (rappel)](#3-module-accounts-rappel)
4. [Composants transverses](#4-composants-transverses)
   - 4.1 [Pop-up de confirmation Execute](#41-pop-up-de-confirmation-execute)
   - 4.2 [Visualisation Conditions tarifaires (fare_basis)](#42-visualisation-conditions-tarifaires)
5. [Logique métier et règles transverses](#5-logique-métier-et-règles-transverses)

---

## 1. Module PNR

### 1.1 Vue Liste

#### Bouton "Nouveau"

**Position** : barre d'actions en haut de la liste PNR (CTA primaire).

**Comportement** :
- Ouvre une modal **"Charger un PNR"**
- Champ unique : numéro PNR (record locator, 6 caractères alphanumériques)
- Validation côté front :
  - Longueur exacte : 6 caractères
  - Pattern : `^[A-Z0-9]{6}$`
  - Conversion automatique en majuscules à la saisie
- Boutons : `Annuler` / `Charger`

**Action sur "Charger"** :
1. Appel API : `POST /api/pnrs/load` avec body `{ pnr: "ABC123" }`
2. Backend déclenche un message vers Messager → Octopus → Amadeus pour un Load PNR (graphique)
3. Le PNR est créé immédiatement côté ESTAIR avec :
   - `account_uuid` = agence demandeuse (de l'utilisateur connecté)
   - `status` = `En attente`
   - Aucun contenu visible (segments, passagers, presales = vides)
4. Affichage d'un toast : `PNR ABC123 en cours de chargement…`
5. La modal se ferme et la liste se rafraîchit

**À la réception du fichier AIR Amadeus** (asynchrone) :
- Le PNR est mis à jour avec les données réelles
- L'`account_uuid` est **réécrit** selon l'office ID retourné par Amadeus (voir [§5 - Logique métier](#5-logique-métier-et-règles-transverses))
- Le `status` passe à `À émettre`
- Notification temps réel envoyée via WebSocket à l'utilisateur (et au panneau de la liste si ouvert)

#### Statuts PNR — référentiel complet

Les statuts métier d'un PNR sont définis dans CHAPS et synchronisés via metadata. Les 5 statuts du référentiel actuel :

| Statut | Sens | Couleur badge | Lignes grisées |
|--------|------|---------------|----------------|
| `En attente` | PNR créé via Load PNR, en attente du fichier AIR Amadeus. Contenu vide. | Orange | Oui |
| `À émettre` | PNR chargé avec données, presales en place, **aucun** presale émis | Bleu | Non |
| `En cours` | Exécution en cours (Execute lancé, attente retours Amadeus) | Bleu clair (animé) | Non |
| `Émission partielle` | Certains presales émis, d'autres en échec ou pending | Jaune | Non |
| `Émis` | Tous les presales émis (au moins un état de vente créé) | Vert | Non |

**Affichage colonne `Status` dans la liste** :
- Badge `VChip` avec couleur correspondante
- Label en français tel que défini ci-dessus

**Cas spécifique `En attente`** :
- Lignes grisées (opacité 60%)
- Aucune action contextuelle disponible (pas de menu kebab, pas de CTA)
- Le clic sur la ligne ouvre la vue détail mais affiche un état vide avec message :
  > *Ce PNR est en cours de chargement. Les informations seront disponibles dès la réception du fichier AIR Amadeus.*

#### Mise à jour temps réel — WebSocket

**Mécanisme** : abonnement WebSocket par utilisateur (canal authentifié via Keycloak token).

**Canaux** :
- Canal privé utilisateur : `private-user.{user_id}` — événements globaux (notifications)
- Canal privé agence : `private-account.{account_uuid}` — événements PNR de l'agence (changements de statut, nouveaux PNR chargés)

**Événements écoutés** :

| Événement | Payload | Action UI |
|-----------|---------|-----------|
| `pnr.status.updated` | `{ pnr_id, old_status, new_status }` | Mise à jour du badge sans rechargement complet de la liste |
| `pnr.air.received` | `{ pnr_id, account_uuid }` | Mise à jour du PNR dans la liste (statut + contenu) ; toast si l'utilisateur est sur la liste ou le détail de ce PNR |
| `pnr.account.reassigned` | `{ pnr_id, old_account_uuid, new_account_uuid }` | Si le PNR a été réaffecté à une autre agence, retrait silencieux de la liste de l'utilisateur d'origine |
| `presale.fare_basis.created` | `{ presale_id, pnr_id }` | Mise à jour de l'indicateur fare_basis (⏳ → ✓) sans rechargement |
| `edv.created` | `{ edv_id, pnr_id }` | Rafraîchissement de l'onglet États de Vente du détail PNR si ouvert |
| `pnr.execute.completed` | `{ pnr_id, success, items_results }` | Mise à jour du statut PNR + toasts par presale |

**Implémentation côté Nuxt 3** :
- Plugin `~/plugins/websocket.client.ts` (suffixe `.client.ts` obligatoire pour éviter SSR crash, cohérent avec le pattern Keycloak)
- Connexion établie après authentification réussie (Keycloak token disponible)
- Reconnexion automatique en cas de perte de connexion
- Nettoyage des abonnements à la déconnexion

**Fallback** : en cas d'indisponibilité WebSocket (firewall, réseau), polling automatique 60s comme solution de secours, **uniquement** pour les PNR en `En attente` ou `En cours` visibles à l'écran.

---

### 1.2 Vue Détail

#### Structure générale

La vue détail PNR utilise le pattern **Vuexy detail panel with grow tabs** déjà défini dans la spec principale. Elle comprend :

- **En-tête** : numéro PNR, statut, agence, date de création, dernière mise à jour
- **Onglets (grow tabs)** :
  - Vue d'ensemble (segments, passagers, contacts)
  - Presales (vue contextuelle) ← cible de cette section
  - États de vente
  - Documents
  - Historique
- **Zone d'actions principales** (en haut à droite) : CTA `Execute` + autres actions (selon contexte)

#### CTA "Execute" (mode global)

**Position** : zone d'actions principales du détail PNR (CTA primaire).

**Visibilité** :
- Visible uniquement si `pnr.status` ∈ `{ "À émettre", "Émission partielle" }`
- Caché si `pnr.status` ∈ `{ "En attente", "En cours", "Émis" }`
- En `Émission partielle`, le bouton n'agit que sur les presales encore non émis (filtrage automatique côté backend)

**Permissions** :
- Récupérées dynamiquement via les **metadata CTA** (configurées dans Admin CHAPS, voir [§5.5](#55-permissions-via-cta-metadata))
- Vérification finale côté backend à chaque appel

**Comportement au clic** :
1. Récupération de **tous les presales** liés au PNR avec statut exécutable
2. Construction de la syntaxe cryptique Amadeus pour chaque presale (côté backend)
3. Ouverture de la **pop-up de confirmation Execute** (voir [§4.1](#41-pop-up-de-confirmation-execute)) qui affiche la commande complète
4. Sur confirmation, exécution séquentielle des commandes via Messager
5. Toast de retour : succès / erreur par presale

#### Mode sélectif

L'exécution sélective s'effectue depuis la **vue contextuelle Presales** dans l'onglet Presales du détail PNR (voir [§1.3](#13-vue-contextuelle-presales)). **Pas d'exécution sélective depuis cette zone d'actions principales.**

---

### 1.3 Vue contextuelle Presales

**Localisation** : onglet `Presales` de la vue détail PNR.

#### Structure

Liste tabulaire des presales du PNR uniquement, avec :
- Checkboxes par ligne (sélection multiple)
- Colonnes : type (TST/EMD), passager, route, montant, statut, fare_basis présent (oui/non), actions ligne
- Pagination intégrée si > 25 lignes

#### Sélection multiple

**Comportement** :
- Checkbox dans l'en-tête : sélectionne/désélectionne toute la page
- Checkbox par ligne : sélection individuelle
- **Barre d'actions contextuelle** (sticky en haut de la liste) :
  - Apparaît dès qu'**au moins un presale est sélectionné**
  - Affiche le compteur : `3 presale(s) sélectionné(s)`
  - Boutons : `Exécuter la sélection` / `Désélectionner tout`
  - Disparaît quand la sélection retombe à 0

**Règle métier critique** : la sélection est limitée au PNR courant. Aucune sélection cross-PNR possible (par construction de la vue).

#### Bouton "Exécuter la sélection"

**Comportement** :
1. Vérification côté front : au moins un presale sélectionné
2. **Vérification dépendance EMD ↔ TST** :
   - Si un EMD sélectionné dépend d'un TST non sélectionné → afficher un avertissement bloquant :
     > *L'EMD #X dépend du TST #Y qui doit être exécuté en même temps. Voulez-vous l'ajouter à la sélection ?*
   - Boutons : `Ajouter le TST` (force l'inclusion) / `Annuler`
3. Construction de la commande cryptique Amadeus pour la sélection
4. Ouverture de la pop-up de confirmation Execute (voir [§4.1](#41-pop-up-de-confirmation-execute))
5. Sur confirmation, exécution via Messager

#### Indicateur fare_basis

Colonne `Conditions tarifaires` dans la liste presales :
- Icône verte ✓ si `fare_basis` existe sur ce presale
- Icône grise ⏳ si `fare_basis` absent (Read PNR pas encore exécuté ou en cours)
- Clic sur la cellule → ouvre la modal **Conditions tarifaires** (voir [§4.2](#42-visualisation-conditions-tarifaires))

---

### 1.4 Triggers automatiques

Ces actions sont déclenchées **côté backend** via Laravel Observers. Le frontend n'intervient pas dans le déclenchement, mais doit refléter visuellement les changements de statut.

| Événement | Trigger backend | Reflet UI |
|-----------|-----------------|-----------|
| PNR créé via Load PNR | Aucun (attente fichier AIR) | Statut `En attente`, lignes grisées |
| PNR mis à jour via fichier AIR | Réécriture `account_uuid` selon office ID | Statut → `À émettre`, données visibles, événement WS `pnr.air.received` |
| PNR mis à jour via fichier AIR + presales sans fare_basis | Dispatch `ReadPnrJob` (graphique) | Indicateur ⏳ sur les presales concernés |
| Read PNR retourne | Création `fare_basis` liés aux presales | Indicateur ✓ + accessibilité de la modal Conditions, événement WS `presale.fare_basis.created` |
| Execute lancé | Modification statut PNR | Statut → `En cours`, animation badge |
| Execute terminé (tous OK) | Création EDV + clone fare_basis | Statut → `Émis`, événement WS `pnr.execute.completed` |
| Execute terminé (partiel) | Création partielle EDV | Statut → `Émission partielle` |
| EDV créé avec presale ayant fare_basis | Clone des fare_basis du presale vers l'EDV | Indicateur ✓ sur l'EDV |
| EDV créé avec presale sans fare_basis | Dispatch `ReadPnrJob` puis double association (presale + EDV) | Indicateur ⏳ puis ✓ |

**Notification utilisateur** :
- À l'arrivée d'un Read PNR retour, toast discret en bas à droite : *Conditions tarifaires mises à jour pour le PNR ABC123*
- Pas de notification pour la réécriture de `account_uuid` (transparent pour l'utilisateur)

---

## 2. Module États de Vente

### 2.1 Vue Liste

**Pas d'évolution** par rapport à la spec existante, **sauf** :
- **Aucune action groupée** : retirer la barre d'actions contextuelle si elle existait
- **Aucune checkbox de sélection multiple** sur les lignes
- **Aucun bouton d'édition ou exécution en masse**

> Toute action sur un EDV nécessite obligatoirement de naviguer vers la vue détail.

### 2.2 Vue Détail

#### Zone d'actions principales

**Logique d'affichage du CTA principal** :

```pseudocode
si edv.date_emission == aujourd'hui (timezone agence) :
    afficher CTA "Void" (rouge)
sinon :
    afficher CTA "Refund" (orange)
```

**Règle exclusive** : un seul CTA visible à la fois. Pas de menu déroulant ni de choix.

#### CTA "Void"

**Conditions d'affichage** :
- `edv.date_emission == aujourd'hui` (date d'émission est la date courante en timezone agence)
- `edv.status` permet le void (pas déjà voidé, pas déjà remboursé)

**Permissions** :
- Récupérées dynamiquement via les **metadata CTA** (Admin CHAPS, voir [§5.5](#55-permissions-via-cta-metadata))
- Le frontend ne connaît pas les rôles autorisés : il reçoit du backend la liste des CTAs disponibles pour l'EDV courant et l'utilisateur connecté

**Comportement au clic** :
1. Ouverture de la pop-up de confirmation Execute (voir [§4.1](#41-pop-up-de-confirmation-execute))
2. Affichage :
   - Intitulé : `Annulation du jour (Void)`
   - Détails de l'EDV (passager, billet/EMD, montant)
   - Mode de paiement : EasyPay (readonly, voir [§5](#5-logique-métier-et-règles-transverses))
   - Commande cryptique Amadeus générée (avec syntaxe code IATA 172 conforme)
3. Sur confirmation, exécution via Messager
4. Toast de retour + rafraîchissement de la vue détail (statut EDV passe à `voided`)

#### CTA "Refund"

**Conditions d'affichage** :
- `edv.date_emission != aujourd'hui`
- `edv.status` permet le refund

**Permissions** :
- Récupérées dynamiquement via les **metadata CTA** (Admin CHAPS, voir [§5.5](#55-permissions-via-cta-metadata))

**Comportement au clic** :
1. Ouverture de la pop-up de confirmation Execute (voir [§4.1](#41-pop-up-de-confirmation-execute))
2. Affichage spécifique Refund :
   - Intitulé : `Remboursement (Refund)`
   - Détails de l'EDV
   - **Détail des taxes refundables vs non-refundables** (voir bloc dédié ci-dessous)
   - Mode de paiement : EasyPay (readonly)
   - Commande cryptique Amadeus générée
3. Sur confirmation, exécution via Messager
4. Toast de retour + rafraîchissement de la vue détail

##### Bloc "Détail des taxes" dans la pop-up Refund

Affichage tabulaire :

| Code | Libellé | Montant | Refundable |
|------|---------|---------|------------|
| YQ | Carrier surcharge | 25.00 EUR | ✓ Oui |
| YR | Carrier surcharge | 15.00 EUR | ✗ Non |
| FR | Tax FR | 12.50 EUR | ✗ Non |
| QX | Tax QX | 8.00 EUR | ✓ Oui |

**⚠ Important — Logique YQ/YR** : la refundability des taxes YQ et YR n'est **pas** une règle blanket non-refundable. Elle dépend des **fare rules** retournées par le Read PNR (catégorie `refundable_taxes` dans `fare_basis.rule_categories`).

Le frontend affiche **fidèlement** les valeurs `refundable: true|false` retournées par le backend, sans appliquer de logique métier propre. Un tooltip d'aide informe l'utilisateur :

> *La refundabilité des taxes YQ/YR dépend des conditions tarifaires du billet (fare rules). Consultez le détail des conditions tarifaires pour plus d'informations.*

Avec un lien `Voir conditions tarifaires` qui ouvre la modal correspondante (voir [§4.2](#42-visualisation-conditions-tarifaires)).

**Total remboursé** affiché en bas du bloc :
- `Total à rembourser : XX.XX EUR` (somme des montants refundables)
- `Non remboursable : YY.YY EUR` (somme des montants non-refundables)

---

## 3. Module Accounts (rappel)

> Cette section est un rappel des triggers automatiques liés aux comptes. La spec UI complète des Accounts reste celle du document principal.

### Trigger : Verify Office ID

**Déclenchement** :
- À la création d'un compte
- À la mise à jour d'un compte si `verified_office_id` est vide

**Action** : appel automatique vers Amadeus via Messager → Octopus pour vérifier l'office ID. Le résultat met à jour `account.verified_office_id`.

**Reflet UI** dans la vue détail Account :
- Champ `Office ID vérifié` :
  - Si `verified_office_id` rempli : badge vert ✓ + valeur affichée
  - Si `verified_office_id` vide et trigger en cours : badge orange ⏳ `Vérification en cours…`
  - Si `verified_office_id` vide et trigger échoué : badge rouge ✗ `Vérification échouée` + bouton `Relancer`

**Bouton "Relancer la vérification"** :
- Visible uniquement si statut = échec
- Permissions : récupérées dynamiquement via les **metadata CTA** (Admin CHAPS, voir [§5.5](#55-permissions-via-cta-metadata))
- Au clic : redéclenche le trigger côté backend

---

## 4. Composants transverses

### 4.1 Pop-up de confirmation Execute

**Composant réutilisable** : `<ExecuteConfirmDialog>`

Utilisé pour **toutes** les actions GDS Amadeus déclenchées depuis le front :
- Execute PNR global
- Execute Presales sélectionnés
- Void EDV
- Refund EDV

#### Structure

```
┌─────────────────────────────────────────────────┐
│ [INTITULÉ DE L'ACTION]                      ✕  │
├─────────────────────────────────────────────────┤
│                                                 │
│ Description claire de ce qui va être exécuté    │
│ (ex: "Émission de 2 TST et 1 EMD pour le PNR   │
│  ABC123")                                       │
│                                                 │
│ ┌─ Détails ─────────────────────────────────┐  │
│ │ • Passager 1 : DUPONT JEAN — TST 3        │  │
│ │ • Passager 2 : DUPONT MARIE — TST 4       │  │
│ │ • EMD 5 lié au TST 3                      │  │
│ └───────────────────────────────────────────┘  │
│                                                 │
│ ┌─ Mode de paiement ────────────────────────┐  │
│ │ EasyPay                          [readonly]│  │
│ │ ⓘ Modification disponible en V2            │  │
│ └───────────────────────────────────────────┘  │
│                                                 │
│ ┌─ Commande Amadeus générée ────────────────┐  │
│ │ TTP/T1                                    │  │
│ │ TTP/T2                                    │  │
│ │ EMDI/M5                                   │  │
│ │                                            │  │
│ │                              [📋 Copier]   │  │
│ └───────────────────────────────────────────┘  │
│                                                 │
│                          [Annuler] [Confirmer]  │
└─────────────────────────────────────────────────┘
```

#### Props du composant

```typescript
interface ExecuteConfirmDialogProps {
  open: boolean;
  title: string;                    // Intitulé clair de l'action
  description: string;              // Description courte
  items: ExecuteItem[];             // Liste des éléments à exécuter
  amadeusCommand: string;           // Commande cryptique générée (multi-lignes possible)
  paymentMode: 'easypay' | 'cb';    // V1 : toujours 'easypay'
  paymentModeReadonly: boolean;     // V1 : toujours true
  onConfirm: () => Promise<void>;
  onCancel: () => void;
  loading?: boolean;
}

interface ExecuteItem {
  type: 'tst' | 'emd' | 'void' | 'refund';
  label: string;                    // ex: "Passager 1 : DUPONT JEAN — TST 3"
  metadata?: Record<string, any>;   // Contexte additionnel
}
```

#### Comportement

- **Bouton `Confirmer`** :
  - Désactivé pendant l'exécution (loader)
  - Au succès : ferme la modal, toast vert
  - À l'échec : reste ouverte, affiche le message d'erreur
- **Bouton `Annuler`** : ferme la modal sans action
- **Bouton `Copier`** sur la commande : copie dans le presse-papier (utile pour debug ou exécution manuelle de secours)

#### Mode de paiement EasyPay (V1)

- Champ affiché en `readonly` avec valeur `EasyPay`
- Helper text : *Modification du mode de paiement disponible dans une version future*
- Pas de sélecteur, pas de modification possible
- **V2 (roadmap)** : transformation en `<select>` permettant `EasyPay`, `Carte bancaire`, autres formes

---

### 4.2 Visualisation Conditions tarifaires

**Composant réutilisable** : `<FareBasisModal>`

Modal d'affichage des conditions tarifaires (`fare_basis`) liées à un presale ou un EDV.

#### Structure (cf. screenshots de référence)

```
┌──────────────────────────────────────────────────────────────────┐
│ Conditions tarifaires                                        ✕   │
├──────────────────────────────────────────────────────────────────┤
│ Passager                                  Numéro du billet/TST   │
│ ○ - GBEHI ANGE                           TST 3                   │
│ ● [CHD] GBEHI SHILO                      TST 4                   │
├──────────────────────────────────────────────────────────────────┤
│ De/À      Type pax    Cie  Classe  Type tarif      Base tarif   │
│ ● ABJ-PAR CNN-CHILD   AF   L       ECONOMY RT...   LRQ8BBRA      │
├─────────────┬────────────────────────────────────────────────────┤
│ Modif. v.   │ ⓘ Aucun récapitulatif n'a été trouvé              │
│ Rembours.   │                                                    │
│ Advance...  │ Conditions tarifaires                              │
│ Minimum     │                                                    │
│ Maximum     │ FOR LRQ8BBRA TYPE FARES                            │
│ Données...  │                                                    │
│ Point i...  │   IN THE EVENT OF CHANGES TO TICKETED FLIGHTS     │
│ Combi...    │   BEFORE DEPARTURE OF JOURNEY - APPLIES WITHIN... │
│ Pénalités   │   CHARGE EUR 300.00 OR HIGHEST FEE OF ALL...      │
│ Suppl.      │   ...                                              │
│ Transferts  │                                                    │
│ Escales     │                                                    │
│ Autres r.   │                                                    │
│ Remises e.  │                                                    │
│ Cond. app.  │                                                    │
│ Limit. v.   │                                                    │
│ Saisonn.    │                                                    │
│ Taxes r.    │                                                    │
│ Services    │                                                    │
└─────────────┴────────────────────────────────────────────────────┘
```

#### Header — Sélecteur Passager / TST

- Liste verticale des passagers du PNR avec radio buttons
- Affichage : badge type passager (ADT/CHD/INF) + nom + numéro TST/billet
- Sélection unique (un seul passager affiché à la fois)
- Au changement, la zone de droite se met à jour avec les `fare_basis` du passager sélectionné

#### Header — Sélecteur fare_component

> ⚠ **Note** : selon la décision validée (1 fare_basis par presale = 1 par TST), il n'y a pas de "fare_components" multiples. Cette ligne affiche directement les caractéristiques de la fare_basis unique du TST sélectionné.

Affichage en lecture seule des données (depuis les tables existantes du PNR, **pas** depuis `fare_basis`) :
- De/À : routing du segment
- Type de passager (ex: `CNN - CHILD`)
- Compagnie (ex: `AF`)
- Classe de réservation (ex: `L`)
- Type de tarif (ex: `ECONOMY RT UNBUNDLED LEVEL`)
- Base tarifaire (ex: `LRQ8BBRA`) — depuis `fare_basis.fare_basis_code`

#### Sidebar — 19 catégories de règles

Liste verticale scrollable des catégories disponibles dans `fare_basis.rule_categories` :

1. Modifications volontaires
2. Remboursements volontaires
3. Advance purchase
4. Minimum
5. Maximum
6. Données diverses
7. Point intermédiaire plus haut tarif
8. Possibilité de combinaison
9. Pénalités
10. Suppléments
11. Transferts
12. Escales
13. Autres remises
14. Remises enfant
15. Conditions d'application
16. Conditions d'application (tarif)
17. Limitations de vente
18. Saisonnalité
19. Taxes remboursables
20. Services

**Comportement** :
- Catégorie active : surlignée (fond bleu clair Vuexy)
- Catégorie sans contenu : grisée et non-cliquable, avec tooltip *Pas de données pour cette catégorie*
- Au clic : la zone de droite affiche le `raw_text` brut

#### Zone de contenu

- Affichage du `raw_text` en **police monospace** (Roboto Mono ou Menlo) pour préserver la mise en forme Amadeus
- Préservation des sauts de ligne et indentations
- Bandeau d'info bleu si pas de récapitulatif structuré disponible :
  > ⓘ *Aucun récapitulatif n'a été trouvé pour cette catégorie de prix*
- Bouton `📋 Copier` en haut à droite pour copier le contenu brut

#### Permissions

- Rôles autorisés : tous les rôles ayant accès au PNR/EDV concerné
- Pas de permission spécifique au-delà de la lecture du parent

#### États

| État | Affichage |
|------|-----------|
| `fare_basis` non encore récupéré | Loader avec message *Récupération des conditions tarifaires en cours…* |
| `fare_basis` récupéré et valide | Affichage normal |
| `fare_basis` récupéré mais vide | Message *Aucune condition tarifaire disponible pour ce billet* |
| Erreur de récupération | Message d'erreur + bouton `Relancer la récupération` (admin/support uniquement) |

---

## 5. Logique métier et règles transverses

### 5.1 Réécriture `account_uuid` à la réception du fichier AIR

**Contexte** : à la création d'un PNR via Load PNR, on ne peut pas garantir que le PNR demandé appartient effectivement à l'agence demandeuse. La sécurité est assurée par la réécriture automatique de `account_uuid` à partir de l'office ID retourné par Amadeus.

**Règle** :
1. À la création (Load PNR) : `pnr.account_uuid` = agence de l'utilisateur demandeur
2. `pnr.status` = `pending_load` (PNR vide, aucun contenu visible)
3. À la réception du fichier AIR :
   - Lecture de l'office ID Amadeus retourné
   - Mapping `office_id` → `account_uuid` (table `accounts.verified_office_id`)
   - Si l'office ID correspond à `pnr.account_uuid` initial → aucune action
   - Si différent → **réécriture** de `pnr.account_uuid` avec le bon compte
4. `pnr.status` = `loaded`, contenu visible

**Sécurité** : pendant la fenêtre `pending_load`, le PNR est vide donc aucun contenu sensible n'est exposé à l'agence demandeuse même en cas de mauvaise affectation initiale.

**Audit** : toutes les réécritures `account_uuid` sont loggées (audit log distinct) avec horodatage, ancien/nouveau compte, source (fichier AIR + identifiant message Messager).

### 5.2 Règle EMD ↔ TST

**Règle métier** : un EMD lié à un TST (presale de type ticket) doit obligatoirement être exécuté **en même temps** que son TST parent.

**Application** :
- **Mode global Execute PNR** : tous les presales sont exécutés ensemble, donc la règle est respectée par construction
- **Mode sélectif** : la sélection d'un EMD sans son TST parent déclenche un avertissement bloquant (voir [§1.3](#13-vue-contextuelle-presales))

### 5.3 Mode de paiement V1

**Règle V1** : tous les Execute (émission TST, émission EMD, void, refund) utilisent **EasyPay** comme mode de paiement, **non modifiable**.

**Affichage** : champ readonly dans la pop-up Execute, helper text indiquant la modification possible en V2.

**V2 (roadmap)** : transformation en sélecteur multi-modes (EasyPay, CB, autres formes de paiement). Spec V2 à rédiger ultérieurement.

### 5.4 Logique YQ/YR refundability

**Règle** : la refundabilité des taxes YQ/YR n'est **pas** un blanket non-refundable. Elle est déterminée par les fare rules (catégorie `refundable_taxes` dans `fare_basis.rule_categories`).

**Application frontend** :
- Le backend retourne pour chaque taxe son statut `refundable: boolean`
- Le frontend affiche **fidèlement** ce statut dans la pop-up Refund (voir [§2.2](#22-vue-détail))
- Tooltip d'aide pour informer l'utilisateur de la dépendance aux fare rules
- Lien `Voir conditions tarifaires` vers la modal `<FareBasisModal>` à la catégorie `Taxes remboursables`

### 5.5 Permissions via CTA metadata

**Principe** : les permissions sur les CTAs (Execute, Void, Refund, Verify Office ID, etc.) ne sont **pas figées dans le frontend**. Elles sont :
1. Configurées dans **Admin CHAPS** (interface d'administration des CTAs et de leurs métadonnées)
2. Récupérées dynamiquement par ESTAIR Connect via API
3. Attachées à chaque entité (PNR, EDV, Account, etc.) sous forme de liste de CTAs autorisés pour l'utilisateur courant

**Format de réponse API attendu** :

Quand le frontend récupère un PNR ou un EDV, le backend joint la liste des CTAs disponibles avec leurs métadonnées :

```json
{
  "id": 123,
  "pnr": "ABC123",
  "status": "À émettre",
  "...": "...",
  "available_ctas": [
    {
      "code": "execute_pnr",
      "label": "Execute",
      "icon": "mdi-play",
      "color": "primary",
      "endpoint": "/api/pnrs/123/execute",
      "method": "POST",
      "confirmation_required": true,
      "metadata": {
        "type": "amadeus_command",
        "payment_mode": "easypay",
        "payment_modifiable": false
      }
    }
  ]
}
```

**Comportement frontend** :
- Affiche uniquement les CTAs présents dans `available_ctas`
- N'effectue **aucune** vérification de rôle propre — fait confiance à la liste retournée
- Vérification finale côté backend à chaque appel d'endpoint (double sécurité)

**Avantages** :
- Permissions modifiables sans déploiement frontend
- Configuration par agence possible (V2 — un compte peut avoir des CTAs personnalisés)
- Cohérent avec l'architecture modulaire activatable (NDC, etc.)

**Migration depuis CASL** : les règles CASL `ability.ts` continuent de gouverner l'accès aux **modules** (lecture liste, création, édition générique). Les CTAs spécifiques aux actions GDS (Execute, Void, Refund) sont gérés par metadata. Pas de double-définition.

### 5.6 Récapitulatif rapide des actions GDS

| Action | Source de permission | Vérification frontend |
|--------|---------------------|----------------------|
| Accès liste PNR / EDV / Accounts | CASL ability | `can('read', module)` |
| Création / édition générique | CASL ability | `can('create' / 'update', module)` |
| Load PNR | CTA metadata sur module PNR | Présence dans `available_ctas` du module |
| Execute PNR (global / sélectif) | CTA metadata sur entité PNR | Présence dans `pnr.available_ctas` |
| Void EDV | CTA metadata sur entité EDV | Présence dans `edv.available_ctas` |
| Refund EDV | CTA metadata sur entité EDV | Présence dans `edv.available_ctas` |
| Verify Office ID (relance) | CTA metadata sur entité Account | Présence dans `account.available_ctas` |

---

## Annexe — Endpoints API attendus

| Endpoint | Méthode | Description |
|----------|---------|-------------|
| `/api/pnrs/load` | POST | Déclenche le Load PNR (graphique) |
| `/api/pnrs/{id}/execute` | POST | Execute global du PNR |
| `/api/pnrs/{id}/execute-selection` | POST | Execute sélectif (body : `{ presale_ids: [] }`) |
| `/api/etats-de-ventes/{id}/void` | POST | Void d'un EDV |
| `/api/etats-de-ventes/{id}/refund` | POST | Refund d'un EDV |
| `/api/presales/{id}/fare-basis` | GET | Récupère le `fare_basis` d'un presale |
| `/api/etats-de-ventes/{id}/fare-basis` | GET | Récupère le `fare_basis` d'un EDV |
| `/api/accounts/{id}/verify-office-id` | POST | Relance manuelle du Verify Office ID |

Détails de la structure de réponse à spécifier dans la spec backend (cf. document `BACKEND_FARE_BASIS_SPEC.md`).

---

## Annexe — Composants Vuexy à utiliser

| Élément spec | Composant Vuexy / Vue3 |
|--------------|------------------------|
| Modal Load PNR | `VDialog` (Vuetify) avec `VForm` + `VTextField` |
| Pop-up Execute | `VDialog` avec layout custom (composant `<ExecuteConfirmDialog>`) |
| Modal Conditions tarifaires | `VDialog` avec layout custom (composant `<FareBasisModal>`) |
| Liste presales avec checkboxes | `VDataTable` avec `show-select` |
| Barre d'actions contextuelle | `VAlert` sticky ou layout custom |
| Badges statuts | `VChip` avec couleurs Vuexy (success/warning/error) |
| Sidebar catégories conditions | `VList` avec `VListItem` actifs |
| Toasts | Plugin `vue3-toastify` ou équivalent Vuexy |

---

**Fin du patch**
