35 Commits

Author SHA1 Message Date
fa4fc02e9f chore(theme): drop "consigliato" hint from the light scheme label
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-10 18:14:04 +02:00
86a901d598 feat(theme): reduce Aspetto to a color-scheme preset, drop the token editor
The per-token editor (accent, button background, button text, font,
radius, max width, custom CSS) let a merchant produce an unreadable or
inaccessible form: the accent token only drives the keyboard focus ring,
so a light accent hid it; button background/text had no contrast guard;
custom CSS was arbitrary CSS injected into the page. Per the merchant's
call, the appearance is now a fixed, accessible design with a single
choice: light (default), dark, or auto.

- Settings "Aspetto" tab: keep only the Schema colore select; remove the
  color fields, ColorField, the hex/int/font validators and their state.
- proxy loadTheme: read only themeScheme; stop applying the other tokens.
- Preview iframe made non-interactive (sandbox="allow-scripts", inert,
  tabIndex -1, pointer-events:none) with scrolling moved to the wrapper,
  since it demonstrates a fixed design and must not look clickable.

The token columns stay in the schema, unread, so the customization
surface can be re-exposed later without a migration. theme.ts (the
engine) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-10 18:04:56 +02:00
168b11c73b feat(theme): let the merchant choose the form color scheme
PAGE_CSS carried an unconditional `@media (prefers-color-scheme: dark)`
block that redefined --bg, --surface, --text and --border. The merchant
theme override only sets accent, button and typography tokens, so a
visitor whose OS is in dark mode saw a dark card with the merchant's
button colour on it, and no setting could change that. The form is
rendered in a modal on top of the shop theme, which the app cannot read
and which is almost always light.

Move the dark palette out of PAGE_CSS into DARK_VARS and let renderShell
apply it according to a new "themeScheme" setting: light (default), dark,
or auto (follows prefers-color-scheme, the previous behaviour).

The cascade is now base light palette -> chosen scheme -> merchant
override, so custom accent and button colours win under either scheme.
`color-scheme` and its meta tag follow the same setting, which keeps the
native controls consistent.

Existing rows have themeScheme NULL and fall back to the light default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-10 14:56:39 +02:00
cadf88683e fix(proxy): prevent caching of the withdrawal form
The App Proxy HTML response carried no cache headers. Two consequences:

- Browsers could serve a stale copy of /apps/recesso from cache, so theme
  changes made by the merchant were not reflected for the customer.
- The page renders the order name, the customer email and the free-text
  withdrawal declaration. That content must not be stored by the browser
  or by any intermediate proxy.

Send no-store (plus Pragma for HTTP/1.0 caches) and Referrer-Policy on
every response produced by htmlResponse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-10 14:41:28 +02:00
99935ab6fe R6: tab Aspetto con anteprima fedele del form
Sesto tab delle Impostazioni. Il merchant configura accento, sfondo/testo del
bottone, carattere, raggio, larghezza e CSS custom; l'anteprima e' resa con lo
STESSO renderer e lo STESSO CSS dello storefront (recesso.view), non con una
ricostruzione approssimata.

- Validazione al salvataggio: un valore non valido NON viene scritto (colori solo
  esadecimali, raggio e larghezza clampati, font da whitelist). Cosi' il form
  ricade sul default invece di rompersi.
- ColorField: campo testo + selettore nativo, con errore se l'esadecimale e' malformato.
- Verificato con build di produzione che nel bundle client non finisca codice
  server-only (crypto.server, nodemailer, PrismaClient, APP_ENCRYPTION_KEY: zero
  occorrenze); la vista pura invece c'e', come deve.
2026-07-10 14:18:07 +02:00
e51828a8d0 Estrai recesso.view.ts: vista pura, condivisibile con l'admin
PAGE_CSS, renderShell, stepLayout, renderStep1..4, escapeHtml/attr e
PROXY_STOREFRONT_PATH spostati in un modulo PURO (nessun node/server).
recesso.server.ts li ri-esporta: nessun chiamante cambia (proxy invariato).

