# Tariffe non orarie: dominio e persistenza

Questo blocco introduce la base dati dei titoli di sosta non orari senza modificare ancora preventivo, acquisto o interfacce.

## Compatibilità

Le tariffe esistenti ricevono automaticamente:

```text
pricing_mode = hourly
validity_mode = hourly
```

Restano quindi tariffe orarie e continuano a generare normali `ParkingSession`.

## Modalità predisposte

Le tariffe a importo fisso possono usare una delle seguenti regole:

- `service_day_end`: fino al termine dell'orario di pagamento;
- `fixed_hours`: durata fissa in ore, per esempio 24;
- `weekend_window`: finestra settimanale configurabile;
- `fixed_days`: durata fissa in giorni, per esempio 7, 15 o 30;
- `rolling_month`: un mese effettivo dalla decorrenza;
- `calendar_month_end`: fino al termine del mese corrente.

## ParkingPass

Un `ParkingPass` rappresenta il diritto di una targa a sostare in una specifica area per un intervallo. Non rappresenta una permanenza continua: il veicolo può uscire e riparcheggiare nella stessa area durante la validità.

L'intervallo è semiaperto:

```text
[starts_at, ends_at)
```

Il titolo è valido quando `starts_at <= ora < ends_at`.

Gli stati temporali `scheduled`, `active` ed `expired` possono essere derivati dall'ora del server. Gli stati terminali `cancelled`, `revoked` e `failed` restano memorizzati.

## Annullamento

La possibilità di annullamento viene copiata dalla tariffa al titolo tramite `cancellable_before_start`. In questo modo una successiva modifica amministrativa della tariffa non cambia retroattivamente i diritti di un acquisto già concluso.

Un titolo pagato è annullabile dall'utente solo quando:

```text
ora del server < starts_at
```

Dopo l'inizio non è interrompibile. La revoca amministrativa sarà un flusso distinto e motivato.

## Pagamenti e audit

Il titolo conserva separatamente:

- stato del titolo;
- stato del pagamento;
- stato del rimborso;
- importi e commissione;
- riferimenti alle transazioni wallet/carta;
- dati di annullamento e revoca;
- snapshot del calcolo.

Le tabelle `wallet_transactions` e `payment_transactions` ricevono il collegamento `parking_pass_id`.

## Motore temporale deterministico

Il servizio `ParkingPassValidityService` calcola la decorrenza e la scadenza senza creare record né eseguire addebiti. Il risultato usa sempre un intervallo semiaperto `[starts_at, ends_at)` e può quindi essere salvato nello snapshot del futuro acquisto.

Regole applicate:

- `service_day_end`: acquistabile nella finestra generale `start_time`/`end_time`; il titolo resta valido fino alla fine della fascia anche durante un'eventuale pausa gratuita;
- `fixed_hours`: durata in secondi reali; 24 ore equivalgono sempre a 86.400 secondi anche nei cambi d'ora;
- `fixed_days`: scadenza alla stessa ora locale dopo il numero di giorni configurato;
- `weekend_window`: prima dell'apertura viene programmato, durante la finestra parte subito, dopo la chiusura usa il fine settimana successivo; l'orario finale configurato è l'ultimo minuto incluso;
- `rolling_month`: il giorno di attivazione è incluso e la scadenza è alle 00:00 del giorno corrispondente del mese successivo; se quel giorno non esiste, il titolo resta valido fino al termine del mese più corto;
- `calendar_month_end`: scadenza alle 00:00 del primo giorno del mese successivo.

`valid_from` e `valid_to` controllano la data in cui il titolo può iniziare. Il motore non modifica tariffe, soste, titoli o transazioni.


## Limite intenzionale dei primi due blocchi

Il motore calcola soltanto un intervallo verificabile e serializzabile. Non crea ancora `ParkingPass`, non addebita il wallet o la carta e non modifica preventivi, API o schermate. Questi collegamenti verranno introdotti nei blocchi successivi.

## Blocco 3 — preventivo e selezione tariffaria

Il preventivo usa ora un contratto unico per tariffe orarie e titoli non orari.
Ogni alternativa espone `product_type`:

- `parking_session` per la sosta oraria;
- `parking_pass` per il titolo associato a targa e area.

Per un titolo non orario il server ignora l'ora finale scelta nel primo modulo ai
fini della validità e calcola autonomamente `start_at` ed `end_at` tramite
`ParkingPassValidityService`. Il valore iniziale di `end_at` viene conservato
come `requested_end_at` soltanto per compatibilità con i client già esistenti.

Le risposte web e mobile mostrano esplicitamente:

- modalità di prezzo e validità;
- decorrenza e scadenza effettive;
- importo fisso, costo di servizio e totale;
- possibilità di spostare il veicolo e riparcheggiare nella stessa area;
- regola di annullamento prima dell'inizio;
- eventuale decorrenza programmata, per esempio per il weekend successivo.

Una tariffa non oraria non applicabile alla decorrenza richiesta viene esclusa
dall'elenco delle alternative senza nascondere le altre tariffe valide. Se viene
selezionata esplicitamente, il server restituisce invece l'errore preciso della
regola temporale.

In questo blocco l'acquisto dei titoli resta deliberatamente disabilitato. Il
preventivo è consultabile, ma i pulsanti di pagamento non vengono mostrati e i
controller web/mobile rifiutano comunque un tentativo diretto prima di creare
soste, titoli, movimenti wallet o pagamenti. Questa protezione evita che una
tariffa fissa venga registrata per errore come normale `ParkingSession` mentre il
flusso economico di `ParkingPass` non è ancora attivo.

