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 = '

Sehr geehrte Kundin, sehr geehrter Kunde'.($name ? ' '.htmlspecialchars($name) : '').',

'; $msg .= '

vielen Dank für Ihre Bestellung '.htmlspecialchars($order_number).'.

'; $msg .= '

Widerrufsrecht

'; $msg .= '

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.

'; $msg .= '

Um Ihr Widerrufsrecht elektronisch auszuüben, klicken Sie bitte auf den folgenden Button:

'; $msg .= '

' . '' . htmlspecialchars(REVOCATION_BUTTON_LABEL) . '

'; $msg .= '

Falls der Button nicht funktioniert, kopieren Sie bitte ' . 'diesen Link in Ihren Browser:
'.htmlspecialchars($link).'

'; $msg .= '

Der Link ist bis '.date('d.m.Y', strtotime(RevocationHelper::calc_expires_at($order->order_date))).' gültig.

'; $msg .= '

Alternativ können Sie uns Ihren Widerruf auch formlos (E-Mail, Brief) mitteilen. ' . 'Unsere ausführliche Widerrufsbelehrung finden Sie auf unserer Website.

'; $msg .= '

'.REVOCATION_RETURN_COMPANY.'

'; 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 = '

Widerruf eingegangen

'; $msg .= ''; $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 .= '
'; $msg .= '

Bitte Rückzahlung über die Warenwirtschaft ' . 'veranlassen. Die Zahlung muss über dieselbe Zahlungsart wie die ursprüngliche ' . 'Bestellung erfolgen.

'; $msg .= '

Admin: Widerrufsliste öffnen

'; $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 ?? '')); $subject = 'Bestätigung Ihres Widerrufs – Bestellung '.$order_number; $msg = '

Sehr geehrte Kundin, sehr geehrter Kunde'.($name ? ' '.htmlspecialchars($name) : '').',

'; $msg .= '

hiermit bestätigen wir den Eingang Ihrer Widerrufserklärung zu Bestellung ' . ''.htmlspecialchars($order_number).' am ' . date('d.m.Y H:i', strtotime($revocation->submitted_at)).' Uhr.

'; $msg .= '

Nächste Schritte

'; $msg .= '

Bitte senden Sie die Waren unverzüglich, spätestens binnen 14 Tagen, an:

'; $msg .= '

' . ''.REVOCATION_RETURN_COMPANY.'
' . REVOCATION_RETURN_STREET.'
' . REVOCATION_RETURN_CITY.'

'; $msg .= '

'.REVOCATION_RETURN_COSTS_NOTE.'

'; $msg .= '

Die Rückzahlung erfolgt innerhalb von 14 Tagen nach Eingang Ihres Widerrufs über ' . 'dieselbe Zahlungsart, die Sie bei der ursprünglichen Bestellung verwendet haben – ' . 'es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart.

'; $msg .= '

Bei Rückfragen erreichen Sie uns jederzeit unter ' . ''.REVOCATION_OWNER_EMAIL.'.

'; $msg .= '

'.REVOCATION_RETURN_COMPANY.'

'; $ok = $mailer->send_mail($subject, $msg, $revocation->customer_email); 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 ''.htmlspecialchars($label) . ':'.($value ?: '–').''; } // ============================================================ // 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; } }