Motivo: l'anteprima nell'admin deve rendere lo stesso identico markup e CSS
dello storefront, e non puo' farlo via <iframe src=route-admin> perche' l'admin
embedded si autentica con session token via App Bridge, che una navigazione
iframe non trasporta. Serve anche per il livello 2 (eredita il tema).

Rimosso il parametro 'theme' spurio che il replace globale aveva aggiunto a
stepLayout (non lo usa: il tema lo applica renderShell).
2026-07-10 14:14:24 +02:00
d331e609a5 R6 L1+L3: motore di stile del form (token + CSS custom)
Il form era gia' tokenizzato (~30 var CSS in :root): non riscriviamo nulla,
iniettiamo un blocco di override DOPO il CSS di base, cosi' un errore di
configurazione del merchant non puo' rompere il default.

- theme.ts (modulo PURO, condiviso con la futura anteprima admin): ThemeTokens,
  FONT_PRESETS, themeStyle(). Sanificazione a monte: colori solo esadecimali,
  raggio e larghezza clampati, font da whitelist, CSS custom con rimozione di
  </style, @import, expression(), javascript: e cap a 4000 char.
  Derivati: --focus-ring da accent (rgba), --primary-bg-hover per shading.
- recesso.server: tokenizzati anche font (--font) e larghezza (--card-max), che
  erano hardcoded; renderShell(inner, theme) inietta l'override; renderStep1..4
  accettano e propagano il tema.
- Settings: themeAccent/ButtonBg/ButtonText/Radius/Font/Width/CustomCss + migrazione.
- proxy: loadTheme(shop) in loader e action, propagato ai 22 punti di render.
  Nell'early-return senza sessione il tema e' null (non conosciamo lo shop).

fly.toml: min_machines_running resta 0 per scelta (TODO go-live, vedi
PROTECTED-CUSTOMER-DATA.md §5.2).
2026-07-10 13:07:43 +02:00
787553af22 Protected Customer Data: autovalutazione + macchina sempre calda
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.
2026-07-10 12:55:00 +02:00
5796e94154 Admin: avviso critico se non c'e' alcun SMTP configurato
Il tab diceva 'Vuoto = provider di default dell'app', ma questa installazione non
ha secret SMTP_*: vuoto significava 'nessuna ricevuta parte', in silenzio - mentre
CHECKLIST-COMPLIANCE-MERCHANT promette 'ricevuta sempre inviata'.

Il loader ora espone appDefaultSmtp (presenza di SMTP_HOST lato app). Se non c'e'
ne' quello ne' l'SMTP per-shop, banner critico: la ricevuta su supporto durevole
e' un obbligo (art. 54-bis), la funzione non e' conforme finche' non e' configurato.
2026-07-10 12:42:19 +02:00
97bb3998dd Notifica merchant: includi la dichiarazione del cliente
La dichiarazione e' un campo editabile: il cliente puo' scriverci un intento
parziale (es. 'recedo solo per 2 confezioni su 3'). Finiva nel DB e nella ricevuta
al cliente, ma NON nella notifica al merchant, che agiva senza averla mai letta -
mentre l'app apriva un reso totale. Ora la notifica riporta il testo integrale,
con l'avvertenza di leggerlo prima di agire.
2026-07-10 10:57:46 +02:00
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
fa8ee9c93f Idempotenza del recesso + rate-limit sulla conferma
Due difetti sullo stesso endpoint:

1) Nessun controllo duplicati: ogni POST 'confirm' creava una nuova
   WithdrawalRequest. Il diritto di recesso si esercita UNA volta per contratto:
   una seconda dichiarazione e' senza oggetto e, peggio, inquina l'onere della
   prova (N timestamp di trasmissione per un atto che deve averne uno).
   Ora: se esiste gia' una richiesta per (shop, orderId) non REJECTED, non se ne
   crea una seconda; niente secondo reso, tag, notifica o annullo automatico.
   Audit 'withdrawal_duplicate'. La schermata conferma il primo atto col suo
   timestamp originale (nessun blocco del flusso: non e' un dark pattern).
   Ricevuta re-inviabile al massimo 1 volta/ora (aiuta chi non l'ha ricevuta
   senza amplificare), tracciata da audit 'receipt_resent'; il timestamp legale
   e receiptSentAt originali non vengono toccati.

2) checkRateLimit era applicato SOLO al case 'lookup'. La conferma era libera:
   ogni POST inviava una ricevuta al cliente + una notifica al merchant e
   bruciava quota Admin API -> amplificatore di email raggiungibile da chiunque
   navighi lo store. Ora e' limitata, con audit 'withdrawal_confirm_rate_limited'.
