# Contratti di integrazione proposti · non implementati

Nessun endpoint è chiamato dal prototipo. Una futura API `/api/v1` usa JSON, importi interi in centesimi EUR, timestamp ISO 8601 UTC, visualizzazione Europe/Rome. Il browser invia ID prodotto/opzioni/quantità, mai prezzi autorevoli. Validare tipo, lunghezza, enum e limiti anche lato server. Errori uniformi: `{error:{code,message,fields,retryable},requestId}` senza PII nei log.

| Metodo e percorso | Accesso | Richiesta / risposta essenziale |
|---|---|---|
| GET /catalog | Pubblico | catalogVersion, categories, products (id, ingredienti, allergeni, priceCents, optionGroups con min/max, disponibilità) |
| GET /availability?mode=… | Pubblico | serviceStatus, serverTime, zone/slot e capacità indicativa; non riserva |
| POST /quotes | Ospite, rate limit | items[{productId,variantId,extraIds,quantity}], mode, zone, address, slot → quoteId, versione, snapshot, subtotalCents, feeCents, totalCents, expiresAt, changes, zoneStatus |
| Riserva interna a POST /checkouts | Server | Lock atomico quantità/capacità cucina e dipendente, vincolato a quote e scadenza |
| POST /checkouts | Ospite, idempotente | quoteId, version, guest, fulfilment, notes → orderId, reservationExpiresAt e providerCheckoutUrl; pagamenti solo provider |
| POST /webhooks/payment | Firma provider sul corpo originale | Deduplica eventId; registra durevolmente prima di rispondere, elabora in coda con retry e gestione fuori sequenza |
| GET /orders/{id} | Sessione ospite cookie protetto | Stato operativo e finanziario separati, riepilogo, promisedAt, reason, version; no-store, noindex |
| GET /staff/orders | Sessione operatore | Lista paginata con cursor/version e ultimo aggiornamento |
| GET /staff/events | Sessione staff | SSE con eventId e ripresa; fallback polling e rilettura snapshot dopo disconnessione |
| POST /staff/orders/{id}/transitions | Operatore autorizzato | expectedVersion, command, reason, promisedAt → stato/versione; acquisizione fondi prima di ordine lavorabile |
| POST /staff/orders/{id}/assignments | Operatore | expectedVersion, employeeId → assegnazione verificata |
| GET /driver/assignments | Dipendente nominativo | Solo assegnati e campi strettamente necessari |
| POST /driver/assignments/{id}/events | Dipendente assegnato | expectedVersion, departure/delivered/problem, note; problema non completa né rimborsa |
| POST /staff/orders/{id}/refunds | Titolare, conferma rafforzata | amountCents, reason, expectedVersion → refundId, pending; no doppio rimborso oltre acquisito |
| PATCH /staff/catalog/{id} | Gestore menu | expectedVersion, availability/price/options; storico ordini immutabile |
| PATCH /staff/settings | Titolare | expectedVersion, pause modalità, fasce, zone, soglie |
| POST /staff/heartbeat | Sessione tablet | deviceId, lastSeenEvent → serverTime/serviceStatus; assenza prolungata sospende checkout server |
| POST /staff/push-subscriptions | Sessione staff | subscription autorizzata browser → subscriptionId; revoca DELETE /…/{id} |
| GET /staff/reports/daily?date=… | Titolare | Ordinato, acquisito, rimborsato, differiti, commissioni, netto, anomalie; esportazione autorizzata |

## Invarianti e risposte

I comandi con effetti durevoli (non il preventivo di sola lettura) hanno `Idempotency-Key`, ambito utente/operazione, hash payload e risposta durevole; stessa chiave con payload diverso → 409. Transizioni con `expectedVersion` e audit in transazione (autore, prima/dopo, motivo, timestamp); versione concorrente → 409 STALE_VERSION. Quote cambiata → 409 QUOTE_CHANGED con riepilogo da accettare esplicitamente prima di pagare. Disponibilità terminata → 409 CAPACITY_UNAVAILABLE; zona esclusa → 422 ZONE_EXCLUDED; confine → 422 MANUAL_REVIEW_REQUIRED; chiusura → 409 SERVICE_PAUSED; scadenza → 410 RESERVATION_EXPIRED; dati invalidi → 422; senza login → 401; ruolo non consentito → 403; limite tentativi → 429; errore temporaneo → 503 senza dichiarare successo.

