---
title: Viva Local Terminal SDK - Documentation
version: 1.0.0
language: PHP
package: qrcommunication/viva-local-terminal-sdk
github: https://github.com/QrCommunication/sdk-php-viva-local-terminal
---

# Viva.com Local Terminal SDK — Documentation PHP

SDK PHP pour la **Local Terminal API** de Viva.com (`/pos/v1/`) — communication **peer-to-peer (P2P)** directe avec les terminaux de paiement EFT POS sur le réseau local. Couvre **3 ressources** : Transactions, Sessions et Device.

- **Version** : 1.0.0
- **Prérequis** : PHP 8.2+, `ext-json`, `guzzlehttp/guzzle ^7.8`
- **Licence** : MIT
- **Packagist** : `qrcommunication/viva-local-terminal-sdk`

---

## ⚠️ À lire en premier : ce n'est PAS l'API cloud Viva

Ce SDK **ne communique pas** avec `api.vivapayments.com`. La Local Terminal API est un **protocole peer-to-peer** : votre application parle **directement au terminal de carte** sur votre réseau local, en adressant **l'adresse IP et le port** du terminal lui-même.

| | API cloud Viva | **Local Terminal API (ce SDK)** |
|---|---|---|
| Base URL | `https://api.vivapayments.com` | **`https://<ip-terminal>:<port>`** |
| Réseau | Internet | **Réseau local (LAN), P2P** |
| Authentification | OAuth2 / Basic / Bearer | **Aucune** — réseau fermé, pas de header `Authorization` |
| TLS | CA publique | Certificat **auto-signé** du terminal (généralement) |
| Erreurs 400 | JSON structuré | **Chaîne brute** (texte du terminal verbatim) |

> Extrait de la spec officielle : *« In a closed network environment, authentication is not required for peer-to-peer communication. This means that you can target the terminal without authentication, eliminating the need to include an Authorization tag in the header. »*

### Modèle asynchrone (polling)

Les opérations de transaction (`sale`, `refund`, `capturePreauth`, …) **retournent immédiatement** avec un `state` (typiquement `PROCESSING`) et un `sessionId`. **Vous générez vous-même le `sessionId`** (UUID), puis vous interrogez `sessions->get($sessionId)` jusqu'à ce que `state` vaille `SUCCESS` ou `FAILURE` et que `payloadData` soit renseigné.

### Prérequis réseau

