Gutscheine "nur einmal pro Person" + Bestell-E-Mail-Benachrichtigung
1) Gutscheincodes können jetzt im Adminbereich auf "nur einmal pro Person einlösbar" gestellt werden. Wird serverseitig durchgesetzt (neue Spalte orders.gutschein_code + Prüfung in functions/api/orders.js gegen die echte Bestelldatenbank, case-insensitiv nach E-Mail-Adresse) — kann also NICHT durch Löschen von localStorage/einen anderen Browser umgangen werden, anders als die bisherige rein clientseitige Rabattlogik. Stornierte Bestellungen zählen nicht als "schon benutzt". Ausführlich getestet: echter Browser-Checkout mit Einmal-Code funktioniert beim ersten Versuch, wird beim zweiten Versuch (gleiche E-Mail) korrekt mit klarer Fehlermeldung abgelehnt, mit anderer E-Mail weiterhin nutzbar. 2) Bestell-E-Mail-Benachrichtigung an [email protected]: recherchiert und vorbereitet (existierte vorher nirgends im Code). Läuft über Resend (kostenlos bis 3.000 E-Mails/Monat), im Hintergrund per context.waitUntil() nach jeder neuen Bestellung — verzögert nie die Antwort an die Kundschaft und kann eine Bestellung niemals zum Scheitern bringen, selbst wenn der Versand fehlschlägt (mit ungültigem Testschlüssel verifiziert: Bestellung bleibt trotzdem erfolgreich gespeichert). Wird automatisch aktiv, sobald ein RESEND_API_KEY als Secret hinterlegt wird. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0c1c8f075f
commit
af729ba571
@@ -0,0 +1,58 @@
|
||||
/* =====================================================================
|
||||
functions/_shared/email.js — E-Mail-Versand über Resend (kostenlos bis 3.000 E-Mails/Monat,
|
||||
100/Tag — für einen kleinen Handmade-Shop mehr als ausreichend, siehe Recherche im Vault-
|
||||
Tagesprotokoll). Bewusst NICHT über Cloudflare Email Routing/Workers Send-Email-Binding
|
||||
gelöst, weil das einen bereits verifizierten, eigenen Domain-Versand voraussetzt — die Seite
|
||||
läuft aktuell noch auf *.pages.dev ohne eigene Domain. Resend funktioniert dagegen sofort mit
|
||||
der kostenlosen "[email protected]"-Absenderadresse, ganz ohne eigene Domain-Verifizierung.
|
||||
|
||||
WICHTIG: E-Mail-Versand darf NIE eine Bestellung zum Scheitern bringen — die Bestellung ist
|
||||
in der Datenbank (D1) längst sicher gespeichert, bevor diese Funktion überhaupt aufgerufen
|
||||
wird. Deshalb wirft sendResendEmail() nie einen Fehler nach außen, sondern gibt nur
|
||||
{ ok: true/false, error? } zurück — der Aufrufer entscheidet selbst, ob/wie er einen
|
||||
Fehlschlag protokolliert (siehe functions/api/orders.js). ===================================================================== */
|
||||
|
||||
const RESEND_API_URL = "https://api.resend.com/emails";
|
||||
const STANDARD_ABSENDER = "Van's DIY & Bastelbedarf <[email protected]>";
|
||||
|
||||
/**
|
||||
* Verschickt eine E-Mail über die Resend-API. Gibt IMMER ein Ergebnisobjekt zurück (nie eine
|
||||
* geworfene Exception), damit der Aufrufer den Versand einfach "fire and forget" behandeln kann.
|
||||
* `env.RESEND_API_KEY` fehlt einfach so lange, bis VanVan/qciga einen kostenlosen Resend-Account
|
||||
* eingerichtet haben — bis dahin liefert diese Funktion sauber `{ ok: false, error: "..." }`
|
||||
* zurück, ohne irgendetwas kaputtzumachen.
|
||||
*/
|
||||
export async function sendeEmail(env, { to, subject, html, text, from }) {
|
||||
if (!env.RESEND_API_KEY) {
|
||||
return { ok: false, error: "RESEND_API_KEY nicht gesetzt — E-Mail-Versand noch nicht eingerichtet." };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(RESEND_API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.RESEND_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: from || env.RESEND_FROM || STANDARD_ABSENDER,
|
||||
to: Array.isArray(to) ? to : [to],
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const fehlertext = await res.text().catch(() => "");
|
||||
return { ok: false, error: `Resend antwortete mit Status ${res.status}: ${fehlertext}` };
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
/** Escaped Text sicher fürs Einbetten in eine HTML-E-Mail (verhindert kaputtes HTML bei z.B.
|
||||
* Sonderzeichen im Kundennamen). */
|
||||
export function escapeHtmlFuerEmail(str) {
|
||||
return String(str ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
Reference in New Issue
Block a user