Base legale (ANALISI-REQUISITI-LEGALI.md §5): il considerando 37 della Dir. (UE)
2023/2673 dice che il professionista *can* offrire il recesso su parte del
contratto, non che *deve*. Un pulsante a livello di ordine e' conforme. Ma il form
non lo diceva, mentre le automazioni agivano in modo totale.
- statementTemplate: 'relativo all'INTERO ordine #X' (la dichiarazione ora
corrisponde a cio' che l'app fa davvero).
- SCOPE_HINT sotto la dichiarazione: il recesso riguarda l'intero ordine, per il
parziale contattare il negozio (canale modulo tipo/email, sempre valido).
- Admin: banner quando autoCancelUnfulfilled e' attivo -> annulla e rimborsa
l'INTERO ordine, irreversibile; con ordini multi-articolo meglio tenerlo spento.
- PLAN: A6-ter (recesso parziale) come attivita' OPZIONALE con toggle per-shop
partialWithdrawalEnabled, default OFF. Include la nota che l'idempotenza
(shop, orderId) di fa8ee9c andra' riportata a (shop, orderId, articoli).
- ANALISI-REQUISITI-LEGALI.md §5: punto chiarito + panorama concorrenti (Revize
fa il parziale, Rescindly no, Shopify non ha pulsante nativo, LegalBlink non e'
un concorrente ma un generatore di documenti).
21 KiB
Pizeta Recesso — App Shopify per la funzione di recesso
Piano di sviluppo multi-agente
Obiettivo: App Shopify riutilizzabile che rende qualsiasi negozio conforme alla funzione elettronica di recesso ("pulsante di recesso") obbligatoria — Art. 54-bis Codice del Consumo (D.Lgs 209/2025), recepimento Dir. (UE) 2023/2673. In vigore per contratti conclusi dal 19 giugno 2026. I merchant sono esposti da adesso.
Base legale verificata: vedi
ANALISI-REQUISITI-LEGALI.md(fonti primarie/secondarie). Le voci ⚠ restano da confermare su EUR-Lex prima del go-live.
0. Decisioni bloccate
| Tema | Decisione | Nota |
|---|---|---|
| Distribuzione | Custom distribution ora (2-3 store live) → Public molto più avanti | Custom = no review, ok per pochi store. Account separati → 1 registrazione custom per store. Codice riusato per il futuro public |
| Backend | Template Shopify Remix (Node) + Polaris | Strada ufficiale Shopify |
| Hosting | Fly.io | Riuso setup esistente |
| Database | Postgres + Prisma | Fly Postgres (default) o Supabase (ho le creds) — decido in Fase 0 |
| Email (ricevuta durevole) | Stub in MVP, provider (Resend/Postmark) da decidere prima del go-live | Ricevuta con timestamp |
| Store di test | SOLO pcrt-reso-test (usa e getta) |
⚠ MAI pizeta-pharma-2 o store da trasferire: shopify app dev/install app DISABILITA il transfer in modo irreversibile |
| Consegna storefront | Theme App Extension + App Proxy | installazione senza toccare il tema, guest-capable |
La cartella tema (pizeta-pharma/) NON è l'app — è un tema Liquid, solo target di test.
L'app è questo repo separato.
0-bis. ⚠️ VINCOLO CRITICO — trasferimento store (IRREVERSIBILE)
Il landmine è STRETTO: solo le DRAFT app / shopify app dev disabilitano il transfer.
Lanciare shopify app dev (draft app) contro un development store — o abilitare developer preview —
disabilita il trasferimento in modo permanente e irreversibile (CLI issue #3946). Uno store
transfer-disabled non si trasferisce più: andrebbe ricostruito da zero.
Cosa NON disabilita il transfer (verificato):
- Custom distribution via install link (la nostra app recesso finita): si installa come una normale app, non applica "Transfer Disabled". Installabile anche su store del cliente (via link store-specifico, scadenza 7 giorni; ownership non richiesta).
- Legacy/admin custom app (es. "Import Negozio"): non disabilita il transfer.
Regole operative:
- Sviluppo/test (
shopify app dev= draft) solo supcrt-reso-test(usa e getta). ← unica vera protezione. - Mai
shopify app dev/ draft contro pizeta-pharma-2 o store destinati al cliente. - App finita (custom link): installabile su pizeta + altri store, prima o dopo il transfer, senza rompere niente. Per zero-rischi assoluti, installa dopo il transfer (banale, via link).
- ⚠ Nuance da confermare empiricamente: install custom su un client-transfer store ancora "in your org" (pre-transfer) — fonti in conflitto. Se vuoi installare pre-transfer, testa prima.
0-ter. Strategia distribuzione & isolamento ambienti
Principio: una build public-grade, distribuita Custom ora. Non due sviluppi.
Vincolo di piattaforma (verificato): una app Public — anche unlisted — richiede review Shopify per installarsi su store live. Solo Custom installa su live senza review. Quindi:
- ORA: distribuzione Custom (unico canale per il live senza review) su 2-3 store clienti.
- DOMANI: nuova registrazione Public + review, stesso codice. La distribuzione non è convertibile, ma non si converte: si aggiunge una seconda registrazione.
Codice public-grade dal giorno 1 (così il public futuro = re-registrazione, non rebuild):
- Template Shopify CLI/Remix standard — OAuth multi-tenant, session token (no scorciatoia single-store).
- Dati multi-tenant per-shop dal giorno 1, anche con soli 2-3 negozi.
- Webhook GDPR obbligatori subito.
- Billing API dietro flag (
isPublic): off in custom, on in public. Unico ramo di codice divergente. - Performance/sicurezza a livello review man mano, non retrofit.
Isolamento ambienti (anti-rottura) — creato separato dal giorno 1 in A0:
pcrt-reso-test → staging (Fly) → recesso-custom (Fly, CLIENTI LIVE)
sviluppo collaudo stabile — deploy solo testati — DB proprio
recesso-public (Fly, DOMANI) → registrazione Public + DB proprio
stesso codice, deployment SEPARATO → non tocca mai recesso-custom
Registrazioni Partner distinte + Fly app distinti + DB distinti → il deploy della public non può impattare gli store live sulla custom (isolamento strutturale, non per disciplina).
Regole anti-rottura sulla custom-live: test su pcrt-reso-test prima; migrazioni DB solo
additive/backward-compatible; theme extension versionate (rollback); API version pinnata.
1. Architettura
STOREFRONT (acquirente)
├─ Theme App Extension (app embed) → pulsante "recedere dal contratto qui"
└─ App Proxy /apps/recesso → flusso recesso guest-capable sul dominio dello shop
1. lookup: Order ID + email (guest — buona prassi per "facilmente accessibile")
2. form: nome, dichiarazione, email (precompilato se loggato)
3. STEP 2 conferma: "conferma recesso" (funzione dedicata, anti-accidentale)
4. successo + ricevuta durevole inviata
BACKEND (Fly.io)
├─ App Remix (admin embedded, Polaris)
├─ Postgres (Prisma): richieste, regole, impostazioni, audit
├─ Motore compliance: calcolo finestra, esclusioni Art. 59, tipo-prodotto, scadenza
├─ Ricevuta durevole: email con testo dichiarazione + timestamp di TRASMISSIONE
├─ Webhook: fulfillment (avvio finestra) + 3 GDPR obbligatori
└─ Billing (aggiunto in fase App Store)
Perché Theme App Extension + App Proxy: il pulsante acquirente + form guest devono vivere sul
dominio del negozio e sopravvivere agli aggiornamenti del tema senza che il merchant tocchi il
Liquid. App Proxy consente a un URL storefront (/apps/recesso) di raggiungere il backend
restando sul dominio dello shop — necessario per l'accesso guest e per "facile come acquistare".
2. Matrice requisiti (il contratto su cui ogni agente costruisce)
Corretta e integrata dopo ANALISI-REQUISITI-LEGALI.md.
| # | Requisito legale | Funzionalità | Criteri di accettazione |
|---|---|---|---|
| R1 | Funzione visibile, facilmente accessibile, continuativamente disponibile per tutta la finestra | Pulsante extension + pagina proxy sempre attiva, legata al singolo ordine | Label "recedere dal contratto qui"; funzione attiva tutta la finestra di quell'ordine |
| R2 | Accesso facile (guest = buona prassi, non obbligo) | Lookup Order ID + email | Guest completa il recesso con solo n° ordine + email; nessun login forzato |
| R3 | Raccolta nome, id contratto, mezzo elettronico, dichiarazione | Form recesso | I 4 dati raccolti + persistiti |
| R4 | Conferma a 2 step, funzione dedicata | Funzione "conferma recesso" | Invio impossibile senza 2ª azione esplicita; no dark pattern |
| R5 | Ricevuta su supporto durevole senza ritardo, con dichiarazione + timestamp di trasmissione | Email transazionale | Email < 1 min; contiene testo dichiarazione + data/ora di trasmissione |
| R6 | Onere probatorio del professionista | Persistenza DB + audit log immutabile | Record immutabile richiesta + timestamp + invio ricevuta |
| R7 | Funzione non più onerosa della conclusione (anti dark pattern) | UX ≤ passaggi del checkout | ≤ passaggi/attriti del checkout; no pop-up dissuasivi, pre-flag, retention nudge |
| R8 | Termine 14gg, decorrenza per tipo | Motore compliance | Scadenza corretta per beni (consegna/ultimo lotto) / servizi (conclusione) / digitale |
| R9 | Esclusioni Art. 59 (su misura, deperibili, sigillati igiene) | Regole esclusione (prodotto/collezione/tag) | Item escluso segnalato con motivo; no recesso "cieco" |
| R10 | Non conformità → 14gg → 12 mesi+14gg (Art. 53) | Warning misconfig + calcolo corretto | Admin avvisa su misconfig; scadenza riflette proroga quando applicabile |
| R11 | Pulsante AGGIUNTIVO, non sostitutivo | Coesistenza modulo tipo Allegato I-B + email | App non disabilita/nasconde gli altri canali |
| R12 | Obblighi info Art. 49 (ubicazione funzione) | Testo info recesso + link ubicazione | Info diritto di recesso + posizione funzione mostrate |
| R13 | Rimborso: spese consegna incluse; reso a carico consumatore se informato; diminuzione valore | Logica rimborso + informativa | Schermata informa su chi paga reso; calcolo include spedizione standard |
| R14 | Lingua del contratto/offerta | i18n (IT/EN/DE/FR/ES…) | Flusso acquirente + ricevuta localizzati |
| R15 | Solo B2C | Filtro ordini | Ordini B2B esclusi dal flusso obbligatorio |
| R16 | Webhook GDPR obbligatori Shopify | customers/data_request,customers/redact,shop/redact |
Tutti e 3 implementati + HMAC verificato |
| R17 | Sanzione pulsante non conforme = pratica scorretta Art. 27 (AGCM, fino €10M/4%) | (rischio, non feature) | Documentato; guida merchant alla conformità |
3. Modello dati (bozza Prisma)
- Shop — dominio, accessToken(cifrato), piano, installedAt
- Settings — labelPulsante, brandColors, indirizzoReso, giorniFinestraDefault, overrideMercati, testoInfoRecesso, email (emailSubject/emailIntro/emailNote), + toggle per-shop: notifyEnabled/notifyEmail, tagEnabled, enforceWindow, enforceExclusions, stateAwareEmail, autoCancelUnfulfilled, returnAtCustomerExpense, returnInstructions
- ExclusionRule — scope(prodotto|collezione|tag|tutto), targetId, motivo(sumisura|deperibile|igiene), attiva
- WithdrawalRequest — shopId, orderId, orderName, nomeCliente, email, testoDichiarazione, trasmessoAt (ts), locale, canale(guest|account), tipoProdotto, stato, ricevutaInviataAt, scadenzaCalcolata
- AuditLog — shopId, evento, hashPayload, ts (append-only, immutabile)
- WebhookEvent — topic, ricevutoAt, processato (idempotenza)
4. Piano multi-agente (agenti SPECIALIZZATI, esecuzione SEQUENZIALE)
Preferenza utente: agenti super-specializzati, eseguiti uno alla volta (parallelo solo dove strettamente utile), minimo consumo di token. Ogni agente = un brief focalizzato con input / deliverable / criteri di uscita espliciti. Nessun avvio automatico — parto al tuo ok.
Fase 0 — Fondamenta (gate: tutto il resto dipende da qui)
- A0 architect — init app Remix, schema Prisma, config Fly, igiene segreti, collegamento ai 2 dev store, CI. Uscita: app installata su entrambi i dev store,
/healthverde su Fly, DB migrato.
Fase 1 — Specifica compliance (gate: il contratto di accettazione)
- A1 compliance-mapper — trasforma
ANALISI-REQUISITI-LEGALI.md+ §2 in criteri di accettazione testabili + copy deck (etichette IT esatte, template ricevuta, testo info recesso), risolve i punti ⚠. Uscita: file criteri approvato contro cui ogni agente testa.
Fase 2 — MVP core (minimo legalmente conforme) (sequenziale)
- A2 storefront-extension — Theme App Extension + App Proxy lookup guest (R1,R2,R7,R11).
- A3 backend-flow — endpoint recesso, conferma 2 step, persistenza + timestamp (R3,R4,R6).
- A4 receipt — email durevole con dichiarazione + timestamp trasmissione (provider stub) (R5).
- Uscita: guest su dev store immacolato completa il recesso → record + ricevuta. Questo è già conforme.
Fase 3 — Admin merchant (sequenziale; A5→A6)
- A5 admin-UI + motore di stile estensibile — Polaris: dashboard richieste, impostazioni, testo info (R12). Theming a 3 livelli (requisito utente: "super estendibile, personalizzabile da template"):
- Token da Settings (no-code) — colore brand, font, raggio, logo, larghezza → override delle CSS variables del form (base già pronta: il form del design è tutto a token
:root). - Eredita tema — opzione App Proxy in Liquid → il form è reso dentro il tema del negozio (look nativo, "template del merchant").
- CSS custom — escape hatch per personalizzazione totale. Merchant sceglie il livello; default = card a token controllata. Può diventare un agente dedicato (theming) se troppo grande per A5.
- Token da Settings (no-code) — colore brand, font, raggio, logo, larghezza → override delle CSS variables del form (base già pronta: il form del design è tutto a token
- A6 compliance-engine — esclusioni Art. 59 + calcolo finestra/scadenza per tipo prodotto + warning misconfig + logica rimborso (R8,R9,R10,R13,R15). Uscita: merchant configura esclusioni/finestre; item escluso si comporta secondo R9.
- A6-bis operatività per stato ordine (confermata call Pizeta 2026-06-16 +
AUDIT-STATI-ORDINE.md) — comportamenti aggiuntivi, tutti toggle per-shop (app generica: ognuno decide). Dati già raccolti dal lookup:displayFulfillmentStatus,deliveredAt,cancelledAt,financialStatus.- Ricevuta differenziata per stato (
stateAwareEmail, default ON): la ricevuta durevole (sempre inviata) include un blocco operativo diverso —- non evaso → "ordine annullato, procederemo al rimborso";
- spedito/consegnato → istruzioni di reso (indirizzo, spese a carico cliente, prodotto integro, rimborso dopo il rientro).
- Auto-annullamento ordini non evasi (
autoCancelUnfulfilled, default OFF — irreversibile, opt-in): al recesso su ordine non evaso →orderCancel(refund+restock). Bonus: Shopify emetteorders/cancelled→ si ferma il bot di remarketing del merchant. - Istruzioni di reso configurabili:
returnAddress(già in schema),returnAtCustomerExpense(default ON — Art. 57: informa + rende il cliente responsabile delle spese),returnInstructions(testo opzionale). Resta MANUALE per scelta Pizeta (nessuna automazione richiesta): resi parziali + rimborso proporzionale (pannello Shopify), emissione rimborso (Shopify manda la sua mail), verifica data consegna se il corriere non passa l'evento a Shopify. Uscita: merchant attiva/disattiva ciascun comportamento; il flusso si adatta allo stato dell'ordine.
- Ricevuta differenziata per stato (
- A6-ter recesso parziale (OPZIONALE — non è un obbligo di legge) — Base legale in
ANALISI-REQUISITI-LEGALI.md§5: il considerando 37 Dir. (UE) 2023/2673 dice che il professionista "can" offrire il recesso su parte del contratto, non che deve. Un pulsante a livello di ordine è conforme; il parziale è differenziazione di prodotto (lo fa Revize, non lo fa Rescindly) e copre il caso reale Pizeta (3 confezioni, ne rende 2).- Toggle per-shop
partialWithdrawalEnabled(default OFF = comportamento attuale, un clic = recesso totale). - Passo 2: elenco articoli dell'ordine con quantità; default tutti selezionati (nessun attrito per chi vuole il totale).
- Dichiarazione generata sugli articoli scelti (oggi dice "intero ordine": non si può lasciare così se il cliente seleziona un sottoinsieme).
createShopifyReturn: reso limitato alle righe/quantità selezionate.autoCancelUnfulfilled: si attiva solo se la selezione copre l'intero ordine. Parziale + non evaso → order edit / rimborso parziale, che resta manuale.- Idempotenza da rivedere: oggi la chiave è
(shop, orderId)(commitfa8ee9c), corretta solo nel mondo total-only. Col parziale deve diventare(shop, orderId, articoli): il cliente può recedere per l'articolo A oggi e per il B domani, entrambi legittimi nella finestra. - Dati da aggiungere:
lookupOrderdeve restituire titoli, quantità e ID riga; mappaturalineItem ↔ fulfillmentLineItemper il reso; nuova colonna (es.items Json) suWithdrawalRequest. - Uscita: merchant può abilitare il parziale; con toggle OFF il comportamento resta identico a oggi.
- Toggle per-shop
Fase 4 — Robustezza (A7→A8 sequenziali, poi A9 audit)
- A7 i18n — localizzazione IT/EN/DE/FR/ES (R14).
- A8 compliance-webhooks — 3 webhook GDPR + verifica HMAC + idempotenza (R16).
- A9 qa-security-auditor — avversariale: HMAC, abuso lookup guest (enumeration/rate-limit), gestione PII, edge case (fulfillment parziale, beni digitali, calcolo scadenza). Uscita: report audit, zero criticità.
Fase 5 — App Store pubblica (quando custom → public)
- A10 billing — Shopify Billing API + piani.
- A11 app-store-submission — listing, screenshot, privacy, checklist review. Uscita: pacchetto pronto per submission.
Grafo dipendenze
A0 → A1 → A2 → A3 → A4 → A5 → A6 → A7 → A8 → A9 → A10 → A11
(sequenziale per default; A7/A8 parallelizzabili solo se serve accelerare)
4-ter. Piano consolidato — stato attuale + residuo (agg. 2026-07-07)
Vista unica: cosa è FATTO e cosa RESTA, con A6-bis (Pizeta) e i gap dell'audit integrati. Dettaglio stato/commit nella memoria di progetto + git.
✅ Fatto (custom-grade, testato su pcrt-reso-test)
- A0–A1 fondamenta + spec compliance + copy deck.
- A2 Theme App Extension (app block + app embed) + modal storefront (iframe, redesign de-AI, popover info).
- A3 App Proxy: flusso guest 2-step + persistenza + timestamp trasmissione + audit (anti-enumeration).
- A4 ricevuta durevole (nodemailer/Mailpit dev) + dati per-shop dinamici (nome/URL/link ordine da Shopify) + template editabile vincolato (oggetto/intro/nota + anteprima live + ripristino).
- A5 (parziale) admin Polaris: dashboard Recessi, Impostazioni (email/notifiche/regole), Esclusioni (CRUD). Theming: solo token base.
- A6 (parziale) engine finestra + esclusioni Art. 59 (dietro toggle, default OFF).
- A8 (stub) webhook GDPR + HMAC.
- EXTRA (oltre il piano originale): integrazione Resi Shopify (
returnCreatesu ordini evasi), notifica merchant + tag (toggle), fix P1 (finestra sulla CONSEGNA, skip ordini annullati/rimborsati), avviso ordini chiusi, audit stati ordine (AUDIT-STATI-ORDINE.md).
⏳ Residuo (prioritizzato)
- R1 — A6-bis operatività per stato ordine (Pizeta-confirmed) — 3 toggle per-shop:
stateAwareEmail(ricevuta differenziata non-evaso/spedito-consegnato),autoCancelUnfulfilled(annullo automatico ordini non evasi),returnAtCustomerExpense+returnInstructions+returnAddress(istruzioni reso, Art. 57). Dettaglio: sezione A6-bis. - R2 — A6 residuo copy compliance — G7: promemoria rimborso 14gg + facoltà di trattenuta (Art. 56) nella notifica al merchant. (G6 spese-reso assorbito da R1.)
- R2-bis — A6-ter recesso parziale (opzionale, toggle
partialWithdrawalEnabled, default OFF) — non è compliance ma prodotto: selezione articoli/quantità, dichiarazione sugli articoli scelti, reso limitato alle righe scelte, auto-annullo solo se selezione = intero ordine, idempotenza su(ordine, articoli). Vedi A6-ter eANALISI-REQUISITI-LEGALI.md§5. - R3 — Deploy custom (go-live) — Fly.io + Postgres prod + URL stabile + tunnel Cloudflare nominato + link install custom sui 2-3 store live. (Serve per usarlo davvero; occhio deadline transfer pizeta-pharma-2 ~metà luglio 2026.)
- R4 — A9 hardening / QA-security — rate-limit robusto, retry ricevuta fallita, idempotenza webhook, review avversariale (enumeration lookup, HMAC, PII), Protected Customer Data (approvazione Shopify per leggere
order.emailin prod). - R5 — A7 i18n — IT/EN/DE/FR/ES.
- R6 — A5 residuo theming — motore 3 livelli completo (token no-code / eredita-tema Liquid / CSS custom).
- R7 — Pubblica — A10 billing + A11 submission App Store (registrazione Partner separata → non impatta le custom live).
Ordine consigliato
R1 → R2 → R3 (deploy) → R4 (hardening) → R5/R6 → R7
R1/R2 chiudono valore-cliente + compliance copy; R3 mette live; R4 mette in sicurezza prima del traffico reale; R5/R6 rifiniscono; R7 quando si va public.
5. Milestone
- M1 (Fase 0–1): scaffold + contratto compliance. Nulla di visibile, ma toglie rischio a tutto.
- M2 (Fase 2): MVP conforme live su pizeta. ← primo valore reale, azzera esposizione legale.
- M3 (Fase 3–4): production-grade, configurabile, auditato.
- M4 (Fase 5): submission App Store.
6. Rischi / punti di attenzione
- Sovrapposizione nativo Shopify — Shopify ha regole reso/cancellazione UE; non ricostruire i resi, avvolgerli. Verificare in A1 cosa è nativo vs gap.
- Competitor esistenti (Rescindly, REVER) — differenziare su conformità IT esatta + semplicità + prezzo.
- Lookup guest = superficie di abuso — enumeration ordini; serve rate-limit + match email + nessun leak su miss (A9).
- Prova supporto durevole — ricevuta riproducibile/loggata, non fire-and-forget (A4/A6).
- Calcolo scadenza — consegna vs conclusione, beni digitali, fulfillment parziale; fonte unica di verità nel motore (A6).
- PII — dichiarazione + email = dati personali; cifratura at rest + webhook GDPR non negoziabili (A8).
- Base legale ⚠ — confermare numeri articoli UE e inquadramento Art. 27 su fonte primaria (A1).
7. Punti aperti (non bloccanti)
- DB: Fly Postgres vs Supabase Postgres — decido in A0.
- Provider email — decido prima di M2.
- Automazione rimborso — fuori scope MVP; confermare che il tracking manuale basta al lancio.