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

# Viva Cloud Terminal SDK — Documentation PHP

SDK PHP pour la **Cloud Terminal API** de Viva Wallet — la variante **marchand**
(`/ecr/v1/`) de la solution EFT POS de Viva. Il permet à votre back-office
(ECR / caisse / application web) de piloter un terminal de paiement physique
via l'API REST de Viva : initier des ventes, capturer des pré-autorisations,
rembourser, et poller le résultat des sessions.

- **Version** : 1.0.0
- **Prérequis** : PHP 8.2+, `guzzlehttp/guzzle ^7.8`
- **Licence** : MIT
- **Packagist** : `qrcommunication/viva-cloud-terminal-sdk`
- **Namespace** : `QrCommunication\VivaCloudTerminal\`
- **Auth** : OAuth2 `client_credentials` → Bearer token
- **Montants** : toujours en **centimes** (`int`) — `1170` = 11,70 EUR

> Ce SDK couvre **uniquement** les endpoints marchand `/ecr/v1/`. Pour la
> variante ISV / Partner (`/ecr/isv/v1/`, marchands connectés, composite auth),
> utiliser un SDK distinct.

---

## Table des matières

1. [Quoi de neuf en v1.0.0](#changelog)
2. [Installation](#installation)
3. [Démarrage rapide](#démarrage-rapide)
4. [Configuration](#configuration)
5. [Authentification](#authentification)
6. [Ressources](#ressources)
   - [Devices](#1-devices--vivadevices)
   - [Transactions](#2-transactions--vivatransactions)
   - [Sessions](#3-sessions--vivasessions)
   - [Polling](#4-polling-poll-untilcomplete)
7. [Enums](#enums)
8. [Gestion des erreurs](#gestion-des-erreurs)
9. [Carte de test](#carte-de-test)

---

## Quoi de neuf en v1.0.0

Première version stable du SDK Cloud Terminal (marchand `/ecr/v1/`).

- **Client** : `VivaCloudTerminalClient` — point d'entrée unique avec auth OAuth2
  automatique (Bearer token caché et rafraîchi 60s avant expiration).
- **`devices`** — `search()` (`POST /ecr/v1/devices:search`).
- **`transactions`** — `sale()`, `capturePreauth()`, `refund()`,
  `unreferencedRefund()`, `fastRefund()`, `rebate()`, `createAction()`,
  `getAction()`.
- **`sessions`** — `get()`, `listByDate()`, `abort()`, `pollUntilComplete()`.
- **`pollUntilComplete()`** — disponible aussi comme raccourci sur le client.
- **Enum `EcrEventId`** — interprétation des `eventId` de session (polling).
- Montants en **centimes**, payloads en **camelCase**.

---

## Installation

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

Prérequis : PHP 8.2+ avec `guzzlehttp/guzzle ^7.8`. Compatible Laravel, Symfony
ou tout projet PHP.

---

## Démarrage rapide

```php
use QrCommunication\VivaCloudTerminal\VivaCloudTerminalClient;

$viva = new VivaCloudTerminalClient(
    clientId:     'your-client-id',
    clientSecret: 'your-client-secret',
    environment:  'demo', // 'demo' (sandbox) ou 'production'
);

// 1. (recommandé) confirmer que le terminal est Live avant de transiger
$devices = $viva->devices->search(statusId: 1);

// 2. initier une vente sur le terminal (montant en centimes)
$sale = $viva->transactions->sale(
    terminalId:        '16000010',
    amount:            1170,        // 11,70 EUR
    cashRegisterId:    'CR-01',
    merchantReference: 'order-42',
);

// 3. poller jusqu'à ce que le client termine (ou décline) sur le terminal
$result = $viva->pollUntilComplete($sale['session_id']);