1. L'hôte qui exécute votre code et le terminal doivent être sur le **même LAN**.
2. Vous devez connaître **l'adresse IP et le port** du terminal (découvrables via l'app Viva.com Terminal, votre réseau, ou zeroconf/mDNS).
3. Le terminal sert du **HTTPS**, généralement avec un **certificat auto-signé**. Vous devez donc soit pinner son bundle CA, soit désactiver la vérification TLS (modèle de confiance LAN-only) — voir [TLS](#tls--certificats-auto-signés).

---

## Table des matières

1. [Installation](#installation)
2. [Démarrage rapide](#démarrage-rapide)
3. [Configuration](#configuration)
4. [TLS — certificats auto-signés](#tls--certificats-auto-signés)
5. [Ressources](#ressources)
   - [Transactions](#1-transactions--terminaltransactions)
   - [Sessions](#2-sessions--terminalsessions)
   - [Device](#3-device--terminaldevice)
6. [Enums](#enums)
7. [Gestion des erreurs](#gestion-des-erreurs)
8. [Variantes Merchant vs ISV](#variantes-merchant-vs-isv)
9. [Changelog](#changelog)

---

## Installation

```bash
composer require qrcommunication/viva-local-terminal-sdk
```

Requiert PHP 8.2+ avec `ext-json`. Compatible Laravel, Symfony ou tout projet PHP standard.

---

## Démarrage rapide

```php
use QrCommunication\VivaLocalTerminal\VivaLocalTerminalClient;

// 1. Pointer le client sur l'IP + port du terminal sur le LAN
$terminal = new VivaLocalTerminalClient(
    terminalBaseUrl: 'https://192.168.1.50:8080', // IP + port du terminal
    verifyTls:       false,                        // certificat auto-signé sur LAN fermé
);

// 2. Démarrer une vente (montant en centimes) — vous générez le sessionId
$sessionId = '4bdebe62-c211-4ca0-a994-b2fbea2061c5'; // UUID que vous générez
$resp = $terminal->transactions->sale(
    sessionId:         $sessionId,
    amount:            1170,        // 11,70 EUR
    currencyCode:      978,         // EUR
    merchantReference: 'order-123',
);
// $resp = ['state' => 'PROCESSING', 'sessionType' => 'SALE', 'sessionId' => ...]

// 3. Interroger le résultat une fois le client ayant payé
$session = $terminal->sessions->get($sessionId);
// $session['state'] === 'SUCCESS' une fois la carte traitée
// $session['payloadData'] contient le détail de la transaction

// 4. Contrôler le terminal
$terminal->device->screenControl(wakeUpLock: true);
$terminal->device->brightness(0.5);
```

> Pour générer un UUID en PHP : utilisez `\Symfony\Component\Uid\Uuid::v4()` ou `ramsey/uuid` (`Uuid::uuid4()->toString()`).

---

## Configuration

### Constructeur `VivaLocalTerminalClient`

```php
new VivaLocalTerminalClient(
    string $terminalBaseUrl,
    bool|string $verifyTls = true,
    bool $useIsvEndpoints = false,
    int $timeout = 30,
    int $connectTimeout = 10,
)
```

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `terminalBaseUrl` | `string` | **requis** | URL de base du terminal : scheme + IP + port, ex : `https://192.168.1.50:8080`. Le slash final est retiré. Le scheme (`https://`) est obligatoire. |
| `verifyTls` | `bool\|string` | `true` | Vérification TLS. `true` = vérifie contre le bundle CA système ; une `string` = chemin vers un bundle CA/PEM personnalisé (cert auto-signé) ; `false` = désactive la vérification (modèle LAN fermé). |
| `useIsvEndpoints` | `bool` | `false` | Cible les variantes ISV (chemins avec slash final, ex : `/pos/v1/sale/`). |
| `timeout` | `int` | `30` | Timeout par requête en secondes. |
| `connectTimeout` | `int` | `10` | Timeout de connexion TCP en secondes. |

> **Aucune authentification.** Le constructeur n'accepte ni clé API, ni client ID, ni secret. Le terminal fait confiance à tout appelant sur le réseau fermé.

### Méthode utilitaire du client

```php
$terminal->getConfig(): Config   // Configuration courante (introspection)
```

### Propriétés publiques

```php
$terminal->transactions  // Transactions
$terminal->sessions      // Sessions
$terminal->device        // Device
```

---

## TLS — certificats auto-signés

Le terminal sert du HTTPS, le plus souvent avec un **certificat auto-signé**. Trois modes :

```php
// 1. Vérification standard (CA système) — défaut. Échoue sur cert auto-signé.
$terminal = new VivaLocalTerminalClient('https://192.168.1.50:8080');

// 2. Pinner le bundle CA du terminal (recommandé en production)
$terminal = new VivaLocalTerminalClient(
    terminalBaseUrl: 'https://192.168.1.50:8080',
    verifyTls:       '/chemin/vers/terminal-ca.pem',
);

// 3. Désactiver la vérification (modèle de confiance LAN-only)
$terminal = new VivaLocalTerminalClient(
    terminalBaseUrl: 'https://192.168.1.50:8080',
    verifyTls:       false,
);
```

> `verifyTls` est transmis tel quel à l'option Guzzle `verify`. Privilégiez le pinning du CA (option 2) plutôt que `false` dès que possible.

---

## Ressources

### 1. Transactions — `$terminal->transactions`

Opérations de transaction sur la Local Terminal API. Chaque opération qui démarre une transaction **retourne immédiatement** avec un `state` (typiquement `PROCESSING`) et le `sessionId`. Le résultat final s'obtient en interrogeant `sessions->get()`. Les montants sont **toujours des entiers en centimes**.

**Retour commun :** `array{state?: string, sessionType?: string, sessionId?: string}`

#### `sale()` — Initier une vente

```php
public function sale(
    string $sessionId,
    int $amount,
    ?string $merchantReference = null,
    ?string $customerTrns = null,
    ?bool $preauth = null,
    ?int $tipAmount = null,
    ?string $paymentMethod = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?int $currencyCode = null,
    ?bool $skipSurcharge = null,
    ?string $saleToAcquirerData = null,
    ?int $maxInstalments = null,
    ?string $aadeProviderId = null,
    ?string $aadeProviderSignatureData = null,
    ?string $aadeProviderSignature = null,
    ?bool $aadePreloaded = null,
    ?int $aadePreloadedDuration = null,
    ?array $fiscalisationData = null,
    ?array $isvDetails = null,
): array
```

Endpoint : `POST /pos/v1/sale`. Le terminal affiche le montant et attend la carte.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de session de transaction généré par l'appelant |
| `amount` | `int` | **requis** | Montant à autoriser, en centimes |
| `merchantReference` | `?string` | `null` | Référence libre côté marchand |
| `customerTrns` | `?string` | `null` | Référence libre côté client |
| `preauth` | `?bool` | `null` | Le paiement est-il une pré-autorisation |
| `tipAmount` | `?int` | `null` | Pourboire en centimes (incompatible avec `preauth`) |
| `paymentMethod` | `?string` | `null` | Méthode affichée en premier (ex : `CardPresent`) |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat à l'écran |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat à l'écran |
| `currencyCode` | `?int` | `null` | Code ISO 4217 numérique (devise marchand) |
| `skipSurcharge` | `?bool` | `null` | Désactiver le surcharge même sur cartes éligibles |
| `saleToAcquirerData` | `?string` | `null` | JSON base64 de métadonnées acquéreur supplémentaires |
| `maxInstalments` | `?int` | `null` | Versements max (marchands grecs uniquement) |
| `aadeProviderId` | `?string` | `null` | ID provider AADE (Grèce) |
| `aadeProviderSignatureData` | `?string` | `null` | Données de signature provider AADE (Grèce) |
| `aadeProviderSignature` | `?string` | `null` | Signature provider AADE (Grèce) |
| `aadePreloaded` | `?bool` | `null` | Flag préchargé AADE (Grèce) |
| `aadePreloadedDuration` | `?int` | `null` | Durée d'expiration préchargée AADE (Grèce) |
| `fiscalisationData` | `?array` | `null` | Objet Viva Fiscalisation |
| `isvDetails` | `?array` | `null` | Objet partenaire / fee / multi-marchand ISV (ISV uniquement) |

```php
$resp = $terminal->transactions->sale(
    sessionId:         '4bdebe62-c211-4ca0-a994-b2fbea2061c5',
    amount:            1170,        // 11,70 EUR
    currencyCode:      978,         // EUR
    merchantReference: 'order-123',
    customerTrns:      'Consultation',
    showReceipt:       true,
);

echo $resp['state'];       // 'PROCESSING'
echo $resp['sessionType']; // 'SALE'
echo $resp['sessionId'];   // '4bdebe62-...'

// Puis polling :
$session = $terminal->sessions->get($resp['sessionId']);
```

#### `capturePreauth()` — Capturer une pré-autorisation

```php
public function capturePreauth(
    string $sessionId,
    int $amount,
    string $transactionId,
    ?string $merchantReference = null,
    ?string $customerTrns = null,
    ?int $currencyCode = null,
    ?string $saleToAcquirerData = null,
    ?string $aadeProviderId = null,
    ?string $aadeProviderSignatureData = null,
    ?string $aadeProviderSignature = null,
    ?bool $aadePreloaded = null,
    ?int $aadePreloadedDuration = null,
): array
```

Endpoint : `POST /pos/v1/preauth-completion`. Capture (complète) une vente précédemment pré-autorisée.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de session de transaction généré par l'appelant |
| `amount` | `int` | **requis** | Montant à capturer, en centimes |
| `transactionId` | `string` | **requis** | ID de la transaction pré-autorisée initiale |
| `merchantReference` | `?string` | `null` | Référence libre côté marchand |
| `customerTrns` | `?string` | `null` | Référence libre côté client |
| `currencyCode` | `?int` | `null` | Code ISO 4217 numérique |
| `saleToAcquirerData` | `?string` | `null` | JSON base64 de métadonnées acquéreur |
| `aadeProviderId` … `aadePreloadedDuration` | `?string`/`?bool`/`?int` | `null` | Paramètres AADE (Grèce) |

```php
$resp = $terminal->transactions->capturePreauth(
    sessionId:     'a-new-uuid',
    amount:        1500,        // 15,00 EUR
    transactionId: 'preauth-transaction-id',
);
```

#### `refund()` — Rembourser (référencé)

```php
public function refund(
    string $sessionId,
    int $amount,
    string $transactionId,
    ?int $orderCode = null,
    ?int $shortOrderCode = null,
    ?string $merchantReference = null,
    ?string $customerTrns = null,
    ?int $currencyCode = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $txnDateFrom = null,
    ?string $txnDateTo = null,
    ?string $aadeProviderId = null,
    ?string $aadeProviderSignatureData = null,
    ?string $aadeProviderSignature = null,
): array
```

Endpoint : `POST /pos/v1/refund`. Rembourse / annule une transaction par son ID d'origine.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de session généré par l'appelant |
| `amount` | `int` | **requis** | Montant à rembourser, en centimes |
| `transactionId` | `string` | **requis** | ID de la transaction de vente d'origine |
| `orderCode` | `?int` | `null` | Code d'ordre de la vente d'origine |
| `shortOrderCode` | `?int` | `null` | Short order code (filtre) |
| `merchantReference` | `?string` | `null` | Référence libre côté marchand |
| `customerTrns` | `?string` | `null` | Référence libre côté client |
| `currencyCode` | `?int` | `null` | Code ISO 4217 numérique |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat à l'écran |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat à l'écran |
| `txnDateFrom` | `?string` | `null` | Borne inférieure ISO 8601 pour localiser la transaction |
| `txnDateTo` | `?string` | `null` | Borne supérieure ISO 8601 |
| `aadeProvider*` | `?string` | `null` | Paramètres AADE (Grèce) |

```php
$resp = $terminal->transactions->refund(
    sessionId:     'a-new-uuid',
    amount:        1170,
    transactionId: 'original-sale-transaction-id',
);
```

#### `unreferencedRefund()` — Remboursement non référencé

```php
public function unreferencedRefund(
    string $sessionId,
    int $amount,
    ?string $merchantReference = null,
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $aadeProviderId = null,
    ?string $aadeProviderSignatureData = null,
    ?string $aadeProviderSignature = null,
    ?array $fiscalisationData = null,
): array
```

Endpoint : `POST /pos/v1/unreferenced-refund`. Remboursement non lié à une transaction d'origine.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de session généré par l'appelant |
| `amount` | `int` | **requis** | Montant à rembourser, en centimes |
| `merchantReference` | `?string` | `null` | Référence libre côté marchand |
| `customerTrns` | `?string` | `null` | Référence libre côté client |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat à l'écran |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat |
| `aadeProvider*` | `?string` | `null` | Paramètres AADE (Grèce) |
| `fiscalisationData` | `?array` | `null` | Objet Viva Fiscalisation |

```php
$resp = $terminal->transactions->unreferencedRefund(
    sessionId: 'a-new-uuid',
    amount:    500, // 5,00 EUR
);
```

#### `abort()` — Annuler une session de vente en cours

```php
public function abort(string $sessionId): array
```

Endpoint : `POST /pos/v1/abort`. Interrompt une session `SALE` en cours.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | ID de la session SALE à interrompre |

```php
$terminal->transactions->abort('4bdebe62-c211-4ca0-a994-b2fbea2061c5');
```

#### `aadeFimControl()` — Action de contrôle AADE FIM (Grèce)

```php
public function aadeFimControl(string $token): array
// array{status?: string, message?: string}
```

Endpoint : `POST /pos/v1/aade-fim-control`. Crée l'Echo Message (récupération de la Master Key) et le Control Message (création de la Session Key), valide la Master Key, le KCV et la Session Key chiffrée (fiscalisation AADE grecque).

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `token` | `string` | **requis** | Token de contrôle FIMAS incluant le token AADE |

```php
$resp = $terminal->transactions->aadeFimControl(
    'ECR0210U/RCFB77000041/CMAC_K:1ED9F7AE0B2509281BBC2DE38EF2A12B:CC5FFF',
);
echo $resp['status'];  // 'success'
echo $resp['message']; // 'Fim control success'
```

#### `retrieve()` — Lister les transactions historiques

```php
public function retrieve(
    ?string $cardNumber = null,
    ?string $orderCode = null,
    int|array|null $transactionTypes = null,
    ?string $transactionStatus = null,
    ?string $startDate = null,
    ?string $endDate = null,
    ?bool $isAadeAutonomouslyOnly = null,
): array
// array{isSuccess?: bool, transactions?: array<int, array<string, mixed>>}
```

Endpoint : `GET /pos/v1/transactions`. Tous les filtres sont optionnels. `transactionTypes` accepte un id unique ou une liste.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `cardNumber` | `?string` | `null` | Filtre par numéro de carte masqué |
| `orderCode` | `?string` | `null` | Filtre par code d'ordre |
| `transactionTypes` | `int\|array\|null` | `null` | Id(s) de type de transaction (5 = sale, 4 = refund, …) |
| `transactionStatus` | `?string` | `null` | Filtre par statut |
| `startDate` | `?string` | `null` | Borne inférieure ISO 8601 |
| `endDate` | `?string` | `null` | Borne supérieure ISO 8601 |
| `isAadeAutonomouslyOnly` | `?bool` | `null` | Filtrer les transactions AADE autonomes uniquement |

```php
$result = $terminal->transactions->retrieve(
    transactionTypes: [5, 4], // ventes + remboursements
    startDate:        '2026-03-01T00:00:00Z',
    endDate:          '2026-03-31T23:59:59Z',
);

foreach ($result['transactions'] ?? [] as $txn) {
    echo $txn['orderCode'] . "\n";
}
```

---

### 2. Sessions — `$terminal->sessions`

Récupération de session. Après qu'une opération de transaction a retourné `PROCESSING`, interrogez la session pour obtenir le résultat final. Le champ `payloadData` n'est renseigné qu'une fois la transaction terminée (`state` `SUCCESS` ou `FAILURE`).

#### `get()` — Récupérer une session

```php
public function get(string $sessionId): array
// array{state?: string, sessionType?: string, sessionId?: string, payloadData?: array<string, mixed>}
```

Endpoint : `GET /pos/v1/sessions/{sessionId}`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de la session |

```php
use QrCommunication\VivaLocalTerminal\Enums\SessionState;

// Boucle de polling typique
do {
    $session = $terminal->sessions->get($sessionId);
    $state = $session['state'] ?? null;

    if ($state === SessionState::SUCCESS->value) {
        // Paiement réussi — $session['payloadData'] contient le détail
        break;
    }
    if ($state === SessionState::FAILURE->value) {
        // Paiement échoué
        break;
    }

    sleep(2); // attendre avant de re-poller
} while ($state === SessionState::PROCESSING->value);
```

---

### 3. Device — `$terminal->device`

Opérations de configuration du terminal physique : réveil/veille de l'écran et luminosité.

**Retour commun :** `array{isSuccess?: bool}`

#### `screenControl()` — Contrôler l'écran / mode veille

```php
public function screenControl(bool $wakeUpLock): array
```

Endpoint : `POST /pos/v1/awake-lock`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `wakeUpLock` | `bool` | **requis** | `true` : le terminal sort de veille et l'écran reste actif jusqu'au déverrouillage. `false` : retour au comportement de veille normal. |

```php
$resp = $terminal->device->screenControl(wakeUpLock: true);
echo $resp['isSuccess']; // true
```

#### `brightness()` — Régler la luminosité

```php
public function brightness(float $brightness): array
```

Endpoint : `POST /pos/v1/brightness`. Lève `\InvalidArgumentException` si la valeur n'est pas dans `(0, 1]`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `brightness` | `float` | **requis** | Valeur dans `(0, 1]`, où `1.00` = luminosité maximale. La valeur `0` n'est **pas** autorisée. |

```php
$terminal->device->brightness(0.5); // 50 %
$terminal->device->brightness(1.0); // maximum
// $terminal->device->brightness(0.0); // ❌ InvalidArgumentException
```

---

## Enums

Tous les enums vivent sous `QrCommunication\VivaLocalTerminal\Enums`.

### `SessionState`

```php
enum SessionState: string
{
    case PROCESSING              = 'PROCESSING';
    case BUSY_ERROR              = 'BUSY_ERROR';
    case SERVER_ERROR            = 'SERVER_ERROR';
    case SESSION_NOT_FOUND_ERROR = 'SESSION_NOT_FOUND_ERROR';
    case SUCCESS                 = 'SUCCESS';
    case FAILURE                 = 'FAILURE';
}
```

| Valeur | Description |
|--------|-------------|
| `PROCESSING` | L'opération est en cours sur le terminal |
| `BUSY_ERROR` | Une autre transaction est déjà en cours sur le terminal |
| `SERVER_ERROR` | Le terminal a signalé une erreur côté serveur |
| `SESSION_NOT_FOUND_ERROR` | Le sessionId fourni est introuvable (abort) |
| `SUCCESS` | La transaction s'est terminée avec succès (résultat de session) |
| `FAILURE` | La transaction s'est terminée en erreur (résultat de session) |

### `SessionType`

```php
enum SessionType: string
{
    case SALE                = 'SALE';
    case CAPTURE_PREAUTH     = 'CAPTURE_PREAUTH';
    case CANCEL              = 'CANCEL';
    case UNREFERENCED_REFUND = 'UNREFERENCED_REFUND';
    case ABORT               = 'ABORT';
}
```

| Valeur | Description |
|--------|-------------|
| `SALE` | Transaction de vente |
| `CAPTURE_PREAUTH` | Capture (complétion) d'une vente pré-autorisée |
| `CANCEL` | Remboursement / annulation référençant une transaction d'origine |
| `UNREFERENCED_REFUND` | Remboursement non référencé |
| `ABORT` | Interruption d'une session SALE en cours |

### `TransactionTypeId`

```php
enum TransactionTypeId: int
{
    case SALE        = 5;
    case REFUND      = 4;
    case REVERSAL    = 7;
    case IRIS_SALE   = 60;
    case IRIS_REFUND = 61;
    case IRIS_VOID   = 85;
}
```

| Constante | Valeur | Description |
|-----------|--------|-------------|
| `SALE` | 5 | Vente |
| `REFUND` | 4 | Remboursement |
| `REVERSAL` | 7 | Annulation (reversal) |
| `IRIS_SALE` | 60 | Vente IRIS |
| `IRIS_REFUND` | 61 | Remboursement IRIS |
| `IRIS_VOID` | 85 | Annulation IRIS |

### `PaymentMethod`

```php
enum PaymentMethod: string
{
    case CARD_PRESENT = 'CardPresent';
    case IRIS         = 'Iris';
}
```

Accepté par le paramètre `paymentMethod` de `sale()` — spécifie la méthode affichée en premier. L'utilisateur peut toujours en choisir une autre sur le terminal.

---

## Gestion des erreurs

> ⚠️ **Erreurs 400 = chaînes brutes.** La Local Terminal API ne retourne **pas** de codes d'erreur structurés. Sur une 400, elle renvoie un **corps en chaîne de caractères brute** (ex : `"ZeroconfException error message Amount cannot be null"`). Le SDK préserve ce texte verbatim.

Toutes les exceptions héritent de `VivaException` (extends `RuntimeException`).

### Hiérarchie

```
RuntimeException
└── VivaException     // Classe de base — httpStatus, responseBody, getErrorText()
    └── ApiException  // Levée sur 4xx ET sur échec de transport (terminal injoignable)
```

### Propriétés et méthodes de `VivaException`

| Membre | Type | Description |
|--------|------|-------------|
| `$httpStatus` | `int` | Code HTTP de la réponse (`0` si le terminal est injoignable) |
| `$responseBody` | `?array` | Corps décodé. Si le terminal a renvoyé une chaîne brute, elle est préservée sous `['raw' => '...']` |
| `getErrorText()` | `?string` | Texte d'erreur : `message`, `error`, puis fallback sur `raw` (la chaîne brute du terminal) |

### Exemple complet

```php
use QrCommunication\VivaLocalTerminal\Exceptions\ApiException;
use QrCommunication\VivaLocalTerminal\Exceptions\VivaException;

try {
    $resp = $terminal->transactions->sale(
        sessionId: $sessionId,
        amount:    1170,
    );

} catch (ApiException $e) {
    // Erreur retournée par le terminal (4xx) OU terminal injoignable (httpStatus === 0)
    echo $e->httpStatus;          // 400, ou 0 si injoignable sur le LAN
    echo $e->getErrorText();      // ex: "ZeroconfException error message Amount cannot be null"
    echo $e->responseBody['raw']; // chaîne brute du terminal (si 400 non-JSON)

    if ($e->httpStatus === 0) {
        // Le terminal n'est pas joignable sur le LAN — vérifier IP/port/réseau
    }

} catch (VivaException $e) {
    echo $e->getMessage();
}
```

> **Terminal injoignable :** si la requête ne peut pas atteindre le terminal (mauvaise IP, hors réseau, terminal éteint), une `ApiException` est levée avec `httpStatus === 0` et un message indiquant de vérifier l'accessibilité du terminal sur le LAN.

---

## Variantes Merchant vs ISV

La spec expose chaque endpoint en deux variantes : **merchant** (sans slash final) et **ISV** (avec slash final, ex : `/pos/v1/sale/`). Plutôt que de dupliquer les méthodes, le SDK utilise **un seul jeu de méthodes** plus le toggle global `useIsvEndpoints`.

```php
// Mode ISV : sale/refund acceptent le paramètre `isvDetails`,
// et toutes les requêtes ciblent les chemins à slash final (/pos/v1/sale/)
$terminal = new VivaLocalTerminalClient(
    terminalBaseUrl: 'https://192.168.1.50:8080',
    useIsvEndpoints: true,
);

$terminal->transactions->sale(
    sessionId:  $sessionId,
    amount:     1170,
    isvDetails: [/* objet partenaire / fee / multi-marchand ISV */],
);
```

`isvDetails` est omis du corps quand il vaut `null`, il est donc inoffensif pour les appels merchant.

---

## Changelog

### v1.0.0

- Version initiale du SDK.
- Modèle **P2P sans authentification** : communication directe avec le terminal sur le LAN, aucun header `Authorization`.
- TLS configurable (`verifyTls` : `true` / chemin PEM / `false`) pour les certificats auto-signés.
- Ressource **Transactions** : `sale()`, `capturePreauth()`, `refund()`, `unreferencedRefund()`, `abort()`, `aadeFimControl()`, `retrieve()`.
- Ressource **Sessions** : `get()` (polling du résultat).
- Ressource **Device** : `screenControl()`, `brightness()`.
- Enums : `SessionState`, `SessionType`, `TransactionTypeId`, `PaymentMethod`.
- Support des variantes Merchant et ISV via `useIsvEndpoints`.
- Préservation des erreurs 400 sous forme de chaîne brute ; `ApiException` avec `httpStatus === 0` sur terminal injoignable.