Stati operativi: checkout → da accettare → accettazione in corso → accettato → preparazione → pronto → consegna (solo domicilio) → completato. Ritiro: pronto → completato dopo consegna al cliente. Rifiutato/annullato/scaduto terminali. Stato pagamento distinto: attesa/autorizzato/acquisizione in corso/pagato/fallito/annullato/rimborso in corso/rimborsato parziale o totale/contestato. Timeout acquisizione resta in verifica, nessuna cucina finché non confermato. Nessun redirect browser prova un pagamento.

Scadenza riserva, accettazione e pagamento tardivo si risolvono atomicamente; job durevoli liberano capacità, annullano autorizzazioni e riconciliano provider. Pagamento tardivo senza capacità va in verifica, poi annullamento/rimborso se non servibile. Soglie 60/120 secondi e 5 minuti sono proposte da approvare, non timer affidati al browser.

## Confini di sicurezza e infrastruttura

Sessioni staff cookie Secure/HttpOnly/SameSite, CSRF dove applicabile, controllo ruolo e assegnazione su ogni richiesta, account revocabili, MFA titolare. Sessioni ospite imprevedibili con scadenza/revoca, autorizzazione specifica per ordine, no solo progressivo. Vietare cache persistente API, pagine staff, indirizzi e token; il futuro service worker memorizza solo asset statici. Rete assente: nessun comando confermato offline. Aggiornamento PWA proposto all’operatore, nessun reload durante accettazione.

Database relazionale, outbox, webhook deduplicati, job persistenti, email transazionali, monitoraggio senza dati personali, backup cifrato e ripristino provato. Segreti solo configurazione server. Contanti/POS esterni disabilitati finché approvati; riconciliazione distinta. Provider e testi definitivi ancora da scegliere. Nessun contratto qui costituisce implementazione, certificazione di sicurezza o policy legale.

## Adattatore eseguibile del checkout (profilo statico, fase 3)

`api-adapter.js` espone `createOrderApi({baseUrl, csrfToken, transport?})` e non viene istanziato dalla demo. Il profilo statico usa esclusivamente `OrderCore` e memoria volatile: nessuna chiamata di rete, carta, autorizzazione o incasso. Non collegare semplicemente l'adattatore per dichiarare il sito operativo: occorrono server, autenticazione e prove d'integrazione.

- `quote(input)` → `POST /quotes`: inviare solo `items[{productId,variantId,extraIds,quantity}]`, `mode`, `zone`, `slot` e, nella futura integrazione, indirizzo necessario alla verifica geografica. Il server valida indirizzo/zona, apertura, disponibilità e listino. Risposta: `quoteId`, `version`, `expiresAt`, righe snapshot, importi interi EUR in centesimi, minimo, tariffa, fascia con fuso Europe/Rome. La scelta manuale della zona della demo non verifica un indirizzo reale.
- `checkout({quoteId,version,guest:{name,phone,email},fulfilment:{mode,address,intercom,slot},notes}, idempotencyKey)` → `POST /checkouts`. Stessa chiave per ritentare lo stesso tentativo e stesso payload; chiave nuova solo per un nuovo tentativo esplicito. Il server ignora importi client e riserva atomicamente cucina e consegna in transazione con vincoli di capacità, versione e scadenza. Una riserva non può essere assegnata a due clienti; scadenza/accettazione devono avere un unico vincitore. Risposta: `orderId`, `reservationExpiresAt`, `providerCheckoutUrl`. Validare sul server e nel client futuro l'URL HTTPS e l'allowlist del provider prima del redirect; nessun campo carta nel sito.
- `status(orderId)` → `GET /orders/:id`: cookie di sessione ospite HttpOnly, Secure, SameSite, limitato agli ordini autorizzati. L'ID opaco non è una credenziale sufficiente. Mai token nella query, cache o localStorage. Risposta con `version`, stato operativo e finanziario separati, riepilogo, motivi pubblici, scadenze. Nessuna area staff pubblica in produzione.

Tutte le chiamate usano `credentials: include` e `cache: no-store`; POST con token CSRF fornito dal server. Preferire API sullo stesso origin; eventuale CORS deve ammettere solo origini esplicite. Risposte senza dati personali superflui e con `Cache-Control: no-store`. L'adattatore propaga `status` e `code` degli errori JSON. Errori nel formato `{error:{code,message,fields,retryable},requestId}`. Contratto errori: 409 `QUOTE_CHANGED` (nuovo preventivo da riconfermare), `CAPACITY_UNAVAILABLE`, `UNAVAILABLE`; 410 `QUOTE_EXPIRED`; 422 `VALIDATION_ERROR`; 401/403 sessione non autorizzata; 429 limite richieste. Timeout o errore ambiguo: interrogare lo stato protetto e riusare la chiave, mai creare un pagamento duplicato.

