Files
pcrt-legal-return/PLAN.md
tommaso aa0c77289d Ambito del recesso esplicito (intero ordine) + avviso auto-annullo + A6-ter in roadmap
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).
2026-07-10 10:56:50 +02:00

21 KiB
Raw Blame History

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)

  • A0A1 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 01): 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 34): 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.