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
This commit is contained in:
43
CHECKLIST-COMPLIANCE-MERCHANT.md
Normal file
43
CHECKLIST-COMPLIANCE-MERCHANT.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Recesso - Cosa fa l'app vs cosa deve fare il merchant
|
||||||
|
|
||||||
|
Ripartizione delle responsabilita' di conformita' (Art. 54-bis + Codice del
|
||||||
|
Consumo). L'app copre la FUNZIONE elettronica di recesso; alcuni obblighi restano
|
||||||
|
in capo al merchant. Da consegnare col progetto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cosa GARANTISCE l'app (automatico, non disattivabile)
|
||||||
|
|
||||||
|
- **Pulsante/funzione di recesso sempre accessibile** (footer o dove scelto), **guest**, senza login.
|
||||||
|
- Raccolta della **dichiarazione inequivocabile** (nome, n. ordine, email, testo) con **conferma dedicata a 2 step** (nessun dark pattern).
|
||||||
|
- Registrazione con **timestamp di TRASMISSIONE** (onere della prova, Art. 54-bis).
|
||||||
|
- **Ricevuta su supporto durevole** al consumatore: dichiarazione + timestamp + avviso di legge - **sempre inviata, contenuto legale non modificabile** dal merchant.
|
||||||
|
- **Audit log immutabile** delle richieste ed eventi.
|
||||||
|
- Coesistenza col reso/rimborso **nativo Shopify** (crea il Reso per gli ordini evasi).
|
||||||
|
|
||||||
|
## 2. Cosa il merchant CONFIGURA nell'app (Impostazioni, opzionale)
|
||||||
|
|
||||||
|
- Testi email (oggetto/introduzione/nota) - le parti legali restano fisse.
|
||||||
|
- Notifica al merchant (on/off + indirizzo email).
|
||||||
|
- Tag "Recesso" sull'ordine (on/off).
|
||||||
|
- Finestra 14 gg: enforcement on/off + giorni (default 14, calcolata sulla **consegna**).
|
||||||
|
- Esclusioni Art. 59 (prodotti/tag non recedibili) + enforcement on/off.
|
||||||
|
- **[R1 A6-bis]** Ricevuta differenziata per stato ordine; annullo automatico ordini non evasi; indirizzo reso + spese a carico cliente + istruzioni di reso.
|
||||||
|
|
||||||
|
## 3. Cosa il merchant deve fare FUORI dall'app (obblighi propri)
|
||||||
|
|
||||||
|
- **Informativa precontrattuale** sul diritto di recesso (Art. 49) nelle pagine/checkout.
|
||||||
|
- Mettere a disposizione il **modulo tipo** di recesso (Allegato I, parte B) - coesiste col pulsante.
|
||||||
|
- **Emettere il rimborso entro 14 gg** dalla notifica (Art. 56) se non usa l'annullo/rimborso automatico dell'app. Puo' trattenere fino a riconsegna merce o prova di spedizione.
|
||||||
|
- Gestire **resi parziali** e rimborsi proporzionali (pannello ordini Shopify).
|
||||||
|
- Definire la **politica di reso** (indirizzo, spese, integrita' prodotto) coerente con quanto mostra l'app.
|
||||||
|
- **Verifica manuale della data di consegna** se il corriere non trasmette l'evento a Shopify (in tal caso la finestra automatica non blocca).
|
||||||
|
- Privacy policy, condizioni di vendita, gestione dati (GDPR) del negozio.
|
||||||
|
- Configurare le **esclusioni Art. 59** solo per prodotti realmente esclusi (mala-config = negare il diritto a torto).
|
||||||
|
|
||||||
|
## 4. Note
|
||||||
|
|
||||||
|
- L'app e' uno **strumento** di conformita', non sostituisce la consulenza legale.
|
||||||
|
- Prima del go-live (e soprattutto per la versione pubblica/App Store) consigliata una **review legale**, anche per chiudere i punti ⚠ dottrinali in `ANALISI-REQUISITI-LEGALI.md`.
|
||||||
|
- Ambito: **beni B2C online**. Servizi e beni digitali hanno decorrenza/esclusioni diverse (Art. 52/59) - da valutare per merchant fuori scope.
|
||||||
|
- Approfondimento stati ordine: `AUDIT-STATI-ORDINE.md`.
|
||||||
@@ -61,11 +61,45 @@ export function renderSubject(subjectTpl: string | null | undefined, vars: Email
|
|||||||
return substPlain(tpl, vars).trim() || substPlain(DEFAULT_SUBJECT, vars);
|
return substPlain(tpl, vars).trim() || substPlain(DEFAULT_SUBJECT, vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperationalConfig {
|
||||||
|
state: "unfulfilled" | "shipped" | "delivered";
|
||||||
|
returnAddress?: string | null;
|
||||||
|
atCustomerExpense: boolean;
|
||||||
|
instructions?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blocco operativo per stato ordine (A6-bis): annullo (non evaso) o istruzioni reso. */
|
||||||
|
function renderOperationalBlock(op: OperationalConfig): string {
|
||||||
|
let inner: string;
|
||||||
|
if (op.state === "unfulfilled") {
|
||||||
|
inner =
|
||||||
|
"Il tuo ordine non risultava ancora spedito: procederemo all'annullamento e al rimborso. Non devi restituire nulla.";
|
||||||
|
} else {
|
||||||
|
const stateWord = op.state === "delivered" ? "consegnato" : "spedito";
|
||||||
|
const addr =
|
||||||
|
op.returnAddress && op.returnAddress.trim()
|
||||||
|
? `<strong>${escHtml(op.returnAddress.trim())}</strong>`
|
||||||
|
: "l'indirizzo che ti comunicheremo";
|
||||||
|
const spese = op.atCustomerExpense
|
||||||
|
? "Le spese di restituzione sono a tuo carico."
|
||||||
|
: "Le spese di restituzione sono a nostro carico.";
|
||||||
|
const instr =
|
||||||
|
op.instructions && op.instructions.trim()
|
||||||
|
? `<br>${nl2br(escHtml(op.instructions.trim()))}`
|
||||||
|
: "";
|
||||||
|
inner = `Il prodotto risulta ${stateWord}. Per ottenere il rimborso, restituisci la merce integra a: ${addr}. ${spese}${instr}<br>Il rimborso sara' disposto dopo il rientro della merce.`;
|
||||||
|
}
|
||||||
|
return `<tr><td style="padding:4px 32px 8px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td style="padding:14px 16px;background:#fff7e6;border:1px solid #ffe2a8;border-radius:8px;font-size:14px;line-height:1.6;color:#6a5518;">${inner}</td></tr></table>
|
||||||
|
</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Corpo HTML fisso con intro/nota editabili inseriti. */
|
/** Corpo HTML fisso con intro/nota editabili inseriti. */
|
||||||
export function renderReceiptHtml(
|
export function renderReceiptHtml(
|
||||||
vars: EmailVars,
|
vars: EmailVars,
|
||||||
introTpl: string | null | undefined,
|
introTpl: string | null | undefined,
|
||||||
noteTpl: string | null | undefined,
|
noteTpl: string | null | undefined,
|
||||||
|
operational?: OperationalConfig | null,
|
||||||
): string {
|
): string {
|
||||||
const intro = richText((introTpl && introTpl.trim()) || DEFAULT_INTRO, vars);
|
const intro = richText((introTpl && introTpl.trim()) || DEFAULT_INTRO, vars);
|
||||||
const noteVal = noteTpl && noteTpl.trim() ? richText(noteTpl, vars) : "";
|
const noteVal = noteTpl && noteTpl.trim() ? richText(noteTpl, vars) : "";
|
||||||
@@ -107,6 +141,7 @@ export function renderReceiptHtml(
|
|||||||
<tr><td style="padding:12px 32px 4px;">
|
<tr><td style="padding:12px 32px 4px;">
|
||||||
<p style="margin:0;font-size:12.5px;line-height:1.6;color:#8a8a8a;">Questa comunicazione costituisce la ricevuta su supporto durevole ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora indicate attestano il momento della trasmissione.</p>
|
<p style="margin:0;font-size:12.5px;line-height:1.6;color:#8a8a8a;">Questa comunicazione costituisce la ricevuta su supporto durevole ai sensi dell'art. 54-bis del Codice del Consumo. La data e l'ora indicate attestano il momento della trasmissione.</p>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
${operational ? renderOperationalBlock(operational) : ""}
|
||||||
${note}
|
${note}
|
||||||
<tr><td style="padding:18px 32px 24px;border-top:1px solid #ececec;">
|
<tr><td style="padding:18px 32px 24px;border-top:1px solid #ececec;">
|
||||||
<p style="margin:0 0 8px;font-size:12.5px;line-height:1.6;color:#999;">Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso.</p>
|
<p style="margin:0 0 8px;font-size:12.5px;line-height:1.6;color:#999;">Ti invieremo separatamente le istruzioni per l'eventuale reso e i tempi di rimborso.</p>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
renderReceiptHtml,
|
renderReceiptHtml,
|
||||||
renderSubject,
|
renderSubject,
|
||||||
type EmailVars,
|
type EmailVars,
|
||||||
|
type OperationalConfig,
|
||||||
} from "./emailTemplate";
|
} from "./emailTemplate";
|
||||||
|
|
||||||
function buildTransport() {
|
function buildTransport() {
|
||||||
@@ -65,6 +66,7 @@ export async function sendWithdrawalReceipt(params: {
|
|||||||
subject?: string | null;
|
subject?: string | null;
|
||||||
intro?: string | null;
|
intro?: string | null;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
|
operational?: OperationalConfig | null;
|
||||||
}): Promise<ReceiptResult> {
|
}): Promise<ReceiptResult> {
|
||||||
const transport = buildTransport();
|
const transport = buildTransport();
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
@@ -72,7 +74,12 @@ export async function sendWithdrawalReceipt(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subject = renderSubject(params.subject, params.vars);
|
const subject = renderSubject(params.subject, params.vars);
|
||||||
const html = renderReceiptHtml(params.vars, params.intro, params.note);
|
const html = renderReceiptHtml(
|
||||||
|
params.vars,
|
||||||
|
params.intro,
|
||||||
|
params.note,
|
||||||
|
params.operational,
|
||||||
|
);
|
||||||
const text = htmlToText(html);
|
const text = htmlToText(html);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -203,6 +203,23 @@ export function checkExclusions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- A6-bis: stato operativo ordine --------------------------------------
|
||||||
|
export type OrderState = "unfulfilled" | "shipped" | "delivered" | "closed";
|
||||||
|
|
||||||
|
/** Stato per il flusso recesso (email differenziata + auto-annullo). */
|
||||||
|
export function orderState(match: MatchedOrder): OrderState {
|
||||||
|
if (
|
||||||
|
match.cancelledAt ||
|
||||||
|
match.financialStatus === "REFUNDED" ||
|
||||||
|
match.financialStatus === "VOIDED"
|
||||||
|
) {
|
||||||
|
return "closed";
|
||||||
|
}
|
||||||
|
if (match.deliveredAt) return "delivered";
|
||||||
|
if (match.fulfilledAt) return "shipped";
|
||||||
|
return "unfulfilled";
|
||||||
|
}
|
||||||
|
|
||||||
interface OrderLookupGraphQL {
|
interface OrderLookupGraphQL {
|
||||||
data?: {
|
data?: {
|
||||||
orders?: {
|
orders?: {
|
||||||
@@ -493,6 +510,51 @@ export async function tagOrderRecesso(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ORDER_CANCEL_MUTATION = `#graphql
|
||||||
|
mutation recessoOrderCancel($id: ID!) {
|
||||||
|
orderCancel(orderId: $id, reason: CUSTOMER, refund: true, restock: true, notifyCustomer: false, staffNote: "Recesso art. 54-bis") {
|
||||||
|
job { id }
|
||||||
|
orderCancelUserErrors { field message }
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
interface OrderCancelGraphQL {
|
||||||
|
data?: {
|
||||||
|
orderCancel?: {
|
||||||
|
orderCancelUserErrors?: Array<{ message?: string | null }> | null;
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Annulla l'ordine (recesso su ordine non evaso): refund + restock. Best-effort. */
|
||||||
|
export async function cancelOrder(
|
||||||
|
admin: AdminApiContext,
|
||||||
|
orderGid: string,
|
||||||
|
): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
const res = await admin.graphql(ORDER_CANCEL_MUTATION, {
|
||||||
|
variables: { id: orderGid },
|
||||||
|
});
|
||||||
|
const body = (await res.json()) as OrderCancelGraphQL;
|
||||||
|
const errs = body.data?.orderCancel?.orderCancelUserErrors ?? [];
|
||||||
|
if (errs.length) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: errs
|
||||||
|
.map((e) => e?.message ?? "")
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("; "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: e instanceof Error ? e.message : "orderCancel fallito",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function lookupOrder(
|
export async function lookupOrder(
|
||||||
admin: AdminApiContext,
|
admin: AdminApiContext,
|
||||||
orderInput: string,
|
orderInput: string,
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
enforceWindow: settings?.enforceWindow ?? false,
|
enforceWindow: settings?.enforceWindow ?? false,
|
||||||
windowDays: settings?.defaultWindowDays ?? 14,
|
windowDays: settings?.defaultWindowDays ?? 14,
|
||||||
enforceExclusions: settings?.enforceExclusions ?? false,
|
enforceExclusions: settings?.enforceExclusions ?? false,
|
||||||
|
stateAwareEmail: settings?.stateAwareEmail ?? true,
|
||||||
|
autoCancelUnfulfilled: settings?.autoCancelUnfulfilled ?? false,
|
||||||
|
returnAtCustomerExpense: settings?.returnAtCustomerExpense ?? true,
|
||||||
|
returnAddress: settings?.returnAddress ?? "",
|
||||||
|
returnInstructions: settings?.returnInstructions ?? "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,6 +75,12 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
Math.max(1, Number(form.get("windowDays")) || 14),
|
Math.max(1, Number(form.get("windowDays")) || 14),
|
||||||
),
|
),
|
||||||
enforceExclusions: form.get("enforceExclusions") === "true",
|
enforceExclusions: form.get("enforceExclusions") === "true",
|
||||||
|
stateAwareEmail: form.get("stateAwareEmail") === "true",
|
||||||
|
autoCancelUnfulfilled: form.get("autoCancelUnfulfilled") === "true",
|
||||||
|
returnAtCustomerExpense: form.get("returnAtCustomerExpense") === "true",
|
||||||
|
returnAddress: String(form.get("returnAddress") ?? "").trim() || null,
|
||||||
|
returnInstructions:
|
||||||
|
String(form.get("returnInstructions") ?? "").trim() || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.settings.upsert({
|
await db.settings.upsert({
|
||||||
@@ -98,6 +109,17 @@ export default function SettingsPage() {
|
|||||||
const [enforceExclusions, setEnforceExclusions] = useState(
|
const [enforceExclusions, setEnforceExclusions] = useState(
|
||||||
data.enforceExclusions,
|
data.enforceExclusions,
|
||||||
);
|
);
|
||||||
|
const [stateAwareEmail, setStateAwareEmail] = useState(data.stateAwareEmail);
|
||||||
|
const [autoCancelUnfulfilled, setAutoCancelUnfulfilled] = useState(
|
||||||
|
data.autoCancelUnfulfilled,
|
||||||
|
);
|
||||||
|
const [returnAtCustomerExpense, setReturnAtCustomerExpense] = useState(
|
||||||
|
data.returnAtCustomerExpense,
|
||||||
|
);
|
||||||
|
const [returnAddress, setReturnAddress] = useState(data.returnAddress);
|
||||||
|
const [returnInstructions, setReturnInstructions] = useState(
|
||||||
|
data.returnInstructions,
|
||||||
|
);
|
||||||
const [showSaved, setShowSaved] = useState(false);
|
const [showSaved, setShowSaved] = useState(false);
|
||||||
|
|
||||||
const saving = nav.state === "submitting";
|
const saving = nav.state === "submitting";
|
||||||
@@ -127,6 +149,11 @@ export default function SettingsPage() {
|
|||||||
fd.set("enforceWindow", String(enforceWindow));
|
fd.set("enforceWindow", String(enforceWindow));
|
||||||
fd.set("windowDays", windowDays);
|
fd.set("windowDays", windowDays);
|
||||||
fd.set("enforceExclusions", String(enforceExclusions));
|
fd.set("enforceExclusions", String(enforceExclusions));
|
||||||
|
fd.set("stateAwareEmail", String(stateAwareEmail));
|
||||||
|
fd.set("autoCancelUnfulfilled", String(autoCancelUnfulfilled));
|
||||||
|
fd.set("returnAtCustomerExpense", String(returnAtCustomerExpense));
|
||||||
|
fd.set("returnAddress", returnAddress);
|
||||||
|
fd.set("returnInstructions", returnInstructions);
|
||||||
submit(fd, { method: "post" });
|
submit(fd, { method: "post" });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,6 +302,52 @@ export default function SettingsPage() {
|
|||||||
</BlockStack>
|
</BlockStack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<BlockStack gap="400">
|
||||||
|
<Text as="h2" variant="headingMd">
|
||||||
|
Reso e stato ordine
|
||||||
|
</Text>
|
||||||
|
<Checkbox
|
||||||
|
label="Adatta la ricevuta allo stato dell'ordine"
|
||||||
|
checked={stateAwareEmail}
|
||||||
|
onChange={setStateAwareEmail}
|
||||||
|
helpText="Non evaso → annullo+rimborso; spedito/consegnato → istruzioni di reso."
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Annulla automaticamente gli ordini non ancora spediti"
|
||||||
|
checked={autoCancelUnfulfilled}
|
||||||
|
onChange={setAutoCancelUnfulfilled}
|
||||||
|
helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile."
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Spese di restituzione a carico del cliente (Art. 57)"
|
||||||
|
checked={returnAtCustomerExpense}
|
||||||
|
onChange={setReturnAtCustomerExpense}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Indirizzo per il reso"
|
||||||
|
value={returnAddress}
|
||||||
|
onChange={setReturnAddress}
|
||||||
|
autoComplete="off"
|
||||||
|
multiline={2}
|
||||||
|
placeholder="Via ..., CAP Città (PR)"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Istruzioni di reso (opzionale)"
|
||||||
|
value={returnInstructions}
|
||||||
|
onChange={setReturnInstructions}
|
||||||
|
autoComplete="off"
|
||||||
|
multiline={3}
|
||||||
|
helpText="Testo aggiuntivo mostrato nella ricevuta per ordini spediti/consegnati."
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" loading={saving} onClick={handleSave}>
|
||||||
|
Salva
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</BlockStack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<BlockStack gap="200">
|
<BlockStack gap="200">
|
||||||
<Text as="h2" variant="headingMd">
|
<Text as="h2" variant="headingMd">
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
MVP_LOCALE,
|
MVP_LOCALE,
|
||||||
checkExclusions,
|
checkExclusions,
|
||||||
checkRateLimit,
|
checkRateLimit,
|
||||||
|
cancelOrder,
|
||||||
clientIp,
|
clientIp,
|
||||||
createShopifyReturn,
|
createShopifyReturn,
|
||||||
formatTransmittedAt,
|
formatTransmittedAt,
|
||||||
@@ -42,6 +43,7 @@ import {
|
|||||||
isValidEmail,
|
isValidEmail,
|
||||||
isWindowExpired,
|
isWindowExpired,
|
||||||
lookupOrder,
|
lookupOrder,
|
||||||
|
orderState,
|
||||||
renderStep1,
|
renderStep1,
|
||||||
renderStep2,
|
renderStep2,
|
||||||
renderStep3,
|
renderStep3,
|
||||||
@@ -366,6 +368,18 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
getShopInfo(admin),
|
getShopInfo(admin),
|
||||||
]);
|
]);
|
||||||
const shopName = shopInfo.name || shop.replace(/\.myshopify\.com$/, "");
|
const shopName = shopInfo.name || shop.replace(/\.myshopify\.com$/, "");
|
||||||
|
// A6-bis: blocco operativo per stato ordine (se stateAwareEmail attivo).
|
||||||
|
const state = orderState(match);
|
||||||
|
const operational =
|
||||||
|
settings?.stateAwareEmail !== false &&
|
||||||
|
(state === "unfulfilled" || state === "shipped" || state === "delivered")
|
||||||
|
? {
|
||||||
|
state,
|
||||||
|
returnAddress: settings?.returnAddress,
|
||||||
|
atCustomerExpense: settings?.returnAtCustomerExpense ?? true,
|
||||||
|
instructions: settings?.returnInstructions,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
const receipt = await sendWithdrawalReceipt({
|
const receipt = await sendWithdrawalReceipt({
|
||||||
to: email,
|
to: email,
|
||||||
vars: {
|
vars: {
|
||||||
@@ -380,6 +394,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
subject: settings?.emailSubject,
|
subject: settings?.emailSubject,
|
||||||
intro: settings?.emailIntro,
|
intro: settings?.emailIntro,
|
||||||
note: settings?.emailNote,
|
note: settings?.emailNote,
|
||||||
|
operational,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (receipt.ok) {
|
if (receipt.ok) {
|
||||||
@@ -463,6 +478,22 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A6-bis: auto-annullo ordini non evasi (se abilitato). refund + restock.
|
||||||
|
if (settings?.autoCancelUnfulfilled && state === "unfulfilled") {
|
||||||
|
try {
|
||||||
|
const c = await cancelOrder(admin, match.orderId);
|
||||||
|
await db.auditLog.create({
|
||||||
|
data: {
|
||||||
|
shop,
|
||||||
|
event: c.ok ? "order_auto_cancelled" : "order_auto_cancel_failed",
|
||||||
|
detail: c.ok ? match.orderName : (c.error ?? "").slice(0, 200),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[recesso] auto-annullo fallito:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tag "Recesso" sull'ordine (se abilitato nei Settings). Richiede write_orders.
|
// Tag "Recesso" sull'ordine (se abilitato nei Settings). Richiede write_orders.
|
||||||
if (settings?.tagEnabled) {
|
if (settings?.tagEnabled) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Settings" ADD COLUMN "autoCancelUnfulfilled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "returnAtCustomerExpense" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN "returnInstructions" TEXT,
|
||||||
|
ADD COLUMN "stateAwareEmail" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -57,6 +57,10 @@ model Settings {
|
|||||||
tagEnabled Boolean @default(true)
|
tagEnabled Boolean @default(true)
|
||||||
enforceWindow Boolean @default(false)
|
enforceWindow Boolean @default(false)
|
||||||
enforceExclusions Boolean @default(false)
|
enforceExclusions Boolean @default(false)
|
||||||
|
stateAwareEmail Boolean @default(true)
|
||||||
|
autoCancelUnfulfilled Boolean @default(false)
|
||||||
|
returnAtCustomerExpense Boolean @default(true)
|
||||||
|
returnInstructions String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user