Redirect del provider non autorevole. Solo webhook verificati/deduplicati e riconciliazione server decidono pagamento, acquisizione, annullamento e rimborso. Pagamento incerto blocca preparazione; pagamento tardivo su riserva scaduta richiede riconciliazione, non ripristino automatico della capacità. Stati cliente: richiesta ricevuta, accettato, preparazione, pronto, consegna, completato, rifiutato, scaduto, pagamento incerto. La demo permette transizioni manuali dichiarate, non sincronizzazione o protezione reale.

## Versionamento e identificatori (progetto server, da implementare)

Versione HTTP /api/v1/ con slash finale in baseUrl; modifiche additive compatibili, campi ignoti ignorati, enum ignoti mostrati come “in verifica” e azioni bloccate. Cambi incompatibili in /api/v2 con periodo di migrazione e prove consumer; eventi con schemaVersion:1, eventId, aggregateId, aggregateVersion, occurredAt e requestId. Versione numerica per ordine/catalogo e expectedVersion obbligatoria nei comandi staff. PATCH e DELETE richiedono gli stessi controlli CSRF/idempotenza del POST. L'adattatore attuale copre solo quote/checkout/status: le altre API sono contratti da implementare.

Quote: risposta {quoteId,version,expiresAt,items,subtotalCents,feeCents,totalCents,currency:"EUR",slot}. Checkout: {orderId,version,reservationExpiresAt,providerCheckoutUrl}; stato: {orderId,version,state,paymentState,items,totalCents,promisedAt,reason,serverTime}. Nessun recapito nella risposta pubblica. Elenchi staff con {items,nextCursor,serverTime}; eventi con ID monotono per ripresa. Errori già descritti sopra, incluso 409 IDEMPOTENCY_CONFLICT.

Idempotenza server: chiave casuale >=128 bit per tentativo, vincolo unico per soggetto+operazione+chiave e hash canonico payload; risposta persistita atomicamente con comando/outbox. Conservazione minima proposta 7 giorni, da concordare con provider e retention ordini. Retry concorrenti ricevono stessa risorsa o 202 con riferimento protetto. Dopo scadenza della chiave cercare il tentativo protetto prima di crearne un altro; mai dedurre fallimento da timeout. Timeout adattatore non implementato: il consumer reale dovrà usare AbortController e gestire l'esito ambiguo senza reinvio cieco.

Token ordine: generato dal server con CSPRNG >=256 bit, hash nel DB, associato a ordine/sessione, scadenza proposta 24h dalla chiusura, revoca e rinnovo verificato via canale del cliente, rate limit. Preferire cookie HttpOnly Secure SameSite e lettura same-origin. Se serve recupero via email, codice monouso scambiato con POST e poi invalidato; nessun token ordine in URL, log, referrer, analytics, cache o localStorage. ID ordine opaco non concede accesso. Staff con sessione revocabile, ruolo e ambito locale riletti sul DB; CSRF verificato sul server, MFA per titolare e conferma rafforzata per rimborsi. CORS solo origini esplicite, HTTPS, no-store sulle API, rate limit per sessione/IP/risorsa.

## Ruoli e transizioni autorizzate (server richiesto)

| Attore | Risorsa/azione consentita | Negazione obbligatoria |
|---|---|---|
| Ospite | Catalogo, preventivo, checkout; stato dei propri ordini | Ordini altrui, comandi staff |
| Operatore | Ordini del proprio locale, accetta/rifiuta motivando, promette tempo, prepara, pronto, assegna dipendente, completa ritiro | Rimborsi, gestione account e configurazione riservata titolare |
| Dipendente | Solo consegne assegnate; pronto → consegna → completato; segnala problema | Altri incarichi, prezzi, rimborsi, accettazione |
| Titolare | Configurazione, catalogo, account, report e rimborsi motivati; azioni operatore | Ordini di altro locale, rimborso oltre residuo |
| Worker/provider | Aggiorna finanze dopo verifica; scadenze atomiche | Cambi operativi arbitrari o fiducia nel redirect |

