Files
pcrt-legal-return/SPEC-MVP-RECESSO.md
tommaso 8ca4de6c0b MVP core recesso: App Proxy flow + ricevuta durevole + design storefront
- A1: SPEC-MVP-RECESSO.md (criteri + copy IT)
- A3: App Proxy /apps/recesso — lookup guest ordine+email (anti-leak), form 2-step,
  funzione dedicata Conferma recesso, persist WithdrawalRequest + transmittedAt + AuditLog
- A4: ricevuta durevole via nodemailer (mailer.server.ts) + receiptSentAt + AuditLog
- Design: recesso.server.ts restyle token CSS light/dark, coerente Shopify
- Fix: @shopify/shopify-api pinnato 13.1.0 (dedupe) -> tsc clean
- PLAN: A5 esteso a motore di stile a 3 livelli

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 09:52:47 +02:00

327 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-MVP-RECESSO — Contratto operativo di build
> **Ruolo:** riferimento vincolante per gli agenti di build (A2/A3/A4) e di test (A9).
> Trasforma `ANALISI-REQUISITI-LEGALI.md` + `PLAN.md §2` in criteri testabili + copy IT esatto.
> **Base legale:** Art. 54-bis Cod. Consumo (D.Lgs 209/2025, recepimento Dir. UE 2023/2673), in
> vigore per contratti conclusi dal 19/6/2026.
>
> **Principi non negoziabili (da ANALISI):**
> 1. Timestamp = **TRASMISSIONE** (momento in cui il consumatore esercita), **non** ricezione.
> 2. Il pulsante è **AGGIUNTIVO**, non sostitutivo (restano modulo Allegato I-B + email/PEC).
> 3. La conferma è una **funzione dedicata a 2 step** ("conferma recesso"), **non** una checkbox.
> 4. **Vietati dark pattern**: recesso non più oneroso della conclusione del contratto.
>
> **Legenda scope:** 🟢 MVP (fasi 2) · 🟡 Fase successiva (A6/A7/A8) · ⚠ da confermare.
---
## 1. Criteri di accettazione testabili (Given/When/Then)
Verificabili da QA (A9). Focus MVP: R1R7, R11, R12.
### R1 — 🟢 Funzione visibile, accessibile, continuativamente disponibile
- **Dato** un ordine B2C con finestra di recesso ancora aperta,
**Quando** l'acquirente visita una qualsiasi pagina dove è attivo l'app embed,
**Allora** vede il pulsante con etichetta esatta **"Recedere dal contratto qui"**.
- **Dato** un ordine consegnato oggi (giorno 0),
**Quando** l'acquirente apre la funzione in qualunque momento entro la finestra,
**Allora** la funzione è raggiungibile e operativa (nessuno stato "disabilitato").
- **Dato** un ordine con finestra **scaduta**,
**Quando** l'acquirente tenta il recesso,
**Allora** il sistema mostra un messaggio di finestra chiusa e NON consente l'invio.
- **Negativo:** l'etichetta non deve mai essere ambigua ("Gestisci ordine", "Resi") → fail.
### R2 — 🟢 Accesso guest (buona prassi, non login forzato)
- **Dato** un utente NON autenticato,
**Quando** inserisce numero ordine + email corretti,
**Allora** completa l'intero flusso senza creare account né effettuare login.
- **Negativo:** se in qualunque step compare un obbligo di login/registrazione → fail (viola "facilmente accessibile").
- **Dato** un utente autenticato, **Quando** apre la funzione, **Allora** i campi noti (nome, email, ordine) sono precompilati ma modificabili.
### R3 — 🟢 Raccolta dei 4 dati di legge
- **Dato** il form di recesso,
**Quando** l'acquirente lo compila,
**Allora** sono raccolti e persistiti: (a) nome e cognome, (b) identificativo ordine/contratto,
(c) email (mezzo elettronico per la conferma), (d) testo della dichiarazione di recesso.
- **Negativo:** invio con uno dei 4 campi obbligatori vuoto → bloccato con messaggio di campo mancante.
- **Dato** l'invio riuscito, **Quando** si ispeziona il DB, **Allora** esiste un record `WithdrawalRequest` con tutti e 4 i valori.
### R4 — 🟢 Conferma a 2 step, funzione dedicata, anti-dark-pattern
- **Dato** il form compilato,
**Quando** l'acquirente conferma la compilazione,
**Allora** viene mostrata una **schermata di riepilogo** con una funzione dedicata etichettata **"Conferma recesso"**.
- **Dato** la schermata di riepilogo,
**Quando** l'acquirente NON attiva "Conferma recesso",
**Allora** nessuna richiesta viene registrata (l'invio è impossibile senza la 2ª azione esplicita).
- **Negativo (dark pattern):** la conferma NON deve essere una checkbox pre-flaggata, un timer, un
pop-up dissuasivo o un retention nudge → qualsiasi di questi = fail.
- **Dato** i due step, **Quando** si contano le azioni, **Allora** sono esattamente 2 (compila → conferma), senza attriti aggiuntivi.
### R5 — 🟢 Ricevuta su supporto durevole con timestamp di TRASMISSIONE
- **Dato** una conferma andata a buon fine (attivazione di "Conferma recesso"),
**Quando** il backend registra la richiesta,
**Allora** `transmittedAt` = istante dell'attivazione della conferma (NON istante di ricezione/lettura email).
- **Dato** la richiesta registrata,
**Quando** trascorre meno di 1 minuto,
**Allora** viene inviata al consumatore una ricevuta (email, in MVP via provider stub/log) che contiene:
**(1)** testo integrale della dichiarazione, **(2)** identificativo ordine, **(3)** data e ora di **trasmissione**, **(4)** nome del consumatore.
- **Negativo:** ricevuta priva anche di uno solo di questi 4 elementi → fail.
- **Dato** MVP con provider stub, **Quando** l'email non parte davvero, **Allora** il contenuto della ricevuta è comunque generato e loggato in modo riproducibile (valore probatorio).
### R6 — 🟢 Onere probatorio / audit log immutabile
- **Dato** ogni evento (creazione richiesta, invio ricevuta),
**Quando** l'evento accade,
**Allora** è scritto un record `AuditLog` **append-only** con timestamp e hash del payload.
- **Negativo:** possibilità di UPDATE/DELETE su `AuditLog` dall'applicazione → fail.
- **Dato** una richiesta, **Quando** la si audita mesi dopo, **Allora** si può dimostrare data/ora e contenuto della dichiarazione dal server (non solo dall'email).
### R7 — 🟢 Non più onerosa della conclusione del contratto
- **Dato** il flusso di recesso completo,
**Quando** si contano i passaggi utente,
**Allora** sono **≤** ai passaggi del checkout dello stesso store.
- **Negativo:** presenza di pop-up di dissuasione, offerte di trattenimento, campi non necessari,
step aggiuntivi rispetto al checkout → fail.
### R11 — 🟢 Pulsante aggiuntivo, non sostitutivo
- **Dato** l'app installata,
**Quando** si ispezionano i canali di recesso esistenti (modulo tipo Allegato I-B, email, PEC),
**Allora** restano tutti attivi e raggiungibili; l'app non li disabilita né li nasconde.
- **Dato** il flusso app, **Quando** l'acquirente lo usa, **Allora** vede la **nota di coesistenza** (§2) che ricorda gli altri mezzi.
### R12 — 🟢 Obblighi informativi Art. 49 (ubicazione della funzione)
- **Dato** il flusso di recesso,
**Quando** l'acquirente lo apre,
**Allora** vede il **testo informativo sul diritto di recesso** (§2) PRIMA dell'invio.
- **Dato** le informazioni precontrattuali,
**Quando** il merchant configura l'app,
**Allora** è disponibile un testo che indica **dove si trova** la funzione nell'interfaccia (per pagine legali/checkout).
- **Nota:** l'omissione di questa informazione attiva la proroga Art. 53 (12 mesi + 14 gg) → vedi §5.
### R9 — 🟡 Item escluso Art. 59 (criterio anticipato, motore in A6)
- **Dato** un item marcato come escluso (su misura / deperibile / sigillato igiene),
**Quando** l'acquirente apre il recesso per quell'item,
**Allora** l'item è segnalato come non recedibile con il **motivo** (messaggio §2), senza recesso "cieco".
- **MVP:** in fase 2 non è richiesto il motore di esclusione; il copy è pronto per A6.
---
## 2. Copy deck IT — stringhe ESATTE (hardcodabili)
> Tutte le stringhe sono in IT (lingua del contratto per gli store target). i18n = 🟡 Fase A7.
> Placeholder con `{{doppie graffe}}`. Il testo tra virgolette è letterale.
### 2.1 Etichetta pulsante di avvio 🟢
```
Recedere dal contratto qui
```
Varianti ammesse (equivalenti statutari, se serve adattare la resa grafica — mantenere il verbo "recedere"):
- `Recedi dal contratto qui`
- `Recedere dal contratto qui` *(forma preferita, coerente col testo di legge)*
### 2.2 Etichetta funzione di conferma (step 2) 🟢
```
Conferma recesso
```
### 2.3 Label dei campi del form 🟢
| Campo | Label esatta | Placeholder / hint |
|---|---|---|
| Nome e cognome | `Nome e cognome` | `Mario Rossi` |
| Identificativo ordine/contratto | `Numero dell'ordine` | `es. #1234` |
| Email (mezzo elettronico per la conferma) | `Email` | `La tua email dell'ordine` |
| Testo della dichiarazione di recesso | `Dichiarazione di recesso` | vedi testo precompilato sotto |
**Testo precompilato (editabile) della dichiarazione** (buona prassi, deriva dall'Allegato I-B):
```
Con la presente comunico il recesso dal contratto di vendita relativo all'ordine {{orderName}}.
```
### 2.4 Testo informativo sul diritto di recesso (Art. 49) — mostrato prima dell'invio 🟢
```
Hai diritto di recedere da questo contratto entro 14 giorni senza dover fornire alcuna motivazione.
Il termine decorre dalla consegna del bene (o dalla conclusione del contratto per i servizi).
Compilando e confermando questo modulo eserciti il recesso: riceverai via email una ricevuta con
la data e l'ora di trasmissione della tua dichiarazione.
```
### 2.5 Template email — ricevuta su supporto durevole 🟢
**Oggetto:**
```
Ricevuta della tua richiesta di recesso — Ordine {{orderName}}
```
**Corpo:**
```
Gentile {{customerName}},
confermiamo di aver ricevuto la Sua dichiarazione di recesso relativa all'ordine {{orderName}},
trasmessa tramite la funzione di recesso presente sul nostro sito.
Dettagli della richiesta:
- Data e ora di trasmissione: {{transmittedAt}}
- Ordine: {{orderName}}
- Nome del consumatore: {{customerName}}
Testo integrale della dichiarazione di recesso trasmessa:
"{{statementText}}"
Questa comunicazione costituisce la ricevuta su supporto durevole della Sua dichiarazione di
recesso, ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora sopra indicate
attestano il momento della trasmissione della dichiarazione.
Le invieremo separatamente le istruzioni per l'eventuale restituzione dei beni e i tempi di rimborso.
Restano comunque validi anche gli altri mezzi per esercitare il recesso (modulo tipo di cui
all'Allegato I, parte B, o qualsiasi altra dichiarazione esplicita, anche via email): questa
funzione è aggiuntiva e non sostituisce tali strumenti.
{{shopName}}
```
> **Vincolo build:** `{{transmittedAt}}` va reso in formato leggibile e conservabile con fuso
> orario esplicito, es. `06/07/2026, 14:32:07 (Europe/Rome, CEST)`. È lo stesso istante di `transmittedAt` del DB.
### 2.6 Messaggi di successo / conferma finale 🟢
**Schermata finale (dopo "Conferma recesso"):**
```
Recesso trasmesso correttamente.
Abbiamo registrato la tua dichiarazione di recesso per l'ordine {{orderName}} in data {{transmittedAt}}.
Ti abbiamo inviato una ricevuta all'indirizzo {{email}}.
```
**Sotto-testo (coesistenza):** vedi §2.9.
### 2.7 Messaggi di errore 🟢
| Caso | Messaggio (UI) |
|---|---|
| Ordine non trovato **o** email non corrispondente (stesso messaggio, no leak) | `Non abbiamo trovato un ordine con questi dati. Verifica il numero dell'ordine e l'email usata per l'acquisto.` |
| Campo obbligatorio mancante | `Compila tutti i campi obbligatori per continuare.` |
| Email in formato non valido | `Inserisci un indirizzo email valido.` |
| Finestra di recesso scaduta | `Il termine di 14 giorni per il recesso su questo ordine è terminato. Puoi comunque contattarci per altre richieste.` |
| Errore generico invio | `Si è verificato un problema. Riprova tra qualche istante; se persiste, contattaci.` |
> **Regola anti-enumeration (R2/§6):** "ordine inesistente" ed "email non combaciante" DEVONO
> mostrare lo **stesso identico messaggio**. Nessun dettaglio che riveli l'esistenza dell'ordine.
### 2.8 Messaggio prodotto escluso (Art. 59) 🟡 (copy MVP-ready, logica in A6)
```
Per questo prodotto il diritto di recesso non è previsto ({{motivoEsclusione}}, ai sensi
dell'art. 59 del Codice del Consumo). Per informazioni o altre richieste, contattaci.
```
Valori di `{{motivoEsclusione}}`:
- `prodotto realizzato su misura o personalizzato`
- `prodotto deperibile o a rapida scadenza`
- `prodotto sigillato, aperto dopo la consegna, non restituibile per motivi igienici o di salute`
### 2.9 Nota di coesistenza (pulsante aggiuntivo) 🟢
```
Questa funzione è un modo aggiuntivo per esercitare il recesso. Puoi comunque usare il modulo
tipo (Allegato I, parte B) o inviare qualsiasi dichiarazione esplicita, anche via email.
```
---
## 3. Campi raccolti (UI) → modello dati Prisma `WithdrawalRequest`
> Modello sorgente: `PLAN.md §3`. I nomi Prisma nel PLAN sono in italiano; qui affiancati agli
> alias EN citati nel brief. **⚠ Scelta da confermare:** standardizzare i nomi campo (IT vs EN).
| Campo UI | Campo Prisma (PLAN §3) | Alias EN (brief) | Obbligatorio | Note |
|---|---|---|---|---|
| Nome e cognome | `nomeCliente` | `customerName` | **Sì** | Art. 54-bis lett. (a) |
| Numero dell'ordine | `orderName` (+ `orderId` interno) | `orderName` / `orderId` | **Sì** | `orderName` = mostrato (#1234); `orderId` = GID Shopify risolto dal lookup |
| Email | `email` | `email` | **Sì** | Mezzo elettronico per la conferma, lett. (c) |
| Dichiarazione di recesso | `testoDichiarazione` | `statementText` | **Sì** | Testo integrale trasmesso, va nella ricevuta |
| — (generato al submit) | `trasmessoAt` | `transmittedAt` | **Sì (auto)** | Istante di TRASMISSIONE = click "Conferma recesso" |
| — (dal contesto) | `canale` | `channel` | **Sì** | `guest` \| `account` |
| — (dal contesto) | `locale` | `locale` | **Sì** | MVP fisso `it`; multi-lingua = 🟡 A7 |
| — (derivato) | `tipoProdotto` | — | 🟡 A6 | beni/servizi/digitale, per calcolo scadenza |
| — (sistema) | `stato` | — | Sì (default) | `trasmessa` all'invio |
| — (sistema) | `ricevutaInviataAt` | — | No | valorizzato dopo invio ricevuta |
| — (derivato) | `scadenzaCalcolata` | — | 🟡 A6 | dal motore compliance (§5) |
**Obbligatori MVP (blocco invio se mancanti):** `nomeCliente`, `orderName`, `email`, `testoDichiarazione`.
**Auto MVP:** `trasmessoAt`, `canale`, `locale`, `stato`.
---
## 4. Flusso a 2 step (anti-dark-pattern) 🟢
```
STEP 0 — Avvio
Pulsante "Recedere dal contratto qui" (Theme App Extension) → apre /apps/recesso (App Proxy).
STEP 1 — Lookup guest
Input: Numero ordine + Email. Nessun login richiesto.
→ match OK: prosegue. → miss: messaggio unico anti-leak (§2.7), nessun dettaglio.
STEP 2 — Form dati + dichiarazione
Campi: Nome e cognome, Email (precompilata), Dichiarazione (precompilata, editabile).
Mostrato SOPRA il form: testo informativo Art. 49 (§2.4).
Azione: "Continua" → schermata di riepilogo.
STEP 3 — Riepilogo + conferma dedicata
Mostra: ordine, nome, email, testo dichiarazione.
Funzione dedicata: "Conferma recesso" (§2.2). ← unica azione che registra la richiesta.
Al click: transmittedAt = ora, persistenza record, AuditLog, invio ricevuta.
STEP 4 — Successo
Messaggio §2.6 + nota coesistenza §2.9.
```
**Vincoli vincolanti (fail se violati):**
- Numero azioni utente ≤ passaggi del checkout dello store (R7).
- Nessun pop-up dissuasivo, offerta di trattenimento, timer, o retention nudge.
- Nessuna checkbox pre-flaggata; la conferma è **funzione/pulsante dedicato**, non un flag.
- Nessun login o registrazione forzati in nessuno step.
- Gli altri canali di recesso restano visibili/attivi (R11).
---
## 5. Regole di scadenza (compliance engine) 🟡 (spec per A6)
> Fonte: ANALISI §2 punto 6, §2 punto 8, §3. Tipi: **beni**, **servizi**, **contenuto digitale**.
> Timezone di riferimento: `Europe/Rome`. La finestra è di **14 giorni**.
**Formule (giorno 0 = data evento di decorrenza):**
- **Beni:** `scadenza = dataConsegna + 14 giorni`.
- Beni in **più lotti/pezzi** (un solo ordine): decorre dalla **consegna dell'ultimo** bene → `dataConsegnaUltimoLotto + 14 giorni`.
- **Servizi:** `scadenza = dataConclusioneContratto + 14 giorni`.
- **Contenuto digitale:** decorrenza propria (a parte); gestione dedicata, **non** assimilare ai beni.
- **Estensione per omessa informazione (Art. 53):** se il merchant NON ha fornito l'informazione
sul diritto/ubicazione della funzione (R12/Art. 49):
`scadenza = decorrenzaBase + 12 mesi + 14 giorni`.
Se l'informazione viene fornita entro i 12 mesi: la finestra dei 14 giorni riparte dalla data in
cui l'informazione è fornita.
**Note per A6:**
- Fonte di verità unica nel motore compliance; input consegna dai webhook fulfillment.
- Fulfillment parziale → tracciare per-item; usare la consegna più recente pertinente all'ordine.
- Alla scadenza, il flusso mostra il messaggio "finestra chiusa" (§2.7) e blocca l'invio.
---
## 6. Lookup guest & prevenzione abuso 🟢
**Regole di match:**
- Chiave di ricerca: **Numero ordine (`orderName`) + Email**, entrambi devono combaciare.
- **Nessun leak:** ordine inesistente ed email non corrispondente producono lo **stesso** messaggio
(§2.7). Nessuna differenza in testo, codice di stato o tempo di risposta osservabile.
**Anti-abuso (superficie di enumeration ordini) — nota per A9/A3:**
- **Rate-limit** sul lookup: per IP e/o per email (es. N tentativi / finestra temporale) → 🟢 predisporre in A3, verifica avversariale in A9.
- Considerare **backoff/captcha** dopo soglia di tentativi falliti.
- Log dei tentativi falliti in `AuditLog` (senza PII sensibile in chiaro non necessaria).
- Risposte a **tempo costante** per non rivelare l'esistenza dell'ordine via timing.
---
## Confini MVP (esplicito)
| In MVP 🟢 | Fase successiva 🟡 |
|---|---|
| Pulsante + flusso guest 2 step | Motore esclusioni Art. 59 (A6) |
| Raccolta 4 dati + persistenza | Calcolo scadenza per tipo prodotto (A6) |
| Conferma dedicata anti-dark-pattern | Logica rimborso / diminuzione valore (A6) |
| Ricevuta durevole con timestamp trasmissione (provider **stub**) | Provider email reale (pre-go-live) |
| AuditLog append-only | i18n multi-lingua EN/DE/FR/ES (A7) |
| Copy IT hardcoded | Webhook GDPR (A8), Billing (A10) |
| Rate-limit base + anti-leak | Rate-limit avanzato / captcha (A9) |