Node.js/Express-Server als Ersatz für Cloudflare Pages Functions + D1 (läuft auf eigenem Server statt Cloudflare)
Build & Deploy / deploy (push) Waiting to run
Build & Deploy / deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
/* =====================================================================
|
||||
lib/bestellung-erstellen.js — die eigentliche "Bestellung anlegen"-Logik. 1:1 portiert aus
|
||||
functions/_shared/bestellung-erstellen.js, D1-Aufrufe (async, .bind().run()) auf better-
|
||||
sqlite3 (synchron) umgestellt. Gleiche Validierung, gleiche "nur einmal pro Person"-Logik,
|
||||
gleicher E-Mail-Versand.
|
||||
===================================================================== */
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { db } from "../db.js";
|
||||
import { sendeEmail, escapeHtmlFuerEmail } from "./email.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const gutscheineRaw = JSON.parse(
|
||||
readFileSync(join(__dirname, "..", "..", "src", "content", "gutscheine.json"), "utf-8")
|
||||
);
|
||||
|
||||
const ERLAUBTE_LAENDER = ["de", "at", "ch", "lu"];
|
||||
const gutscheine = gutscheineRaw?.codes || [];
|
||||
const BENACHRICHTIGUNGS_EMAIL = "[email protected]";
|
||||
const LAND_NAME = { de: "Deutschland", at: "Österreich", ch: "Schweiz", lu: "Luxemburg" };
|
||||
|
||||
function formatPreis(n) {
|
||||
return `${Number(n).toFixed(2).replace(".", ",")} €`;
|
||||
}
|
||||
|
||||
function bestellBenachrichtigungHtml({ bestellnummer, name, email, land, artikel, summe, status }) {
|
||||
const artikelZeilen = artikel
|
||||
.map((a) => `<li>${a.menge}× ${escapeHtmlFuerEmail(a.name)} — ${formatPreis(a.preis * a.menge)}</li>`)
|
||||
.join("");
|
||||
const statusZeile =
|
||||
status === "zahlungOffen"
|
||||
? `<p style="color:#b06a00;"><strong>⏳ Zahlung noch ausstehend</strong> — Überweisung, bitte Zahlungseingang abwarten, bevor die Ware verschickt wird.</p>`
|
||||
: `<p style="color:#1f9254;"><strong>✅ Zahlung bestätigt</strong> (PayPal)</p>`;
|
||||
return `
|
||||
<div style="font-family: sans-serif; color: #1a1a1a; line-height: 1.6;">
|
||||
<h2 style="margin: 0 0 0.5em;">🩵 Neue Bestellung ${escapeHtmlFuerEmail(bestellnummer)}</h2>
|
||||
${statusZeile}
|
||||
<p><strong>Von:</strong> ${escapeHtmlFuerEmail(name)} (${escapeHtmlFuerEmail(email)})<br />
|
||||
<strong>Land:</strong> ${escapeHtmlFuerEmail(LAND_NAME[land] || land)}</p>
|
||||
<p><strong>Artikel:</strong></p>
|
||||
<ul>${artikelZeilen}</ul>
|
||||
<p><strong>Summe: ${formatPreis(summe)}</strong></p>
|
||||
<p><a href="https://vans-diy-bastelbedarf.de/verwaltung/">→ Zur Bestellverwaltung</a></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function findeGutschein(code) {
|
||||
const normalisiert = String(code || "").trim().toUpperCase();
|
||||
if (!normalisiert) return null;
|
||||
return gutscheine.find((g) => String(g.code || "").toUpperCase() === normalisiert) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Bestelldaten, legt sie in SQLite an und verschickt die Benachrichtigungsmail.
|
||||
* @param {*} body Roh-Payload vom Checkout (gleiches Format wie bisher)
|
||||
* @param {{status: "zahlungOffen"|"bezahlt", paypalOrderId?: string|null}} optionen
|
||||
*/
|
||||
export async function erstelleBestellung(body, optionen) {
|
||||
const { status, paypalOrderId = null } = optionen;
|
||||
|
||||
const land = String(body.land || "").toLowerCase();
|
||||
if (!ERLAUBTE_LAENDER.includes(land)) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültiges Land." };
|
||||
}
|
||||
|
||||
const kunde = body.kunde && typeof body.kunde === "object" ? body.kunde : {};
|
||||
const name = String(kunde.name || "").trim();
|
||||
const email = String(kunde.email || "").trim();
|
||||
if (!name || !email || !email.includes("@")) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültige Kundendaten." };
|
||||
}
|
||||
|
||||
const artikel = Array.isArray(body.artikel) ? body.artikel : [];
|
||||
if (artikel.length === 0) {
|
||||
return { ok: false, httpStatus: 400, error: "Die Bestellung enthält keine Artikel." };
|
||||
}
|
||||
for (const a of artikel) {
|
||||
if (!a || typeof a.slug !== "string" || !a.slug.trim() || typeof a.name !== "string" || !a.name.trim()) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültige Artikeldaten." };
|
||||
}
|
||||
if (!Number.isFinite(a.menge) || a.menge <= 0) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültige Artikelmenge." };
|
||||
}
|
||||
if (!Number.isFinite(a.preis) || a.preis < 0) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültiger Artikelpreis." };
|
||||
}
|
||||
}
|
||||
|
||||
const summe = Number(body.summe);
|
||||
if (!Number.isFinite(summe) || summe < 0) {
|
||||
return { ok: false, httpStatus: 400, error: "Ungültige Bestellsumme." };
|
||||
}
|
||||
const zwischensumme = Number.isFinite(Number(body.zwischensumme)) ? Number(body.zwischensumme) : 0;
|
||||
const versandkosten = Number.isFinite(Number(body.versandkosten)) ? Number(body.versandkosten) : 0;
|
||||
const rabattGesamt = Number.isFinite(Number(body.rabattGesamt)) ? Number(body.rabattGesamt) : 0;
|
||||
|
||||
const gutscheinCodeRoh = typeof body.gutscheinCode === "string" ? body.gutscheinCode.trim() : "";
|
||||
let gutscheinCodeNormalisiert = null;
|
||||
if (gutscheinCodeRoh) {
|
||||
const gutschein = findeGutschein(gutscheinCodeRoh);
|
||||
gutscheinCodeNormalisiert = gutscheinCodeRoh.toUpperCase();
|
||||
if (gutschein && gutschein.einmaligProPerson) {
|
||||
const { anzahl } = db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as anzahl FROM orders WHERE gutschein_code = ? AND LOWER(kunde_email) = LOWER(?) AND status != 'storniert'`
|
||||
)
|
||||
.get(gutscheinCodeNormalisiert, email);
|
||||
if (anzahl > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
httpStatus: 400,
|
||||
error: "Dieser Gutscheincode wurde bereits mit dieser E-Mail-Adresse eingelöst und gilt nur einmal pro Person.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const insertOrder = db
|
||||
.prepare(
|
||||
`INSERT INTO orders
|
||||
(created_at, updated_at, status, kunde_name, kunde_email, land, strasse, plz, ort,
|
||||
zahlungsart, zwischensumme, versandkosten, rabatt_gesamt, summe,
|
||||
treuebonus_verwendet, abo_rabatt_verwendet, gutschein_code, paypal_order_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||
)
|
||||
.run(
|
||||
now,
|
||||
now,
|
||||
status,
|
||||
name,
|
||||
email,
|
||||
land,
|
||||
String(kunde.strasse || ""),
|
||||
String(kunde.plz || ""),
|
||||
String(kunde.ort || ""),
|
||||
String(body.zahlungsart || ""),
|
||||
zwischensumme,
|
||||
versandkosten,
|
||||
rabattGesamt,
|
||||
summe,
|
||||
body.treuebonusVerwendet ? 1 : 0,
|
||||
body.aboRabattVerwendet ? 1 : 0,
|
||||
gutscheinCodeNormalisiert,
|
||||
paypalOrderId
|
||||
);
|
||||
|
||||
const orderId = insertOrder.lastInsertRowid;
|
||||
const jahr = new Date().getFullYear();
|
||||
const bestellnummer = `VDB-${jahr}-${String(orderId).padStart(4, "0")}`;
|
||||
db.prepare(`UPDATE orders SET bestellnummer = ? WHERE id = ?`).run(bestellnummer, orderId);
|
||||
|
||||
const insertItem = db.prepare(
|
||||
`INSERT INTO order_items (order_id, produkt_slug, name, kategorie_slug, menge, preis, gratis)
|
||||
VALUES (?,?,?,?,?,?,?)`
|
||||
);
|
||||
const insertAlleItems = db.transaction((items) => {
|
||||
for (const a of items) {
|
||||
insertItem.run(orderId, a.slug, a.name, a.kategorie || null, Math.round(a.menge), a.preis, a.gratis ? 1 : 0);
|
||||
}
|
||||
});
|
||||
insertAlleItems(artikel);
|
||||
|
||||
await sendeEmail({
|
||||
to: BENACHRICHTIGUNGS_EMAIL,
|
||||
subject: `${status === "zahlungOffen" ? "⏳" : "🩵"} Neue Bestellung ${bestellnummer}`,
|
||||
html: bestellBenachrichtigungHtml({ bestellnummer, name, email, land, artikel, summe, status }),
|
||||
text: `Neue Bestellung ${bestellnummer} von ${name} (${email}), ${artikel.length} Artikel, Summe ${formatPreis(summe)}. Status: ${status === "zahlungOffen" ? "Zahlung ausstehend (Überweisung)" : "Bezahlt (PayPal)"}.`,
|
||||
});
|
||||
|
||||
return { ok: true, id: orderId, bestellnummer };
|
||||
} catch (err) {
|
||||
return { ok: false, httpStatus: 500, error: "Bestellung konnte nicht gespeichert werden." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user