Da accettare + autorizzato → accettazione in corso solo operatore/titolare; server acquisisce, conferma provider → accettato/pagato. Fallimento resta bloccato/in verifica. Accettato/pagato → preparazione → pronto solo operatore/titolare. Pronto ritiro → completato al banco; pronto domicilio → assegnazione → consegna → completato dal dipendente assegnato. Rifiuto motivato prima accettazione revoca autorizzazione; dopo accettazione l'annullamento è procedura titolare con motivo e compensazione finanziaria. Terminali non riapribili. Rimborso non completa e non riapre ordine; rimborso totale prima della cucina blocca avvio e richiede risoluzione operativa. La demo OperationsCore non implementa ruoli né tutti questi vincoli: non riutilizzarla come autorizzazione server.

## Webhook, pagamenti e riconciliazione

Usare checkout ospitato dal provider: chiavi private e webhook secret solo server. Verificare firma con SDK/algoritmo del provider sui byte originali prima del parsing, confronto costante, timestamp e finestra replay proposta 5 minuti, secret con rotazione controllata. Eventi invalidi → 400/401 senza effetti; autenticati → inbox durevole con vincolo provider+eventId prima del 2xx. Coda con retry esponenziale/jitter 1–60 secondi, limite tentativi, dead-letter e allarme. Nessun endpoint pubblico di replay: solo titolare/servizio autorizzato e audit.

Worker verifica merchant, valuta, importo, orderId e riferimento pagamento tramite API provider. Gestisce duplicati e fuori sequenza senza regredire uno stato confermato; se ambiguo interroga il provider. Transazione DB aggiorna finanze, ledger, versione e outbox; chiave stabile anche sulle chiamate provider. Autorizzazione, acquisizione e rimborso sono operazioni distinte. Rimborso pending riserva atomicamente il residuo, include già pending nel tetto, conferma solo da provider. Fallimento libera residuo e genera allarme, nessun doppio rimborso manuale.

Riconciliazione proposta ogni 5 minuti per pending/timeout e giornaliera per acquisiti, rimborsi, commissioni e accrediti; lock/lease e cursore durevoli, confronto ledger/provider, allarme scarti, export ristretto titolare. Nessuna correzione contabile silenziosa. Validare cadenze, SLA e retention prima del go-live.

## Email, push, scadenze e recupero

Outbox nella stessa transazione dello stato; worker invia email ricevuta richiesta (non accettazione), accettazione/tempo, rifiuto/scadenza, problema rilevante, rimborso richiesto e confermato. Template versionati, variabili allowlist con escaping (nome, riferimento pubblico, riepilogo, tempo, stato), niente dati carta o token persistenti. Provider transazionale con dominio autenticato, bounce/complaint verificati, deduplica ordine+evento+destinatario, retry e dead-letter; errore email non annulla ordine. Log mascherati con messageId, nessun corpo/recapito completo. Marketing separato, nessuna iscrizione automatica.

Push staff solo consenso esplicito, abbonamenti per utente/dispositivo revocabili, chiave VAPID privata sul server, sola chiave pubblica nel client futuro. Payload minimo “Aggiornamento ordini”, nessun indirizzo o token; aprire vista che rilegge dati autenticati. Rimuovere subscription 404/410, deduplicare eventi. Push e audio non garantiscono ricezione: heartbeat tablet e monitoraggio server sospendono checkout se presidio assente secondo soglia approvata.

Preventivi, riserve, accettazione e sessioni scadono in UTC sul server; timer browser solo visualizzazione. Worker durevoli con lock impediscono gara scadenza/accettazione/pagamento; pagamento tardivo va in verifica e compensazione se non servibile. Soglie proposte da validare: quote 5 minuti, riserva 5 minuti, decisione 120 secondi, heartbeat ogni 30 secondi con sospensione dopo 120 secondi.

Offline: bloccare comandi e mostrare ultimo aggiornamento; nessuna coda di pagamenti/transizioni offline. Riconnessione: riautenticare se necessario, rileggere snapshot e versione, riprendere SSE da Last-Event-ID; cursore scaduto → snapshot completo. Fallback polling con backoff/jitter fino a 60 secondi. Conflitto 409 → rilettura e riconferma umana. Timeout dopo comando → stato tentativo e stessa chiave/payload, mai nuovo addebito automatico. Service worker solo allowlist statica, mai API, token o dati personali.