if (($result['success'] ?? false) === true) {
    // $result['transactionId'], $result['orderCode'], $result['amount'], ...
}
```

---

## Configuration

### Constructeur `VivaCloudTerminalClient`

```php
new VivaCloudTerminalClient(
    string $clientId,
    string $clientSecret,
    string|Environment $environment = Environment::DEMO,
)
```

| Paramètre | Type | Requis | Description |
|-----------|------|--------|-------------|
| `clientId` | `string` | Oui | Client ID OAuth2 (grant `client_credentials`) |
| `clientSecret` | `string` | Oui | Secret OAuth2 |
| `environment` | `string\|Environment` | Non | `'demo'` (défaut, sandbox) ou `'production'` |

### Méthodes utilitaires du client

```php
$viva->getConfig(): CloudTerminalConfig   // Configuration courante
$viva->invalidateToken(): void            // Forcer un nouveau handshake OAuth
$viva->pollUntilComplete($sessionId): array // Raccourci vers sessions->pollUntilComplete()
```

### Propriétés publiques

```php
$viva->devices        // Devices
$viva->transactions   // Transactions
$viva->sessions       // Sessions
```

---

## Authentification

La Cloud Terminal API utilise un **seul** mode d'authentification :

| Étape | Hôte | Auth |
|-------|------|------|
| Token | `accounts.vivapayments.com/connect/token` (demo : `demo-accounts...`) | Basic `client_id:client_secret`, `grant_type=client_credentials` |
| Appels API | `api.vivapayments.com/ecr/v1/...` (demo : `demo-api...`) | `Authorization: Bearer {token}` |

Le SDK récupère et met en cache le Bearer token automatiquement (rafraîchi 60s
avant expiration). Appeler `$viva->invalidateToken()` pour forcer un nouveau
handshake.

> **CRITIQUE** : tous les payloads `/ecr/v1/` sont en **camelCase**.

---

## Ressources

### 1. Devices — `$viva->devices`

Découverte des terminaux POS (EFT POS) et de leur statut. Recommandé en
pré-flight avant toute transaction, surtout sur les connexions WAN, pour
confirmer que le terminal est `Live` (`statusId` 1).

#### `search()` — Rechercher les terminaux POS

```php
public function search(?int $statusId = null, ?string $sourceCode = null): array
// array<int|string, mixed>
```

Endpoint : `POST /ecr/v1/devices:search`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `statusId` | `?int` | `null` | Filtrer par statut du terminal |
| `sourceCode` | `?string` | `null` | Code d'identification personnalisé assigné au terminal par le marchand |

Valeurs `statusId` : `0` = WareHouse, `1` = Live, `2` = Ready To Ship,
`3` = In Stock, `4` = Pending Key Injection, `5` = Lost, `6` = Broken, ...

**Retourne :** une liste de terminaux. Chaque entrée contient : `terminalId`
(string), `statusId` (int), `sourceCode` (string), `virtualTerminalId` (string).

```php
$devices = $viva->devices->search(statusId: 1);

