Initial scaffold: Shopify recesso (withdrawal) compliance app

- Remix (TypeScript) + Polaris, official Shopify app template
- Prisma multi-tenant schema (Settings, ExclusionRule, WithdrawalRequest, AuditLog, WebhookEvent) on Postgres
- Mandatory GDPR compliance webhooks (data_request, redact, shop/redact) + HMAC handlers
- API version pinned 2026-04, scopes read_orders/read_products
- Fly deploy config; two-env strategy (custom now, public later)
- Dev setup: shopify.web.toml + Vite allowedHosts for tunnels
- Docs: PLAN.md, ANALISI-REQUISITI-LEGALI.md (Art. 54-bis compliance)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
This commit is contained in:
2026-07-06 18:02:17 +02:00
commit 4586b2e557
62 changed files with 2968 additions and 0 deletions

215
PLAN.md Normal file
View File

@@ -0,0 +1,215 @@
# 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
- **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** — Polaris: dashboard richieste, impostazioni, branding, testo info (R12).
- **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.
### 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)
```
---
## 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.