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).
This commit is contained in:
2026-07-10 10:56:50 +02:00
parent fa8ee9c93f
commit aa0c77289d
5 changed files with 91 additions and 2 deletions

View File

@@ -56,3 +56,52 @@
2. ⚠ Inquadramento sanzionatorio definitivo (Art. 27 vs Artt. 62-66 Cod. Cons.) e importi post-Omnibus. 2. ⚠ Inquadramento sanzionatorio definitivo (Art. 27 vs Artt. 62-66 Cod. Cons.) e importi post-Omnibus.
3. Testo integrale Art. 54-bis da Normattiva (non solo Brocardi) per etichette e dati esatti. 3. Testo integrale Art. 54-bis da Normattiva (non solo Brocardi) per etichette e dati esatti.
4. Verifica cosa Shopify copre nativamente (regole reso/cancellazione UE) vs gap reali. 4. Verifica cosa Shopify copre nativamente (regole reso/cancellazione UE) vs gap reali.
---
## 5. Recesso parziale — punto CHIARITO (2026-07-10)
**Domanda:** il recesso si applica all'intero ordine o si puo' spacchettare per singolo bene?
**Risposta su due piani distinti.**
### 5.1 Diritto sostanziale: il recesso parziale e' AMMESSO
Il recesso puo' avere ad oggetto tutti i beni (totale) o soltanto parte di essi (parziale).
Coerente col **modulo tipo** (Allegato I, parte B), che prevede di indicare "i seguenti beni".
Nota: sul parziale le **spese di consegna non si rimborsano** (importo fisso, indipendente dal
numero di pezzi).
### 5.2 La FUNZIONE di recesso (art. 54-bis) NON deve offrire la selezione dei beni
Fonte primaria — **Considerando 37, Direttiva (UE) 2023/2673**:
> "If the consumer has ordered multiple goods or services within the same distance contract,
> the trader **can** provide the consumer with the possibility to withdraw from a part rather
> than the whole of the contract."
**"can", non "must"**: offrire il recesso parziale nella funzione e' una **facolta' del
professionista**, non un obbligo. Conformita' minima = il consumatore deve poter identificare
**il contratto** da cui recede.
Sei fonti italiane indipendenti convergono sull'elenco dei dati richiesti dall'art. 54-bis:
nome + **elementi identificativi del contratto** + mezzo elettronico per la conferma. Nessuna
menziona la selezione dei beni. (Stefanelli, CMS, Studio Legale MP, LegalBlink, Alexander Greco,
Rescindly.)
⚠ Un commento anglosassone (NatLawReview) sostiene il contrario ("the interface must allow the
customer to select the particular product(s)"): e' una **sovra-lettura del considerando 37**.
### 5.3 Conseguenza per l'app
Una funzione di recesso **a livello di ordine e' conforme**. Il consumatore conserva comunque il
diritto al recesso parziale attraverso gli **altri canali** (modulo tipo / email), che per legge
restano validi e non vanno disabilitati.
Il recesso parziale nel pulsante e' quindi una scelta di **prodotto**, non un obbligo di legge.
### 5.4 Panorama concorrenti (2026-07-10)
- **Revize** (revize.app): implementa la selezione articoli ("the customer selects which items to withdraw"); pre-compila e blocca nome/contratto/contatto dall'ordine.
- **Rescindly** (rescindly.eu): form = numero ordine, nome, email, motivo. **Nessuna selezione articoli** -> livello ordine.
- **Guida IFG eCommerce**: raccomanda esplicitamente l'implementazione **a livello di ordine**.
- **LegalBlink**: NON e' un concorrente — e' un generatore di documenti legali (condizioni di vendita, privacy, condizioni di recesso), non un widget di recesso.
- **Shopify**: **nessun pulsante di recesso nativo** (a maggio 2026). Gli strumenti annulla/rimborsa sono lato merchant e non soddisfano i requisiti art. 11a (funzione rivolta al consumatore, 2 step, etichettata, con ricevuta).
### 5.5 Sanzione confermata
Mancata conformita' -> il termine di recesso si estende automaticamente di **12 mesi**.

10
PLAN.md
View File

@@ -181,6 +181,15 @@ input / deliverable / criteri di uscita espliciti. **Nessun avvio automatico**
3. **Istruzioni di reso configurabili**: `returnAddress` (già in schema), `returnAtCustomerExpense` (default ON — Art. 57: informa + rende il cliente responsabile delle spese), `returnInstructions` (testo opzionale). 3. **Istruzioni di reso configurabili**: `returnAddress` (già in schema), `returnAtCustomerExpense` (default ON — Art. 57: informa + rende il cliente responsabile delle spese), `returnInstructions` (testo opzionale).
**Resta MANUALE per scelta Pizeta** (nessuna automazione richiesta): resi parziali + rimborso proporzionale (pannello Shopify), emissione rimborso (Shopify manda la sua mail), verifica data consegna se il corriere non passa l'evento a Shopify. **Resta MANUALE per scelta Pizeta** (nessuna automazione richiesta): resi parziali + rimborso proporzionale (pannello Shopify), emissione rimborso (Shopify manda la sua mail), verifica data consegna se il corriere non passa l'evento a Shopify.
**Uscita:** merchant attiva/disattiva ciascun comportamento; il flusso si adatta allo stato dell'ordine. **Uscita:** merchant attiva/disattiva ciascun comportamento; il flusso si adatta allo stato dell'ordine.
- **A6-ter recesso parziale** *(OPZIONALE — non è un obbligo di legge)* — Base legale in `ANALISI-REQUISITI-LEGALI.md` §5: il **considerando 37 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 è conforme; il parziale è **differenziazione di prodotto** (lo fa Revize, non lo fa Rescindly) e copre il caso reale Pizeta (3 confezioni, ne rende 2).
- Toggle per-shop **`partialWithdrawalEnabled`** (default **OFF** = comportamento attuale, un clic = recesso totale).
- Passo 2: elenco articoli dell'ordine con quantità; **default tutti selezionati** (nessun attrito per chi vuole il totale).
- Dichiarazione **generata sugli articoli scelti** (oggi dice "intero ordine": non si può lasciare così se il cliente seleziona un sottoinsieme).
- `createShopifyReturn`: reso limitato alle **righe/quantità selezionate**.
- `autoCancelUnfulfilled`: si attiva **solo** se la selezione copre l'intero ordine. Parziale + non evaso → order edit / rimborso parziale, che resta manuale.
- **Idempotenza da rivedere**: oggi la chiave è `(shop, orderId)` (commit `fa8ee9c`), corretta solo nel mondo total-only. Col parziale deve diventare `(shop, orderId, articoli)`: il cliente può recedere per l'articolo A oggi e per il B domani, entrambi legittimi nella finestra.
- Dati da aggiungere: `lookupOrder` deve restituire titoli, quantità e ID riga; mappatura `lineItem ↔ fulfillmentLineItem` per il reso; nuova colonna (es. `items Json`) su `WithdrawalRequest`.
- **Uscita:** merchant può abilitare il parziale; con toggle OFF il comportamento resta identico a oggi.
### Fase 4 — Robustezza *(A7→A8 sequenziali, poi A9 audit)* ### Fase 4 — Robustezza *(A7→A8 sequenziali, poi A9 audit)*
- **A7 i18n** — localizzazione IT/EN/DE/FR/ES (R14). - **A7 i18n** — localizzazione IT/EN/DE/FR/ES (R14).
@@ -218,6 +227,7 @@ integrati. Dettaglio stato/commit nella memoria di progetto + git.
- **R1 — A6-bis operatività per stato ordine (Pizeta-confirmed)** — 3 toggle per-shop: `stateAwareEmail` (ricevuta differenziata non-evaso/spedito-consegnato), `autoCancelUnfulfilled` (annullo automatico ordini non evasi), `returnAtCustomerExpense`+`returnInstructions`+`returnAddress` (istruzioni reso, Art. 57). Dettaglio: sezione A6-bis. - **R1 — A6-bis operatività per stato ordine (Pizeta-confirmed)** — 3 toggle per-shop: `stateAwareEmail` (ricevuta differenziata non-evaso/spedito-consegnato), `autoCancelUnfulfilled` (annullo automatico ordini non evasi), `returnAtCustomerExpense`+`returnInstructions`+`returnAddress` (istruzioni reso, Art. 57). Dettaglio: sezione A6-bis.
- **R2 — A6 residuo copy compliance** — G7: promemoria rimborso 14gg + facoltà di trattenuta (Art. 56) nella notifica al merchant. *(G6 spese-reso assorbito da R1.)* - **R2 — A6 residuo copy compliance** — G7: promemoria rimborso 14gg + facoltà di trattenuta (Art. 56) nella notifica al merchant. *(G6 spese-reso assorbito da R1.)*
- **R2-bis — A6-ter recesso parziale** *(opzionale, toggle `partialWithdrawalEnabled`, default OFF)* — non è compliance ma prodotto: selezione articoli/quantità, dichiarazione sugli articoli scelti, reso limitato alle righe scelte, auto-annullo solo se selezione = intero ordine, idempotenza su `(ordine, articoli)`. Vedi A6-ter e `ANALISI-REQUISITI-LEGALI.md` §5.
- **R3 — Deploy custom (go-live)** — Fly.io + Postgres prod + URL stabile + tunnel Cloudflare nominato + link install custom sui 2-3 store live. *(Serve per usarlo davvero; occhio deadline transfer pizeta-pharma-2 ~metà luglio 2026.)* - **R3 — Deploy custom (go-live)** — Fly.io + Postgres prod + URL stabile + tunnel Cloudflare nominato + link install custom sui 2-3 store live. *(Serve per usarlo davvero; occhio deadline transfer pizeta-pharma-2 ~metà luglio 2026.)*
- **R4 — A9 hardening / QA-security** — rate-limit robusto, retry ricevuta fallita, idempotenza webhook, review avversariale (enumeration lookup, HMAC, PII), Protected Customer Data (approvazione Shopify per leggere `order.email` in prod). - **R4 — A9 hardening / QA-security** — rate-limit robusto, retry ricevuta fallita, idempotenza webhook, review avversariale (enumeration lookup, HMAC, PII), Protected Customer Data (approvazione Shopify per leggere `order.email` in prod).
- **R5 — A7 i18n** — IT/EN/DE/FR/ES. - **R5 — A7 i18n** — IT/EN/DE/FR/ES.

View File

@@ -23,9 +23,20 @@ export const FIELD = {
// Dichiarazione precompilata (editabile), deriva dall'Allegato I-B. // Dichiarazione precompilata (editabile), deriva dall'Allegato I-B.
export function statementTemplate(orderName: string): string { export function statementTemplate(orderName: string): string {
return `Con la presente comunico il recesso dal contratto di vendita relativo all'ordine ${orderName}.`; return `Con la presente comunico il recesso dal contratto di vendita relativo all'intero ordine ${orderName}.`;
} }
/**
* Ambito del recesso esercitato tramite questa funzione. La funzione dell'art.
* 54-bis puo' legittimamente operare a livello di CONTRATTO (ordine): offrire
* il recesso parziale e' una facolta' del professionista (considerando 37 Dir.
* UE 2023/2673, "the trader CAN provide..."), non un obbligo. Il diritto al
* recesso parziale resta comunque esercitabile dagli altri canali (modulo tipo,
* email), che non vanno mai disabilitati: per questo li indichiamo.
*/
export const SCOPE_HINT =
"Questo recesso riguarda l'intero ordine. Per restituire solo alcuni articoli, contatta il negozio.";
// Informazioni sul diritto di recesso (Art. 49) + alternative (coesistenza). // Informazioni sul diritto di recesso (Art. 49) + alternative (coesistenza).
// Mostrate su richiesta dal pulsante info "i", per non appesantire il flusso. // Mostrate su richiesta dal pulsante info "i", per non appesantire il flusso.
export const INFO_TITLE = "Il tuo diritto di recesso"; export const INFO_TITLE = "Il tuo diritto di recesso";

View File

@@ -16,7 +16,14 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import type { AdminApiContext } from "@shopify/shopify-app-remix/server"; import type { AdminApiContext } from "@shopify/shopify-app-remix/server";
import type { ExclusionRule } from "@prisma/client"; import type { ExclusionRule } from "@prisma/client";
import { FIELD, INFO_TITLE, INFO_BODY, CONFIRM_LABEL, PAGE_TITLE } from "./recesso.copy"; import {
FIELD,
INFO_TITLE,
INFO_BODY,
CONFIRM_LABEL,
PAGE_TITLE,
SCOPE_HINT,
} from "./recesso.copy";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Costanti path storefront (prefix "apps" + subpath "recesso" da shopify.app.toml). // Costanti path storefront (prefix "apps" + subpath "recesso" da shopify.app.toml).
@@ -1107,6 +1114,7 @@ export function renderStep2(data: {
<label for="statementText">${escapeHtml(FIELD.statement.label)}</label> <label for="statementText">${escapeHtml(FIELD.statement.label)}</label>
<textarea id="statementText" name="statementText" required>${escapeHtml(data.statementText)}</textarea> <textarea id="statementText" name="statementText" required>${escapeHtml(data.statementText)}</textarea>
<p class="hint">${escapeHtml(SCOPE_HINT)}</p>
<label for="email">${escapeHtml(FIELD.email.label)}</label> <label for="email">${escapeHtml(FIELD.email.label)}</label>
<input type="email" id="email" name="email" value="${attr(data.email)}" autocomplete="email" required> <input type="email" id="email" name="email" value="${attr(data.email)}" autocomplete="email" required>

View File

@@ -536,6 +536,17 @@ export default function SettingsPage() {
onChange={setAutoCancelUnfulfilled} onChange={setAutoCancelUnfulfilled}
helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile." helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile."
/> />
{autoCancelUnfulfilled ? (
<Banner tone="warning" title="Annulla l'INTERO ordine">
<Text as="p">
Il recesso da questa funzione riguarda tutto l'ordine.
Se il cliente voleva restituire solo alcuni articoli,
l'annullo automatico gli cancella e rimborsa l'intero
ordine, e non è reversibile. Con ordini multi-articolo
valuta di tenerlo spento e decidere caso per caso.
</Text>
</Banner>
) : null}
<Checkbox <Checkbox
label="Spese di restituzione a carico del cliente (Art. 57)" label="Spese di restituzione a carico del cliente (Art. 57)"
checked={returnAtCustomerExpense} checked={returnAtCustomerExpense}