feat(widerruf): Anonymous-Pfad + Token-Prefill für § 356a-Konformität
Deploy to Dev / deploy (push) Successful in 0s

- widerruf.php zeigt jetzt Formular (Name + Bestellnr + E-Mail) ohne Token-Zwang
- Token-Link aus Bestätigungs-Mail bleibt als Convenience, füllt Felder vor
- submit_anonymous() in Revocation-Klasse: Lookup via order_number + customer_email
- Anonymous-Eintrag wird inline angelegt, falls noch kein Token-basierter Record existiert
- Button-Beschriftung 'Vertrag widerrufen' (Gesetzes-Empfehlung)
- prefill_from_token() für Vorausfüllung bei Token-Link
This commit is contained in:
Thomas Bartelt
2026-06-14 23:02:54 +02:00
parent afd2cedf92
commit 68a03080d1
2 changed files with 258 additions and 59 deletions
+79 -59
View File
@@ -1,11 +1,12 @@
<?php
/**
* Carteasy – Landingpage Widerruf (EU-RL 2026/2673)
* Carteasy – Landingpage Widerruf (§ 356a BGB, ab 19.06.2026)
*
* GET ?t=<token> → Token validieren, Widerrufs-Formular anzeigen
* POST → Widerruf erfassen, Bestätigungsseite anzeigen
* GET → Leeres Formular
* GET ?t=<token> → Formular mit vorausgefüllten Feldern (Convenience aus Bestell-Mail)
* POST → Widerruf erfassen, Bestätigungsseite anzeigen
*
* Standalone-Entry-Point, nutzt das Shop-Base-Objekt für DB + Config.
* Submit läuft IMMER über Bestellnr + E-Mail Lookup, nicht über Token.
*
* @copyright Wlanium / Thomas Bartelt
* @since 2026-04-19
@@ -26,11 +27,10 @@ $base_object = new base();
Registry::set('base', $base_object);
include_once './core/revocation.class.php';
$revocation = new Revocation($base_object);
$view = 'form';
$payload = null;
$prefill = ['name' => '', 'order_number' => '', 'email' => ''];
$error = null;
// ------------------------------------------------------------
@@ -39,48 +39,69 @@ $error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = isset($_POST['t']) ? (string)$_POST['t'] : '';
$reason_id = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
$reason_text = isset($_POST['reason_text']) ? (string)$_POST['reason_text'] : '';
$confirm = isset($_POST['confirm']) && $_POST['confirm'] === '1';
$form_name = isset($_POST['name']) ? trim((string)$_POST['name']) : '';
$form_order_number = isset($_POST['order_number']) ? trim((string)$_POST['order_number']) : '';
$form_email = isset($_POST['email']) ? trim((string)$_POST['email']) : '';
$reason_id = isset($_POST['reason_id']) ? (int)$_POST['reason_id'] : 0;
$reason_text = isset($_POST['reason_text']) ? (string)$_POST['reason_text'] : '';
$confirm = isset($_POST['confirm']) && $_POST['confirm'] === '1';
// Eingaben für Fehlerfall ins Formular zurückspielen
$prefill = [
'name' => $form_name,
'order_number' => $form_order_number,
'email' => $form_email,
];
if (!$confirm) {
// Zweite Stufe nicht aktiv bestätigt → zurück zum Form mit Hinweis
$payload = $revocation->validate_token($token);
if ($payload) {
$view = 'form';
$error = 'Bitte bestätigen Sie den Widerruf durch Setzen des Hakens.';
} else {
$view = 'error';
$error = $revocation->get_error();
}
$view = 'form';
$error = 'no_confirm';
} else {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if ($revocation->submit($token, $reason_id, $reason_text, $ip, $ua)) {
if ($revocation->submit_anonymous(
$form_name, $form_order_number, $form_email,
$reason_id, $reason_text, $ip, $ua
)) {
$view = 'success';
} else {
$view = 'error';
$view = 'form';
$error = $revocation->get_error();
}
}
} else {
$token = isset($_GET['t']) ? (string)$_GET['t'] : '';
$payload = $revocation->validate_token($token);
if (!$payload) {
$view = 'error';
$error = $revocation->get_error();
// GET — bei Token-Link: Felder vorausfüllen
$token = isset($_GET['t']) ? (string)$_GET['t'] : '';
if ($token !== '') {
$pf = $revocation->prefill_from_token($token);
if ($pf) {
$prefill = $pf;
} elseif ($revocation->get_error() === 'feature_disabled') {
$view = 'error';
$error = 'feature_disabled';
}
// Bei anderen Token-Fehlern: leeres Formular anbieten ohne Hinweistext
}
}
// ------------------------------------------------------------
// View rendering
// ------------------------------------------------------------
$reasons = $revocation->fetch_reasons();
$messages = [
'no_confirm' => 'Bitte bestätigen Sie den Widerruf durch Setzen des Hakens.',
'missing_order' => 'Bitte geben Sie Ihre Bestellnummer an.',
'invalid_email' => 'Bitte geben Sie eine gültige E-Mail-Adresse an.',
'order_not_found' => 'Wir konnten keine Bestellung mit dieser Bestellnummer und E-Mail-Adresse finden. '
. 'Bitte prüfen Sie Ihre Eingaben.',
'expired' => 'Die 14-tägige Widerrufsfrist ist überschritten. '
. 'Wenden Sie sich bitte direkt an uns.',
'already_submitted' => 'Für diese Bestellung wurde bereits ein Widerruf erklärt.',
'feature_disabled' => 'Die elektronische Widerrufsfunktion ist aktuell noch nicht aktiv.',
'not_eligible' => 'Für diese Bestellung steht der elektronische Widerruf nicht zur Verfügung.',
];
$display_error = isset($messages[$error]) ? $messages[$error] : null;
?><!doctype html>
<html lang="de">
<head>
@@ -98,10 +119,10 @@ $reasons = $revocation->fetch_reasons();
.meta{background:#f4f4f4;padding:12px 16px;border-left:4px solid #c00;
font-size:.9em;margin:16px 0;}
label{display:block;margin:12px 0 4px;font-weight:600;}
select,textarea,input[type=text]{width:100%;padding:10px;font-size:1em;
select,textarea,input[type=text],input[type=email]{width:100%;padding:10px;font-size:1em;
border:1px solid #ccc;border-radius:4px;box-sizing:border-box;}
textarea{min-height:90px;font-family:inherit;}
.check{margin:20px 0;font-size:.95em;}
.check{margin:20px 0;font-size:.95em;font-weight:normal;}
.btn{background:#c00;color:#fff;border:0;padding:14px 28px;
font-size:1.05em;font-weight:bold;border-radius:4px;cursor:pointer;}
.btn:hover{background:#a00;}
@@ -117,22 +138,30 @@ $reasons = $revocation->fetch_reasons();
<?php if ($view === 'form'): ?>
<h1>Widerruf Ihrer Bestellung</h1>
<h1>Vertrag widerrufen</h1>
<p>Gemäß § 355 BGB haben Sie das Recht, Ihren Vertrag binnen 14 Tagen
ohne Angabe von Gründen zu widerrufen.</p>
ohne Angabe von Gründen zu widerrufen. Füllen Sie bitte die folgenden
drei Pflichtfelder aus.</p>
<div class="meta">
<strong>Bestellnummer:</strong> <?= htmlspecialchars($payload->order_number ?? ('#'.$payload->order_id)) ?><br>
<strong>Bestelldatum:</strong> <?= htmlspecialchars($payload->order_date ?? '–') ?><br>
<strong>E-Mail:</strong> <?= htmlspecialchars($payload->customer_email) ?>
</div>
<?php if ($display_error): ?><div class="err"><?= htmlspecialchars($display_error) ?></div><?php endif; ?>
<?php if ($error): ?><div class="err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post" action="<?= htmlspecialchars(strtok($_SERVER['REQUEST_URI'], '?')) ?>">
<form method="post" action="<?= htmlspecialchars($_SERVER['REQUEST_URI']) ?>">
<input type="hidden" name="t" value="<?= htmlspecialchars($_GET['t'] ?? $_POST['t'] ?? '') ?>">
<label for="name">Vor- und Nachname</label>
<input type="text" id="name" name="name" required maxlength="120"
value="<?= htmlspecialchars($prefill['name']) ?>">
<label for="reason_id">Grund (optional)</label>
<label for="order_number">Bestellnummer</label>
<input type="text" id="order_number" name="order_number" required maxlength="32"
value="<?= htmlspecialchars($prefill['order_number']) ?>"
placeholder="z.B. 102345">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" required maxlength="120"
value="<?= htmlspecialchars($prefill['email']) ?>"
placeholder="Die E-Mail, mit der Sie bestellt haben">
<label for="reason_id">Grund (optional, freiwillige Angabe)</label>
<select name="reason_id" id="reason_id">
<option value="0">— bitte wählen —</option>
<?php foreach ($reasons as $r): ?>
@@ -142,14 +171,14 @@ $reasons = $revocation->fetch_reasons();
<label for="reason_text">Anmerkung (optional)</label>
<textarea name="reason_text" id="reason_text" maxlength="2000"
placeholder="Sie können uns hier eine Mitteilung hinterlassen – muss nicht."></textarea>
placeholder="Sie können uns hier eine Mitteilung hinterlassen – muss nicht."></textarea>
<label class="check">
<input type="checkbox" name="confirm" value="1" required>
Ich erkläre hiermit den Widerruf meiner oben genannten Bestellung.
</label>
<button type="submit" class="btn">Widerruf jetzt absenden</button>
<button type="submit" class="btn">Vertrag widerrufen</button>
<p class="muted" style="margin-top:20px;">Nach dem Absenden erhalten Sie eine
Bestätigungs-E-Mail mit der Rücksendeadresse und weiteren Informationen.</p>
@@ -169,22 +198,13 @@ $reasons = $revocation->fetch_reasons();
<?php else: ?>
<h1>Widerruf nicht möglich</h1>
<?php
$messages = [
'invalid_token' => 'Der Widerrufs-Link ist ungültig oder nicht (mehr) vorhanden.',
'expired' => 'Der Widerrufs-Link ist abgelaufen. Die 14-tägige Widerrufsfrist '
. 'zzgl. Puffer wurde überschritten. Wenden Sie sich bitte direkt an uns.',
'already_submitted'=> 'Für diese Bestellung wurde bereits ein Widerruf erklärt.',
'feature_disabled' => 'Die elektronische Widerrufsfunktion ist aktuell noch nicht aktiv.',
'not_eligible' => 'Für diese Bestellung steht der elektronische Widerruf nicht zur Verfügung.',
];
$msg = $messages[$error] ?? 'Widerruf zurzeit nicht verarbeitbar.';
?>
<div class="err"><?= htmlspecialchars($msg) ?></div>
<div class="err"><?= htmlspecialchars($display_error ?: 'Widerruf zurzeit nicht verarbeitbar.') ?></div>
<p>Sie können Ihren Widerruf jederzeit auch formlos per E-Mail an
<a href="mailto:<?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?>"><?= htmlspecialchars(REVOCATION_OWNER_EMAIL) ?></a>
oder per Brief erklären.</p>
<p class="muted">Intelectra GmbH · Sachsenring 20 · 94315 Straubing</p>
<p class="muted"><?= htmlspecialchars(REVOCATION_RETURN_COMPANY) ?> ·
<?= htmlspecialchars(REVOCATION_RETURN_STREET) ?> ·
<?= htmlspecialchars(REVOCATION_RETURN_CITY) ?></p>
<?php endif; ?>