diff --git a/PROTECTED-CUSTOMER-DATA.md b/PROTECTED-CUSTOMER-DATA.md new file mode 100644 index 0000000..68b5973 --- /dev/null +++ b/PROTECTED-CUSTOMER-DATA.md @@ -0,0 +1,119 @@ +# 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](https://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.email` e + `order.statusPageUrl` sono 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-test` **non 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](https://shopify.dev/changelog/level-2-protected-customer-data-requirements-are-now-needed-to-access-the-order-statuspageurl-field)). +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 + +1. **Cambiare la password Fly** (era in chiaro). — *utente* +2. **`min_machines_running = 1`** nel `fly.toml`. — *codice, banale* +3. **Backup off-site** dei record legali (dump periodico). — *da progettare* +4. **Privacy policy dell'app** + **retention policy** scritte. — *documenti* +5. **Incident response policy** (chi, cosa, entro quanto). — *documento* +6. Dichiarare dati e campi nel **Dev Dashboard** (testi al §3). — *utente* +7. *(Per R7, app pubblica)* valutare la **rimozione di `statusPageUrl`** per minimizzazione. diff --git a/app/fly.toml b/app/fly.toml index 5a90c8c..9fb91c1 100644 --- a/app/fly.toml +++ b/app/fly.toml @@ -17,7 +17,10 @@ primary_region = "fra" # Frankfurt (EU data residency) force_https = true auto_stop_machines = true auto_start_machines = true - min_machines_running = 0 + # Una macchina sempre calda. Con 0, un cold start misurato ha richiesto ~38s: + # l'art. 54-bis pretende una funzione "sempre accessibile" e "facilmente + # utilizzabile", e 38 secondi dopo il clic sono un ostacolo. Costo: 1 macchina. + min_machines_running = 1 [[vm]] size = "shared-cpu-1x"