Files
qciganoandClaude Sonnet 5 81c175d03c Zahlungsarten überarbeitet: echtes PayPal, Klarna & eigene Kartenmaske raus
Auf ausdrücklichen Wunsch, mit Fokus auf rechtliche Absicherung:

- Klarna komplett entfernt (hätte eigene Händlerprüfung + Bonitätsprüfungs-
  Pflichten nach der neuen EU-Verbraucherkreditrichtlinie vorausgesetzt).
- Eigene Kreditkarten-Eingabemaske komplett entfernt (wäre PCI-DSS-pflichtig
  gewesen — für einen kleinen Shop praktisch nicht stemmbar). Kartenzahlung
  bleibt möglich: PayPals eigener, PCI-zertifizierter Gast-Checkout bietet
  Kredit-/Debitkarte an, ohne dass Kartendaten je unsere Seite berühren.
- PayPal ist jetzt ECHT server-seitig verifiziert statt dem Browser blind zu
  vertrauen: eigener Cloudflare-Function-Flow (functions/_shared/paypal.js +
  functions/api/paypal/) legt die PayPal-Bestellung server-seitig an, zieht
  die Zahlung nach Bestätigung server-seitig ein und prüft den eingezogenen
  Betrag gegen die Bestellsumme — die Bestellung wird ausschließlich bei
  bestätigter, betragsgleicher Zahlung angelegt. /api/orders lehnt direkte
  PayPal-Bestellungen jetzt ausdrücklich ab (verhindert vorgetäuschte
  "bezahlte" Bestellungen ohne echte Zahlung).
- Neuer, ehrlicherer Bestellstatus "zahlungOffen": Überweisungs-Bestellungen
  starten jetzt so (Geld noch nicht da) statt fälschlich sofort "bezahlt" zu
  heißen — Schutz vor Warenversand, bevor das Geld wirklich angekommen ist.
  Eigene Kachel/Filter/Badge-Farbe in der Verwaltung, Umsatz-/Auswertungs-
  Zahlen zählen "zahlungOffen" bewusst nicht mit.
- Rechtstexte (AGB, Datenschutzerklärung, FAQ, Versand & Zahlung) auf allen
  4 Sprachen aktualisiert: nur noch PayPal + Überweisung erwähnt, inkl. DSGVO-
  Hinweis zur internationalen Datenübertragung an PayPal (Data Privacy
  Framework-Zertifizierung).
- D1-Migration 0003: neue Spalte paypal_order_id (Zahlungsbeleg) + erweiterter
  Status-Wertebereich, auf Live-Datenbank angewendet, bestehende Daten intakt.

Mit echten Testbestellungen lokal verifiziert: Überweisung legt korrekt
"zahlungOffen" an, direkter PayPal-Bypass-Versuch an /api/orders wird
abgelehnt, PayPal-Route meldet sauber "noch nicht eingerichtet" ohne
Zugangsdaten. Checkout-UI zeigt nur noch 2 Zahlungsarten, Bestellknopf ist bei
PayPal ausgeblendet (Zahlung läuft exklusiv über den echten PayPal-Button).

Für den echten Zahlungseingang fehlt noch VanVans eigenes PayPal-Business-
Konto (Client-ID + Secret) — Details in der Vault-Dokumentation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-04 01:06:41 +02:00

82 lines
4.0 KiB
JavaScript