2026-07-10 10:31:47 +02:00
4998795648 Avviso: email di notifica uguale al mittente SMTP
Spedire da un indirizzo a se stesso via relay esterno (Brevo) viene spesso
scartato dai provider: era il caso in prod (notifyEmail == smtpFrom), la
notifica risultava 'merchant_notified' ma non veniva consegnata.
2026-07-10 10:16:48 +02:00
0b9726a778 Notifica merchant: link all'ordine nell'ADMIN (non la pagina cliente)
Il bottone puntava a order.statusPageUrl, cioe' la pagina di stato lato cliente:
inutile per il merchant, che il reso lo gestisce dal pannello. Ora punta a
admin.shopify.com/store/<handle>/orders/<id>, ricavato dal GID dell'ordine.
Etichetta contestuale: 'Gestisci il reso' quando un reso esiste o e' stato creato,
altrimenti 'Apri l'ordine'.
2026-07-10 10:12:41 +02:00
b15567c8e1 Deliverability: Message-ID allineato al dominio mittente + checklist dominio
- trySend(): genera un Message-ID nel dominio del From (il default di nodemailer
  usa l'hostname del container -> dominio incoerente, segnale antispam).
- CHECKLIST-COMPLIANCE-MERCHANT: l'autenticazione del dominio mittente (SPF/DKIM/
  DMARC) e la configurazione SMTP diventano obblighi espliciti del merchant; senza,
  la ricevuta durevole non raggiunge il consumatore. Chiarito che l'app la invia
  sempre ma la consegna dipende dal provider/dominio del merchant.
2026-07-10 10:01:11 +02:00
23f6a0a3e8 Fix invio email: TLS derivato dalla porta (465 diretto / 587 STARTTLS)
Causa reale del mancato invio in prod: Settings aveva porta 587 con 'Connessione
sicura diretta' spuntata -> nodemailer apriva subito TLS su una porta che parla
in chiaro + STARTTLS -> handshake fallito -> receipt_failed.

- buildTransport: secure derivato dalla porta standard (465=true, 587=false),
  requireTLS su 587. Su porte non standard resta la scelta del merchant.
- Checkbox 'Connessione sicura' ora dichiara che e' ignorata sulle porte standard.
- Avviso se notifiche attive ma 'Email notifiche' vuota (era null in prod: per
  questo non arrivava neanche la notifica al merchant).
2026-07-10 09:53:13 +02:00
f5d2c8664f Impostazioni: invio email di prova + diagnostica SMTP
- sendTestEmail(): transport.verify() prima dell'invio (errori auth/connessione
  espliciti), 1 solo tentativo, ritorna l'errore SMTP grezzo.
- Tab 'Email (SMTP)': campo destinatario + bottone 'Invia email di prova' (usa i
  valori del form anche se non salvati; password digitata oppure quella salvata
  decifrata). Banner con l'errore esatto.
- Guardia: se Host SMTP e' impostato ma il Mittente (From) e' vuoto, l'invio
  fallisce con messaggio chiaro (i provider rifiutano mittenti non verificati)
  + banner di avviso nel tab.
- Audit: receipt_failed / merchant_notify_failed ora salvano l'errore SMTP con
  le email mascherate (diagnosticabile, senza PII) invece di una stringa generica.
