# Canali OTA

# Canali OTA

Superficie di prenotazione in ingresso per le OTA (partner tipo Viator).
Il contratto è indipendente dal provider e stabile — gli stessi percorsi
servono ogni provider, `{provider}` seleziona la mappatura del payload.
Viator è uno skeleton documentato, `generic` parla questo contratto così
com'è per integrazioni su misura, GetYourGuide risponde `501` fino alla
certificazione. Solo in ingresso: l'app non spinge mai disponibilità al
partner.

## Endpoint

```
GET  /api/ota/{provider}/availability?product_id={id}&date=YYYY-MM-DD[&to_date=YYYY-MM-DD]
POST /api/ota/{provider}/bookings
POST /api/ota/{provider}/bookings/{external_reference}/cancel
```

`{provider}` è uno tra `viator`, `getyourguide`, `generic`. Provider
sconosciuti o disabilitati rispondono `404`.

Rate limit: 60 richieste al minuto per collegamento.

## Autenticazione

Ogni richiesta porta due header:

- `X-Ota-Connection: {public_id}` — l'id pubblico del collegamento, visibile
  nella pagina admin dei canali OTA. Mai l'id interno.
- `X-Ota-Signature: {hex}` — HMAC-SHA256 esadecimale con chiave il segreto di
  firma del collegamento (mostrato una sola volta alla creazione o rotazione)
  calcolato su:

```
{METHOD}\n{REQUEST-URI}\n{RAW-BODY}
```

`REQUEST-URI` è l'esatto request target inclusa la query string —
`/api/ota/viator/availability?product_id=P-1&date=2027-01-15`. `RAW-BODY` è
il corpo non parsato (stringa vuota per le GET).

```php
$signature = hash_hmac(
    'sha256',
    "POST\n/api/ota/viator/bookings\n".$rawJsonBody,
    $webhookSecret,
);
```

Errori: collegamento sconosciuto `404 {"error":"unknown_connection"}`; firma
mancante o errata `401 {"error":"bad_signature"}`. L'azienda si risolve
sempre dal collegamento — il payload non può scegliere il tenant.

## Mappatura prodotti

I partner vendono valori `external_product_id`. Ogni id si mappa
nell'interfaccia admin a un solo prenotabile — un evento o un'area — più una
`price_key` opzionale (null usa la risoluzione prezzo predefinita del
prenotabile). Gli id non mappati rispondono `404 {"error":"unknown_product"}`.

## Disponibilità

```json
GET /api/ota/generic/availability?product_id=P-1&date=2027-01-15

{
  "product_id": "P-1",
  "days": [
    {"date": "2027-01-15", "available": true, "remaining": 7}
  ]
}
```

- `available` false significa chiuso, esaurito o fuori dalla finestra vendita.
- `remaining` è la capacità residua; `null` significa senza limite.
- I prodotti-area restituiscono in più `windows: [{from, to, remaining}]` —
  i segmenti vendibili del giorno. Ogni segmento ha la propria disponibilità
  (capienza del segmento meno il picco simultaneo, limitata da ogni pool
  giornaliero che il segmento occupa); il `remaining` del giorno è il
  gruppo più grande che entra in UN solo segmento, non il totale del giorno.
- Il `remaining` di un evento è limitato da OGNI data del suo periodo —
  una prenotazione consuma il pool di ogni giorno che l'evento occupa.
- Un evento senza fine vale solo nel suo giorno di inizio.
- `to_date` estende l'intervallo fino a 31 giorni.

## Creare una prenotazione

```json
POST /api/ota/generic/bookings

{
  "product_id": "P-1",
  "external_reference": "partner-ref-1001",
  "customer": {"name": "Mario Bianchi", "email": "mario@example.com", "phone": "+39002"},
  "number": 2,
  "from": "2027-01-15T10:00:00+00:00",
  "to": "2027-01-15T11:00:00+00:00",
  "date": "2027-01-15"
}
```

`from`/`to` valgono per i prodotti-area (finestre vendibili); `date` è il
giorno di calendario per i prodotti-evento e deve cadere dentro il periodo
dell'evento.

I prodotti-evento prenotano la foglia che il canale può indirizzare: un
evento semplice si prenota direttamente, un evento con fasce orarie prenota
la fascia che inizia a `from` (`from` diventa obbligatorio — mancante o non
corrispondente risponde `422`), e un evento con posti risponde
`available: false` invece di pubblicizzare uno slot che il partner non
potrebbe mai prenotare (la scelta del posto resta sul widget).

Risposta:

```json
{"booking": {"reference": "partner-ref-1001", "status": "confirmed",
             "booking_code": "BK-AB12", "number": 2,
             "created_at": "2027-01-10T09:30:00+00:00"}}
```

Le prenotazioni arrivano gratuite — se il prenotabile mappato richiederebbe
un pagamento la prenotazione è rifiutata con `422 {"error":"payment_required"}`
e la capacità impegnata viene rilasciata. Il denaro incassato dal partner si
riconcilia fuori dall'app.

Gli errori di validazione rispondono `422` con `error` (`booking_failed`) e
una mappa `fields`. I replay di un riferimento fallito restituiscono lo
stesso esito registrato.

### Idempotenza

`external_reference` è la chiave di idempotenza, unica per collegamento. Un
retry che ri-invia lo stesso riferimento riceve l'esito registrato — `200`
con lo stesso payload di prenotazione — senza toccare di nuovo la capacità.
Non riusare mai un riferimento per ordini diversi.

## Annullamento

```
POST /api/ota/{provider}/bookings/{external_reference}/cancel
```

Annulla la prenotazione sottostante attraverso il normale flusso di
cancellazione e porta la riga del registro a `cancelled`. Riferimento
sconosciuto → `404 {"error":"unknown_reference"}`. Annullare un riferimento
già annullato è un no-op `200`. Il rimborso del denaro incassato dal partner
è affare del partner — l'endpoint non innesca mai un rimborso.

## Checklist onboarding partner

1. Owner/admin crea un collegamento nella pagina dei canali OTA
   (`/ota`, feature di piano `ota`) e copia il `public_id`.
2. Ruota/copia il segreto di firma una volta — dopo è solo in scrittura.
3. Mappa ogni `external_product_id` del partner su un evento o un'area
   (+ `price_key` opzionale).
4. Il partner firma `METHOD\nREQUEST-URI\nRAW-BODY` a ogni chiamata e invia
   entrambi gli header.
5. Smoke-test `availability` → `bookings` → `cancel` con un riferimento
   usa-e-getta, poi go-live.


---
> Source: https://placeful-app.com/docs/developer/ota
