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

17 KiB
Raw Permalink Blame History

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 Art. 54-bis lett. (a)
Numero dell'ordine orderName (+ orderId interno) orderName / orderId orderName = mostrato (#1234); orderId = GID Shopify risolto dal lookup
Email email email Mezzo elettronico per la conferma, lett. (c)
Dichiarazione di recesso testoDichiarazione statementText Testo integrale trasmesso, va nella ricevuta
— (generato al submit) trasmessoAt transmittedAt Sì (auto) Istante di TRASMISSIONE = click "Conferma recesso"
— (dal contesto) canale channel guest | account
— (dal contesto) locale locale 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)