/* =====================================================================
POST /api/paypal/capture-order — zieht eine zuvor angelegte PayPal-Zahlung EIN und legt die
Bestellung erst danach an.
Das ist der entscheidende Sicherheits-Schritt der ganzen PayPal-Anbindung: Der Browser meldet
hier "die Kundin hat bei PayPal auf Bezahlen geklickt" (`onApprove`-Callback), aber wir
glauben das NICHT einfach — stattdessen fragen wir direkt bei PayPal selbst nach (server-zu-
server, mit VanVans eigenen App-Zugangsdaten), ob die Zahlung wirklich abgeschlossen ist UND
ob der eingezogene Betrag zur Bestellsumme passt. Nur wenn beides stimmt, wird die Bestellung
in der Datenbank angelegt — mit Status "bezahlt" und der echten PayPal-Bestell-ID als Beleg.
Schlägt die Zahlungsprüfung fehl, wird NICHTS gespeichert — die Kundin sieht eine klare
Fehlermeldung im Checkout und kann es erneut versuchen, statt dass VanVan eine "Bestellung"
ohne echtes Geld dahinter bekommt. ===================================================================== */
import { json } from "../../_shared/http.js";
import { paypalKonfiguriert, erfassePaypalZahlung } from "../../_shared/paypal.js";
import { erstelleBestellung } from "../../_shared/bestellung-erstellen.js";
// Kleine Toleranz für Rundungsdifferenzen (Cent-Rundung zwischen unserer Berechnung und PayPals
// eigener Darstellung) — alles darüber gilt als verdächtige Abweichung und wird abgelehnt.
const BETRAG_TOLERANZ_EURO = 0.02;
export async function onRequestPost(context) {
const { request, env } = context;
if (!paypalKonfiguriert(env)) {
return json(503, { ok: false, error: "PayPal ist auf dieser Seite noch nicht eingerichtet." });
}
let body;
try {
body = await request.json();
} catch {
return json(400, { ok: false, error: "Ungültige Anfrage." });
}
const paypalOrderId = typeof body.paypalOrderId === "string" ? body.paypalOrderId.trim() : "";
if (!paypalOrderId) {
return json(400, { ok: false, error: "Fehlende PayPal-Bestell-ID." });
}
const summe = Number(body.summe);
if (!Number.isFinite(summe) || summe <= 0) {
return json(400, { ok: false, error: "Ungültige Bestellsumme." });
}
// ── Schritt 1: Zahlung server-seitig bei PayPal einziehen + verifizieren. ──
const zahlung = await erfassePaypalZahlung(env, paypalOrderId);
if (!zahlung.ok) {
return json(402, { ok: false, error: `Zahlung konnte nicht bestätigt werden: ${zahlung.error}` });
}
// ── Schritt 2: Betrag gegenprüfen — verhindert, dass hier ein anderer (z.B. kleinerer) Betrag
// als der tatsächlich fällige "durchgeschmuggelt" wird. ──
if (Math.abs(zahlung.betragEuro - summe) > BETRAG_TOLERANZ_EURO) {
return json(402, {
ok: false,
error: `Der bei PayPal gezahlte Betrag (${zahlung.betragEuro.toFixed(2)} €) stimmt nicht mit der Bestellsumme (${summe.toFixed(2)} €) überein.`,
});
}
// ── Schritt 3: Erst JETZT, mit bestätigter echter Zahlung, die Bestellung anlegen. ──
const ergebnis = await erstelleBestellung(env, { ...body, zahlungsart: "paypal" }, {
status: "bezahlt",
paypalOrderId,
});
if (!ergebnis.ok) {
// Die Zahlung ist zu diesem Zeitpunkt bereits bei PayPal eingezogen, konnte aber nicht
// gespeichert werden (z.B. D1 kurzzeitig nicht erreichbar) — das ist ein seltener, aber
// ernster Fall: VanVan hat das Geld, aber keine Bestellung in der Verwaltung. Deshalb bekommt
// die Kundin eine Meldung, die sie NICHT zum erneuten Bezahlen auffordert, sondern zur
// Kontaktaufnahme, um Doppelzahlungen zu vermeiden.
return json(500, {
ok: false,
error: `Deine Zahlung wurde erfolgreich bei PayPal abgebucht (Beleg-ID: ${zahlung.captureId}), aber die Bestellung konnte nicht gespeichert werden. Bitte kontaktiere uns mit dieser Beleg-ID, damit wir das von Hand nachtragen — bitte NICHT erneut bezahlen.`,
});
}
return json(201, { ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer });
}