foreach ($devices as $device) {
    echo $device['terminalId'] . ' — statut ' . $device['statusId'] . "\n";
    // 16000010 — statut 1
}
```

---

### 2. Transactions — `$viva->transactions`

Opérations transactionnelles marchand : vente, capture de pré-autorisation,
remboursements, rebate et actions sur le terminal.

> **Body vide sur succès** : la plupart des endpoints `/ecr/v1/transactions:*`
> répondent HTTP 200 **sans body** — c'est le succès du *dispatch* vers le
> terminal, pas le résultat du paiement. Chaque méthode retourne donc
> `['session_id' => string, 'response' => array]` : pollez la session pour
> obtenir le résultat réel.

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

```php
public function sale(
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    int $tipAmount = 0,
    ?bool $preauth = null,
    ?string $customerTrns = null,
    ?string $paymentMethod = null,
    ?bool $skipSurcharge = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:sale`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `terminalId` | `string\|int` | **requis** | ID du terminal cible (ex : `'16000010'`) |
| `amount` | `int` | **requis** | Montant à autoriser, en centimes |
| `cashRegisterId` | `string` | **requis** | Identification de la caisse (défini par le marchand) |
| `merchantReference` | `?string` | `null` | Référence marchand (défaut : `"SDK-{sessionId}"`) |
| `currencyCode` | `string` | `'978'` | Code ISO 4217 numérique en string (`'978'` = EUR) |
| `tipAmount` | `int` | `0` | Pourboire en centimes (incompatible avec `preauth`) |
| `preauth` | `?bool` | `null` | Flag de pré-autorisation (nécessite activation Viva) |
| `customerTrns` | `?string` | `null` | Référence client (texte libre) |
| `paymentMethod` | `?string` | `null` | Méthode de paiement affichée par défaut (ex : `'CardPresent'`) |
| `skipSurcharge` | `?bool` | `null` | Désactiver la surcharge pour cette transaction |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat sur le terminal |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat sur le terminal |
| `sessionId` | `?string` | `null` | UUID de session (auto-généré si `null`) |
| `extra` | `array` | `[]` | Champs API additionnels fusionnés tels quels (ex : `fiscalisationData`) |

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
$sale = $viva->transactions->sale(
    terminalId:     '16000010',
    amount:         2599,          // 25,99 EUR
    cashRegisterId: 'CR-01',
    customerTrns:   'Table 12',
    showReceipt:    true,
);

$final = $viva->pollUntilComplete($sale['session_id'], timeoutSeconds: 90);
```

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

```php
public function capturePreauth(
    string $parentSessionId,
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:preauth-completion`. Capture le montant
retenu par la pré-autorisation initiale identifiée par `parentSessionId`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `parentSessionId` | `string` | **requis** | UUID de session de la pré-autorisation à capturer |
| `terminalId` | `string\|int` | **requis** | ID du terminal cible |
| `amount` | `int` | **requis** | Montant à capturer, en centimes |
| `cashRegisterId` | `string` | **requis** | Identification de la caisse |
| `merchantReference` | `?string` | `null` | Référence marchand |
| `currencyCode` | `string` | `'978'` | Code ISO 4217 numérique en string |
| `customerTrns` | `?string` | `null` | Référence client |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat sur le terminal |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat sur le terminal |
| `sessionId` | `?string` | `null` | UUID de session pour la capture (auto-généré si `null`) |
| `extra` | `array` | `[]` | Champs API additionnels |

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
// Pré-autoriser (nécessite preauth activé sur le compte Viva)
$preauth = $viva->transactions->sale(
    terminalId:     '16000010',
    amount:         5000,
    cashRegisterId: 'CR-01',
    preauth:        true,
);
$viva->pollUntilComplete($preauth['session_id']);

// Plus tard, capturer le montant final (peut être inférieur à l'autorisé)
$capture = $viva->transactions->capturePreauth(
    parentSessionId: $preauth['session_id'],
    terminalId:      '16000010',
    amount:          4200,         // capturer moins que l'autorisé
    cashRegisterId:  'CR-01',
);
$viva->pollUntilComplete($capture['session_id']);
```

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

```php
public function refund(
    string $parentSessionId,
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:refund`. Un nouveau `sessionId` est généré
pour l'opération de remboursement ; `parentSessionId` référence la vente
originale.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `parentSessionId` | `string` | **requis** | UUID de session de la vente originale à rembourser |
| `terminalId` | `string\|int` | **requis** | ID du terminal cible |
| `amount` | `int` | **requis** | Montant à rembourser, en centimes |
| `cashRegisterId` | `string` | **requis** | Identification de la caisse |
| `merchantReference` | `?string` | `null` | Référence marchand |
| `currencyCode` | `string` | `'978'` | Code ISO 4217 numérique en string |
| `customerTrns` | `?string` | `null` | Référence client |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat sur le terminal |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat sur le terminal |
| `sessionId` | `?string` | `null` | UUID de session pour le remboursement (auto-généré si `null`) |
| `extra` | `array` | `[]` | Champs API additionnels |

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
$refund = $viva->transactions->refund(
    parentSessionId: $sale['session_id'],
    terminalId:      '16000010',
    amount:          1170,
    cashRegisterId:  'CR-01',
);
$viva->pollUntilComplete($refund['session_id']);
```

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

```php
public function unreferencedRefund(
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:unreferenced-refund`. Remboursement
autonome, non lié à une vente originale.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `terminalId` | `string\|int` | **requis** | ID du terminal cible |
| `amount` | `int` | **requis** | Montant à rembourser, en centimes |
| `cashRegisterId` | `string` | **requis** | Identification de la caisse |
| `merchantReference` | `?string` | `null` | Référence marchand |
| `currencyCode` | `string` | `'978'` | Code ISO 4217 numérique en string |
| `customerTrns` | `?string` | `null` | Référence client |
| `showTransactionResult` | `?bool` | `null` | Afficher le résultat sur le terminal |
| `showReceipt` | `?bool` | `null` | Afficher le reçu + résultat sur le terminal |
| `sessionId` | `?string` | `null` | UUID de session (auto-généré si `null`) |
| `extra` | `array` | `[]` | Champs API additionnels (ex : `fiscalisationData`) |

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
$refund = $viva->transactions->unreferencedRefund(
    terminalId:     '16000010',
    amount:         1500,
    cashRegisterId: 'CR-01',
);
$viva->pollUntilComplete($refund['session_id']);
```

#### `fastRefund()` — Remboursement rapide (Visa/MC/Maestro)

```php
public function fastRefund(
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:fast-refund`. Remboursement swift sur
Visa / Mastercard / Maestro. Mêmes paramètres que `unreferencedRefund()`.

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
$refund = $viva->transactions->fastRefund(
    terminalId:     '16000010',
    amount:         1170,
    cashRegisterId: 'CR-01',
);
$viva->pollUntilComplete($refund['session_id']);
```

#### `rebate()` — Rabais sur carte

```php
public function rebate(
    string|int $terminalId,
    int $amount,
    string $cashRegisterId,
    ?string $merchantReference = null,
    string $currencyCode = '978',
    ?string $customerTrns = null,
    ?bool $showTransactionResult = null,
    ?bool $showReceipt = null,
    ?string $sessionId = null,
    array $extra = [],
): array // array{session_id: string, response: array<string, mixed>}
```

Endpoint : `POST /ecr/v1/transactions:rebate`. Rabais vers une carte
Visa / Mastercard / Maestro. Mêmes paramètres que `unreferencedRefund()`.

**Retourne :** `array{session_id: string, response: array<string, mixed>}`

```php
$rebate = $viva->transactions->rebate(
    terminalId:     '16000010',
    amount:         500,
    cashRegisterId: 'CR-01',
);
$viva->pollUntilComplete($rebate['session_id']);
```

#### `createAction()` — Créer une action sur le terminal

```php
public function createAction(array $payload): array
// array{terminalId?: string, cashRegisterId?: string, actionType?: string, actionId?: string}
```

Endpoint : `POST /ecr/v1/actions`. Crée une action à invoquer sur le terminal
(ex : `aade-fim-control`). Le payload est transmis tel quel (camelCase) et doit
inclure `terminalId`, `cashRegisterId`, `actionType` et `request`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `payload` | `array` | **requis** | Corps de la requête d'action |

**Retourne :** `array{terminalId?, cashRegisterId?, actionType?, actionId?}` —
notamment l'`actionId` à passer à `getAction()`.

```php
$action = $viva->transactions->createAction([
    'terminalId'     => '16000010',
    'cashRegisterId' => 'XDE384678UY',
    'actionType'     => 'aade-fim-control',
    'request'        => [
        'token' => 'ECR0210U/RCFB77000041/CMAC_K:299BFD51...:CC5FFF',
    ],
]);

echo $action['actionId']; // f7287549-e93a-4d33-b936-000027d326d0
```

#### `getAction()` — Récupérer le résultat d'une action

```php
public function getAction(string $actionId): array
// array<string, mixed>
```

Endpoint : `GET /ecr/v1/actions/{actionId}`. Retourne HTTP 202 (body vide → `[]`)
tant que l'action est en cours de traitement, ou le résultat une fois terminée.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `actionId` | `string` | **requis** | ID de l'action (retourné par `createAction()`) |

**Retourne :** `array<string, mixed>` (`successfullyProcessed`, `response`, ...).

```php
$result = $viva->transactions->getAction('f7287549-e93a-4d33-b936-000027d326d0');

if (($result['successfullyProcessed'] ?? false) === true) {
    echo $result['response']['status'];  // 'success'
    echo $result['response']['message']; // 'Set decimal amount successful'
}
```

---

### 3. Sessions — `$viva->sessions`

Récupération, listing et abort des sessions Cloud Terminal. Une session
représente le cycle de vie d'une transaction sur un terminal. Après avoir initié
une transaction, pollez la session par son id pour observer son résultat
(`eventId`, `success`, `transactionId`, ...).

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

```php
public function get(string $sessionId): array
// array<string, mixed>
```

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

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

**Retourne :** données de session (`success`, `eventId`, `transactionId`,
`amount`, ...).

```php
$session = $viva->sessions->get($sale['session_id']);

echo $session['eventId'];       // 0 = succès
echo $session['transactionId']; // UUID de la transaction Viva
```

#### `listByDate()` — Lister les sessions d'une date

```php
public function listByDate(string $date, ?bool $aadeAutonomouslyOnly = null): array
// array<int|string, mixed>
```

Endpoint : `GET /ecr/v1/sessions?date=Y-m-d`.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `date` | `string` | **requis** | Date au format `Y-m-d` |
| `aadeAutonomouslyOnly` | `?bool` | `null` | Filtrer aux sessions AADE-autonomes uniquement (Grèce) |

**Retourne :** une liste de sessions.

```php
$sessions = $viva->sessions->listByDate('2026-06-06');

foreach ($sessions as $session) {
    echo ($session['sessionId'] ?? '') . ' — ' . ($session['eventId'] ?? '') . "\n";
}
```

#### `abort()` — Annuler une session active

```php
public function abort(string $sessionId, string $cashRegisterId): array
// array<string, mixed>
```

Endpoint : `DELETE /ecr/v1/sessions/{sessionId}?cashRegisterId=...`. Seule la
caisse ayant créé la transaction peut l'annuler.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de la session à annuler |
| `cashRegisterId` | `string` | **requis** | Identification de la caisse |

**Retourne :** `array<string, mixed>`

```php
$viva->sessions->abort($sale['session_id'], cashRegisterId: 'CR-01');
```

---

### 4. Polling — `pollUntilComplete()`

```php
public function pollUntilComplete(
    string $sessionId,
    int $timeoutSeconds = 120,
    int $intervalMs = 3000,
): array // array<string, mixed>
```

Poll une session jusqu'à un état terminal ou expiration du timeout. Continue
de poller tant que `eventId` vaut `IN_PROGRESS` (1100), en dormant `intervalMs`
entre les tentatives. S'arrête aussi sur un `eventId` inconnu (fail-safe).

Disponible sur la ressource `sessions` **et** comme raccourci sur le client.

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `sessionId` | `string` | **requis** | UUID de la session à poller |
| `timeoutSeconds` | `int` | `120` | Temps d'attente total maximum (secondes) |
| `intervalMs` | `int` | `3000` | Délai entre deux tentatives (millisecondes) |

**Retourne :** l'état final de la session. Si le timeout est atteint sans
réponse, retourne `['success' => false, 'eventId' => 1003, 'message' => 'SDK poll timeout']`.

```php
// Sur le client (raccourci)
$result = $viva->pollUntilComplete($sale['session_id'], timeoutSeconds: 90);

// Ou sur la ressource sessions
$result = $viva->sessions->pollUntilComplete($sale['session_id'], 90, 2000);

use QrCommunication\VivaCloudTerminal\Enums\EcrEventId;

$event = EcrEventId::tryFrom($result['eventId'] ?? -1);
if ($event?->isSuccessful()) {
    // Paiement confirmé
}
```

---

## Enums

### `EcrEventId`

```php
namespace QrCommunication\VivaCloudTerminal\Enums;

enum EcrEventId: int
{
    case SUCCESS            = 0;
    case TERMINAL_TIMEOUT   = 1003;
    case DECLINED           = 1006;
    case ABORTED            = 1016;
    case INSUFFICIENT_FUNDS = 1020;
    case GENERIC_ERROR      = 1099;
    case IN_PROGRESS        = 1100;
    case BAD_PARAMS         = 6000;

    public function isTerminal(): bool;   // true sauf IN_PROGRESS
    public function isSuccessful(): bool;  // true uniquement pour SUCCESS
    public function shouldPoll(): bool;    // true uniquement pour IN_PROGRESS
    public function label(): string;       // libellé lisible
}
```

| eventId | Cas | `isSuccessful()` | `shouldPoll()` | `label()` |
|---------|-----|:---:|:---:|---------|
| 0 | `SUCCESS` | oui | | Transaction successful |
| 1003 | `TERMINAL_TIMEOUT` | | | Terminal timed out |
| 1006 | `DECLINED` | | | Transaction declined |
| 1016 | `ABORTED` | | | Transaction aborted |
| 1020 | `INSUFFICIENT_FUNDS` | | | Insufficient funds |
| 1099 | `GENERIC_ERROR` | | | Generic error |
| 1100 | `IN_PROGRESS` | | oui | In progress |
| 6000 | `BAD_PARAMS` | | | Bad parameters |

```php
use QrCommunication\VivaCloudTerminal\Enums\EcrEventId;

$event = EcrEventId::tryFrom($result['eventId']);
$event?->isSuccessful(); // true en cas de succès
$event?->shouldPoll();   // true tant que IN_PROGRESS (1100)
$event?->label();        // libellé lisible
```

### `Environment`

```php
namespace QrCommunication\VivaCloudTerminal\Enums;

enum Environment: string
{
    case DEMO       = 'demo';
    case PRODUCTION = 'production';

    public function accountsUrl(): string; // hôte OAuth token (/connect/token)
    public function apiUrl(): string;      // hôte API /ecr/v1/
}
```

| Cas | `accountsUrl()` | `apiUrl()` |
|-----|-----------------|------------|
| `DEMO` | `https://demo-accounts.vivapayments.com` | `https://demo-api.vivapayments.com` |
| `PRODUCTION` | `https://accounts.vivapayments.com` | `https://api.vivapayments.com` |

---

## Gestion des erreurs

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

### Hiérarchie

```
RuntimeException
└── VivaException                    // Classe de base
    ├── ApiException                 // Erreurs HTTP 4xx / 5xx
    └── AuthenticationException      // OAuth2 invalide (401)
```

### Méthodes de `ApiException`

| Méthode | Type | Description |
|---------|------|-------------|
| `getHttpStatus()` | `int` | Code HTTP de la réponse |
| `getErrorText()` | `?string` | Message d'erreur Viva lisible |
| `getErrorCode()` | `?int` | Code d'erreur Viva, si présent |

### Exemple complet

```php
use QrCommunication\VivaCloudTerminal\Exceptions\ApiException;
use QrCommunication\VivaCloudTerminal\Exceptions\AuthenticationException;
use QrCommunication\VivaCloudTerminal\Exceptions\VivaException;

try {
    $viva->transactions->sale(
        terminalId:     '16000010',
        amount:         1170,
        cashRegisterId: 'CR-01',
    );
} catch (AuthenticationException $e) {
    // Handshake OAuth échoué (client_id/secret invalides)
} catch (ApiException $e) {
    // Erreur HTTP 4xx/5xx de l'API
    echo $e->getHttpStatus();   // ex : 409
    echo $e->getErrorText();    // message lisible
    echo $e->getErrorCode();    // code Viva, si présent
} catch (VivaException $e) {
    // Classe de base pour toutes les exceptions SDK
}
```

---

## Carte de test

Environnement `demo` (sandbox).

| Champ | Valeur |
|-------|--------|
| Numéro de carte | `4111 1111 1111 1111` |
| CVV | `111` |
| Mot de passe 3DS | `Secret!33` |

Montants de déclin courants : `9951` (fonds insuffisants), `9954` (carte
expirée), `9920` (carte volée), `9957` (non autorisé), `9961` (limite de
retrait).
