- 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.
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).
- 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.
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.
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
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
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
- 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
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
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
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
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