# 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:** 1. Sviluppo/test (**`shopify app dev` = draft**) **solo su `pcrt-reso-test`** (usa e getta). ← unica vera protezione. 2. **Mai** `shopify app dev` / draft contro **pizeta-pharma-2** o store destinati al cliente. 3. **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). 4. ⚠ 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): 1. Template Shopify CLI/Remix standard — OAuth multi-tenant, session token (no scorciatoia single-store). 2. Dati **multi-tenant per-shop** dal giorno 1, anche con soli 2-3 negozi. 3. **Webhook GDPR obbligatori** subito. 4. **Billing API dietro flag** (`isPublic`): off in custom, on in public. Unico ramo di codice divergente. 5. 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, `/health` verde 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"): 1. **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`). 2. **Eredita tema** — opzione App Proxy in **Liquid** → il form è reso dentro il tema del negozio (look nativo, "template del merchant"). 3. **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. - **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`. 1. **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). 2. **Auto-annullamento ordini non evasi** (`autoCancelUnfulfilled`, default OFF — irreversibile, opt-in): al recesso su ordine non evaso → `orderCancel` (refund+restock). Bonus: Shopify emette `orders/cancelled` → si ferma il bot di remarketing del merchant. 3. **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. - **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)` (commit `fa8ee9c`), 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: `lookupOrder` deve restituire titoli, quantità e ID riga; mappatura `lineItem ↔ fulfillmentLineItem` per il reso; nuova colonna (es. `items Json`) su `WithdrawalRequest`. - **Uscita:** merchant può abilitare il parziale; con toggle OFF il comportamento resta identico a oggi. ### 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** (`returnCreate` su 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 e `ANALISI-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.email` in 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) 1. DB: Fly Postgres vs Supabase Postgres — decido in A0. 2. Provider email — decido prima di M2. 3. Automazione rimborso — fuori scope MVP; confermare che il tracking manuale basta al lancio.