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]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
98028df4ea
commit
81c175d03c
+106
-125
@@ -20,65 +20,22 @@ const t = useTranslations(lang);
|
||||
<div class="checkout-split">
|
||||
<div class="checkout-split-col">
|
||||
<h3>{t.checkout.step2}</h3>
|
||||
{/* Echte Markenlogos statt Platzhaltern: PayPal- und Klarna-Icon in den offiziellen
|
||||
Markenfarben, Visa/Mastercard als unveränderte "Acceptance Marks" nebeneinander —
|
||||
genau die in den jeweiligen Markenrichtlinien vorgesehene Verwendung, um eine
|
||||
Zahlungsmethode im Checkout auszuweisen (siehe Recherche zu PayPal-/Klarna-/Visa-/
|
||||
Mastercard-Markenrichtlinien: Logos dürfen zur Kennzeichnung akzeptierter
|
||||
Zahlungsarten verwendet werden, Visa/Mastercard aber nur unverändert/unverfälscht). */}
|
||||
{/* Nur noch zwei Zahlungsarten: Klarna raus (Bonitätsprüfungs-Pflichten), keine eigene
|
||||
Kreditkarten-Eingabemaske (PCI-DSS-pflichtig) — Kartenzahlung läuft stattdessen über
|
||||
PayPals eigenen, PCI-zertifizierten Gast-Checkout. */}
|
||||
<div class="payment-options">
|
||||
<div class="pay-card" style="--brand-color:#009cde; --brand-bg:#003087;">
|
||||
<label class="pay-option">
|
||||
<span class="pay-icon pay-icon-brand" style="background:linear-gradient(160deg, #0091e6, #003087);">
|
||||
<svg viewBox="0 0 24 24" width="30" height="30" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
|
||||
</span>
|
||||
<span>PayPal</span>
|
||||
<span>PayPal<span class="pay-sub">{t.checkout.payPalSub}<span class="pay-sub-cards" aria-hidden="true"><span class="mini-card mini-card-visa"><svg viewBox="0 0 24 24" fill="#1434CB" aria-hidden="true"><path d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"></path></svg></span><span class="mini-card mini-card-mastercard"><svg viewBox="0 0 32 20" aria-hidden="true"><circle cx="13" cy="10" r="7.2" fill="#EB001B"></circle><circle cx="19" cy="10" r="7.2" fill="#F79E1B"></circle><path d="M16 4.4a7.18 7.18 0 010 11.2 7.18 7.18 0 010-11.2z" fill="#FF5F00"></path></svg></span></span></span></span>
|
||||
<input type="radio" name="pay" value="paypal" checked />
|
||||
</label>
|
||||
<div class="pay-panel-inline">
|
||||
<p>{t.checkout.payPalHint}</p>
|
||||
<div id="paypal-button-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#ffb3c7; --brand-bg:#17120f;">
|
||||
<label class="pay-option">
|
||||
<span class="pay-icon pay-icon-brand" style="background:linear-gradient(160deg, #ffd1de, #ffb3c7);">
|
||||
<svg viewBox="0 0 24 24" width="26" height="26" fill="#0a0a0a" aria-hidden="true"><path d="M4.592 2v20H0V2h4.592zm11.46 0c0 4.194-1.583 8.105-4.415 11.068l-.278.283L17.702 22h-5.668l-6.893-9.4 1.779-1.332c2.858-2.14 4.535-5.378 4.637-8.924L11.562 2h4.49zM21.5 17a2.5 2.5 0 110 5 2.5 2.5 0 010-5z"></path></svg>
|
||||
</span>
|
||||
<span>Klarna</span>
|
||||
<input type="radio" name="pay" value="klarna" />
|
||||
</label>
|
||||
<div class="pay-panel-inline">
|
||||
<p>{t.checkout.klarnaHint}</p>
|
||||
<button type="button" class="btn btn-outline pay-connect-btn" id="klarna-connect">{t.checkout.klarnaConnect}</button>
|
||||
<p class="pay-connect-status" id="klarna-status"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#e3b23c; --brand-bg:#241f3d;">
|
||||
<label class="pay-option">
|
||||
<span class="pay-icon-cards" aria-hidden="true">
|
||||
<span class="mini-card mini-card-visa">
|
||||
<svg viewBox="0 0 24 24" width="28" height="28" fill="#1434CB" aria-hidden="true"><path d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"></path></svg>
|
||||
</span>
|
||||
<span class="mini-card mini-card-mastercard">
|
||||
<svg viewBox="0 0 32 20" width="32" height="20" aria-hidden="true"><circle cx="13" cy="10" r="7.2" fill="#EB001B"></circle><circle cx="19" cy="10" r="7.2" fill="#F79E1B"></circle><path d="M16 4.4a7.18 7.18 0 010 11.2 7.18 7.18 0 010-11.2z" fill="#FF5F00"></path></svg>
|
||||
</span>
|
||||
</span>
|
||||
<span>{t.checkout.creditCard}<span class="pay-sub">{t.checkout.creditCardSub}</span></span>
|
||||
<input type="radio" name="pay" value="kreditkarte" />
|
||||
</label>
|
||||
<div class="pay-panel-inline">
|
||||
<p>{t.checkout.cardHint}</p>
|
||||
<div class="card-fields">
|
||||
<div>
|
||||
<label for="cc-number">{t.checkout.cardNumber}</label>
|
||||
<input type="text" id="cc-number" inputmode="numeric" placeholder="1234 5678 9012 3456" maxlength="19" />
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div><label for="cc-expiry">{t.checkout.cardExpiry}</label><input type="text" id="cc-expiry" placeholder="MM/YY" maxlength="5" /></div>
|
||||
<div><label for="cc-cvc">{t.checkout.cardCvc}</label><input type="text" id="cc-cvc" inputmode="numeric" placeholder="123" maxlength="4" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="pay-connect-status" id="paypal-error" style="display:none; color: var(--c-sale);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||||
@@ -137,7 +94,8 @@ const t = useTranslations(lang);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||||
<p id="order-submit-hint">{t.checkout.payPalCompleteHint}</p>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="order-submit-btn">{t.checkout.orderButton}</button>
|
||||
</form>
|
||||
|
||||
<aside class="card checkout-aside">
|
||||
@@ -173,9 +131,9 @@ const t = useTranslations(lang);
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie, payPalCompleteHintLabel: t.checkout.payPalCompleteHint }}>
|
||||
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel };
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel };
|
||||
</script>
|
||||
<script type="module">
|
||||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
|
||||
@@ -185,12 +143,15 @@ const t = useTranslations(lang);
|
||||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
||||
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
|
||||
|
||||
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
|
||||
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel } = window.__checkoutVars;
|
||||
|
||||
// TODO (VanVan/qciga) : "sb" est l'identifiant sandbox gratuit de PayPal — fonctionne
|
||||
// immédiatement sans compte, mais aucun argent réel ne circule. Remplacer par le vrai
|
||||
// identifiant client PayPal Business de VanVan une fois prêt (developer.paypal.com).
|
||||
const PAYPAL_CLIENT_ID = "sb";
|
||||
|
||||
const summary = document.getElementById("checkout-summary");
|
||||
const landSelect = document.getElementById("land");
|
||||
// Nur die im Warenkorb ausgewählten Artikel werden hier bestellt — abgewählte bleiben im
|
||||
// Warenkorb liegen und tauchen im Checkout gar nicht erst auf.
|
||||
const cart = getCart().filter(istAusgewaehlt);
|
||||
const subtotal = cartTotal();
|
||||
let currentTotal = 0;
|
||||
@@ -250,59 +211,103 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
renderSummary();
|
||||
landSelect.addEventListener("change", renderSummary);
|
||||
|
||||
// Zahlungsart-Auswahl: der "Verbinden"-Bereich jeder Karte klappt rein über CSS
|
||||
// (:has(input:checked)) sofort auf, sobald man die Karte anklickt — kein JS, keine Verzögerung.
|
||||
function baueBestellPayload() {
|
||||
return {
|
||||
land: landSelect.value,
|
||||
kunde: {
|
||||
name: `${document.getElementById("vorname").value.trim()} ${document.getElementById("nachname").value.trim()}`.trim(),
|
||||
email: document.getElementById("email").value.trim(),
|
||||
strasse: document.getElementById("strasse").value.trim(),
|
||||
plz: document.getElementById("plz").value.trim(),
|
||||
ort: document.getElementById("ort").value.trim(),
|
||||
},
|
||||
artikel: cart.map((i) => {
|
||||
const produkt = getProduct(i.slug);
|
||||
const einzelpreis = i.gratis ? 0 : zeilenpreisFuer(i) / i.menge;
|
||||
return { slug: i.slug, name: i.name, kategorie: produkt?.kategorie ?? "", menge: i.menge, preis: einzelpreis, gratis: !!i.gratis };
|
||||
}),
|
||||
zwischensumme: subtotal,
|
||||
versandkosten: currentShipping,
|
||||
rabattGesamt: currentRabattGesamt,
|
||||
summe: currentTotal,
|
||||
treuebonusVerwendet: treuebonusDiscount() > 0,
|
||||
aboRabattVerwendet: aboDiscount(subtotal) > 0,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function nachBestellErfolg() {
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
}
|
||||
|
||||
// PayPal: echte PayPal Smart Buttons über das offizielle JS-SDK (Sandbox-Client-ID "sb" —
|
||||
// kostenlos, ohne eigenes Konto testbar). Klick öffnet den echten PayPal-Login-Popup. Für den
|
||||
// Live-Betrieb muss hier nur "sb" durch VanVans echte (kostenlose) PayPal-Business-Client-ID
|
||||
// ersetzt werden, siehe developer.paypal.com.
|
||||
let paypalLoaded = false;
|
||||
const paypalErrorEl = document.getElementById("paypal-error");
|
||||
function zeigePaypalFehler(msg) {
|
||||
if (!paypalErrorEl) return;
|
||||
paypalErrorEl.textContent = "⚠️ " + msg;
|
||||
paypalErrorEl.style.display = "block";
|
||||
}
|
||||
function loadPayPalButtons() {
|
||||
if (paypalLoaded || !document.getElementById("paypal-button-container")) return;
|
||||
paypalLoaded = true;
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.paypal.com/sdk/js?client-id=sb¤cy=EUR&intent=capture&disable-funding=card,credit";
|
||||
script.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=EUR&intent=capture`;
|
||||
script.onload = () => {
|
||||
if (!window.paypal) return;
|
||||
window.paypal.Buttons({
|
||||
style: { layout: "vertical", color: "blue", shape: "pill", label: "paypal" },
|
||||
createOrder: (data, actions) => actions.order.create({
|
||||
purchase_units: [{ amount: { value: Math.max(0.01, currentTotal).toFixed(2), currency_code: "EUR" } }],
|
||||
}),
|
||||
onApprove: (data, actions) => actions.order.capture().then(() => { location.href = thankYouPath; }),
|
||||
createOrder: async () => {
|
||||
if (cart.length === 0) {
|
||||
alert(emptyCartAlert);
|
||||
throw new Error("Panier vide");
|
||||
}
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
const res = await fetch("/api/paypal/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ summe: currentTotal }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
zeigePaypalFehler(data?.error || "Impossible de créer la commande PayPal.");
|
||||
throw new Error(data?.error || "Échec de la commande PayPal.");
|
||||
}
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/paypal/capture-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ paypalOrderId: data.orderID, ...baueBestellPayload() }),
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
if (!res.ok || !result?.ok) {
|
||||
zeigePaypalFehler(result?.error || "Le paiement n'a pas pu être finalisé.");
|
||||
return;
|
||||
}
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
zeigePaypalFehler(err instanceof Error ? err.message : "Erreur de paiement inconnue.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
zeigePaypalFehler("PayPal a signalé une erreur. Réessayez ou choisissez le virement bancaire.");
|
||||
},
|
||||
onCancel: () => {
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
},
|
||||
}).render("#paypal-button-container");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
loadPayPalButtons();
|
||||
|
||||
// Klarna: echtes "Verbinden" braucht eine serverseitige Session mit VanVans Klarna-Händlerkonto
|
||||
// (Phase 2, Cloudflare Worker). Bis dahin ein sauber gestalteter Demo-Verbindungsablauf, damit
|
||||
// die Oberfläche schon fertig aussieht und 1:1 austauschbar ist, sobald die Anbindung steht.
|
||||
const klarnaBtn = document.getElementById("klarna-connect");
|
||||
const klarnaStatus = document.getElementById("klarna-status");
|
||||
function connectKlarna() {
|
||||
if (!klarnaBtn || klarnaBtn.disabled) return;
|
||||
klarnaBtn.disabled = true;
|
||||
klarnaBtn.textContent = klarnaConnectingLabel;
|
||||
setTimeout(() => {
|
||||
klarnaBtn.textContent = klarnaConnectLabel;
|
||||
klarnaBtn.disabled = false;
|
||||
klarnaStatus.textContent = "✅ " + klarnaConnectedLabel;
|
||||
}, 1100);
|
||||
}
|
||||
if (klarnaBtn) {
|
||||
klarnaBtn.addEventListener("click", connectKlarna);
|
||||
const klarnaRadio = document.querySelector('input[name="pay"][value="klarna"]');
|
||||
// Verbindet automatisch, sobald die Klarna-Karte ausgewählt wird — kein separater Klick
|
||||
// auf den Button nötig (der bleibt als "erneut verbinden" erhalten).
|
||||
klarnaRadio?.addEventListener("change", () => { if (!klarnaStatus.textContent) connectKlarna(); });
|
||||
}
|
||||
|
||||
// Banküberweisung (Vorkasse): braucht KEINE externe Anbindung — zeigt einfach VanVans echte,
|
||||
// im Adminbereich hinterlegte Kontodaten an. Das ist die einzige der vier Zahlungsarten, die
|
||||
// schon zu 100% "fertig" funktioniert.
|
||||
const bankDetailsEl = document.getElementById("bank-details");
|
||||
if (bankDetailsEl) {
|
||||
if (bankverbindungVollstaendig(bankverbindung)) {
|
||||
@@ -336,7 +341,7 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
});
|
||||
|
||||
const orderForm = document.getElementById("checkout-form");
|
||||
const submitBtn = orderForm.querySelector('button[type="submit"]');
|
||||
const submitBtn = document.getElementById("order-submit-btn");
|
||||
const orderErrorEl = document.createElement("p");
|
||||
orderErrorEl.className = "small";
|
||||
orderErrorEl.style.color = "var(--c-sale)";
|
||||
@@ -350,53 +355,29 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
return;
|
||||
}
|
||||
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
if (zahlungsart === "paypal") {
|
||||
orderErrorEl.textContent = "⚠️ " + payPalCompleteHintLabel;
|
||||
orderErrorEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
orderErrorEl.style.display = "none";
|
||||
submitBtn.disabled = true;
|
||||
const urspruenglicherBtnText = submitBtn.textContent;
|
||||
submitBtn.textContent = "…";
|
||||
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
const payload = {
|
||||
land: landSelect.value,
|
||||
kunde: {
|
||||
name: `${document.getElementById("vorname").value.trim()} ${document.getElementById("nachname").value.trim()}`.trim(),
|
||||
email: document.getElementById("email").value.trim(),
|
||||
strasse: document.getElementById("strasse").value.trim(),
|
||||
plz: document.getElementById("plz").value.trim(),
|
||||
ort: document.getElementById("ort").value.trim(),
|
||||
},
|
||||
zahlungsart,
|
||||
artikel: cart.map((i) => {
|
||||
const produkt = getProduct(i.slug);
|
||||
const einzelpreis = i.gratis ? 0 : zeilenpreisFuer(i) / i.menge;
|
||||
return { slug: i.slug, name: i.name, kategorie: produkt?.kategorie ?? "", menge: i.menge, preis: einzelpreis, gratis: !!i.gratis };
|
||||
}),
|
||||
zwischensumme: subtotal,
|
||||
versandkosten: currentShipping,
|
||||
rabattGesamt: currentRabattGesamt,
|
||||
summe: currentTotal,
|
||||
treuebonusVerwendet: treuebonusDiscount() > 0,
|
||||
aboRabattVerwendet: aboDiscount(subtotal) > 0,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ ...baueBestellPayload(), zahlungsart }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
throw new Error(data?.error || "Bestellung konnte nicht gespeichert werden.");
|
||||
}
|
||||
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
|
||||
orderErrorEl.style.display = "block";
|
||||
|
||||
Reference in New Issue
Block a user