- 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
17 KiB
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 §2in 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):
- Timestamp = TRASMISSIONE (momento in cui il consumatore esercita), non ricezione.
- Il pulsante è AGGIUNTIVO, non sostitutivo (restano modulo Allegato I-B + email/PEC).
- La conferma è una funzione dedicata a 2 step ("conferma recesso"), non una checkbox.
- 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: R1–R7, 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
WithdrawalRequestcon 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
AuditLogappend-only con timestamp e hash del payload. - Negativo: possibilità di UPDATE/DELETE su
AuditLogdall'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 quiRecedere 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 ditransmittedAtdel 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 personalizzatoprodotto deperibile o a rapida scadenzaprodotto 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 |
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.
- Beni in più lotti/pezzi (un solo ordine): decorre dalla consegna dell'ultimo bene →
- 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) |