## Blocco 4A — acquisto con wallet web

Il primo canale di acquisto attivato per i titoli non orari è il wallet web.
La separazione dei canali è esplicita nel preventivo:

- `wallet_purchase_available`: acquisto wallet dal sito;
- `card_purchase_available`: pagamento diretto con carta;
- `purchase_available`: compatibilità del client mobile corrente.

Per i pass, in questo blocco, il wallet web è disponibile mentre carta e app
restano chiusi. Questo evita che una versione dell'app non ancora capace di
interpretare `ParkingPass` riceva una risposta di pagamento incompatibile.

L'acquisto viene eseguito dal `ParkingPassPurchaseService` dentro una
transazione database. Il servizio:

1. verifica nuovamente prodotto, area, tariffa, targa e validità;
2. serializza gli acquisti concorrenti tramite lock dell'area;
3. blocca sovrapposizioni con titoli e soste della stessa targa/area;
4. crea il titolo nello stato `pending_payment`;
5. addebita il wallet consumando prima il bonus e poi il saldo reale;
6. collega il movimento al titolo;
7. porta il titolo a `scheduled` o `active` e `payment_status=paid`;
8. conserva snapshot del calcolo e composizione dell'addebito.

Un errore di saldo o una sovrapposizione provoca il rollback completo: non
rimangono titoli orfani né movimenti parziali. I titoli gratuiti vengono
attivati senza creare un movimento wallet.

L'utente dispone inoltre delle pagine **I miei titoli** e **Dettaglio titolo**.
Annullamento con rimborso, acquisto mobile e pagamento carta saranno collegati
nei blocchi successivi.

`ParkingPassStatusService` materializza i passaggi `scheduled` → `active` e
`active`/`scheduled` → `expired`. Il comando `vispa:refresh-parking-passes`,
pianificato ogni minuto, accredita inoltre l'eventuale cashback bonus alla
scadenza. Il titolo viene bloccato prima dell'accredito e conserva
`cashback_wallet_transaction_id`; una seconda esecuzione non produce movimenti
duplicati. Le pagine utente richiamano lo stesso servizio come ulteriore rete
di sicurezza qualora lo scheduler locale non sia in esecuzione.

## Visibilità nell'area utente

Le soste orarie e i titoli non orari restano entità distinte nel dominio, ma
sono presentati insieme nei punti di ingresso principali dell'area utente:

- la dashboard mostra una cronologia unica delle attività recenti;
- i contatori distinguono il contributo di soste e titoli alle validità attive
  e programmate;
- la pagina `Soste e titoli` espone subito i titoli recenti e, separatamente,
  lo storico filtrabile delle soste orarie;
- il dettaglio e le azioni restano specifici per tipo: una sosta può essere
  estesa, mentre un titolo apre il proprio dettaglio e non offre interruzioni.

Questa scelta evita di duplicare i `ParkingPass` nella tabella delle soste, ma
impedisce che un diritto di parcheggio acquistato risulti invisibile nelle
panoramiche usate più spesso dall'utente.

## Annullamento utente e rimborso wallet

Un titolo pagato può essere annullato dall'utente soltanto quando tutte le
condizioni seguenti sono vere:

- il pagamento è completato;
- la tariffa acquistata consente l'annullamento prima dell'inizio;
- l'ora del server è strettamente precedente a `starts_at`;
- il titolo appartiene all'utente autenticato;
- il metodo di pagamento è `wallet` oppure il titolo è gratuito.

L'operazione usa un lock sul `ParkingPass` ed è atomica. Per i pagamenti wallet
viene creato un solo movimento `parking_pass_refund`, collegato sia al titolo
sia al movimento originale. La composizione del pagamento viene ricostruita dai
saldi prima/dopo e, come fallback, dai metadata `cash_used` e `bonus_used`:
credito reale e credito bonus tornano quindi nei rispettivi saldi, anche se nel
frattempo il wallet ha avuto altri movimenti.

Dopo l'annullamento:

- `status = cancelled`;
- `payment_status` resta `paid`, perché il pagamento è realmente avvenuto;
- `refund_status = completed` per i titoli wallet rimborsati;
- `refunded_amount` coincide con il totale pagato;
- il titolo resta nello storico e non autorizza più la targa;
- il cashback pendente viene disattivato;
- un secondo tentativo non può creare un doppio accredito.

I titoli gratuiti vengono annullati senza creare movimenti wallet. Dopo
`starts_at` l'utente non può annullare, interrompere o sospendere il titolo.

## Integrazione mobile dei titoli non orari

Le versioni dell'app compatibili con i titoli inviano l'header:

```text
X-Vispa-Parking-Passes: 1
```

L'header mantiene un rollout retrocompatibile: i client precedenti continuano a
ricevere il preventivo del titolo, ma il relativo acquisto wallet resta
bloccato. Il client aggiornato può invece acquistare il titolo e riceve
`parking_pass` nella risposta, lasciando `session` a `null`.

Endpoint autenticati disponibili:

```text
GET  /api/mobile/passes
GET  /api/mobile/passes/{parkingPass}
POST /api/mobile/passes/{parkingPass}/cancel
```

L'annullamento richiede `confirm_cancellation=true`, verifica la proprietà del
titolo e riutilizza lo stesso servizio atomico del flusso web. La dashboard
mobile conserva le chiavi storiche delle soste e aggiunge conteggi, titoli e
`latest_activity` con prodotti misti.