2026-07-10 09:47:52 +02:00
c423a8d9a7 Extension: script non parser-blocking (script_tag -> <script defer>)
Sia app embed che app block caricavano il JS con il filtro script_tag (sincrono,
parser-blocking -> warning ParserBlockingScript). Passato a <script defer>.
Il JS ha gia' la guardia window.__recessoModalInit, quindi il doppio caricamento
(embed + block sulla stessa pagina) resta sicuro.
2026-07-10 09:01:05 +02:00
e7b03da729 Extension: aggiungi locales/en.default.json (fix ENOENT locales su shopify app deploy) 2026-07-07 18:34:31 +02:00
e0190c6a45 Fix app proxy 400 in prod: Docker base node:18-alpine -> node:22-alpine
Node 18-alpine non espone 'crypto' come global dichiarato: la libreria
@shopify/shopify-api getCryptoLib() fa 'crypto?.webcrypto' -> ReferenceError
'crypto is not defined' -> validateAppProxyHmac fallisce -> 400 su ogni richiesta
app proxy. Node 22 (come in dev) espone crypto global -> HMAC ok -> 200.

Diagnosi: log Fly mostravano 'crypto is not defined' prima di ogni 400; verificato
con firma app-proxy calcolata a mano (ora 200 + form recesso).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 18:23:41 +02:00
f3b3b1abc3 Deploy Fly: application_url/app_proxy/redirect -> recesso-custom.fly.dev + escludi .env dall'immagine
- shopify.app.toml: URL Shopify puntano all'app Fly (custom distribution) invece del tunnel dev.
- .dockerignore: esclude .env, *cred*, .shopify (niente segreti dev nell'immagine prod).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 17:50:11 +02:00
3bb7a30c1d SMTP per-shop configurabile dalle Impostazioni (password cifrata AES-256-GCM)
- Settings: smtpHost/Port/User/Pass(cifrata)/Secure/From. Vuoto -> provider
  default dell'app (env); compilato -> invio dal SMTP del merchant.
- crypto.server: encrypt/decrypt AES-256-GCM (chiave APP_ENCRYPTION_KEY).
- mailer: SmtpConfig + buildTransport(smtp) per-shop o env; mailFrom override.
  Sia ricevuta cliente sia notifica merchant usano lo SMTP del merchant.
- proxy: costruisce SmtpConfig (decifra pass), passa ai due invii.
- admin: nuovo tab 'Email (SMTP)'; password mai ri-mostrata (vuoto = invariata).
- Migrazione smtp_settings. APP_ENCRYPTION_KEY: dev in .env, prod = fly secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 17:29:33 +02:00
ad9f76b6be R4 hardening (A9): retry ricevuta + webhook GDPR + rate-limit prune + fix PII/UX
Da review avversariale indipendente:
- Retry invio email (trySend, 3 tentativi backoff) per ricevuta cliente e notifica merchant.
- Webhook GDPR implementati (erano stub) con idempotenza (skip se gia' processed):
  * customers/redact: pseudonimizza PII (nome/email/dichiarazione) nelle
    WithdrawalRequest del cliente, mantiene il record legale (ordine+timestamp)
    come prova art. 54-bis (base art. 17(3) GDPR).
  * shop/redact: purge completa dei dati shop (Settings/Exclusion/Withdrawal/
    Session/AuditLog + WebhookEvent stale).
  * customers/data_request: registra la richiesta + n. record (merchant li recupera
    dalla dashboard Recessi).
- Rate-limit: pruning periodico del bucket in-memory (fix memory leak).
- Ricevuta fallita: messaggio di successo onesto (successMessage riceve receipt.ok)
  invece del falso 'ti abbiamo inviato la ricevuta'.
- PII: rimossa dai detail audit persistiti degli errori SMTP (ricevuta/notifica).

Non modificati (verificati): off-by-1 finestra = permissivo, non blocca a torto;
'doppio escape' = falso allarme (template e valori escapati una volta ciascuno).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 17:07:06 +02:00
ce36f1a657 Fix: reso gia' esistente -> stato 'exists' + messaggio corretto al merchant
Se l'ordine ha gia' un reso (order.returns), createShopifyReturn ritorna 'exists'
invece di fallire: la notifica dice 'esiste gia' un reso, gestiscilo dall'ordine'
al posto del fuorviante 'reso non creato, fallo manualmente'. Audit shopify_return_exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:54:43 +02:00
5fb9bf494c Dashboard Recessi: promemoria rimborso 14gg (Art. 56) + colonna 'Rimborsa entro' (trasmissione + 14gg)
Il promemoria non e' piu' solo nella notifica email: visibile anche dove il
merchant gestisce le richieste (funziona pure con notifiche off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:47:17 +02:00
04a43182cb R2 (G7): promemoria rimborso 14gg + facolta' di trattenuta (Art. 56) nella notifica al merchant
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:40:05 +02:00
a550d8ff2e A6-bis: testi riquadro operativo editabili (segnaposto) + Impostazioni in tab
- opTextUnfulfilled / opTextShipped editabili dal merchant, con segnaposto
  {{returnAddress}} {{returnCost}} {{orderState}} {{customerName}} {{orderName}}
  {{shopName}}. Default = testi attuali. returnCost deriva da returnAtCustomerExpense
  (a tuo/nostro carico, Art. 57). returnInstructions deprecato (assorbito da opTextShipped).
- Pagina Impostazioni riorganizzata in 4 tab (Email / Notifiche / Regole recesso /
  Reso e stato ordine) + anteprima live del riquadro per stato (non evaso e spedito).
- emailTemplate: renderOperationalBlock ora usa i template + sostituzione segnaposto;
  renderOperationalPreview esportato per l'admin. Migrazione op_texts (additiva).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:35:12 +02:00
a6a1900287 R1 A6-bis: operativita' per stato ordine (toggle) + checklist compliance
Comportamenti Pizeta-confirmed, tutti toggle per-shop:
- stateAwareEmail (default ON): la ricevuta durevole include un blocco operativo
  per stato - non evaso => annullo+rimborso; spedito/consegnato => istruzioni reso
  (indirizzo, spese a carico Art.57, prodotto integro, rimborso dopo rientro).
- autoCancelUnfulfilled (default OFF): recesso su ordine non evaso => orderCancel
  (refund+restock). Abilita anche lo stop del remarketing via orders/cancelled.
- returnAtCustomerExpense / returnInstructions / returnAddress: config istruzioni reso.

Impl: helper orderState + cancelOrder (recesso.server); renderOperationalBlock +
param operational in renderReceiptHtml (emailTemplate); mailer passa operational;
proxy calcola stato, passa alla ricevuta, auto-annulla; sezione admin 'Reso e stato
ordine'. Migrazione a6bis_state_ops.

+ CHECKLIST-COMPLIANCE-MERCHANT.md: cosa fa l'app vs obblighi del merchant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:16:22 +02:00
945f1a6230 PLAN: integra A6-bis (operativita' per stato ordine, Pizeta-confirmed) + piano consolidato stato/residuo
- A6-bis: toggle per-shop stateAwareEmail / autoCancelUnfulfilled /
  returnAtCustomerExpense / returnInstructions / returnAddress. Fonte: call
  Pizeta 2026-06-16 + AUDIT-STATI-ORDINE.md.
- Sezione 4-ter: vista unica FATTO vs RESIDUO (R1..R7) con ordine consigliato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 16:05:35 +02:00
883bd82708 Avviso storefront per ordini gia' annullati/rimborsati (info non bloccante)
Al passo 2 del recesso, se l'ordine risulta gia' annullato (cancelledAt) o
rimborsato/voided (financialStatus), mostra un banner informativo ambra (non
blocca): il recesso resta ammesso (diritto incondizionato + funzione sempre
accessibile Art. 54-bis), ma il consumatore e' informato dello stato.

- recesso.copy: NOTICE.orderClosed.
- recesso.server: noticeBanner + campo notice in renderStep2 + stile .notice ambra con icona.
- proxy: calcola orderClosed al lookup e passa la notice a renderStep2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 15:46:51 +02:00
3968d34011 Audit stati ordine + fix P1 (finestra su consegna, ordini chiusi)
Audit AUDIT-STATI-ORDINE.md: matrice stato ordine x normativa (Art. 52/56/57) x
comportamento attuale x gap, con fix prioritizzati.

Fix P1 (correttezza legale):
- G1+G4: la finestra 14gg decorre dalla CONSEGNA (evento fulfillment DELIVERED),
  non dalla spedizione (Art. 52 = possesso fisico). Se non consegnato, la finestra
  non e' iniziata -> computeDeadline null -> non blocca mai.
- G5: ordini annullati (cancelledAt) o rimborsati/voided (displayFinancialStatus)
  -> skip creazione reso (evita doppio reso/rimborso), audit shopify_return_skipped.
- lookupOrder esteso: deliveredAt (da eventi fulfillment), cancelledAt, financialStatus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 15:06:38 +02:00
2c8c2a7429 Resi Shopify + notifiche merchant + A6 (finestra/esclusioni) + dashboard recessi
Integrazione Resi Shopify:
- Alla conferma del recesso, crea un Reso nativo (returnCreate) per gli ordini
  evasi -> gestione nella UI Resi nativa. Ordini non evasi: skip (merchant gestisce
  annullo/rimborso). Query via order.fulfillments (returnableFulfillments non
  esiste nella API 2026-04). Campo WithdrawalRequest.shopifyReturnId.

Notifiche al merchant (dietro toggle Settings):
- Email di notifica a ogni recesso (sendMerchantNotification) + tag "Recesso"
  sull'ordine (tagsAdd, scope write_orders). Toggle notifyEnabled/notifyEmail/tagEnabled.

A6 - completezza compliance (dietro toggle, default OFF, non tocca il flusso testato):
- Finestra 14gg: computeDeadline/isWindowExpired (rif = data evasione o ordine +
  defaultWindowDays). Esclusioni Art. 59: checkExclusions su regole ExclusionRule
  (ALL/PRODUCT/TAG). lookupOrder esteso (fulfillments + lineItems product/tags).
  checkCompliance() aggancia lookup+confirm. Toggle enforceWindow/enforceExclusions.
- Pagina admin Esclusioni (CRUD regole) + sezione Regole di recesso in Impostazioni.

Dashboard: pagina Recessi (registro legale read-only). NavMenu: Recessi/Impostazioni/Esclusioni.
Scope: +write_returns,+write_orders. Editor email vincolato (oggetto/intro/nota + anteprima).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 14:47:07 +02:00
d140d90629 Storefront modal + email templating: theme extension, redesign, editable receipt
Storefront (theme app extension recesso-storefront):
- App block + app embed che linkano a /apps/recesso; modal via iframe con
  fallback progressive-enhancement; apertura immediata, dimensione fissa
- Redesign form: pattern modal moderno (header/footer fissi, corpo scorrevole),
  modalita' embed, popover info "i", testo ridotto, de-AI, trattini al posto degli em-dash

Ricevuta email:
- Dati per-shop dinamici da Shopify (nome, url, link stato ordine) via getShopInfo + statusPageUrl
- Template editabili VINCOLATI (oggetto/introduzione/nota) con segnaposto; layout
  fisso e conforme (dichiarazione, timestamp, avviso art. 54-bis)
- Pagina admin (Polaris) con anteprima live + ripristina default
- Campi email in Settings (Prisma) + migrazioni

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 12:17:07 +02:00
8ca4de6c0b MVP core recesso: App Proxy flow + ricevuta durevole + design storefront
- A1: SPEC-MVP-RECESSO.md (criteri + copy IT)
- A3: App Proxy /apps/recesso — lookup guest ordine+email (anti-leak), form 2-step,
  funzione dedicata Conferma recesso, persist WithdrawalRequest + transmittedAt + AuditLog
- A4: ricevuta durevole via nodemailer (mailer.server.ts) + receiptSentAt + AuditLog
- Design: recesso.server.ts restyle token CSS light/dark, coerente Shopify
- Fix: @shopify/shopify-api pinnato 13.1.0 (dedupe) -> tsc clean
- PLAN: A5 esteso a motore di stile a 3 livelli

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
2026-07-07 09:52:47 +02:00
4586b2e557 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
2026-07-06 18:02:17 +02:00