PROTECTED-CUSTOMER-DATA.md - verdetto: la distribuzione CUSTOM ha accesso 'Always available' a Level 1 e Level 2 (email, statusPageUrl): nessuna approvazione Shopify da attendere per gli store live. La review servira' per l'app pubblica (R7). Il fatto che funzioni oggi su pcrt-reso-test non prova nulla: sui development store la review non e' richiesta comunque. I requisiti di sicurezza L1/L2 restano pero' obbligatori. Autovalutazione con evidenze: cifratura a riposo OK (volume Fly ENCRYPTED=true), backup cifrati OK (snapshot Fly), separazione test/prod OK. Gap: retention non documentata, privacy policy assente, incident response assente, password Fly da cambiare, snapshot con sola retention 5gg e nodo singolo (i record legali sono la PROVA del recesso). fly.toml: min_machines_running 0 -> 1. Un cold start misurato ha richiesto ~38s; l'art. 54-bis pretende una funzione sempre accessibile e facilmente utilizzabile.
6.6 KiB
Protected Customer Data — verdetto e autovalutazione
Stato: 2026-07-10. App: Legal Return PCRT (custom distribution) su Fly recesso-custom.
1. Serve l'approvazione di Shopify?
No, non per la distribuzione custom. Dalla documentazione ufficiale (shopify.dev/docs/apps/launch/protected-customer-data):
| Livello | Public app | Custom app | Admin-created custom app |
|---|---|---|---|
| Level 1 | Requires review | Always available | Always available |
| Level 2 (nome, indirizzo, email, telefono) | Requires review | Always available | Varies by plan |
Conseguenze:
- Oggi (custom, store live): nessuna approvazione da attendere.
order.emaileorder.statusPageUrlsono accessibili. - Domani (R7, app pubblica): la review sarà obbligatoria. Questo documento e' la base della futura richiesta.
- ⚠ Il fatto che oggi funzioni su
pcrt-reso-testnon prova nulla: sui development store la review non e' richiesta comunque. La prova e' la tabella qui sopra, non il test.
Da fare comunque nel Dev Dashboard: dichiarare quali dati e campi protetti si usano (App → API access requests → Protected customer data). Testi pronti al §3.
⚠ Attenzione: order.statusPageUrl richiede Level 2 dal 15 marzo 2024
(changelog).
Lo usiamo solo per il bottone "Vedi il tuo ordine" nella ricevuta al cliente: e' una comodita',
non un dato necessario. Per la data minimization della futura app pubblica valutare di
rimuoverlo.
2. Dati protetti effettivamente trattati
| Dato | Da dove | Perche' e' il minimo necessario |
|---|---|---|
order.email |
Admin API (lookup ordine) | Verifica d'identita' del consumatore guest (senza login, come impone l'art. 54-bis) e destinatario della ricevuta su supporto durevole (obbligo di legge) |
| Nome del consumatore | Inserito dal consumatore nel form | Elemento richiesto dall'art. 54-bis nella dichiarazione |
| Testo della dichiarazione | Inserito dal consumatore | E' l'atto giuridico stesso; va conservato come prova |
order.statusPageUrl |
Admin API | Solo comodita' (link "Vedi il tuo ordine"). Non necessario |
NON trattiamo: indirizzi, telefono, dati di pagamento, profilazione. Nessuna vendita o condivisione a terzi. Unico sub-processor: il provider SMTP scelto dal merchant.
3. Testi per la dichiarazione nel Dev Dashboard
Protected customer data — motivazione:
L'app implementa la funzione di recesso obbligatoria ex art. 54-bis del Codice del Consumo italiano (D.Lgs 209/2025, Dir. UE 2023/2673). Per legge la funzione deve essere utilizzabile senza login: l'app deve quindi verificare l'identita' del consumatore confrontando il numero d'ordine con l'email associata all'ordine, e inviare a quell'indirizzo la ricevuta su supporto durevole con il timestamp di trasmissione. Nessun altro dato del cliente viene letto.
Protected customer field — email:
Necessaria per due obblighi di legge: (1) verificare che chi esercita il recesso sia il titolare dell'ordine (accesso guest, senza autenticazione); (2) recapitare la ricevuta su supporto durevole, che l'art. 54-bis impone di inviare senza indebito ritardo.
Protected customer field — name: non richiesto via API (lo inserisce il consumatore).
4. Requisiti Level 1 / Level 2 — stato reale
| # | Requisito | Stato | Evidenza / Gap |
|---|---|---|---|
| L1 | Minimizzazione dei dati | 🟡 | Ok tranne statusPageUrl (non necessario) |
| L1 | Informare il merchant su dati e finalita' | 🟡 | CHECKLIST-COMPLIANCE-MERCHANT.md c'e'; manca una privacy policy dell'app |
| L1 | Uso limitato alle finalita' dichiarate | ✅ | Nessun uso secondario |
| L1 | Periodi di retention definiti | ❌ | Non documentati. shop/redact fa purge totale; customers/redact pseudonimizza. Manca la policy scritta |
| L1 | Cifratura in transito | ✅ | HTTPS ovunque; SMTP con STARTTLS/TLS |
| L1 | Cifratura a riposo | ✅ | Volume Fly pg_data ENCRYPTED = true. Password SMTP per-shop cifrate AES-256-GCM (crypto.server.ts) |
| L2 | Backup cifrati | ✅ | Snapshot automatici Fly (cifrati perche' il volume lo e') |
| L2 | Separazione test / produzione | ✅ | Dev = Postgres in Docker locale; prod = Fly Postgres. DB e app distinti |
| L2 | Accesso staff limitato | 🟡 | Un solo titolare. Ma la password master Fly era in chiaro nel file Cred Fly: da cambiare |
| L2 | Password robuste | ❌ | Vedi sopra: cambio password Fly non confermato |
| L2 | Access log | 🟡 | Log applicativi Fly (effimeri) + AuditLog append-only con payload hashati. Nessun log di accesso al DB |
| L2 | Incident response policy | ❌ | Non esiste. Va scritta |
| L2 | Data loss prevention | ❌ | Snapshot con retention 5 giorni, nodo singolo, nessuna copia off-site. Vedi §5 |
5. Due rischi che non sono formalita'
5.1 Durabilita' dei record legali
Gli snapshot Fly hanno retention 5 giorni e vivono nella stessa infrastruttura. Sono
disaster recovery, non archivio. Ma le WithdrawalRequest e l'AuditLog sono la prova
del recesso: se il termine si estende a 12 mesi per mancata informativa, o se sorge una
controversia, quei record devono esistere ben oltre 5 giorni. Un solo nodo, nessuna copia
off-site: se l'app Postgres viene cancellata, la prova sparisce.
Fix: dump logico periodico verso storage esterno (o Fly Managed Postgres / Supabase, che hanno backup gestiti con retention lunga). Non e' un requisito Shopify — e' il motivo per cui l'app esiste.
5.2 Cold start sulla funzione di recesso
min_machines_running = 0: la macchina si spegne. Una richiesta a freddo ha impiegato
38 secondi. L'art. 54-bis pretende una funzione "sempre accessibile" e "facilmente
utilizzabile": 38 secondi di attesa dopo il clic sono un ostacolo, e con ogni probabilita'
il consumatore abbandona.
Fix: min_machines_running = 1 (una macchina sempre calda).
6. Azioni, in ordine
- Cambiare la password Fly (era in chiaro). — utente
min_machines_running = 1nelfly.toml. — codice, banale- Backup off-site dei record legali (dump periodico). — da progettare
- Privacy policy dell'app + retention policy scritte. — documenti
- Incident response policy (chi, cosa, entro quanto). — documento
- Dichiarare dati e campi nel Dev Dashboard (testi al §3). — utente
- (Per R7, app pubblica) valutare la rimozione di
statusPageUrlper minimizzazione.