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:
@@ -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
10
PLAN.md
@@ -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.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user