719 lines
25 KiB
PHP
719 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* Carteasy – Revocation (elektronischer Widerruf nach EU-RL 2026/2673)
|
||
*
|
||
* Zuständig für:
|
||
* - Erzeugen eines Widerrufs-Tokens bei Bestellabschluss (B2C only)
|
||
* - Validierung des Tokens auf der Landingpage
|
||
* - Verarbeitung der Widerrufserklärung (Status, Mails an Kunde + Shop)
|
||
*
|
||
* Silent-Modus ist möglich, siehe revocation_config.inc.php.
|
||
*
|
||
* @copyright Wlanium / Thomas Bartelt
|
||
* @since 2026-04-19
|
||
*/
|
||
|
||
include_once $_SERVER['DOCUMENT_ROOT'].'/core/config/revocation_config.inc.php';
|
||
include_once $_SERVER['DOCUMENT_ROOT'].'/core/revocationhelper.class.php';
|
||
include_once $_SERVER['DOCUMENT_ROOT'].'/core/mail.class.php';
|
||
|
||
class Revocation {
|
||
|
||
private $base_object;
|
||
private $db;
|
||
private $error = '';
|
||
|
||
public function __construct($base_object) {
|
||
$this->base_object = $base_object;
|
||
$this->db = $base_object->db;
|
||
}
|
||
|
||
public function get_error() {
|
||
return $this->error;
|
||
}
|
||
|
||
// ============================================================
|
||
// 1) Bei Bestellabschluss aufrufen
|
||
// ============================================================
|
||
|
||
/**
|
||
* Legt ein Revocation-Record für eine Bestellung an, sofern B2C.
|
||
* Versendet (wenn Flag an) die Info-Mail mit Widerrufs-Button.
|
||
*
|
||
* @param int $order_id
|
||
* @return bool true bei erfolgreicher Verarbeitung (auch "nicht zuständig" = true)
|
||
*/
|
||
public function create_for_order($order_id) {
|
||
if (!REVOCATION_ENABLED) {
|
||
return true; // Silent-Mode, nichts tun
|
||
}
|
||
|
||
$order_id = (int)$order_id;
|
||
if ($order_id <= 0) {
|
||
$this->error = 'invalid order_id';
|
||
return false;
|
||
}
|
||
|
||
$order = $this->fetch_order($order_id);
|
||
if (!$order) {
|
||
$this->error = 'order not found';
|
||
return false;
|
||
}
|
||
|
||
$group_id = RevocationHelper::resolve_group_id($this->db, $order->customer_id);
|
||
if (!RevocationHelper::is_b2c($group_id)) {
|
||
return true; // B2B oder unbekannt → kein Widerrufsbutton
|
||
}
|
||
|
||
if (empty($order->customer_email)) {
|
||
$this->error = 'no customer_email on order';
|
||
return false;
|
||
}
|
||
|
||
// Bereits vorhanden? (UK auf order_id → skippen)
|
||
if ($this->exists_for_order($order_id)) {
|
||
return true;
|
||
}
|
||
|
||
$token_plain = RevocationHelper::generate_token();
|
||
$token_hash = RevocationHelper::hash_token($token_plain);
|
||
$expires_at = RevocationHelper::calc_expires_at($order->order_date);
|
||
|
||
$stmt = $this->db->prepare(
|
||
"INSERT INTO revocations
|
||
(order_id, customer_id, customer_email, customer_group_id,
|
||
token_hash, expires_at, status)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'pending')"
|
||
);
|
||
if (!$stmt) {
|
||
$this->error = 'prepare failed: '.$this->db->error;
|
||
return false;
|
||
}
|
||
$customer_id = $order->customer_id ? (int)$order->customer_id : null;
|
||
$stmt->bind_param(
|
||
'iisiss',
|
||
$order_id,
|
||
$customer_id,
|
||
$order->customer_email,
|
||
$group_id,
|
||
$token_hash,
|
||
$expires_at
|
||
);
|
||
if (!$stmt->execute()) {
|
||
$this->error = 'insert failed: '.$stmt->error;
|
||
$stmt->close();
|
||
return false;
|
||
}
|
||
$stmt->close();
|
||
|
||
if (REVOCATION_INFO_MAIL_ENABLED) {
|
||
$this->send_info_mail($order, $token_plain);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============================================================
|
||
// 2) Landingpage – Token validieren
|
||
// ============================================================
|
||
|
||
/**
|
||
* Prüft Token auf Existenz, Gültigkeit, Status.
|
||
* @return object|false Revocation-Row + Order-Row gemerged, oder false
|
||
*/
|
||
public function validate_token($token_plain) {
|
||
if (!REVOCATION_ENABLED) {
|
||
$this->error = 'feature_disabled';
|
||
return false;
|
||
}
|
||
$token_plain = trim((string)$token_plain);
|
||
if (strlen($token_plain) !== REVOCATION_TOKEN_BYTES * 2) {
|
||
$this->error = 'invalid_token';
|
||
return false;
|
||
}
|
||
|
||
$hash = RevocationHelper::hash_token($token_plain);
|
||
$esc = $this->db->real_escape_string($hash);
|
||
|
||
$sql = "SELECT r.*,
|
||
o.order_number, o.order_date, o.order_total,
|
||
o.billing_firstname, o.billing_surname,
|
||
o.payment_method AS payment_method_id
|
||
FROM revocations r
|
||
JOIN orders o ON o.id = r.order_id
|
||
WHERE r.token_hash = '$esc'
|
||
LIMIT 1";
|
||
$res = $this->db->query($sql);
|
||
if (!$res || $res->num_rows === 0) {
|
||
$this->error = 'invalid_token';
|
||
return false;
|
||
}
|
||
$row = $res->fetch_object();
|
||
|
||
if ($row->status !== 'pending') {
|
||
$this->error = ($row->status === 'submitted' || $row->status === 'processed')
|
||
? 'already_submitted' : 'not_eligible';
|
||
return false;
|
||
}
|
||
|
||
if (strtotime($row->expires_at) < time()) {
|
||
// Auto-Expire markieren
|
||
$this->db->query("UPDATE revocations SET status='expired' WHERE id = ".(int)$row->id);
|
||
$this->error = 'expired';
|
||
return false;
|
||
}
|
||
|
||
return $row;
|
||
}
|
||
|
||
// ============================================================
|
||
// 3) Landingpage – Widerruf einreichen
|
||
// ============================================================
|
||
|
||
/**
|
||
* Kunde hat auf der Landingpage den Widerruf abgeschickt.
|
||
* Validiert nochmal, setzt Status, versendet beide Mails.
|
||
*/
|
||
public function submit($token_plain, $reason_id, $reason_text, $ip, $user_agent) {
|
||
$row = $this->validate_token($token_plain);
|
||
if (!$row) {
|
||
return false;
|
||
}
|
||
|
||
$reason_id = ((int)$reason_id > 0) ? (int)$reason_id : null;
|
||
$reason_text = trim((string)$reason_text);
|
||
if (mb_strlen($reason_text) > 2000) {
|
||
$reason_text = mb_substr($reason_text, 0, 2000);
|
||
}
|
||
$ip_anon = RevocationHelper::anonymize_ip($ip);
|
||
$ua = $user_agent ? mb_substr($user_agent, 0, 255) : null;
|
||
|
||
$stmt = $this->db->prepare(
|
||
"UPDATE revocations
|
||
SET status = 'submitted',
|
||
submitted_at = NOW(),
|
||
reason_id = ?,
|
||
reason_text = ?,
|
||
ip_anonymized = ?,
|
||
user_agent = ?
|
||
WHERE id = ? AND status = 'pending'"
|
||
);
|
||
if (!$stmt) {
|
||
$this->error = 'prepare failed: '.$this->db->error;
|
||
return false;
|
||
}
|
||
$id = (int)$row->id;
|
||
$stmt->bind_param('isssi', $reason_id, $reason_text, $ip_anon, $ua, $id);
|
||
$ok = $stmt->execute();
|
||
$affected = $stmt->affected_rows;
|
||
$stmt->close();
|
||
|
||
if (!$ok || $affected < 1) {
|
||
$this->error = 'update failed';
|
||
return false;
|
||
}
|
||
|
||
// Mails senden (beide, auch wenn eine scheitert wir loggen)
|
||
$fresh = $this->fetch_revocation($id);
|
||
if ($fresh) {
|
||
$this->send_owner_notification($fresh);
|
||
$this->send_customer_confirmation($fresh);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============================================================
|
||
// 4) Admin – als bearbeitet markieren
|
||
// ============================================================
|
||
|
||
public function mark_processed($revocation_id, $admin_note = '') {
|
||
$revocation_id = (int)$revocation_id;
|
||
$stmt = $this->db->prepare(
|
||
"UPDATE revocations
|
||
SET status = 'processed', processed_at = NOW(), admin_note = ?
|
||
WHERE id = ? AND status = 'submitted'"
|
||
);
|
||
$stmt->bind_param('si', $admin_note, $revocation_id);
|
||
$ok = $stmt->execute();
|
||
$stmt->close();
|
||
return $ok;
|
||
}
|
||
|
||
// ============================================================
|
||
// Mails
|
||
// ============================================================
|
||
|
||
private function send_info_mail($order, $token_plain) {
|
||
$mailer = new mail_tools($this->base_object);
|
||
$link = RevocationHelper::build_link($token_plain);
|
||
$name = trim(($order->billing_firstname ?? '').' '.($order->billing_surname ?? ''));
|
||
$order_number = $order->order_number ?: ('#'.$order->id);
|
||
|
||
$subject = 'Ihre Widerrufsrechte zu Bestellung '.$order_number;
|
||
|
||
$msg = '<p>Sehr geehrte Kundin, sehr geehrter Kunde'.($name ? ' '.htmlspecialchars($name) : '').',</p>';
|
||
$msg .= '<p>vielen Dank für Ihre Bestellung <strong>'.htmlspecialchars($order_number).'</strong>.</p>';
|
||
$msg .= '<h3>Widerrufsrecht</h3>';
|
||
$msg .= '<p>Sie haben das Recht, binnen 14 Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. '
|
||
. 'Die Widerrufsfrist beträgt 14 Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, '
|
||
. 'der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.</p>';
|
||
$msg .= '<p>Um Ihr Widerrufsrecht elektronisch auszuüben, klicken Sie bitte auf den folgenden Button:</p>';
|
||
$msg .= '<p style="margin:24px 0;text-align:center;">'
|
||
. '<a href="'.htmlspecialchars($link).'" '
|
||
. 'style="background:#c00;color:#fff;padding:14px 28px;text-decoration:none;'
|
||
. 'font-weight:bold;border-radius:4px;display:inline-block;">'
|
||
. htmlspecialchars(REVOCATION_BUTTON_LABEL)
|
||
. '</a></p>';
|
||
$msg .= '<p style="font-size:0.85em;color:#666;">Falls der Button nicht funktioniert, kopieren Sie bitte '
|
||
. 'diesen Link in Ihren Browser:<br><code>'.htmlspecialchars($link).'</code></p>';
|
||
$msg .= '<p>Der Link ist bis '.date('d.m.Y', strtotime(RevocationHelper::calc_expires_at($order->order_date))).' gültig.</p>';
|
||
$msg .= '<p>Alternativ können Sie uns Ihren Widerruf auch formlos (E-Mail, Brief) mitteilen. '
|
||
. 'Unsere ausführliche Widerrufsbelehrung finden Sie auf unserer Website.</p>';
|
||
$msg .= '<hr><p style="font-size:0.85em;color:#666;">'.REVOCATION_RETURN_COMPANY.'</p>';
|
||
|
||
return $mailer->send_mail($subject, $msg, $order->customer_email);
|
||
}
|
||
|
||
private function send_owner_notification($revocation) {
|
||
$mailer = new mail_tools($this->base_object);
|
||
|
||
// Order + Payment-Method-Label + Reason-Label nachladen
|
||
$order_id = (int)$revocation->order_id;
|
||
$order = $this->fetch_order_with_payment($order_id);
|
||
$reason_label = $this->fetch_reason_label($revocation->reason_id);
|
||
|
||
$order_number = $order->order_number ?? ('#'.$order_id);
|
||
$subject = 'Widerruf eingegangen – Bestellung '.$order_number;
|
||
|
||
$msg = '<h2>Widerruf eingegangen</h2>';
|
||
$msg .= '<table cellpadding="4" cellspacing="0" border="0" style="font-family:Arial,sans-serif;">';
|
||
$msg .= $this->mail_row('Bestellnummer', $order_number);
|
||
$msg .= $this->mail_row('Bestelldatum', $order->order_date ?? '');
|
||
$msg .= $this->mail_row('Kunde', trim(($order->billing_firstname ?? '').' '.($order->billing_surname ?? '')));
|
||
$msg .= $this->mail_row('E-Mail', $revocation->customer_email);
|
||
$msg .= $this->mail_row('Bestellsumme', number_format((float)($order->order_total ?? 0), 2, ',', '.').' EUR');
|
||
$msg .= $this->mail_row('Zahlungsart', $order->payment_method_name ?? '–');
|
||
$msg .= $this->mail_row('Widerrufsdatum', $revocation->submitted_at);
|
||
$msg .= $this->mail_row('Grund', $reason_label ?: '(kein Grund angegeben)');
|
||
if (!empty($revocation->reason_text)) {
|
||
$msg .= $this->mail_row('Anmerkung', nl2br(htmlspecialchars($revocation->reason_text)));
|
||
}
|
||
$msg .= $this->mail_row('IP (anon.)', $revocation->ip_anonymized ?? '–');
|
||
$msg .= '</table>';
|
||
$msg .= '<p style="margin-top:20px;"><strong>Bitte prüfen und E-Mail an die Kundin / den Kunden schreiben.</strong></p>';
|
||
$msg .= '<p>Admin: <a href="https://'.($_SERVER['HTTP_HOST'] ?? 'www.intelectra.de')
|
||
. '/index.php?admin_modul=admin_revocation_list">Widerrufsliste öffnen</a></p>';
|
||
|
||
$ok = $mailer->send_mail($subject, $msg, REVOCATION_OWNER_EMAIL);
|
||
|
||
if ($ok) {
|
||
$this->db->query(
|
||
"UPDATE revocations SET owner_notified_at = NOW() WHERE id = ".(int)$revocation->id
|
||
);
|
||
}
|
||
return $ok;
|
||
}
|
||
|
||
private function send_customer_confirmation($revocation) {
|
||
$mailer = new mail_tools($this->base_object);
|
||
|
||
$order = $this->fetch_order((int)$revocation->order_id);
|
||
$order_number = $order->order_number ?? ('#'.$revocation->order_id);
|
||
$name = trim(($order->billing_firstname ?? '').' '.($order->billing_surname ?? ''));
|
||
|
||
$ts = strtotime($revocation->submitted_at);
|
||
$datum = date('d.m.Y', $ts);
|
||
$uhrzeit = date('H:i', $ts);
|
||
|
||
$ok = $this->send_neutral_eingangsbestaetigung(
|
||
$revocation->customer_email,
|
||
$name,
|
||
$revocation->customer_email,
|
||
$order_number,
|
||
$datum,
|
||
$uhrzeit,
|
||
$revocation->reason_text ?? ''
|
||
);
|
||
|
||
if ($ok) {
|
||
$this->db->query(
|
||
"UPDATE revocations SET customer_confirmed_at = NOW() WHERE id = ".(int)$revocation->id
|
||
);
|
||
}
|
||
return $ok;
|
||
}
|
||
|
||
private function mail_row($label, $value) {
|
||
return '<tr><td style="color:#666;padding-right:12px;">'.htmlspecialchars($label)
|
||
. ':</td><td><strong>'.($value ?: '–').'</strong></td></tr>';
|
||
}
|
||
|
||
// ============================================================
|
||
// Queries
|
||
// ============================================================
|
||
|
||
private function fetch_order($order_id) {
|
||
$order_id = (int)$order_id;
|
||
$sql = "SELECT id, order_number, order_date, order_total, customer_id,
|
||
customer_email, billing_firstname, billing_surname,
|
||
payment_method
|
||
FROM orders WHERE id = $order_id LIMIT 1";
|
||
$res = $this->db->query($sql);
|
||
if (!$res || $res->num_rows === 0) return false;
|
||
return $res->fetch_object();
|
||
}
|
||
|
||
private function fetch_order_with_payment($order_id) {
|
||
$order_id = (int)$order_id;
|
||
$sql = "SELECT o.id, o.order_number, o.order_date, o.order_total,
|
||
o.billing_firstname, o.billing_surname,
|
||
pm.name AS payment_method_name
|
||
FROM orders o
|
||
LEFT JOIN payment_methods pm ON pm.id = o.payment_method
|
||
WHERE o.id = $order_id LIMIT 1";
|
||
$res = $this->db->query($sql);
|
||
if (!$res || $res->num_rows === 0) return false;
|
||
return $res->fetch_object();
|
||
}
|
||
|
||
private function fetch_revocation($id) {
|
||
$id = (int)$id;
|
||
$res = $this->db->query("SELECT * FROM revocations WHERE id = $id LIMIT 1");
|
||
if (!$res || $res->num_rows === 0) return false;
|
||
return $res->fetch_object();
|
||
}
|
||
|
||
private function fetch_reason_label($reason_id) {
|
||
if (empty($reason_id)) return null;
|
||
$reason_id = (int)$reason_id;
|
||
$res = $this->db->query("SELECT label FROM revocation_reasons WHERE id = $reason_id LIMIT 1");
|
||
if (!$res || $res->num_rows === 0) return null;
|
||
return $res->fetch_object()->label;
|
||
}
|
||
|
||
private function exists_for_order($order_id) {
|
||
$order_id = (int)$order_id;
|
||
$res = $this->db->query("SELECT 1 FROM revocations WHERE order_id = $order_id LIMIT 1");
|
||
return ($res && $res->num_rows > 0);
|
||
}
|
||
|
||
/**
|
||
* Öffentliche Liste für Admin-View.
|
||
*/
|
||
public function list_all($status = null, $limit = 200) {
|
||
$limit = (int)$limit;
|
||
$where = '';
|
||
if ($status !== null && $status !== '') {
|
||
$status = $this->db->real_escape_string($status);
|
||
$where = "WHERE r.status = '$status'";
|
||
}
|
||
$sql = "SELECT r.id, r.order_id, r.customer_email, r.status,
|
||
r.submitted_at, r.processed_at, r.owner_notified_at,
|
||
o.order_number, o.order_date
|
||
FROM revocations r
|
||
LEFT JOIN orders o ON o.id = r.order_id
|
||
$where
|
||
ORDER BY r.created_at DESC
|
||
LIMIT $limit";
|
||
$res = $this->db->query($sql);
|
||
$out = [];
|
||
if ($res) {
|
||
while ($obj = $res->fetch_object()) $out[] = $obj;
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
public function fetch_reasons() {
|
||
$res = $this->db->query(
|
||
"SELECT id, label FROM revocation_reasons WHERE active = 1 ORDER BY sort, id"
|
||
);
|
||
$out = [];
|
||
if ($res) {
|
||
while ($o = $res->fetch_object()) $out[] = $o;
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
// ============================================================
|
||
// Anonymous-Pfad (§ 356a BGB – Button im Footer, ohne Token)
|
||
// ============================================================
|
||
|
||
/**
|
||
* Vorausfüllung des anonymen Formulars aus einem Token-Link.
|
||
* Liefert ['name','order_number','email'] oder false.
|
||
*/
|
||
public function prefill_from_token($token_plain) {
|
||
if (!REVOCATION_ENABLED) {
|
||
$this->error = 'feature_disabled';
|
||
return false;
|
||
}
|
||
$token_plain = trim((string)$token_plain);
|
||
if (strlen($token_plain) !== REVOCATION_TOKEN_BYTES * 2) {
|
||
$this->error = 'invalid_token';
|
||
return false;
|
||
}
|
||
$hash = RevocationHelper::hash_token($token_plain);
|
||
$esc = $this->db->real_escape_string($hash);
|
||
$sql = "SELECT r.customer_email,
|
||
o.order_number,
|
||
o.billing_firstname, o.billing_surname
|
||
FROM revocations r
|
||
JOIN orders o ON o.id = r.order_id
|
||
WHERE r.token_hash = '$esc'
|
||
AND r.status = 'pending'
|
||
LIMIT 1";
|
||
$res = $this->db->query($sql);
|
||
if (!$res || $res->num_rows === 0) {
|
||
$this->error = 'invalid_token';
|
||
return false;
|
||
}
|
||
$row = $res->fetch_object();
|
||
return [
|
||
'name' => trim(($row->billing_firstname ?? '').' '.($row->billing_surname ?? '')),
|
||
'order_number' => $row->order_number,
|
||
'email' => $row->customer_email,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Widerruf ohne Token.
|
||
* Pflichtfelder: Name + E-Mail. Button-Klick = Erklärung.
|
||
* Bestellnummer + Artikel sind OPTIONAL (Händlerbund-Linie). Wenn Order
|
||
* gefunden → DB-Tracking. Sonst → reiner Anonym-Pfad mit Owner-Mail zur
|
||
* manuellen Zuordnung. Der Widerruf wird in jedem Fall entgegengenommen.
|
||
*/
|
||
public function submit_anonymous($name, $order_number, $email, $article, $ip, $user_agent) {
|
||
if (!REVOCATION_ENABLED) {
|
||
$this->error = 'feature_disabled';
|
||
return false;
|
||
}
|
||
|
||
$name = trim((string)$name);
|
||
$order_number = trim((string)$order_number);
|
||
$email = trim(strtolower((string)$email));
|
||
$article = trim((string)$article);
|
||
|
||
if ($name === '') {
|
||
$this->error = 'missing_name';
|
||
return false;
|
||
}
|
||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
$this->error = 'invalid_email';
|
||
return false;
|
||
}
|
||
|
||
// Order-Lookup nur informativ. Wenn keine Order angegeben oder
|
||
// keine gefunden oder nicht widerrufsfähig → Anonym-Pfad.
|
||
$order = ($order_number !== '')
|
||
? $this->fetch_order_by_number_email($order_number, $email)
|
||
: null;
|
||
|
||
if (!$order) {
|
||
return $this->process_anonymous_unmatched($name, $order_number, $email, $article, $ip, $user_agent);
|
||
}
|
||
|
||
$group_id = RevocationHelper::resolve_group_id($this->db, $order->customer_id);
|
||
if (!RevocationHelper::is_b2c($group_id)) {
|
||
return $this->process_anonymous_unmatched($name, $order_number, $email, $article, $ip, $user_agent);
|
||
}
|
||
|
||
$expires_at = RevocationHelper::calc_expires_at($order->order_date);
|
||
if (strtotime($expires_at) < time()) {
|
||
return $this->process_anonymous_unmatched($name, $order_number, $email, $article, $ip, $user_agent);
|
||
}
|
||
|
||
// Existing Eintrag wiederverwenden, sonst inline anlegen
|
||
$existing = $this->fetch_revocation_by_order((int)$order->id);
|
||
if ($existing) {
|
||
if ($existing->status !== 'pending') {
|
||
$this->error = 'already_submitted';
|
||
return false;
|
||
}
|
||
$revocation_id = (int)$existing->id;
|
||
} else {
|
||
$token_plain = RevocationHelper::generate_token();
|
||
$token_hash = RevocationHelper::hash_token($token_plain);
|
||
$stmt = $this->db->prepare(
|
||
"INSERT INTO revocations
|
||
(order_id, customer_id, customer_email, customer_group_id,
|
||
token_hash, expires_at, status)
|
||
VALUES (?, ?, ?, ?, ?, ?, 'pending')"
|
||
);
|
||
if (!$stmt) {
|
||
$this->error = 'prepare failed: '.$this->db->error;
|
||
return false;
|
||
}
|
||
$customer_id = $order->customer_id ? (int)$order->customer_id : null;
|
||
$order_id = (int)$order->id;
|
||
$stmt->bind_param(
|
||
'iisiss',
|
||
$order_id, $customer_id, $order->customer_email, $group_id,
|
||
$token_hash, $expires_at
|
||
);
|
||
if (!$stmt->execute()) {
|
||
$this->error = 'insert failed: '.$stmt->error;
|
||
$stmt->close();
|
||
return false;
|
||
}
|
||
$revocation_id = (int)$this->db->insert_id;
|
||
$stmt->close();
|
||
}
|
||
|
||
$reason_id = null;
|
||
$reason_text = ($article !== '' && mb_strlen($article) > 2000)
|
||
? mb_substr($article, 0, 2000)
|
||
: $article;
|
||
$ip_anon = RevocationHelper::anonymize_ip($ip);
|
||
$ua = $user_agent ? mb_substr($user_agent, 0, 255) : null;
|
||
|
||
$stmt = $this->db->prepare(
|
||
"UPDATE revocations
|
||
SET status = 'submitted',
|
||
submitted_at = NOW(),
|
||
reason_id = ?,
|
||
reason_text = ?,
|
||
ip_anonymized = ?,
|
||
user_agent = ?
|
||
WHERE id = ? AND status = 'pending'"
|
||
);
|
||
if (!$stmt) {
|
||
$this->error = 'prepare failed: '.$this->db->error;
|
||
return false;
|
||
}
|
||
$stmt->bind_param('isssi', $reason_id, $reason_text, $ip_anon, $ua, $revocation_id);
|
||
$ok = $stmt->execute();
|
||
$affected = $stmt->affected_rows;
|
||
$stmt->close();
|
||
if (!$ok || $affected < 1) {
|
||
$this->error = 'update failed';
|
||
return false;
|
||
}
|
||
|
||
$fresh = $this->fetch_revocation($revocation_id);
|
||
if ($fresh) {
|
||
$this->send_owner_notification($fresh);
|
||
$this->send_customer_confirmation($fresh);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private function fetch_order_by_number_email($order_number, $email) {
|
||
$on = $this->db->real_escape_string($order_number);
|
||
$em = $this->db->real_escape_string($email);
|
||
$sql = "SELECT id, order_number, order_date, order_total, customer_id,
|
||
customer_email, billing_firstname, billing_surname,
|
||
payment_method
|
||
FROM orders
|
||
WHERE order_number = '$on'
|
||
AND LOWER(customer_email) = '$em'
|
||
LIMIT 1";
|
||
$res = $this->db->query($sql);
|
||
if (!$res || $res->num_rows === 0) return false;
|
||
return $res->fetch_object();
|
||
}
|
||
|
||
private function fetch_revocation_by_order($order_id) {
|
||
$order_id = (int)$order_id;
|
||
$res = $this->db->query("SELECT * FROM revocations WHERE order_id = $order_id LIMIT 1");
|
||
if (!$res || $res->num_rows === 0) return false;
|
||
return $res->fetch_object();
|
||
}
|
||
|
||
// ============================================================
|
||
// Anonym-Pfad: ohne (passende) Bestellung
|
||
// ============================================================
|
||
|
||
/**
|
||
* Widerruf ohne Order-Match.
|
||
* Kein DB-Eintrag, dafür Owner-Mail mit Hinweis auf manuelle Zuordnung
|
||
* und Eingangsbestätigung an den Kunden.
|
||
*/
|
||
private function process_anonymous_unmatched($name, $order_number, $email, $article, $ip, $user_agent) {
|
||
$ip_anon = RevocationHelper::anonymize_ip($ip);
|
||
$ua = $user_agent ? mb_substr((string)$user_agent, 0, 255) : null;
|
||
$article = trim((string)$article);
|
||
if (mb_strlen($article) > 2000) {
|
||
$article = mb_substr($article, 0, 2000);
|
||
}
|
||
$this->send_anonymous_owner_notification($name, $order_number, $email, $article, $ip_anon, $ua);
|
||
$this->send_anonymous_customer_confirmation($name, $order_number, $email, $article);
|
||
return true;
|
||
}
|
||
|
||
private function send_anonymous_owner_notification($name, $order_number, $email, $article, $ip_anon, $ua) {
|
||
$mailer = new mail_tools($this->base_object);
|
||
$subject = 'Widerruf eingegangen (manuelle Zuordnung erforderlich)';
|
||
|
||
$msg = '<h2>Widerruf eingegangen</h2>';
|
||
$msg .= '<p style="background:#fff3cd;border-left:4px solid #ffa500;padding:10px;">';
|
||
$msg .= '<strong>Bitte manuell zuordnen.</strong> Der Widerruf wurde ohne (oder mit nicht gefundener) Bestellnummer eingereicht. ';
|
||
$msg .= 'Es wurde kein automatischer DB-Eintrag erzeugt.';
|
||
$msg .= '</p>';
|
||
$msg .= '<table cellpadding="4" cellspacing="0" border="0" style="font-family:Arial,sans-serif;">';
|
||
$msg .= $this->mail_row('Name', htmlspecialchars($name));
|
||
$msg .= $this->mail_row('E-Mail', htmlspecialchars($email));
|
||
$msg .= $this->mail_row('Bestellnummer', $order_number !== '' ? htmlspecialchars($order_number) : '(nicht angegeben)');
|
||
$msg .= $this->mail_row('Artikel/Bemerkung', $article !== '' ? nl2br(htmlspecialchars($article)) : '(nicht angegeben)');
|
||
$msg .= $this->mail_row('Datum/Uhrzeit', date('d.m.Y H:i:s'));
|
||
$msg .= $this->mail_row('IP (anon.)', $ip_anon ?? '–');
|
||
$msg .= '</table>';
|
||
$msg .= '<p style="margin-top:20px;"><strong>Bitte prüfen und E-Mail an die Kundin / den Kunden schreiben.</strong></p>';
|
||
|
||
return $mailer->send_mail($subject, $msg, REVOCATION_OWNER_EMAIL);
|
||
}
|
||
|
||
private function send_anonymous_customer_confirmation($name, $order_number, $email, $article) {
|
||
return $this->send_neutral_eingangsbestaetigung(
|
||
$email,
|
||
$name,
|
||
$email,
|
||
$order_number !== '' ? $order_number : '(nicht angegeben)',
|
||
date('d.m.Y'),
|
||
date('H:i'),
|
||
$article
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Neutrale Eingangsbestätigung gemäß Händlerbund-Vorlage.
|
||
*
|
||
* WICHTIG: Diese E-Mail darf NUR den Eingang der Widerrufserklärung
|
||
* bestätigen und KEINE wirksame Bestätigung des Widerrufs selbst sein.
|
||
* Keine Rücksendeadresse, keine Frist-Hinweise, keine Werbung.
|
||
*
|
||
* @param string $to Empfänger-Mail
|
||
* @param string $consumer_name Name des Verbrauchers für die Anrede
|
||
* @param string $consumer_email E-Mail aus dem Formular (für „Inhalt")
|
||
* @param string $order_number Bestellnr aus dem Formular ('' = nicht angegeben)
|
||
* @param string $date_str Eingangsdatum (dd.mm.yyyy)
|
||
* @param string $time_str Eingangsuhrzeit (HH:MM)
|
||
* @param string $article Artikel/Bemerkung (optional)
|
||
*/
|
||
private function send_neutral_eingangsbestaetigung($to, $consumer_name, $consumer_email,
|
||
$order_number, $date_str, $time_str, $article) {
|
||
|
||
$mailer = new mail_tools($this->base_object);
|
||
$subject = 'Ihr Widerruf ist bei uns eingegangen';
|
||
|
||
$name_safe = htmlspecialchars(trim((string)$consumer_name));
|
||
$anrede = $name_safe !== '' ? 'Hallo '.$name_safe.',' : 'Hallo,';
|
||
|
||
$msg = '<p>'.$anrede.'</p>';
|
||
$msg .= '<p>wir bestätigen hiermit den Eingang Ihres Widerrufs mit folgendem Inhalt:</p>';
|
||
$msg .= '<ul style="line-height:1.6;">';
|
||
$msg .= '<li><strong>Name:</strong> '.$name_safe.'</li>';
|
||
$msg .= '<li><strong>E-Mail:</strong> '.htmlspecialchars($consumer_email).'</li>';
|
||
$msg .= '<li><strong>Bestellnummer:</strong> '.htmlspecialchars($order_number).'</li>';
|
||
if (!empty($article)) {
|
||
$msg .= '<li><strong>Artikel/Bemerkung:</strong> '.nl2br(htmlspecialchars($article)).'</li>';
|
||
}
|
||
$msg .= '<li><strong>Eingang am:</strong> '.htmlspecialchars($date_str).' um '.htmlspecialchars($time_str).' Uhr</li>';
|
||
$msg .= '</ul>';
|
||
$msg .= '<p>Wir prüfen Ihr Anliegen und melden uns zeitnah bei Ihnen.</p>';
|
||
$msg .= '<p>Viele Grüße<br>Ihre '.htmlspecialchars(REVOCATION_RETURN_COMPANY).'</p>';
|
||
|
||
return $mailer->send_mail($subject, $msg, $to);
|
||
}
|
||
}
|