diff --git a/core/payments/AbstractProvider.php b/core/payments/AbstractProvider.php index 9cf7e09..8cb125b 100644 --- a/core/payments/AbstractProvider.php +++ b/core/payments/AbstractProvider.php @@ -128,6 +128,17 @@ abstract class AbstractProvider implements ProviderInterface Log::write($module, 'start', $orderId, $number, $reference, 'open', null, $raw); } + /** + * Gibt es zu dieser Anbieter-Referenz schon einen Vorgang (mit Bestellung)? + * Braucht die Kasse bei der Express-Rueckkehr: verlangt PayPal nach dem Betrags-PATCH + * eine zweite Freigabe, kommt der Kunde ueber die Express-URL zurueck, obwohl die + * Bestellung laengst existiert -- dann ist es eine normale Rueckkehr mit Capture. + */ + public function knownReference(string $reference): bool + { + return $reference !== '' && $this->transactionByReference($reference) !== null; + } + protected function transactionByReference(string $reference): ?object { $stmt = $this->db->prepare('SELECT * FROM payment_transactions WHERE provider=? AND reference=? LIMIT 1'); diff --git a/modules/website_cashdesk.php b/modules/website_cashdesk.php index e707669..00c5e39 100644 --- a/modules/website_cashdesk.php +++ b/modules/website_cashdesk.php @@ -1,1396 +1,1404 @@ -base_object = $base_object; - $this->layout_object = $layout_object; - $this->errors = array(); - } - - // end __construct - - public function run() { - // SUCCESS-Seite nach REDIRECT - verhindert Doppel-Bestellungen - if (isset($_GET['success']) && $_GET['success'] == '1') { - // Bestellnummer aus URL für Template verfügbar machen - if (isset($_GET['order'])) { - $this->layout_object->assign('order_number', $_GET['order']); - } - // Success-Template anzeigen - auch bei Reload! - return $this->layout_object->_fetch('content_cash_desk/completed.tpl'); - } - - // 13.09.2026 -- Rueckkehr von einem Zahlungsanbieter aus core/payments - // (?payment_return= bzw. ?payment_cancel=). Laeuft hier im Cashdesk, - // weil die Session gebraucht wird (Pending-Riegel, Warenkorb). Steht vor dem - // Kunden-/Login-Check: nach dem Sprung ueber den Anbieter ist kein ?guest_order=1 - // mehr in der URL, ein Gast haette sonst keine customer_id. - if (isset($_GET['payment_return']) || isset($_GET['payment_cancel'])) { - return $this->payment_provider_return(); - } - - // 14.09.2026 -- Express aus dem Warenkorb ("Direkt mit PayPal bezahlen"). - // Start: Vorgang beim Anbieter ohne Bestellung, Kunde geht hin und gibt frei. - // Rueckkehr: Adresse/E-Mail vom Anbieter in die Session, weiter in die Gast-Kasse, - // dort ist alles vorbelegt -- ein Klick auf "zahlungspflichtig bestellen" zieht ein. - if (isset($_GET['payment_express_start'])) { - return $this->payment_express_start(); - } - if (isset($_GET['payment_express'])) { - return $this->payment_express_return(); - } - - // 24.08.2026 -- Zielseite des Post/Redirect/Get aus PaypalPaymentMethod::after_order(). - // Reines GET: hier steht der Link zu PayPal, ein Reload laedt nur noch ihn neu und - // legt keine zweite Bestellung mehr an. - // Steht bewusst vor dem Kunden-/Login-Check: nach dem Redirect ist kein - // ?guest_order=1 mehr in der URL, ein Gast haette hier sonst keine customer_id. - if (isset($_GET['pp_open'])) { - $pp_token = trim((string)$_GET['pp_open']); - - if (preg_match('/^EC-[A-Z0-9]{10,30}$/i', $pp_token)) { - $pp_sandbox = (class_exists('Config') && Config::get_value('paypal_dev_mode') == 1); - $pp_host = $pp_sandbox ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; - - $this->layout_object->assign('call', 'payment_open'); - $this->layout_object->assign('paypal_link', - $pp_host.'/cgi-bin/webscr?cmd=_express-checkout&token='.urlencode($pp_token)); - } else { - $this->layout_object->assign('call', 'payment_error'); - $this->layout_object->assign('message', - 'Der PayPal-Vorgang konnte nicht zugeordnet werden. Ihr Warenkorb ist noch da – bitte starten Sie die Bestellung neu.'); - } - - return $this->layout_object->_fetch('content_cash_desk/paypal.tpl'); - } - - if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { - // guest order - $this->base_object->customer->customer_id = $this->base_object->config->shopConfiguration['guest_order_customer_id']; - $this->layout_object->assign('guest_order', 1); - } - $this->payment_express_prefill(); - $action = ''; - - if ($this->base_object->customer->customer_id) { - if (isset($_POST['cashdesk_action'])) { - $action = $_POST['cashdesk_action']; - } else if (isset($_POST['save_address'])) { - $action = 'save_address'; - } else if (isset($_GET['token'])) { - $action = 'change_order'; - $param = 'paypal'; - } - - $this->personal_data = $_POST; - - if ($action == 'ready2fly') { - // MAKE ORDER - - $this->setup_countries(); - $this->setup_customer_data(); - if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { - $this->manage_guest_order_address(); - } else { - $this->manage_address(); - } - $this->manage_shoppingcart(); - $this->manage_payment_method(); - $this->manage_shipping_method(); - $this->manage_gift_certificate(); - $this->managa_conditions(); - - $this->before_saving_order(); - - - if (count($this->errors) == 0) { - if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { - $this->setup_customer_data(); - // check customer already exists by email - $customer_id = 0;//$this->base_object->customer->check_email($_POST['billing']['email']); - - if (!$customer_id) { - // create new customer for guest order - $customer_data = array ( - 'honorific' => $_POST['billing']['honorific'], - 'company' => $_POST['billing']['company'], - 'firstname' => $_POST['billing']['firstname'], - 'surname' => $_POST['billing']['surname'], - 'phone' => $_POST['billing']['phone'], - 'group_id' => $this->base_object->config->shopConfiguration['guest_order_customer_group'], - 'email' => $_POST['billing']['email'] - ); - $result = $this->base_object->customer->create($customer_data); - } else { - $result = $customer_id; - } - - if ($result) { - $this->customer_data->id = $customer_id = $this->base_object->customer->customer_id = $result; - - include_once './core/customeraddress.class.php'; - $customer_address_object = new CustomerAddress($this->base_object); - $billing_address = array ( - 'honorific' => $_POST['billing']['honorific'], - 'company' => $_POST['billing']['company'], - 'firstname' => $_POST['billing']['firstname'], - 'surname' => $_POST['billing']['surname'], - 'street' => $_POST['billing']['street'], - 'house_number' => $_POST['billing']['house_number'], - 'city' => $_POST['billing']['city'], - 'zip_code' => $_POST['billing']['zip_code'], - 'country' => $_POST['billing']['country'], - 'customer_id' => $customer_id - ); - $res = $customer_address_object->create($billing_address); - $this->billing_address['address'] = $res; - $shipping_address = array ( - 'honorific' => $_POST['shipping']['honorific'], - 'company' => $_POST['shipping']['company'], - 'firstname' => $_POST['shipping']['firstname'], - 'surname' => $_POST['shipping']['surname'], - 'street' => $_POST['shipping']['street'], - 'house_number' => $_POST['shipping']['house_number'], - 'city' => $_POST['shipping']['city'], - 'zip_code' => $_POST['shipping']['zip_code'], - 'country' => $_POST['shipping']['country'], - 'customer_id' => $customer_id - ); - - $res = $customer_address_object->create($shipping_address); - $this->shipping_address['address'] = $res; - - } - } - - if (isset($_POST['billing']['email'])) { - $data = array( - 'id' => $this->base_object->customer->customer_id, - 'email' => $_POST['billing']['email'] - ); - $this->base_object->customer->save($data); - } - - // 25.08.2026 -- Stufe 4: ein Bestellvorgang, eine Order. - // Traegt dieser POST einen Nonce, der schon zu einer Bestellung gefuehrt - // hat, ist es derselbe Vorgang -- egal ob eine Sekunde oder eine Stunde - // vergangen ist. Dann zurueck auf das Ergebnis von damals, statt ein - // zweites Mal zu bestellen. - $erledigt = $this->bestellvorgang_bereits_erledigt(); - - if ($erledigt) { - $offen = isset($_SESSION['easyshop']['paypal_pending']) - ? $_SESSION['easyshop']['paypal_pending'] : null; - - // Laeuft zu dieser Order noch ein unbezahlter PayPal-Vorgang, - // gehoert der Kunde dorthin und nicht auf die Erfolgsseite. - if ($offen && !empty($offen['token']) - && (int)$offen['order_id'] === (int)$erledigt['order_id'] - && !$this->paypal_order_is_paid($erledigt['order_id'])) { - - error_log('Checkout: Vorgang '.$erledigt['order_number'] - . ' bereits bestellt, zurueck auf offenen PayPal-Vorgang ' - . $offen['token']); - header('Location: /Kasse/?pp_open='.urlencode($offen['token'])); - exit; - } - - // 13.09.2026: dasselbe fuer Anbieter aus core/payments -- zurueck zur - // Approve-URL des offenen Vorgangs statt einer zweiten Bestellung. - $offen_pv = $this->payment_provider_pending(); - if ($offen_pv && (int)$offen_pv['order_id'] === (int)$erledigt['order_id']) { - error_log('Checkout: Vorgang '.$erledigt['order_number'] - . ' bereits bestellt, zurueck auf offenen '.$offen_pv['module'].'-Vorgang'); - header('Location: '.$offen_pv['approve_url']); - exit; - } - - error_log('Checkout: Vorgang bereits bestellt (Order ' - . $erledigt['order_number'].') -- keine zweite Order angelegt'); - - if (function_exists('shop_event')) { - shop_event('duplicate_blocked', array( - 'order_id' => $erledigt['order_id'], - 'order_number' => $erledigt['order_number'], - 'betrag' => $erledigt['total'], - 'detail' => 'zweiter Bestell-POST desselben Vorgangs' - )); - } - - header('Location: /Kasse/?success=1&order='.urlencode($erledigt['order_number'])); - exit; - } - - // 24.08.2026 -- zweiter Riegel gegen Doppelbestellungen bei PayPal. - // Kommt trotz PRG noch einmal ein ready2fly-POST (Zurueck-Taste, - // zweiter Tab, ungeduldiges Klicken) und liegt zu dieser Session - // bereits ein PayPal-Vorgang ueber denselben Betrag offen, der noch - // nicht bezahlt ist: zurueck auf dessen Link, statt eine zweite Order - // anzulegen. Ist der Vorgang bezahlt oder aelter als eine Stunde, - // laeuft alles normal weiter -- dann ist es ein neuer Einkauf. - $pending = isset($_SESSION['easyshop']['paypal_pending']) - ? $_SESSION['easyshop']['paypal_pending'] : null; - - $pending_module = isset($this->payment_methods[$this->payment_method_id]->module) - ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) - : ''; - - if ($pending && !empty($pending['token']) - && $pending_module === 'paypal' - && abs(round((float)$this->shoppingcart_data['order_total'], 2) - (float)$pending['total']) < 0.01 - && (time() - (int)(isset($pending['time']) ? $pending['time'] : 0)) < 3600 - && !$this->paypal_order_is_paid($pending['order_id'])) { - - error_log("PayPal Fix: doppelter ready2fly-POST abgefangen, zurueck auf offenen Vorgang {$pending['token']}"); - header('Location: /Kasse/?pp_open='.urlencode($pending['token'])); - exit; - } - - // 13.09.2026: derselbe Riegel fuer Anbieter aus core/payments. Der offene - // Vorgang liegt in $_SESSION['easyshop']['payment_pending'] (gesetzt vom - // Provider beim Start) -- gleiche Zahlart, gleicher Betrag, juenger als - // eine Stunde, nicht bezahlt: zurueck zur Approve-URL. - $pending_pv = $this->payment_provider_pending(); - if ($pending_pv - && $pending_pv['module'] === $pending_module - && abs(round((float)$this->shoppingcart_data['order_total'], 2) - (float)$pending_pv['total']) < 0.01) { - - error_log('Payments: doppelter ready2fly-POST abgefangen, zurueck auf offenen ' - . $pending_pv['module'].'-Vorgang '.$pending_pv['reference']); - header('Location: '.$pending_pv['approve_url']); - exit; - } - - $is_saved = $this->save_order(); - - if ($is_saved) { - return $this->after_saving_order(); - } - } - } else if ($action == 'change_order') { - return $this->change_order($param); - } else if ($action == 'save_address') { - // SAVE ADDRESS - $this->setup_customer_data(); - $this->save_address(); - - } else { - // SHOW ORDER - - $this->setup_countries(); - $this->setup_customer_data(); - if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { - $this->manage_guest_order_address(); - } else { - $this->manage_address(); - } - $this->manage_shoppingcart(); - $this->manage_payment_method(); - $this->manage_shipping_method(); - $this->manage_gift_certificate(); - $this->managa_conditions(); - } - //echo var_dump($this->personal_data); - $this->manage_errors(); - $this->manage_data_persistence(); - - // customer info - if (isset($_SESSION['customer_info']) && $_SESSION['customer_info']) { - $this->layout_object->assign('customer_info', $_SESSION['customer_info']); - } - - if (!$this->base_object->config->shopConfiguration['menu_in_cashdesk']) { - $this->layout_object->assign('site_type', 'full_width'); - } - - // 25.08.2026: Nonce fuers Kassenformular -- macht einen zweiten Klick auf - // "Bestellen" als denselben Vorgang erkennbar (siehe save_order()). - $this->layout_object->assign('checkout_nonce', $this->checkout_nonce_holen()); - - return $this->layout_object->_fetch('content_cash_desk.tpl'); - - } else if (isset($_GET['token']) && $_GET['token']) { - return $this->change_order('paypal'); - } else { - // login - $_SESSION['easyshop']['shopAction'] = 'cash_desk_customer_registration'; - - $this->layout_object->assign('go_to_cash_desk', 1); - return $this->layout_object->_fetch('content_login.tpl'); - } - } - - // end run - - // ------------------------------------------------------------ Express - - /** Start aus dem Warenkorb: /Kasse/?payment_express_start= */ - private function payment_express_start() { - $module = strtolower(trim((string)$_GET['payment_express_start'])); - $provider = $this->payment_provider($module); - if (!$provider || !$provider->supportsExpress()) { - header('Location: /Warenkorb/'); - exit; - } - $shoppingcart_object = new Shoppingcart($this->base_object); - $cart = $shoppingcart_object->get_data(); - if (empty($cart['items']) || (float)$cart['price_sum_gross'] <= 0) { - header('Location: /Warenkorb/'); - exit; - } - try { - $url = $provider->startExpress(round((float)$cart['price_sum_gross'], 2)); - } catch (Throwable $e) { - error_log('Payments express start '.$module.': '.$e->getMessage()); - $this->layout_object->assign('call', 'payment_error'); - $this->layout_object->assign('provider_label', $provider->label()); - $this->layout_object->assign('message', $provider->label().' konnte nicht gestartet werden. Ihr Warenkorb ist noch da – bitte gehen Sie normal zur Kasse.'); - return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); - } - header('Location: '.$url); - exit; - } - - /** Rueckkehr im Express-Weg: /Kasse/?payment_express=&token=... */ - private function payment_express_return() { - $module = strtolower(trim((string)$_GET['payment_express'])); - $provider = $this->payment_provider($module); - $data = null; - if ($provider && $provider->supportsExpress()) { - try { - $data = $provider->onExpressReturn($_GET); - } catch (Throwable $e) { - error_log('Payments express return '.$module.': '.$e->getMessage()); - } - } - if (!$data) { - $this->layout_object->assign('call', 'payment_error'); - $this->layout_object->assign('provider_label', $provider ? $provider->label() : 'Zahlung'); - $this->layout_object->assign('message', 'Der Express-Vorgang konnte nicht übernommen werden. Ihr Warenkorb ist noch da – bitte gehen Sie normal zur Kasse.'); - return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); - } - // Erlaubte Laender fuer Express (Setting _express_countries, Standard DE): - // PayPal-Konten gibt es weltweit, geliefert wird aber nur, wohin der Shop liefert. - $allowed = array_filter(array_map('trim', explode(',', strtoupper($provider->expressCountries())))); - $iso = strtoupper((string)$data['country_iso']); - $country_id = in_array($iso, $allowed, true) ? $provider->countryIdByIso($iso) : 0; - if ($country_id <= 0) { - $this->layout_object->assign('call', 'payment_error'); - $this->layout_object->assign('provider_label', $provider->label()); - $this->layout_object->assign('message', 'In das bei '.$provider->label().' hinterlegte Land ('.htmlspecialchars((string)$data['country_iso']).') liefern wir leider nicht. Bitte gehen Sie normal zur Kasse und wählen Sie eine Lieferadresse.'); - return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); - } - $data['module'] = $module; - $data['country_id'] = $country_id; - $data['time'] = time(); - $_SESSION['easyshop']['payment_express'] = $data; - - // Eingeloggter Kunde: eigene Kasse mit eigenen Adressen, nur Zahlart vorbelegt. - $logged_in = $this->base_object->customer->customer_id - && (int)$this->base_object->customer->customer_id !== (int)$this->base_object->config->shopConfiguration['guest_order_customer_id']; - header('Location: /Kasse/'.($logged_in ? '' : '?guest_order=1')); - exit; - } - - /** - * Kasse mit den Express-Daten vorbelegen (Gast: Adresse + E-Mail + Zahlart, - * eingeloggt: nur Zahlart). Nur beim reinen Anzeigen -- ein POST des Kunden gewinnt. - */ - private function payment_express_prefill() { - $e = isset($_SESSION['easyshop']['payment_express']) ? $_SESSION['easyshop']['payment_express'] : null; - if (!is_array($e) || empty($e['module']) || empty($e['reference'])) { - return; - } - if ((time() - (int)$e['time']) > 3600) { - unset($_SESSION['easyshop']['payment_express']); - return; - } - $pm_id = 0; - $res = $this->base_object->db->query("SELECT id FROM payment_methods WHERE module='".$this->base_object->db->real_escape_string($e['module'])."' AND active=1 LIMIT 1"); - if ($res && ($r = $res->fetch_object())) { - $pm_id = (int)$r->id; - } - $this->layout_object->assign('express_active', 1); - $this->layout_object->assign('express_label', ucfirst(str_replace('_', ' ', $e['module']))); - - if (isset($_POST['cashdesk_action']) || isset($_POST['save_address'])) { - return; // Kunde hat das Formular selbst abgeschickt - } - if ($pm_id) { - $_POST['payment_method'] = $pm_id; - } - $is_guest = isset($_GET['guest_order']) && $_GET['guest_order'] == 1; - if (!$is_guest) { - return; - } - $addr = array( - 'honorific' => '', - 'company' => (string)$e['company'], - 'firstname' => (string)$e['firstname'], - 'surname' => (string)$e['surname'], - 'street' => (string)$e['street'], - 'house_number' => (string)$e['house_number'], - 'zip_code' => (string)$e['zip_code'], - 'city' => (string)$e['city'], - 'country' => (int)$e['country_id'], - ); - $_POST['billing'] = $addr + array('email' => (string)$e['email'], 'address' => '', 'billing_address' => ''); - $_POST['shipping'] = $addr + array('address' => -1); // -1 = wie Rechnungsadresse - $this->personal_data = $_POST; - } - - /** - * 13.09.2026 -- Zahlungsanbieter aus core/payments. - * - * Bei diesen Zahlarten (und beim alten PayPal) ist die Bestellung erst nach der - * Rueckkehr vom Anbieter bezahlt: Bestellbestaetigung und Warenkorb-Leerung warten - * bis dahin. Alle anderen Zahlarten sind mit dem Abschicken verbindlich. - */ - private function zahlung_erst_spaeter($module) { - $module = strtolower((string)$module); - if ($module === 'paypal') { - return true; // Legacy-NVP-PayPal, raeumt in PaypalPaymentMethod::change_order() selbst auf - } - $provider = $this->payment_provider($module); - return $provider ? $provider->paysAfterRedirect() : false; - } - - /** @return \Payments\ProviderInterface|null */ - private function payment_provider($module) { - if (!class_exists('\Payments\Registry') || !\Payments\Registry::has((string)$module)) { - return null; - } - try { - return \Payments\Registry::get((string)$module, $this->base_object); - } catch (Throwable $e) { - error_log('Payments: Provider '.$module.' nicht ladbar: '.$e->getMessage()); - return null; - } - } - - /** - * Offener, unbezahlter Vorgang eines Anbieters in dieser Session (juenger als 1 h), - * sonst null. Grundlage der Riegel gegen Doppelbestellungen. - */ - private function payment_provider_pending() { - $p = isset($_SESSION['easyshop']['payment_pending']) ? $_SESSION['easyshop']['payment_pending'] : null; - if (!is_array($p) || empty($p['module']) || empty($p['approve_url']) || empty($p['order_id'])) { - return null; - } - if ((time() - (int)(isset($p['time']) ? $p['time'] : 0)) >= 3600) { - unset($_SESSION['easyshop']['payment_pending']); - return null; - } - $provider = $this->payment_provider($p['module']); - if (!$provider || $provider->isPaid((int)$p['order_id'])) { - unset($_SESSION['easyshop']['payment_pending']); - return null; - } - return $p; - } - - /** - * Rueckkehr vom Anbieter (?payment_return= | ?payment_cancel=). - * Bezahlt -> Redirect auf die Erfolgsseite (F5-sicher). Sonst Hinweisseite, der - * Warenkorb bleibt in jedem Fall stehen. - */ - private function payment_provider_return() { - $is_cancel = isset($_GET['payment_cancel']); - $module = strtolower(trim((string)($is_cancel ? $_GET['payment_cancel'] : $_GET['payment_return']))); - $provider = $this->payment_provider($module); - - if (!$provider) { - $this->layout_object->assign('call', 'payment_error'); - $this->layout_object->assign('message', 'Die Zahlungsart konnte nicht zugeordnet werden. Ihr Warenkorb ist noch da.'); - return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); - } - - try { - $result = $is_cancel ? $provider->onCancel($_GET) : $provider->onReturn($_GET); - } catch (Throwable $e) { - error_log('Payments: '.$module.' return/cancel: '.$e->getMessage()); - $result = \Payments\Result::error('Bei der Rueckmeldung des Zahlungsanbieters ist ein Fehler aufgetreten. Ihr Warenkorb ist noch da.'); - } - - if ($result->status === \Payments\Result::PAID) { - header('Location: /Kasse/?success=1&order='.urlencode($result->orderNumber)); - exit; - } - - $this->layout_object->assign('provider_label', $provider->label()); - $this->layout_object->assign('order_number', $result->orderNumber); - $this->layout_object->assign('message', $result->message); - $this->layout_object->assign('call', - $result->status === \Payments\Result::CANCELLED ? 'payment_cancelled' - : ($result->status === \Payments\Result::PENDING ? 'payment_pending' : 'payment_error')); - return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); - } - - private function setup_countries() { - $country_object = new Country($this->base_object); - - $this->countries = $country_object->get_all(); - - $this->layout_object->assign('countries', $this->countries); - } - - // end setup_countries - - private function setup_customer_data() { - $customer_id = $this->base_object->customer->customer_id; - - $customer_object = new Customer($this->base_object); - - $this->customer_data = $customer_object->get_by_id($customer_id); - - // customer data is assigned in manage_address to take care of potential missing default address data - - $this->layout_object->assign('shoppingcartVATUsage', $this->base_object->customer_group->show_tax); - $this->layout_object->assign('show_tax', $this->base_object->customer_group->show_tax); - $this->layout_object->assign('customer_data', $this->customer_data); - } - - // end setup_customer_data - - private function manage_guest_order_address() { - $address_object = new CustomerAddress($this->base_object); - - $address_object->set_customer_id($this->customer_data->id); - $this->customer_addresses = $address_object->get_all(); - - if (!isset($this->customer_addresses[$this->customer_data->default_address])) { - $new_default = current($this->customer_addresses); - $this->customer_data->default_address_country_id = $new_default->country; - $this->customer_data->default_address_countryname = $this->countries[$new_default->country]->name; - } - - if (isset($_POST['shipping']) && $_POST['shipping']['address'] == -1) { - $this->shipping_address = $_POST['billing']; - $_POST['shipping'] = $_POST['billing']; - } else if (isset($_POST['shipping'])) { - if (!$this->shipping_address['country']) { - $_POST['shipping']['country'] = $this->customer_data->default_address_country_id; - $this->layout_object->assign('default_address_country_id', $this->customer_data->default_address_country_id); - } - $this->shipping_address = $_POST['shipping']; - } else { - $this->shipping_address['country'] = $this->customer_data->default_address_country_id; - } - - $this->layout_object->assign('shipping_address', $this->shipping_address); - - if (isset($_POST['billing'])) { - $this->billing_address = $_POST['billing']; - } else { - $this->billing_address['country'] = $this->customer_data->default_address_country_id; - } - - $this->layout_object->assign('billing_address', $this->billing_address); - - $this->layout_object->assign('customer', $this->customer_data); - $shipping_country_id = $this->shipping_address['country']; - - $this->shipping_area_id = Country::getShippingAreaById($shipping_country_id, $this->customer_data->group_id); - - $this->layout_object->assign('addresses', $this->customer_addresses); - } // end manage_address - - private function manage_address() { - $address_object = new CustomerAddress($this->base_object); - - $address_object->set_customer_id($this->customer_data->id); - $this->customer_addresses = $address_object->get_all(); - - - // hack for missing default address - if (is_array($this->customer_addresses) && !isset($this->customer_addresses[$this->customer_data->default_address])) { - $new_default = current($this->customer_addresses); - - $this->customer_data->default_address = $new_default->id; - $this->customer_data->default_address_company = $new_default->company; - $this->customer_data->default_address_honorific = $new_default->honorific; - $this->customer_data->default_address_firstname = $new_default->firstname; - $this->customer_data->default_address_surname = $new_default->surname; - $this->customer_data->default_address_street = $new_default->street; - $this->customer_data->default_address_house_number = $new_default->house_number; - $this->customer_data->default_address_zip_code = $new_default->zip_code; - $this->customer_data->default_address_city = $new_default->city; - $this->customer_data->default_address_country_id = $new_default->country; - $this->customer_data->default_address_countryname = $this->countries[$new_default->country]->name; - } - - if (isset($_POST['shipping']['address']) && $_POST['shipping']['address'] == -1) { - $this->shipping_address = $_POST['billing']; - } else if (isset($_POST['shipping'])) { - $this->shipping_address = $_POST['shipping']; - } else { - $this->shipping_address['address'] = $this->customer_data->default_address; - $this->shipping_address['honorific'] = $this->customer_data->default_address_honorific; - $this->shipping_address['firstname'] = $this->customer_data->default_address_firstname; - $this->shipping_address['surname'] = $this->customer_data->default_address_surname; - $this->shipping_address['company'] = $this->customer_data->default_address_company; - $this->shipping_address['street'] = $this->customer_data->default_address_street; - $this->shipping_address['house_number'] = $this->customer_data->default_address_house_number; - $this->shipping_address['zip_code'] = $this->customer_data->default_address_zip_code; - $this->shipping_address['city'] = $this->customer_data->default_address_city; - $this->shipping_address['country'] = $this->customer_data->default_address_country_id; - } - - if (isset($_POST['billing'])) { - $this->billing_address = $_POST['billing']; - } else { - $this->billing_address['address'] = $this->customer_data->default_address; - $this->billing_address['honorific'] = $this->customer_data->default_address_honorific; - $this->billing_address['firstname'] = $this->customer_data->default_address_firstname; - $this->billing_address['surname'] = $this->customer_data->default_address_surname; - $this->billing_address['company'] = $this->customer_data->default_address_company; - $this->billing_address['street'] = $this->customer_data->default_address_street; - $this->billing_address['house_number'] = $this->customer_data->default_address_house_number; - $this->billing_address['zip_code'] = $this->customer_data->default_address_zip_code; - $this->billing_address['city'] = $this->customer_data->default_address_city; - $this->billing_address['country'] = $this->customer_data->default_address_country_id; - } - $billing_country_id = $this->billing_address['country']; - - $this->layout_object->assign('customer', $this->customer_data); - - $shipping_country_id = $this->shipping_address['country']; - $this->shipping_area_id = Country::getShippingAreaById($shipping_country_id, $this->customer_data->group_id); - - $this->layout_object->assign('addresses', $this->customer_addresses); - } - - // end manage_address - - private function manage_shoppingcart() { - $shoppingcart_object = new Shoppingcart($this->base_object); - $this->shoppingcart_data = $shoppingcart_object->get_data(); - - // can't show shoppingcart data here, gift certificates can change the data - } - - // end manage_shoppingcart - - private function manage_payment_method() { - $payment_method_object = new Payment_method($this->base_object); - - $this->payment_methods = $payment_method_object->getMethodsForGroup($this->customer_data->group_id, $this->shoppingcart_data['price_sum_gross'], $this->shipping_area_id); - if (isset($_POST['payment_method']) && $_POST['payment_method'] > 0) { - $this->payment_method_id = $_POST['payment_method']; - } else { - //$this->personal_data['payment_method'] = $this->payment_method_id = current($this->payment_methods)->id; - foreach ($this->payment_methods as $pm) { - if ($pm->activeType > 0) { - $this->personal_data['payment_method'] = $this->payment_method_id = $pm->id; - break; - } - } - } - - $this->layout_object->assign('payment_methods', $this->payment_methods); - } - - // end manage_payment_method - - private function manage_shipping_method() { - $shipping_method_object = new Deliverer($this->base_object); - - $shoppingcart_object = new Shoppingcart($this->base_object); - $this->shoppingcart_data = $shoppingcart_object->get_data(); - - $this->shipping_methods = $shipping_method_object->getAllActive( - $this->shipping_area_id, $this->shoppingcart_data['standard_shipping_items'], $this->shoppingcart_data['bulk_goods'], $this->shoppingcart_data['item_weight'], $this->shoppingcart_data['price_sum_gross'] - ); - - if (isset($_POST['shipping_method']) && $_POST['shipping_method'] > 0) { - foreach ($this->shipping_methods as $sm) { - if ($sm->id == $_POST['shipping_method']) { - $this->shipping_method_id = $_POST['shipping_method']; - } - } - } - - if (!$this->shipping_method_id) { - $methods = $this->shipping_methods; - $method = array_shift($methods); - $this->personal_data['shipping_method'] = $this->shipping_method_id = $method->id; - } - $this->layout_object->assign('deliverers', $this->shipping_methods); - } - - // end manage_shipping_method - - private function manage_gift_certificate() { - $this->shoppingcart_data['order_total'] = $this->shoppingcart_data['price_sum_gross'] + $this->payment_methods[$this->payment_method_id]->paymentAdd + $this->shipping_methods[$this->shipping_method_id]->price_add; - - $this->shoppingcart_data['payment_method_vat'] = $this->payment_methods[$this->payment_method_id]->paymentAddVat; - $this->shoppingcart_data['shipping_method_vat'] = $this->shipping_methods[$this->shipping_method_id]->price_add_vat; - - if (isset($_POST['giftcertificate']['gift_certificat_code']) && $_POST['giftcertificate']['gift_certificat_code'] != '') { - $this->has_gift_certificate = true; - - $this->gift_certificate_code = $_POST['giftcertificate']['gift_certificat_code']; - $this->layout_object->assign('giftcode', $this->gift_certificate_code); - $this->gift_certificate_is_valid = GiftCertificateHelper::code_is_valide($this->customer_data->id, $this->gift_certificate_code, $this->shoppingcart_data['price_sum_gross']); - - if ($this->gift_certificate_is_valid) { - $this->shoppingcart_data['gift_certificate_code'] = $this->gift_certificate_code; - $this->gift_certificate = GiftCertificateHelper::get_certificate_by_code($this->gift_certificate_code); - - $this->gift_certificate_value = GiftCertificateHelper::get_certificate_value($this->gift_certificate_code, $this->shoppingcart_data['price_sum_gross'], $this->customer_data->group_id); - - $this->layout_object->assign('giftcertificatevalue', $this->gift_certificate_value->with_vat); - $this->layout_object->assign('giftcertificatmessage', "Code akzeptiert."); - - // calculate the part of the gift certificate for each tax rate - $part = $this->gift_certificate_value->with_vat / $this->shoppingcart_data['price_sum_gross']; - } else { - $this->gift_certificate_error = GiftCertificateHelper::get_error_text(); - - $this->layout_object->assign('gftcertificateerrormessage', $this->gift_certificate_error); - $this->layout_object->assign('giftcertificatmessage', 'Code wird nicht akzeptiert.'); - } - } else { - $this->has_gift_certificate = false; - } - - foreach ($this->shoppingcart_data['tax_values'] as $key => $value) { - $this->shoppingcart_data['tax_values'][$key]['gift']['percent'] = $percent = round(($value['gross'] / $this->shoppingcart_data['price_sum_gross'] * 100), 2); - $this->shoppingcart_data['tax_values'][$key]['gift']['gross'] = $new_gross = $this->shoppingcart_data['price_sum_gross'] / 100 * $percent; - $this->shoppingcart_data['tax_values'][$key]['gift']['net'] = $new_net = $new_gross / (100 + $key) * 100; - $this->shoppingcart_data['tax_values'][$key]['gift']['vat'] = $new_gross - $new_net; - if ($this->base_object->customer_group->show_tax) { - $this->shoppingcart_data['tax_values'][$key]['gift']['show'] = $new_gross; - } else { - $this->shoppingcart_data['tax_values'][$key]['gift']['show'] = $new_net; - } - } - - // TODO: HACK, generalize this - if (isset($this->payment_methods[9])) { - if ((isset($this->base_object->config->shopConfiguration['klarna_eid']) && - $this->base_object->config->shopConfiguration['klarna_eid'] != '') && - (isset($this->base_object->config->shopConfiguration['klarna_secret']) && - $this->base_object->config->shopConfiguration['klarna_secret'] != '')) { - include_once('./core/shop_klarna.class.php'); - - // klarna installments - $klarna_object = new Shop_klarna($this->base_object); - if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { - $klarna_installments = $klarna_object->get_installments($this->shoppingcart_data['order_total'] - $this->gift_certificate_value->with_vat); - } else { - $klarna_installments = $klarna_object->get_installments($this->shoppingcart_data['order_total']); - } - $this->layout_object->assign('klarna_installments', $klarna_installments); - } - } - - // since the gift certificate can change the cart data this must be done here - $this->layout_object->assign('shoppingcart', $this->shoppingcart_data); - } - - // end manage_gift_certificate - - private function managa_conditions() { - $site_content_object = new SiteContent($this->base_object); - - $agb_data = array(); - // Datenschutz: aktuelle Version aus struct 10737 (DSGVO-Update). Alte struct 55 (id 50) ist veraltet. - $agb_data['datenschutz'] = $site_content_object->get_textbox_data_by_id(10737); - $agb_data['agbs'] = $site_content_object->get_textbox_data_by_id(56); - $agb_data['agbs_b2b'] = $site_content_object->get_textbox_data_by_id(10632); - $agb_data['widerruf'] = $site_content_object->get_textbox_data_by_id(60); - - $this->layout_object->assign('agb_data', $agb_data); - } - - // end manage_conditions - - private function manage_errors() { - $this->layout_object->assign('error_message', $this->errors); - } - - // end manage_errors - - private function manage_data_persistence() { - // ugly hack for data persistence - $this->layout_object->assign('personal_data', $this->personal_data); - } - - // end manage_data_persistence - - private function save_address() { - $address = $_POST; - unset($address['save_address']); - $address_id = false; - if ($address['address'] != '') { - $address_id = $address['address']; - } - unset($address['address']); - $address['customer_id'] = $this->customer_data->id; - - $customer_address_object = new CustomerAddress($this->base_object); - - $ret = array(); - if ($customer_address_object->validate($address)) { - if ($address_id) { - $customer_address_object->update($address, $address_id); - } else { - $address_id = $customer_address_object->create($address); - } - - $ret['id'] = $address_id; - $ret['stat'] = 'OK'; - } else { - $ret['stat'] = 'ERR'; - $ret['data'] = $customer_address_object->get_validation_errors(); - } - - echo json_encode($ret); - exit(); - } - - // end save_address - - private function before_saving_order() { - // check order - if (!isset($_GET['guest_order']) || $_GET['guest_order'] != 1) { - if (!is_numeric($_POST['billing']['address'])) { - $this->errors[] = 'Bitte wählen Sie eine Rechnungsadresse'; - } - - if (!is_numeric($_POST['shipping']['address'])) { - $this->errors[] = 'Bitte wählen Sie eine Lieferadresse'; - } - } - - if (!is_numeric($_POST['payment_method'])) { - $this->errors[] = 'Bitte wählen Sie eine Zahlungsart'; - } else { - $payment_method_id = $_POST['payment_method']; - - if ($this->payment_methods[$payment_method_id]->module != '') { - $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; - $module_name = ucfirst($this->payment_methods[$payment_method_id]->module).'PaymentMethod'; - $file_name = strtolower($module_name).'.class.php'; - - if (file_exists($path.$file_name)) { - include_once('./core/paymentmethods/'.$file_name); - - $payment_method_module = new $module_name($this->base_object, $this->layout_object); - - $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id]); - - if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { - $payment_method_module->set_gift_certificate($this->gift_certificate_value); - } - - if ($this->payment_methods[$payment_method_id]->argument != '') { - $return = $payment_method_module->before_order($this->payment_methods[$payment_method_id]->argument); - } else { - $return = $payment_method_module->before_order(); - } - - if ($return) { - $this->errors[] = $return; - } - } - } - } - - if (!is_numeric($_POST['shipping_method'])) { - $this->errors[] = 'Bitte wählen Sie eine Versandart'; - } - - if (!is_numeric($_POST['agreements_accepted'])) { - $this->errors[] = 'Bitte akzeptieren Sie unsere Bedingungen'; - } - } - - // end before_saving_order - - private function save_order() { - // 25.08.2026 -- FIX 1 ersetzt (Stufe 4). - // - // Hier stand eine Duplikatspruefung, die auf - // order_date >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) - // beruhte. `orders.order_date` ist aber Typ DATE, nicht DATETIME -- MySQL macht aus - // dem Datum '2026-08-25 00:00:00', und damit war die Bedingung fuer den GANZEN TAG - // wahr statt fuer fuenf Minuten. Wer am selben Tag ein zweites Mal bestellte, bekam - // seine ERSTE Bestellnummer als "erfolgreich" angezeigt, und der neue Warenkorb - // wurde nie bestellt. Ein stiller Verlust, der niemandem auffiel. - // - // Reparieren liess sich das nicht: die Tabelle `orders` hat ueberhaupt keine Spalte - // mit Uhrzeit (alle Datumsfelder sind DATE), und `order_history.timestamp` wird bei - // der Bestellanlage nicht gefuellt. Es gibt also keinen Zeitstempel, auf den man - // eine Fuenf-Minuten-Regel stuetzen koennte. - // - // Statt der Uhr entscheidet jetzt der Vorgang: ein checkout_nonce pro Kassenformular - // (siehe run() und checkout_nonce_holen()). Wird derselbe Nonce ein zweites Mal - // abgeschickt, ist es derselbe Bestellvorgang -- unabhaengig davon, wie viel Zeit - // vergangen ist. Als Netz fuer Absendungen ohne Nonce dient das Zeitfenster in - // $_SESSION['easyshop']['last_order'], das eine echte Uhrzeit fuehrt. - // - // Der Empty-Cart-Schutz weiter unten (FIX 2) bleibt unveraendert. - - // 🚀 FIX 2: Verhindere 0€ Bestellungen ohne Items (Race Condition / Session Loss) - if (empty($this->shoppingcart_data) || - !isset($this->shoppingcart_data['items']) || - empty($this->shoppingcart_data['items']) || - !isset($this->shoppingcart_data['amount_sum']) || - intval($this->shoppingcart_data['amount_sum']) <= 0) { - - error_log("ORDER PREVENTED: Empty cart or 0€ order attempted - Customer: " . - ($this->customer_data->id ?? 'unknown') . - ", Cart Items: " . (count($this->shoppingcart_data['items'] ?? [])) . - ", Amount Sum: " . ($this->shoppingcart_data['amount_sum'] ?? '0')); - - $this->errors[] = 'Ihr Warenkorb ist leer. Bitte fügen Sie Artikel hinzu.'; - return false; - } - - if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { - $orderDataObject = OrderHelper::createOrderObject(array( - 'customerId' => $this->customer_data->id, - 'cartId' => session_id(), - 'billingAddressId' => $this->billing_address['address'], - 'shippingAddressId' => $this->shipping_address['address'], - 'paymentMethodId' => $this->payment_method_id, - 'shippingMethodId' => $this->shipping_method_id, - 'giftCertificate' => $this->gift_certificate_code - ), $this->base_object); - } else { - $orderDataObject = OrderHelper::createOrderObject(array( - 'customerId' => $this->base_object->customer->customer_id, - 'cartId' => session_id(), - 'billingAddressId' => $this->billing_address['address'], - 'shippingAddressId' => $this->shipping_address['address'], - 'paymentMethodId' => $this->payment_method_id, - 'shippingMethodId' => $this->shipping_method_id - ), $this->base_object); - } - - // TODO : HACK - // fix up paymemt method with ALL the data - $orderDataObject->payment->methodId = $this->payment_method_id; - $orderDataObject->payment->areaId = $this->shipping_area_id; - $orderDataObject->payment->priceGross = $this->payment_methods[$this->payment_method_id]->paymentAdd; - $orderDataObject->payment->priceNet = $this->payment_methods[$this->payment_method_id]->paymentAddNet; - $orderDataObject->payment->priceVat = $this->payment_methods[$this->payment_method_id]->paymentAddVat; - $orderDataObject->payment->vatRate = '19.00'; - - // TODO : HACK - // fix up shipping method with ALL the data - $orderDataObject->shipping->methodId = $this->shipping_method_id; - $orderDataObject->shipping->areaId = $this->shipping_area_id; - $orderDataObject->shipping->priceGross = $this->shipping_methods[$this->shipping_method_id]->price_add; - $orderDataObject->shipping->priceNet = $this->shipping_methods[$this->shipping_method_id]->price_add_net; - $orderDataObject->shipping->priceVat = $this->shipping_methods[$this->shipping_method_id]->price_add_vat; - $orderDataObject->shipping->vatRate = '19.00'; - - if (isset($_POST['customer_info']) && $_POST['customer_info'] != '') { - $orderDataObject->customer_info = $_POST['customer_info']; - } - - if ($this->payment_methods[$this->payment_method_id]->module != '') { - $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; - $module_name = ucfirst($this->payment_methods[$this->payment_method_id]->module).'PaymentMethod'; - $file_name = strtolower($module_name).'.class.php'; - - if (file_exists($path.$file_name)) { - include_once('./core/paymentmethods/'.$file_name); - - $payment_method_module = new $module_name($this->base_object, $this->layout_object); - - $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id]); - - if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { - $payment_method_module->set_gift_certificate($this->gift_certificate_value); - } - - - if ($this->payment_methods[$this->payment_method_id]->argument != '') { - $return = $payment_method_module->during_order($this->payment_methods[$this->payment_method_id]->argument); - } else { - $return = $payment_method_module->during_order(); - } - - if ($return) { - foreach ($return as $key => $value) { - $orderDataObject->$key = $value; - } - } - } - } - - $order_object = new Order($this->base_object); - $this->order_id = $order_object->create($orderDataObject); - $this->order_number = OrderHelper::get_order_number($this->order_id); - - unset($_SESSION['customer_info']); - - if ($this->order_id) { - // 24.08.2026: Bei PayPal wandert die Bestellbestaetigung ans Ende des - // Bezahlvorgangs (PaypalPaymentMethod::bestellbestaetigung_senden). Hier waere - // sie zu frueh -- die Order existiert an dieser Stelle zwar, aber der Kunde war - // noch nicht einmal bei PayPal. Beim Testkauf am 24.08. lagen zwischen dieser - // Mail (23:25:09) und der eingezogenen Zahlung (23:26:45) anderthalb Minuten, - // und wer gar nicht bezahlt, bekam trotzdem eine Bestaetigung. - // Alle anderen Zahlarten sind mit dem Abschicken verbindlich -- dort bleibt der - // Versand genau hier. - $mail_modul = isset($this->payment_methods[$this->payment_method_id]->module) - ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) - : ''; - - if (!$this->zahlung_erst_spaeter($mail_modul)) { - // send email - $ticket_object = new Cs_ticket($this->base_object); - $ticket_object->event(3, $this->customer_data->id, $this->order_id); - } else { - error_log('PayPal Fix: Bestellbestaetigung fuer Order '.$this->order_id - . ' zurueckgestellt bis zur eingezogenen Zahlung'); - } - - // EU-RL 2026/2673: elektronischer Widerruf (B2C) – silent solange Flag aus - try { - include_once './core/revocation.class.php'; - $revocation_obj = new Revocation($this->base_object); - $revocation_obj->create_for_order($this->order_id); - } catch (Throwable $e) { - error_log('Revocation hook failed for order '.$this->order_id.': '.$e->getMessage()); - } - - if ($this->order_number) { - // 25.08.2026: Vorgang als erledigt vermerken, damit ein zweiter Klick auf - // denselben Bestellbutton keine weitere Order erzeugt. - $this->bestellvorgang_vermerken(); - - // Ereignisprotokoll in PG -- der einzige Ort mit einer Uhrzeit zur Bestellung. - if (function_exists('shop_event')) { - shop_event('order_created', array( - 'order_id' => $this->order_id, - 'order_number' => $this->order_number, - 'payment_method' => $this->payment_method_id, - 'betrag' => isset($this->shoppingcart_data['order_total']) - ? $this->shoppingcart_data['order_total'] : null, - 'customer_id' => isset($this->customer_data->id) ? $this->customer_data->id : null, - 'detail' => $this->zahlung_erst_spaeter($mail_modul) - ? $mail_modul.': noch nicht bezahlt' : 'sofort verbindlich' - )); - } - - return true; - } - } - - return false; - } - - // end save_order - - private function after_saving_order() { - $this->layout_object->assign('order_number', $this->order_number); - $this->layout_object->assign('order_amount', $this->shoppingcart_data['order_total']); - - // get trusted shop payment id - $trusted_shop_payment = 'OTHER'; - if ($this->payment_method_id == 1) { - $trusted_shop_payment = 'PREPAYMENT'; - } else if ($this->payment_method_id == 2) { - $trusted_shop_payment = 'CASH_ON_DELIVERY'; - } else if ($this->payment_method_id == 3) { - $trusted_shop_payment = 'INVOICE'; - } else if ($this->payment_method_id == 4) { - $trusted_shop_payment = 'PAYPAL'; - } else if ($this->payment_method_id == 6) { - $trusted_shop_payment = 'DIRECT_DEBIT'; - } - $this->layout_object->assign('trusted_shop_payment', $trusted_shop_payment); - - // 24.08.2026: Der Warenkorb wird nur noch geleert, wenn die Bestellung mit dieser - // Antwort wirklich abgeschlossen ist. Bei PayPal faengt das Bezahlen hier erst an - // -- vorher flog der Korb schon raus, und wer abbrach, neu lud oder zurueckging, - // stand ohne Warenkorb da ("ORDER PREVENTED: Empty cart"). PayPal leert jetzt - // selbst, sobald das Geld eingezogen ist (PaypalPaymentMethod::change_order). - // Bewusst nur PayPal: die anderen Module raeumen nicht selbst auf. - $payment_module = isset($this->payment_methods[$this->payment_method_id]->module) - ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) - : ''; - - if (!$this->zahlung_erst_spaeter($payment_module)) { - $shoppingcart_object = new Shoppingcart($this->base_object); - $shoppingcart_object->clear(1); - } - - if ($this->payment_methods[$this->payment_method_id]->module != '') { - $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; - $module_name = ucfirst($this->payment_methods[$this->payment_method_id]->module).'PaymentMethod'; - $file_name = strtolower($module_name).'.class.php'; - - if (file_exists($path.$file_name)) { - include_once('./core/paymentmethods/'.$file_name); - - $payment_method_module = new $module_name($this->base_object, $this->layout_object); - - $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id], $this->order_id); - - if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { - $payment_method_module->set_gift_certificate($this->gift_certificate_value); - } - - return $payment_method_module->after_order($this->shoppingcart_data['order_total']); - } - } - - // REDIRECT statt Template - verhindert Doppel-Bestellungen bei F5 - $order_num = $this->order_number ? $this->order_number : 'unbekannt'; - header('Location: /Kasse/?success=1&order=' . urlencode($order_num)); - exit; - } - - // end after_saving_order - - /** - * Nonce fuer das Kassenformular (25.08.2026). - * - * Ein Wert pro Bestellvorgang, der als hidden field im Formular mitgeht. Nach einer - * erfolgreichen Bestellung wird er verbraucht und ein neuer erzeugt -- ein erneut - * abgeschickter alter Nonce ist damit erkennbar derselbe Vorgang. Das ersetzt die - * Zeitfenster-Pruefung, die auf einer DATE-Spalte nicht funktionieren konnte. - * - * @return string - */ - private function checkout_nonce_holen() { - if (empty($_SESSION['easyshop']['checkout_nonce'])) { - if (function_exists('random_bytes')) { - try { - $_SESSION['easyshop']['checkout_nonce'] = bin2hex(random_bytes(16)); - } catch (Exception $e) { - $_SESSION['easyshop']['checkout_nonce'] = md5(uniqid(mt_rand(), true)); - } - } else { - $_SESSION['easyshop']['checkout_nonce'] = md5(uniqid(mt_rand(), true)); - } - } - - return $_SESSION['easyshop']['checkout_nonce']; - } - - // end checkout_nonce_holen - - - /** - * Wurde dieser Bestellvorgang schon abgeschlossen? (25.08.2026) - * - * @return array|false Daten der bereits angelegten Order, sonst false - */ - private function bestellvorgang_bereits_erledigt() { - $nonce = isset($_POST['checkout_nonce']) ? trim((string)$_POST['checkout_nonce']) : ''; - - if ($nonce !== '' && isset($_SESSION['easyshop']['checkout_done'][$nonce])) { - return $_SESSION['easyshop']['checkout_done'][$nonce]; - } - - // Netz fuer Absendungen ohne Nonce -- alte Seite aus dem Cache, JS abgeschaltet, - // Formular von Hand nachgebaut. Gleiche Session, gleicher Betrag, innerhalb von - // fuenf Minuten. Die Uhrzeit kommt aus der Session und ist damit eine echte - // Uhrzeit, anders als beim alten Guard auf der DATE-Spalte. - if ($nonce === '' && !empty($_SESSION['easyshop']['last_order'])) { - $letzte = $_SESSION['easyshop']['last_order']; - $summe = round((float)(isset($this->shoppingcart_data['order_total']) - ? $this->shoppingcart_data['order_total'] : 0), 2); - - if (abs($summe - (float)$letzte['total']) < 0.01 - && (time() - (int)$letzte['time']) < 300) { - return $letzte; - } - } - - return false; - } - - // end bestellvorgang_bereits_erledigt - - - /** - * Bestellvorgang als erledigt vermerken (25.08.2026). - * - * Laeuft direkt nach der Order-Anlage. Der verbrauchte Nonce bleibt in der Session - * stehen, damit ein spaeter nachgereichter POST mit demselben Wert erkannt wird; fuer - * die naechste Bestellung wird beim Rendern des Formulars ein frischer erzeugt. - */ - private function bestellvorgang_vermerken() { - $nonce = isset($_POST['checkout_nonce']) ? trim((string)$_POST['checkout_nonce']) : ''; - - $eintrag = array( - 'order_id' => $this->order_id, - 'order_number' => $this->order_number, - 'total' => round((float)(isset($this->shoppingcart_data['order_total']) - ? $this->shoppingcart_data['order_total'] : 0), 2), - 'time' => time() - ); - - if ($nonce !== '') { - if (!isset($_SESSION['easyshop']['checkout_done']) - || !is_array($_SESSION['easyshop']['checkout_done'])) { - $_SESSION['easyshop']['checkout_done'] = array(); - } - - $_SESSION['easyshop']['checkout_done'][$nonce] = $eintrag; - - // Nicht unbegrenzt mitschleppen -- die letzten fuenf reichen. - if (count($_SESSION['easyshop']['checkout_done']) > 5) { - $_SESSION['easyshop']['checkout_done'] = - array_slice($_SESSION['easyshop']['checkout_done'], -5, 5, true); - } - - unset($_SESSION['easyshop']['checkout_nonce']); - } - - $_SESSION['easyshop']['last_order'] = $eintrag; - } - - // end bestellvorgang_vermerken - - - /** - * Ist zu dieser Order bereits Geld eingezogen? (24.08.2026) - * - * Entscheidet, ob ein wiederholter Bestell-POST auf den offenen PayPal-Vorgang - * zurueckgefuehrt wird oder als neuer Einkauf durchlaeuft. - * - * @param int $order_id - * @return bool - */ - private function paypal_order_is_paid($order_id) { - $order_id = (int)$order_id; - if ($order_id <= 0) { - return false; - } - - $result = $this->base_object->db->query(" - SELECT - paypal_transaction_id - FROM - orders - WHERE - id = ".$this->base_object->db->real_escape_string($order_id)." - LIMIT 1 - "); - - if ($result && $result->num_rows > 0) { - $row = $result->fetch_object(); - return !empty($row->paypal_transaction_id); - } - - // Order nicht gefunden -> nicht blockieren, normal weiterbestellen lassen. - return false; - } - - // end paypal_order_is_paid - - private function change_order($param) { - $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; - $module_name = ucfirst($param).'PaymentMethod'; - $file_name = strtolower($module_name).'.class.php'; - - if (file_exists($path.$file_name)) { - include_once('./core/paymentmethods/'.$file_name); - - $payment_method_module = new $module_name($this->base_object, $this->layout_object); - - return $payment_method_module->change_order(); - } - } - - // end change_order - - private function manage_guest_payment_method() { - $payment_method_object = new Payment_method($this->base_object); - /* temp: */ $this->shipping_area_id = 1; - $this->payment_methods = $payment_method_object->getMethodsForGroup($this->base_object->config->shopConfiguration['guest_order_customer_group'], $this->shoppingcart_data['price_sum_gross'], $this->shipping_area_id); - if (isset($_POST['payment_method']) && $_POST['payment_method'] > 0) { - $this->payment_method_id = $_POST['payment_method']; - } else { - $this->personal_data['payment_method'] = $this->payment_method_id = current($this->payment_methods)->id; - } - - $this->layout_object->assign('payment_methods', $this->payment_methods); - } - - // end manage_payment_method - - private function manage_guest_shipping_method() { - $shipping_method_object = new Deliverer($this->base_object); - - $shoppingcart_object = new Shoppingcart($this->base_object); - $this->shoppingcart_data = $shoppingcart_object->get_data(); - $this->shipping_methods = $shipping_method_object->getAllActive( - $this->shipping_area_id, $this->shoppingcart_data['standard_shipping_items'], $this->shoppingcart_data['bulk_goods'], $this->shoppingcart_data['item_weight'] - ); - - if (isset($_POST['shipping_method']) && $_POST['shipping_method'] > 0) { - $this->shipping_method_id = $_POST['shipping_method']; - } else { - $this->personal_data['shipping_method'] = $this->shipping_method_id = current($this->shipping_methods)->id; - } - $this->layout_object->assign('deliverers', $this->shipping_methods); - } - -// end manage_shipping_method -} - -// end website_cashdesk - +base_object = $base_object; + $this->layout_object = $layout_object; + $this->errors = array(); + } + + // end __construct + + public function run() { + // SUCCESS-Seite nach REDIRECT - verhindert Doppel-Bestellungen + if (isset($_GET['success']) && $_GET['success'] == '1') { + // Bestellnummer aus URL für Template verfügbar machen + if (isset($_GET['order'])) { + $this->layout_object->assign('order_number', $_GET['order']); + } + // Success-Template anzeigen - auch bei Reload! + return $this->layout_object->_fetch('content_cash_desk/completed.tpl'); + } + + // 13.09.2026 -- Rueckkehr von einem Zahlungsanbieter aus core/payments + // (?payment_return= bzw. ?payment_cancel=). Laeuft hier im Cashdesk, + // weil die Session gebraucht wird (Pending-Riegel, Warenkorb). Steht vor dem + // Kunden-/Login-Check: nach dem Sprung ueber den Anbieter ist kein ?guest_order=1 + // mehr in der URL, ein Gast haette sonst keine customer_id. + if (isset($_GET['payment_return']) || isset($_GET['payment_cancel'])) { + return $this->payment_provider_return(); + } + + // 14.09.2026 -- Express aus dem Warenkorb ("Direkt mit PayPal bezahlen"). + // Start: Vorgang beim Anbieter ohne Bestellung, Kunde geht hin und gibt frei. + // Rueckkehr: Adresse/E-Mail vom Anbieter in die Session, weiter in die Gast-Kasse, + // dort ist alles vorbelegt -- ein Klick auf "zahlungspflichtig bestellen" zieht ein. + if (isset($_GET['payment_express_start'])) { + return $this->payment_express_start(); + } + if (isset($_GET['payment_express'])) { + return $this->payment_express_return(); + } + + // 24.08.2026 -- Zielseite des Post/Redirect/Get aus PaypalPaymentMethod::after_order(). + // Reines GET: hier steht der Link zu PayPal, ein Reload laedt nur noch ihn neu und + // legt keine zweite Bestellung mehr an. + // Steht bewusst vor dem Kunden-/Login-Check: nach dem Redirect ist kein + // ?guest_order=1 mehr in der URL, ein Gast haette hier sonst keine customer_id. + if (isset($_GET['pp_open'])) { + $pp_token = trim((string)$_GET['pp_open']); + + if (preg_match('/^EC-[A-Z0-9]{10,30}$/i', $pp_token)) { + $pp_sandbox = (class_exists('Config') && Config::get_value('paypal_dev_mode') == 1); + $pp_host = $pp_sandbox ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; + + $this->layout_object->assign('call', 'payment_open'); + $this->layout_object->assign('paypal_link', + $pp_host.'/cgi-bin/webscr?cmd=_express-checkout&token='.urlencode($pp_token)); + } else { + $this->layout_object->assign('call', 'payment_error'); + $this->layout_object->assign('message', + 'Der PayPal-Vorgang konnte nicht zugeordnet werden. Ihr Warenkorb ist noch da – bitte starten Sie die Bestellung neu.'); + } + + return $this->layout_object->_fetch('content_cash_desk/paypal.tpl'); + } + + if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { + // guest order + $this->base_object->customer->customer_id = $this->base_object->config->shopConfiguration['guest_order_customer_id']; + $this->layout_object->assign('guest_order', 1); + } + $this->payment_express_prefill(); + $action = ''; + + if ($this->base_object->customer->customer_id) { + if (isset($_POST['cashdesk_action'])) { + $action = $_POST['cashdesk_action']; + } else if (isset($_POST['save_address'])) { + $action = 'save_address'; + } else if (isset($_GET['token'])) { + $action = 'change_order'; + $param = 'paypal'; + } + + $this->personal_data = $_POST; + + if ($action == 'ready2fly') { + // MAKE ORDER + + $this->setup_countries(); + $this->setup_customer_data(); + if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { + $this->manage_guest_order_address(); + } else { + $this->manage_address(); + } + $this->manage_shoppingcart(); + $this->manage_payment_method(); + $this->manage_shipping_method(); + $this->manage_gift_certificate(); + $this->managa_conditions(); + + $this->before_saving_order(); + + + if (count($this->errors) == 0) { + if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { + $this->setup_customer_data(); + // check customer already exists by email + $customer_id = 0;//$this->base_object->customer->check_email($_POST['billing']['email']); + + if (!$customer_id) { + // create new customer for guest order + $customer_data = array ( + 'honorific' => $_POST['billing']['honorific'], + 'company' => $_POST['billing']['company'], + 'firstname' => $_POST['billing']['firstname'], + 'surname' => $_POST['billing']['surname'], + 'phone' => $_POST['billing']['phone'], + 'group_id' => $this->base_object->config->shopConfiguration['guest_order_customer_group'], + 'email' => $_POST['billing']['email'] + ); + $result = $this->base_object->customer->create($customer_data); + } else { + $result = $customer_id; + } + + if ($result) { + $this->customer_data->id = $customer_id = $this->base_object->customer->customer_id = $result; + + include_once './core/customeraddress.class.php'; + $customer_address_object = new CustomerAddress($this->base_object); + $billing_address = array ( + 'honorific' => $_POST['billing']['honorific'], + 'company' => $_POST['billing']['company'], + 'firstname' => $_POST['billing']['firstname'], + 'surname' => $_POST['billing']['surname'], + 'street' => $_POST['billing']['street'], + 'house_number' => $_POST['billing']['house_number'], + 'city' => $_POST['billing']['city'], + 'zip_code' => $_POST['billing']['zip_code'], + 'country' => $_POST['billing']['country'], + 'customer_id' => $customer_id + ); + $res = $customer_address_object->create($billing_address); + $this->billing_address['address'] = $res; + $shipping_address = array ( + 'honorific' => $_POST['shipping']['honorific'], + 'company' => $_POST['shipping']['company'], + 'firstname' => $_POST['shipping']['firstname'], + 'surname' => $_POST['shipping']['surname'], + 'street' => $_POST['shipping']['street'], + 'house_number' => $_POST['shipping']['house_number'], + 'city' => $_POST['shipping']['city'], + 'zip_code' => $_POST['shipping']['zip_code'], + 'country' => $_POST['shipping']['country'], + 'customer_id' => $customer_id + ); + + $res = $customer_address_object->create($shipping_address); + $this->shipping_address['address'] = $res; + + } + } + + if (isset($_POST['billing']['email'])) { + $data = array( + 'id' => $this->base_object->customer->customer_id, + 'email' => $_POST['billing']['email'] + ); + $this->base_object->customer->save($data); + } + + // 25.08.2026 -- Stufe 4: ein Bestellvorgang, eine Order. + // Traegt dieser POST einen Nonce, der schon zu einer Bestellung gefuehrt + // hat, ist es derselbe Vorgang -- egal ob eine Sekunde oder eine Stunde + // vergangen ist. Dann zurueck auf das Ergebnis von damals, statt ein + // zweites Mal zu bestellen. + $erledigt = $this->bestellvorgang_bereits_erledigt(); + + if ($erledigt) { + $offen = isset($_SESSION['easyshop']['paypal_pending']) + ? $_SESSION['easyshop']['paypal_pending'] : null; + + // Laeuft zu dieser Order noch ein unbezahlter PayPal-Vorgang, + // gehoert der Kunde dorthin und nicht auf die Erfolgsseite. + if ($offen && !empty($offen['token']) + && (int)$offen['order_id'] === (int)$erledigt['order_id'] + && !$this->paypal_order_is_paid($erledigt['order_id'])) { + + error_log('Checkout: Vorgang '.$erledigt['order_number'] + . ' bereits bestellt, zurueck auf offenen PayPal-Vorgang ' + . $offen['token']); + header('Location: /Kasse/?pp_open='.urlencode($offen['token'])); + exit; + } + + // 13.09.2026: dasselbe fuer Anbieter aus core/payments -- zurueck zur + // Approve-URL des offenen Vorgangs statt einer zweiten Bestellung. + $offen_pv = $this->payment_provider_pending(); + if ($offen_pv && (int)$offen_pv['order_id'] === (int)$erledigt['order_id']) { + error_log('Checkout: Vorgang '.$erledigt['order_number'] + . ' bereits bestellt, zurueck auf offenen '.$offen_pv['module'].'-Vorgang'); + header('Location: '.$offen_pv['approve_url']); + exit; + } + + error_log('Checkout: Vorgang bereits bestellt (Order ' + . $erledigt['order_number'].') -- keine zweite Order angelegt'); + + if (function_exists('shop_event')) { + shop_event('duplicate_blocked', array( + 'order_id' => $erledigt['order_id'], + 'order_number' => $erledigt['order_number'], + 'betrag' => $erledigt['total'], + 'detail' => 'zweiter Bestell-POST desselben Vorgangs' + )); + } + + header('Location: /Kasse/?success=1&order='.urlencode($erledigt['order_number'])); + exit; + } + + // 24.08.2026 -- zweiter Riegel gegen Doppelbestellungen bei PayPal. + // Kommt trotz PRG noch einmal ein ready2fly-POST (Zurueck-Taste, + // zweiter Tab, ungeduldiges Klicken) und liegt zu dieser Session + // bereits ein PayPal-Vorgang ueber denselben Betrag offen, der noch + // nicht bezahlt ist: zurueck auf dessen Link, statt eine zweite Order + // anzulegen. Ist der Vorgang bezahlt oder aelter als eine Stunde, + // laeuft alles normal weiter -- dann ist es ein neuer Einkauf. + $pending = isset($_SESSION['easyshop']['paypal_pending']) + ? $_SESSION['easyshop']['paypal_pending'] : null; + + $pending_module = isset($this->payment_methods[$this->payment_method_id]->module) + ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) + : ''; + + if ($pending && !empty($pending['token']) + && $pending_module === 'paypal' + && abs(round((float)$this->shoppingcart_data['order_total'], 2) - (float)$pending['total']) < 0.01 + && (time() - (int)(isset($pending['time']) ? $pending['time'] : 0)) < 3600 + && !$this->paypal_order_is_paid($pending['order_id'])) { + + error_log("PayPal Fix: doppelter ready2fly-POST abgefangen, zurueck auf offenen Vorgang {$pending['token']}"); + header('Location: /Kasse/?pp_open='.urlencode($pending['token'])); + exit; + } + + // 13.09.2026: derselbe Riegel fuer Anbieter aus core/payments. Der offene + // Vorgang liegt in $_SESSION['easyshop']['payment_pending'] (gesetzt vom + // Provider beim Start) -- gleiche Zahlart, gleicher Betrag, juenger als + // eine Stunde, nicht bezahlt: zurueck zur Approve-URL. + $pending_pv = $this->payment_provider_pending(); + if ($pending_pv + && $pending_pv['module'] === $pending_module + && abs(round((float)$this->shoppingcart_data['order_total'], 2) - (float)$pending_pv['total']) < 0.01) { + + error_log('Payments: doppelter ready2fly-POST abgefangen, zurueck auf offenen ' + . $pending_pv['module'].'-Vorgang '.$pending_pv['reference']); + header('Location: '.$pending_pv['approve_url']); + exit; + } + + $is_saved = $this->save_order(); + + if ($is_saved) { + return $this->after_saving_order(); + } + } + } else if ($action == 'change_order') { + return $this->change_order($param); + } else if ($action == 'save_address') { + // SAVE ADDRESS + $this->setup_customer_data(); + $this->save_address(); + + } else { + // SHOW ORDER + + $this->setup_countries(); + $this->setup_customer_data(); + if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) { + $this->manage_guest_order_address(); + } else { + $this->manage_address(); + } + $this->manage_shoppingcart(); + $this->manage_payment_method(); + $this->manage_shipping_method(); + $this->manage_gift_certificate(); + $this->managa_conditions(); + } + //echo var_dump($this->personal_data); + $this->manage_errors(); + $this->manage_data_persistence(); + + // customer info + if (isset($_SESSION['customer_info']) && $_SESSION['customer_info']) { + $this->layout_object->assign('customer_info', $_SESSION['customer_info']); + } + + if (!$this->base_object->config->shopConfiguration['menu_in_cashdesk']) { + $this->layout_object->assign('site_type', 'full_width'); + } + + // 25.08.2026: Nonce fuers Kassenformular -- macht einen zweiten Klick auf + // "Bestellen" als denselben Vorgang erkennbar (siehe save_order()). + $this->layout_object->assign('checkout_nonce', $this->checkout_nonce_holen()); + + return $this->layout_object->_fetch('content_cash_desk.tpl'); + + } else if (isset($_GET['token']) && $_GET['token']) { + return $this->change_order('paypal'); + } else { + // login + $_SESSION['easyshop']['shopAction'] = 'cash_desk_customer_registration'; + + $this->layout_object->assign('go_to_cash_desk', 1); + return $this->layout_object->_fetch('content_login.tpl'); + } + } + + // end run + + // ------------------------------------------------------------ Express + + /** Start aus dem Warenkorb: /Kasse/?payment_express_start= */ + private function payment_express_start() { + $module = strtolower(trim((string)$_GET['payment_express_start'])); + $provider = $this->payment_provider($module); + if (!$provider || !$provider->supportsExpress()) { + header('Location: /Warenkorb/'); + exit; + } + $shoppingcart_object = new Shoppingcart($this->base_object); + $cart = $shoppingcart_object->get_data(); + if (empty($cart['items']) || (float)$cart['price_sum_gross'] <= 0) { + header('Location: /Warenkorb/'); + exit; + } + try { + $url = $provider->startExpress(round((float)$cart['price_sum_gross'], 2)); + } catch (Throwable $e) { + error_log('Payments express start '.$module.': '.$e->getMessage()); + $this->layout_object->assign('call', 'payment_error'); + $this->layout_object->assign('provider_label', $provider->label()); + $this->layout_object->assign('message', $provider->label().' konnte nicht gestartet werden. Ihr Warenkorb ist noch da – bitte gehen Sie normal zur Kasse.'); + return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); + } + header('Location: '.$url); + exit; + } + + /** Rueckkehr im Express-Weg: /Kasse/?payment_express=&token=... */ + private function payment_express_return() { + $module = strtolower(trim((string)$_GET['payment_express'])); + $provider = $this->payment_provider($module); + $data = null; + // 18.09.2026 -- zweite Freigabe: PayPal Live verlangt nach dem Betrags-PATCH (Versand) + // eine erneute Freigabe, der Kunde kommt danach ueber DIESE URL zurueck. Die Bestellung + // und der Vorgang existieren dann schon -> normaler Rueckweg mit Capture, nicht noch + // einmal die Kasse vorbelegen (custom_id ist da auch nicht mehr 'express'). + if ($provider && $provider->knownReference((string)($_GET['token'] ?? ''))) { + $_GET['payment_return'] = $module; + return $this->payment_provider_return(); + } + if ($provider && $provider->supportsExpress()) { + try { + $data = $provider->onExpressReturn($_GET); + } catch (Throwable $e) { + error_log('Payments express return '.$module.': '.$e->getMessage()); + } + } + if (!$data) { + $this->layout_object->assign('call', 'payment_error'); + $this->layout_object->assign('provider_label', $provider ? $provider->label() : 'Zahlung'); + $this->layout_object->assign('message', 'Der Express-Vorgang konnte nicht übernommen werden. Ihr Warenkorb ist noch da – bitte gehen Sie normal zur Kasse.'); + return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); + } + // Erlaubte Laender fuer Express (Setting _express_countries, Standard DE): + // PayPal-Konten gibt es weltweit, geliefert wird aber nur, wohin der Shop liefert. + $allowed = array_filter(array_map('trim', explode(',', strtoupper($provider->expressCountries())))); + $iso = strtoupper((string)$data['country_iso']); + $country_id = in_array($iso, $allowed, true) ? $provider->countryIdByIso($iso) : 0; + if ($country_id <= 0) { + $this->layout_object->assign('call', 'payment_error'); + $this->layout_object->assign('provider_label', $provider->label()); + $this->layout_object->assign('message', 'In das bei '.$provider->label().' hinterlegte Land ('.htmlspecialchars((string)$data['country_iso']).') liefern wir leider nicht. Bitte gehen Sie normal zur Kasse und wählen Sie eine Lieferadresse.'); + return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); + } + $data['module'] = $module; + $data['country_id'] = $country_id; + $data['time'] = time(); + $_SESSION['easyshop']['payment_express'] = $data; + + // Eingeloggter Kunde: eigene Kasse mit eigenen Adressen, nur Zahlart vorbelegt. + $logged_in = $this->base_object->customer->customer_id + && (int)$this->base_object->customer->customer_id !== (int)$this->base_object->config->shopConfiguration['guest_order_customer_id']; + header('Location: /Kasse/'.($logged_in ? '' : '?guest_order=1')); + exit; + } + + /** + * Kasse mit den Express-Daten vorbelegen (Gast: Adresse + E-Mail + Zahlart, + * eingeloggt: nur Zahlart). Nur beim reinen Anzeigen -- ein POST des Kunden gewinnt. + */ + private function payment_express_prefill() { + $e = isset($_SESSION['easyshop']['payment_express']) ? $_SESSION['easyshop']['payment_express'] : null; + if (!is_array($e) || empty($e['module']) || empty($e['reference'])) { + return; + } + if ((time() - (int)$e['time']) > 3600) { + unset($_SESSION['easyshop']['payment_express']); + return; + } + $pm_id = 0; + $res = $this->base_object->db->query("SELECT id FROM payment_methods WHERE module='".$this->base_object->db->real_escape_string($e['module'])."' AND active=1 LIMIT 1"); + if ($res && ($r = $res->fetch_object())) { + $pm_id = (int)$r->id; + } + $this->layout_object->assign('express_active', 1); + $this->layout_object->assign('express_label', ucfirst(str_replace('_', ' ', $e['module']))); + + if (isset($_POST['cashdesk_action']) || isset($_POST['save_address'])) { + return; // Kunde hat das Formular selbst abgeschickt + } + if ($pm_id) { + $_POST['payment_method'] = $pm_id; + } + $is_guest = isset($_GET['guest_order']) && $_GET['guest_order'] == 1; + if (!$is_guest) { + return; + } + $addr = array( + 'honorific' => '', + 'company' => (string)$e['company'], + 'firstname' => (string)$e['firstname'], + 'surname' => (string)$e['surname'], + 'street' => (string)$e['street'], + 'house_number' => (string)$e['house_number'], + 'zip_code' => (string)$e['zip_code'], + 'city' => (string)$e['city'], + 'country' => (int)$e['country_id'], + ); + $_POST['billing'] = $addr + array('email' => (string)$e['email'], 'address' => '', 'billing_address' => ''); + $_POST['shipping'] = $addr + array('address' => -1); // -1 = wie Rechnungsadresse + $this->personal_data = $_POST; + } + + /** + * 13.09.2026 -- Zahlungsanbieter aus core/payments. + * + * Bei diesen Zahlarten (und beim alten PayPal) ist die Bestellung erst nach der + * Rueckkehr vom Anbieter bezahlt: Bestellbestaetigung und Warenkorb-Leerung warten + * bis dahin. Alle anderen Zahlarten sind mit dem Abschicken verbindlich. + */ + private function zahlung_erst_spaeter($module) { + $module = strtolower((string)$module); + if ($module === 'paypal') { + return true; // Legacy-NVP-PayPal, raeumt in PaypalPaymentMethod::change_order() selbst auf + } + $provider = $this->payment_provider($module); + return $provider ? $provider->paysAfterRedirect() : false; + } + + /** @return \Payments\ProviderInterface|null */ + private function payment_provider($module) { + if (!class_exists('\Payments\Registry') || !\Payments\Registry::has((string)$module)) { + return null; + } + try { + return \Payments\Registry::get((string)$module, $this->base_object); + } catch (Throwable $e) { + error_log('Payments: Provider '.$module.' nicht ladbar: '.$e->getMessage()); + return null; + } + } + + /** + * Offener, unbezahlter Vorgang eines Anbieters in dieser Session (juenger als 1 h), + * sonst null. Grundlage der Riegel gegen Doppelbestellungen. + */ + private function payment_provider_pending() { + $p = isset($_SESSION['easyshop']['payment_pending']) ? $_SESSION['easyshop']['payment_pending'] : null; + if (!is_array($p) || empty($p['module']) || empty($p['approve_url']) || empty($p['order_id'])) { + return null; + } + if ((time() - (int)(isset($p['time']) ? $p['time'] : 0)) >= 3600) { + unset($_SESSION['easyshop']['payment_pending']); + return null; + } + $provider = $this->payment_provider($p['module']); + if (!$provider || $provider->isPaid((int)$p['order_id'])) { + unset($_SESSION['easyshop']['payment_pending']); + return null; + } + return $p; + } + + /** + * Rueckkehr vom Anbieter (?payment_return= | ?payment_cancel=). + * Bezahlt -> Redirect auf die Erfolgsseite (F5-sicher). Sonst Hinweisseite, der + * Warenkorb bleibt in jedem Fall stehen. + */ + private function payment_provider_return() { + $is_cancel = isset($_GET['payment_cancel']); + $module = strtolower(trim((string)($is_cancel ? $_GET['payment_cancel'] : $_GET['payment_return']))); + $provider = $this->payment_provider($module); + + if (!$provider) { + $this->layout_object->assign('call', 'payment_error'); + $this->layout_object->assign('message', 'Die Zahlungsart konnte nicht zugeordnet werden. Ihr Warenkorb ist noch da.'); + return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); + } + + try { + $result = $is_cancel ? $provider->onCancel($_GET) : $provider->onReturn($_GET); + } catch (Throwable $e) { + error_log('Payments: '.$module.' return/cancel: '.$e->getMessage()); + $result = \Payments\Result::error('Bei der Rueckmeldung des Zahlungsanbieters ist ein Fehler aufgetreten. Ihr Warenkorb ist noch da.'); + } + + if ($result->status === \Payments\Result::PAID) { + header('Location: /Kasse/?success=1&order='.urlencode($result->orderNumber)); + exit; + } + + $this->layout_object->assign('provider_label', $provider->label()); + $this->layout_object->assign('order_number', $result->orderNumber); + $this->layout_object->assign('message', $result->message); + $this->layout_object->assign('call', + $result->status === \Payments\Result::CANCELLED ? 'payment_cancelled' + : ($result->status === \Payments\Result::PENDING ? 'payment_pending' : 'payment_error')); + return $this->layout_object->_fetch('content_cash_desk/payment_result.tpl'); + } + + private function setup_countries() { + $country_object = new Country($this->base_object); + + $this->countries = $country_object->get_all(); + + $this->layout_object->assign('countries', $this->countries); + } + + // end setup_countries + + private function setup_customer_data() { + $customer_id = $this->base_object->customer->customer_id; + + $customer_object = new Customer($this->base_object); + + $this->customer_data = $customer_object->get_by_id($customer_id); + + // customer data is assigned in manage_address to take care of potential missing default address data + + $this->layout_object->assign('shoppingcartVATUsage', $this->base_object->customer_group->show_tax); + $this->layout_object->assign('show_tax', $this->base_object->customer_group->show_tax); + $this->layout_object->assign('customer_data', $this->customer_data); + } + + // end setup_customer_data + + private function manage_guest_order_address() { + $address_object = new CustomerAddress($this->base_object); + + $address_object->set_customer_id($this->customer_data->id); + $this->customer_addresses = $address_object->get_all(); + + if (!isset($this->customer_addresses[$this->customer_data->default_address])) { + $new_default = current($this->customer_addresses); + $this->customer_data->default_address_country_id = $new_default->country; + $this->customer_data->default_address_countryname = $this->countries[$new_default->country]->name; + } + + if (isset($_POST['shipping']) && $_POST['shipping']['address'] == -1) { + $this->shipping_address = $_POST['billing']; + $_POST['shipping'] = $_POST['billing']; + } else if (isset($_POST['shipping'])) { + if (!$this->shipping_address['country']) { + $_POST['shipping']['country'] = $this->customer_data->default_address_country_id; + $this->layout_object->assign('default_address_country_id', $this->customer_data->default_address_country_id); + } + $this->shipping_address = $_POST['shipping']; + } else { + $this->shipping_address['country'] = $this->customer_data->default_address_country_id; + } + + $this->layout_object->assign('shipping_address', $this->shipping_address); + + if (isset($_POST['billing'])) { + $this->billing_address = $_POST['billing']; + } else { + $this->billing_address['country'] = $this->customer_data->default_address_country_id; + } + + $this->layout_object->assign('billing_address', $this->billing_address); + + $this->layout_object->assign('customer', $this->customer_data); + $shipping_country_id = $this->shipping_address['country']; + + $this->shipping_area_id = Country::getShippingAreaById($shipping_country_id, $this->customer_data->group_id); + + $this->layout_object->assign('addresses', $this->customer_addresses); + } // end manage_address + + private function manage_address() { + $address_object = new CustomerAddress($this->base_object); + + $address_object->set_customer_id($this->customer_data->id); + $this->customer_addresses = $address_object->get_all(); + + + // hack for missing default address + if (is_array($this->customer_addresses) && !isset($this->customer_addresses[$this->customer_data->default_address])) { + $new_default = current($this->customer_addresses); + + $this->customer_data->default_address = $new_default->id; + $this->customer_data->default_address_company = $new_default->company; + $this->customer_data->default_address_honorific = $new_default->honorific; + $this->customer_data->default_address_firstname = $new_default->firstname; + $this->customer_data->default_address_surname = $new_default->surname; + $this->customer_data->default_address_street = $new_default->street; + $this->customer_data->default_address_house_number = $new_default->house_number; + $this->customer_data->default_address_zip_code = $new_default->zip_code; + $this->customer_data->default_address_city = $new_default->city; + $this->customer_data->default_address_country_id = $new_default->country; + $this->customer_data->default_address_countryname = $this->countries[$new_default->country]->name; + } + + if (isset($_POST['shipping']['address']) && $_POST['shipping']['address'] == -1) { + $this->shipping_address = $_POST['billing']; + } else if (isset($_POST['shipping'])) { + $this->shipping_address = $_POST['shipping']; + } else { + $this->shipping_address['address'] = $this->customer_data->default_address; + $this->shipping_address['honorific'] = $this->customer_data->default_address_honorific; + $this->shipping_address['firstname'] = $this->customer_data->default_address_firstname; + $this->shipping_address['surname'] = $this->customer_data->default_address_surname; + $this->shipping_address['company'] = $this->customer_data->default_address_company; + $this->shipping_address['street'] = $this->customer_data->default_address_street; + $this->shipping_address['house_number'] = $this->customer_data->default_address_house_number; + $this->shipping_address['zip_code'] = $this->customer_data->default_address_zip_code; + $this->shipping_address['city'] = $this->customer_data->default_address_city; + $this->shipping_address['country'] = $this->customer_data->default_address_country_id; + } + + if (isset($_POST['billing'])) { + $this->billing_address = $_POST['billing']; + } else { + $this->billing_address['address'] = $this->customer_data->default_address; + $this->billing_address['honorific'] = $this->customer_data->default_address_honorific; + $this->billing_address['firstname'] = $this->customer_data->default_address_firstname; + $this->billing_address['surname'] = $this->customer_data->default_address_surname; + $this->billing_address['company'] = $this->customer_data->default_address_company; + $this->billing_address['street'] = $this->customer_data->default_address_street; + $this->billing_address['house_number'] = $this->customer_data->default_address_house_number; + $this->billing_address['zip_code'] = $this->customer_data->default_address_zip_code; + $this->billing_address['city'] = $this->customer_data->default_address_city; + $this->billing_address['country'] = $this->customer_data->default_address_country_id; + } + $billing_country_id = $this->billing_address['country']; + + $this->layout_object->assign('customer', $this->customer_data); + + $shipping_country_id = $this->shipping_address['country']; + $this->shipping_area_id = Country::getShippingAreaById($shipping_country_id, $this->customer_data->group_id); + + $this->layout_object->assign('addresses', $this->customer_addresses); + } + + // end manage_address + + private function manage_shoppingcart() { + $shoppingcart_object = new Shoppingcart($this->base_object); + $this->shoppingcart_data = $shoppingcart_object->get_data(); + + // can't show shoppingcart data here, gift certificates can change the data + } + + // end manage_shoppingcart + + private function manage_payment_method() { + $payment_method_object = new Payment_method($this->base_object); + + $this->payment_methods = $payment_method_object->getMethodsForGroup($this->customer_data->group_id, $this->shoppingcart_data['price_sum_gross'], $this->shipping_area_id); + if (isset($_POST['payment_method']) && $_POST['payment_method'] > 0) { + $this->payment_method_id = $_POST['payment_method']; + } else { + //$this->personal_data['payment_method'] = $this->payment_method_id = current($this->payment_methods)->id; + foreach ($this->payment_methods as $pm) { + if ($pm->activeType > 0) { + $this->personal_data['payment_method'] = $this->payment_method_id = $pm->id; + break; + } + } + } + + $this->layout_object->assign('payment_methods', $this->payment_methods); + } + + // end manage_payment_method + + private function manage_shipping_method() { + $shipping_method_object = new Deliverer($this->base_object); + + $shoppingcart_object = new Shoppingcart($this->base_object); + $this->shoppingcart_data = $shoppingcart_object->get_data(); + + $this->shipping_methods = $shipping_method_object->getAllActive( + $this->shipping_area_id, $this->shoppingcart_data['standard_shipping_items'], $this->shoppingcart_data['bulk_goods'], $this->shoppingcart_data['item_weight'], $this->shoppingcart_data['price_sum_gross'] + ); + + if (isset($_POST['shipping_method']) && $_POST['shipping_method'] > 0) { + foreach ($this->shipping_methods as $sm) { + if ($sm->id == $_POST['shipping_method']) { + $this->shipping_method_id = $_POST['shipping_method']; + } + } + } + + if (!$this->shipping_method_id) { + $methods = $this->shipping_methods; + $method = array_shift($methods); + $this->personal_data['shipping_method'] = $this->shipping_method_id = $method->id; + } + $this->layout_object->assign('deliverers', $this->shipping_methods); + } + + // end manage_shipping_method + + private function manage_gift_certificate() { + $this->shoppingcart_data['order_total'] = $this->shoppingcart_data['price_sum_gross'] + $this->payment_methods[$this->payment_method_id]->paymentAdd + $this->shipping_methods[$this->shipping_method_id]->price_add; + + $this->shoppingcart_data['payment_method_vat'] = $this->payment_methods[$this->payment_method_id]->paymentAddVat; + $this->shoppingcart_data['shipping_method_vat'] = $this->shipping_methods[$this->shipping_method_id]->price_add_vat; + + if (isset($_POST['giftcertificate']['gift_certificat_code']) && $_POST['giftcertificate']['gift_certificat_code'] != '') { + $this->has_gift_certificate = true; + + $this->gift_certificate_code = $_POST['giftcertificate']['gift_certificat_code']; + $this->layout_object->assign('giftcode', $this->gift_certificate_code); + $this->gift_certificate_is_valid = GiftCertificateHelper::code_is_valide($this->customer_data->id, $this->gift_certificate_code, $this->shoppingcart_data['price_sum_gross']); + + if ($this->gift_certificate_is_valid) { + $this->shoppingcart_data['gift_certificate_code'] = $this->gift_certificate_code; + $this->gift_certificate = GiftCertificateHelper::get_certificate_by_code($this->gift_certificate_code); + + $this->gift_certificate_value = GiftCertificateHelper::get_certificate_value($this->gift_certificate_code, $this->shoppingcart_data['price_sum_gross'], $this->customer_data->group_id); + + $this->layout_object->assign('giftcertificatevalue', $this->gift_certificate_value->with_vat); + $this->layout_object->assign('giftcertificatmessage', "Code akzeptiert."); + + // calculate the part of the gift certificate for each tax rate + $part = $this->gift_certificate_value->with_vat / $this->shoppingcart_data['price_sum_gross']; + } else { + $this->gift_certificate_error = GiftCertificateHelper::get_error_text(); + + $this->layout_object->assign('gftcertificateerrormessage', $this->gift_certificate_error); + $this->layout_object->assign('giftcertificatmessage', 'Code wird nicht akzeptiert.'); + } + } else { + $this->has_gift_certificate = false; + } + + foreach ($this->shoppingcart_data['tax_values'] as $key => $value) { + $this->shoppingcart_data['tax_values'][$key]['gift']['percent'] = $percent = round(($value['gross'] / $this->shoppingcart_data['price_sum_gross'] * 100), 2); + $this->shoppingcart_data['tax_values'][$key]['gift']['gross'] = $new_gross = $this->shoppingcart_data['price_sum_gross'] / 100 * $percent; + $this->shoppingcart_data['tax_values'][$key]['gift']['net'] = $new_net = $new_gross / (100 + $key) * 100; + $this->shoppingcart_data['tax_values'][$key]['gift']['vat'] = $new_gross - $new_net; + if ($this->base_object->customer_group->show_tax) { + $this->shoppingcart_data['tax_values'][$key]['gift']['show'] = $new_gross; + } else { + $this->shoppingcart_data['tax_values'][$key]['gift']['show'] = $new_net; + } + } + + // TODO: HACK, generalize this + if (isset($this->payment_methods[9])) { + if ((isset($this->base_object->config->shopConfiguration['klarna_eid']) && + $this->base_object->config->shopConfiguration['klarna_eid'] != '') && + (isset($this->base_object->config->shopConfiguration['klarna_secret']) && + $this->base_object->config->shopConfiguration['klarna_secret'] != '')) { + include_once('./core/shop_klarna.class.php'); + + // klarna installments + $klarna_object = new Shop_klarna($this->base_object); + if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { + $klarna_installments = $klarna_object->get_installments($this->shoppingcart_data['order_total'] - $this->gift_certificate_value->with_vat); + } else { + $klarna_installments = $klarna_object->get_installments($this->shoppingcart_data['order_total']); + } + $this->layout_object->assign('klarna_installments', $klarna_installments); + } + } + + // since the gift certificate can change the cart data this must be done here + $this->layout_object->assign('shoppingcart', $this->shoppingcart_data); + } + + // end manage_gift_certificate + + private function managa_conditions() { + $site_content_object = new SiteContent($this->base_object); + + $agb_data = array(); + // Datenschutz: aktuelle Version aus struct 10737 (DSGVO-Update). Alte struct 55 (id 50) ist veraltet. + $agb_data['datenschutz'] = $site_content_object->get_textbox_data_by_id(10737); + $agb_data['agbs'] = $site_content_object->get_textbox_data_by_id(56); + $agb_data['agbs_b2b'] = $site_content_object->get_textbox_data_by_id(10632); + $agb_data['widerruf'] = $site_content_object->get_textbox_data_by_id(60); + + $this->layout_object->assign('agb_data', $agb_data); + } + + // end manage_conditions + + private function manage_errors() { + $this->layout_object->assign('error_message', $this->errors); + } + + // end manage_errors + + private function manage_data_persistence() { + // ugly hack for data persistence + $this->layout_object->assign('personal_data', $this->personal_data); + } + + // end manage_data_persistence + + private function save_address() { + $address = $_POST; + unset($address['save_address']); + $address_id = false; + if ($address['address'] != '') { + $address_id = $address['address']; + } + unset($address['address']); + $address['customer_id'] = $this->customer_data->id; + + $customer_address_object = new CustomerAddress($this->base_object); + + $ret = array(); + if ($customer_address_object->validate($address)) { + if ($address_id) { + $customer_address_object->update($address, $address_id); + } else { + $address_id = $customer_address_object->create($address); + } + + $ret['id'] = $address_id; + $ret['stat'] = 'OK'; + } else { + $ret['stat'] = 'ERR'; + $ret['data'] = $customer_address_object->get_validation_errors(); + } + + echo json_encode($ret); + exit(); + } + + // end save_address + + private function before_saving_order() { + // check order + if (!isset($_GET['guest_order']) || $_GET['guest_order'] != 1) { + if (!is_numeric($_POST['billing']['address'])) { + $this->errors[] = 'Bitte wählen Sie eine Rechnungsadresse'; + } + + if (!is_numeric($_POST['shipping']['address'])) { + $this->errors[] = 'Bitte wählen Sie eine Lieferadresse'; + } + } + + if (!is_numeric($_POST['payment_method'])) { + $this->errors[] = 'Bitte wählen Sie eine Zahlungsart'; + } else { + $payment_method_id = $_POST['payment_method']; + + if ($this->payment_methods[$payment_method_id]->module != '') { + $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; + $module_name = ucfirst($this->payment_methods[$payment_method_id]->module).'PaymentMethod'; + $file_name = strtolower($module_name).'.class.php'; + + if (file_exists($path.$file_name)) { + include_once('./core/paymentmethods/'.$file_name); + + $payment_method_module = new $module_name($this->base_object, $this->layout_object); + + $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id]); + + if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { + $payment_method_module->set_gift_certificate($this->gift_certificate_value); + } + + if ($this->payment_methods[$payment_method_id]->argument != '') { + $return = $payment_method_module->before_order($this->payment_methods[$payment_method_id]->argument); + } else { + $return = $payment_method_module->before_order(); + } + + if ($return) { + $this->errors[] = $return; + } + } + } + } + + if (!is_numeric($_POST['shipping_method'])) { + $this->errors[] = 'Bitte wählen Sie eine Versandart'; + } + + if (!is_numeric($_POST['agreements_accepted'])) { + $this->errors[] = 'Bitte akzeptieren Sie unsere Bedingungen'; + } + } + + // end before_saving_order + + private function save_order() { + // 25.08.2026 -- FIX 1 ersetzt (Stufe 4). + // + // Hier stand eine Duplikatspruefung, die auf + // order_date >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) + // beruhte. `orders.order_date` ist aber Typ DATE, nicht DATETIME -- MySQL macht aus + // dem Datum '2026-08-25 00:00:00', und damit war die Bedingung fuer den GANZEN TAG + // wahr statt fuer fuenf Minuten. Wer am selben Tag ein zweites Mal bestellte, bekam + // seine ERSTE Bestellnummer als "erfolgreich" angezeigt, und der neue Warenkorb + // wurde nie bestellt. Ein stiller Verlust, der niemandem auffiel. + // + // Reparieren liess sich das nicht: die Tabelle `orders` hat ueberhaupt keine Spalte + // mit Uhrzeit (alle Datumsfelder sind DATE), und `order_history.timestamp` wird bei + // der Bestellanlage nicht gefuellt. Es gibt also keinen Zeitstempel, auf den man + // eine Fuenf-Minuten-Regel stuetzen koennte. + // + // Statt der Uhr entscheidet jetzt der Vorgang: ein checkout_nonce pro Kassenformular + // (siehe run() und checkout_nonce_holen()). Wird derselbe Nonce ein zweites Mal + // abgeschickt, ist es derselbe Bestellvorgang -- unabhaengig davon, wie viel Zeit + // vergangen ist. Als Netz fuer Absendungen ohne Nonce dient das Zeitfenster in + // $_SESSION['easyshop']['last_order'], das eine echte Uhrzeit fuehrt. + // + // Der Empty-Cart-Schutz weiter unten (FIX 2) bleibt unveraendert. + + // 🚀 FIX 2: Verhindere 0€ Bestellungen ohne Items (Race Condition / Session Loss) + if (empty($this->shoppingcart_data) || + !isset($this->shoppingcart_data['items']) || + empty($this->shoppingcart_data['items']) || + !isset($this->shoppingcart_data['amount_sum']) || + intval($this->shoppingcart_data['amount_sum']) <= 0) { + + error_log("ORDER PREVENTED: Empty cart or 0€ order attempted - Customer: " . + ($this->customer_data->id ?? 'unknown') . + ", Cart Items: " . (count($this->shoppingcart_data['items'] ?? [])) . + ", Amount Sum: " . ($this->shoppingcart_data['amount_sum'] ?? '0')); + + $this->errors[] = 'Ihr Warenkorb ist leer. Bitte fügen Sie Artikel hinzu.'; + return false; + } + + if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { + $orderDataObject = OrderHelper::createOrderObject(array( + 'customerId' => $this->customer_data->id, + 'cartId' => session_id(), + 'billingAddressId' => $this->billing_address['address'], + 'shippingAddressId' => $this->shipping_address['address'], + 'paymentMethodId' => $this->payment_method_id, + 'shippingMethodId' => $this->shipping_method_id, + 'giftCertificate' => $this->gift_certificate_code + ), $this->base_object); + } else { + $orderDataObject = OrderHelper::createOrderObject(array( + 'customerId' => $this->base_object->customer->customer_id, + 'cartId' => session_id(), + 'billingAddressId' => $this->billing_address['address'], + 'shippingAddressId' => $this->shipping_address['address'], + 'paymentMethodId' => $this->payment_method_id, + 'shippingMethodId' => $this->shipping_method_id + ), $this->base_object); + } + + // TODO : HACK + // fix up paymemt method with ALL the data + $orderDataObject->payment->methodId = $this->payment_method_id; + $orderDataObject->payment->areaId = $this->shipping_area_id; + $orderDataObject->payment->priceGross = $this->payment_methods[$this->payment_method_id]->paymentAdd; + $orderDataObject->payment->priceNet = $this->payment_methods[$this->payment_method_id]->paymentAddNet; + $orderDataObject->payment->priceVat = $this->payment_methods[$this->payment_method_id]->paymentAddVat; + $orderDataObject->payment->vatRate = '19.00'; + + // TODO : HACK + // fix up shipping method with ALL the data + $orderDataObject->shipping->methodId = $this->shipping_method_id; + $orderDataObject->shipping->areaId = $this->shipping_area_id; + $orderDataObject->shipping->priceGross = $this->shipping_methods[$this->shipping_method_id]->price_add; + $orderDataObject->shipping->priceNet = $this->shipping_methods[$this->shipping_method_id]->price_add_net; + $orderDataObject->shipping->priceVat = $this->shipping_methods[$this->shipping_method_id]->price_add_vat; + $orderDataObject->shipping->vatRate = '19.00'; + + if (isset($_POST['customer_info']) && $_POST['customer_info'] != '') { + $orderDataObject->customer_info = $_POST['customer_info']; + } + + if ($this->payment_methods[$this->payment_method_id]->module != '') { + $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; + $module_name = ucfirst($this->payment_methods[$this->payment_method_id]->module).'PaymentMethod'; + $file_name = strtolower($module_name).'.class.php'; + + if (file_exists($path.$file_name)) { + include_once('./core/paymentmethods/'.$file_name); + + $payment_method_module = new $module_name($this->base_object, $this->layout_object); + + $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id]); + + if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { + $payment_method_module->set_gift_certificate($this->gift_certificate_value); + } + + + if ($this->payment_methods[$this->payment_method_id]->argument != '') { + $return = $payment_method_module->during_order($this->payment_methods[$this->payment_method_id]->argument); + } else { + $return = $payment_method_module->during_order(); + } + + if ($return) { + foreach ($return as $key => $value) { + $orderDataObject->$key = $value; + } + } + } + } + + $order_object = new Order($this->base_object); + $this->order_id = $order_object->create($orderDataObject); + $this->order_number = OrderHelper::get_order_number($this->order_id); + + unset($_SESSION['customer_info']); + + if ($this->order_id) { + // 24.08.2026: Bei PayPal wandert die Bestellbestaetigung ans Ende des + // Bezahlvorgangs (PaypalPaymentMethod::bestellbestaetigung_senden). Hier waere + // sie zu frueh -- die Order existiert an dieser Stelle zwar, aber der Kunde war + // noch nicht einmal bei PayPal. Beim Testkauf am 24.08. lagen zwischen dieser + // Mail (23:25:09) und der eingezogenen Zahlung (23:26:45) anderthalb Minuten, + // und wer gar nicht bezahlt, bekam trotzdem eine Bestaetigung. + // Alle anderen Zahlarten sind mit dem Abschicken verbindlich -- dort bleibt der + // Versand genau hier. + $mail_modul = isset($this->payment_methods[$this->payment_method_id]->module) + ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) + : ''; + + if (!$this->zahlung_erst_spaeter($mail_modul)) { + // send email + $ticket_object = new Cs_ticket($this->base_object); + $ticket_object->event(3, $this->customer_data->id, $this->order_id); + } else { + error_log('PayPal Fix: Bestellbestaetigung fuer Order '.$this->order_id + . ' zurueckgestellt bis zur eingezogenen Zahlung'); + } + + // EU-RL 2026/2673: elektronischer Widerruf (B2C) – silent solange Flag aus + try { + include_once './core/revocation.class.php'; + $revocation_obj = new Revocation($this->base_object); + $revocation_obj->create_for_order($this->order_id); + } catch (Throwable $e) { + error_log('Revocation hook failed for order '.$this->order_id.': '.$e->getMessage()); + } + + if ($this->order_number) { + // 25.08.2026: Vorgang als erledigt vermerken, damit ein zweiter Klick auf + // denselben Bestellbutton keine weitere Order erzeugt. + $this->bestellvorgang_vermerken(); + + // Ereignisprotokoll in PG -- der einzige Ort mit einer Uhrzeit zur Bestellung. + if (function_exists('shop_event')) { + shop_event('order_created', array( + 'order_id' => $this->order_id, + 'order_number' => $this->order_number, + 'payment_method' => $this->payment_method_id, + 'betrag' => isset($this->shoppingcart_data['order_total']) + ? $this->shoppingcart_data['order_total'] : null, + 'customer_id' => isset($this->customer_data->id) ? $this->customer_data->id : null, + 'detail' => $this->zahlung_erst_spaeter($mail_modul) + ? $mail_modul.': noch nicht bezahlt' : 'sofort verbindlich' + )); + } + + return true; + } + } + + return false; + } + + // end save_order + + private function after_saving_order() { + $this->layout_object->assign('order_number', $this->order_number); + $this->layout_object->assign('order_amount', $this->shoppingcart_data['order_total']); + + // get trusted shop payment id + $trusted_shop_payment = 'OTHER'; + if ($this->payment_method_id == 1) { + $trusted_shop_payment = 'PREPAYMENT'; + } else if ($this->payment_method_id == 2) { + $trusted_shop_payment = 'CASH_ON_DELIVERY'; + } else if ($this->payment_method_id == 3) { + $trusted_shop_payment = 'INVOICE'; + } else if ($this->payment_method_id == 4) { + $trusted_shop_payment = 'PAYPAL'; + } else if ($this->payment_method_id == 6) { + $trusted_shop_payment = 'DIRECT_DEBIT'; + } + $this->layout_object->assign('trusted_shop_payment', $trusted_shop_payment); + + // 24.08.2026: Der Warenkorb wird nur noch geleert, wenn die Bestellung mit dieser + // Antwort wirklich abgeschlossen ist. Bei PayPal faengt das Bezahlen hier erst an + // -- vorher flog der Korb schon raus, und wer abbrach, neu lud oder zurueckging, + // stand ohne Warenkorb da ("ORDER PREVENTED: Empty cart"). PayPal leert jetzt + // selbst, sobald das Geld eingezogen ist (PaypalPaymentMethod::change_order). + // Bewusst nur PayPal: die anderen Module raeumen nicht selbst auf. + $payment_module = isset($this->payment_methods[$this->payment_method_id]->module) + ? strtolower((string)$this->payment_methods[$this->payment_method_id]->module) + : ''; + + if (!$this->zahlung_erst_spaeter($payment_module)) { + $shoppingcart_object = new Shoppingcart($this->base_object); + $shoppingcart_object->clear(1); + } + + if ($this->payment_methods[$this->payment_method_id]->module != '') { + $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; + $module_name = ucfirst($this->payment_methods[$this->payment_method_id]->module).'PaymentMethod'; + $file_name = strtolower($module_name).'.class.php'; + + if (file_exists($path.$file_name)) { + include_once('./core/paymentmethods/'.$file_name); + + $payment_method_module = new $module_name($this->base_object, $this->layout_object); + + $payment_method_module->set_data($this->customer_data, $this->shipping_address, $this->billing_address, $this->shoppingcart_data, $this->shipping_methods[$this->shipping_method_id], $this->payment_methods[$this->payment_method_id], $this->order_id); + + if ($this->has_gift_certificate && $this->gift_certificate_is_valid) { + $payment_method_module->set_gift_certificate($this->gift_certificate_value); + } + + return $payment_method_module->after_order($this->shoppingcart_data['order_total']); + } + } + + // REDIRECT statt Template - verhindert Doppel-Bestellungen bei F5 + $order_num = $this->order_number ? $this->order_number : 'unbekannt'; + header('Location: /Kasse/?success=1&order=' . urlencode($order_num)); + exit; + } + + // end after_saving_order + + /** + * Nonce fuer das Kassenformular (25.08.2026). + * + * Ein Wert pro Bestellvorgang, der als hidden field im Formular mitgeht. Nach einer + * erfolgreichen Bestellung wird er verbraucht und ein neuer erzeugt -- ein erneut + * abgeschickter alter Nonce ist damit erkennbar derselbe Vorgang. Das ersetzt die + * Zeitfenster-Pruefung, die auf einer DATE-Spalte nicht funktionieren konnte. + * + * @return string + */ + private function checkout_nonce_holen() { + if (empty($_SESSION['easyshop']['checkout_nonce'])) { + if (function_exists('random_bytes')) { + try { + $_SESSION['easyshop']['checkout_nonce'] = bin2hex(random_bytes(16)); + } catch (Exception $e) { + $_SESSION['easyshop']['checkout_nonce'] = md5(uniqid(mt_rand(), true)); + } + } else { + $_SESSION['easyshop']['checkout_nonce'] = md5(uniqid(mt_rand(), true)); + } + } + + return $_SESSION['easyshop']['checkout_nonce']; + } + + // end checkout_nonce_holen + + + /** + * Wurde dieser Bestellvorgang schon abgeschlossen? (25.08.2026) + * + * @return array|false Daten der bereits angelegten Order, sonst false + */ + private function bestellvorgang_bereits_erledigt() { + $nonce = isset($_POST['checkout_nonce']) ? trim((string)$_POST['checkout_nonce']) : ''; + + if ($nonce !== '' && isset($_SESSION['easyshop']['checkout_done'][$nonce])) { + return $_SESSION['easyshop']['checkout_done'][$nonce]; + } + + // Netz fuer Absendungen ohne Nonce -- alte Seite aus dem Cache, JS abgeschaltet, + // Formular von Hand nachgebaut. Gleiche Session, gleicher Betrag, innerhalb von + // fuenf Minuten. Die Uhrzeit kommt aus der Session und ist damit eine echte + // Uhrzeit, anders als beim alten Guard auf der DATE-Spalte. + if ($nonce === '' && !empty($_SESSION['easyshop']['last_order'])) { + $letzte = $_SESSION['easyshop']['last_order']; + $summe = round((float)(isset($this->shoppingcart_data['order_total']) + ? $this->shoppingcart_data['order_total'] : 0), 2); + + if (abs($summe - (float)$letzte['total']) < 0.01 + && (time() - (int)$letzte['time']) < 300) { + return $letzte; + } + } + + return false; + } + + // end bestellvorgang_bereits_erledigt + + + /** + * Bestellvorgang als erledigt vermerken (25.08.2026). + * + * Laeuft direkt nach der Order-Anlage. Der verbrauchte Nonce bleibt in der Session + * stehen, damit ein spaeter nachgereichter POST mit demselben Wert erkannt wird; fuer + * die naechste Bestellung wird beim Rendern des Formulars ein frischer erzeugt. + */ + private function bestellvorgang_vermerken() { + $nonce = isset($_POST['checkout_nonce']) ? trim((string)$_POST['checkout_nonce']) : ''; + + $eintrag = array( + 'order_id' => $this->order_id, + 'order_number' => $this->order_number, + 'total' => round((float)(isset($this->shoppingcart_data['order_total']) + ? $this->shoppingcart_data['order_total'] : 0), 2), + 'time' => time() + ); + + if ($nonce !== '') { + if (!isset($_SESSION['easyshop']['checkout_done']) + || !is_array($_SESSION['easyshop']['checkout_done'])) { + $_SESSION['easyshop']['checkout_done'] = array(); + } + + $_SESSION['easyshop']['checkout_done'][$nonce] = $eintrag; + + // Nicht unbegrenzt mitschleppen -- die letzten fuenf reichen. + if (count($_SESSION['easyshop']['checkout_done']) > 5) { + $_SESSION['easyshop']['checkout_done'] = + array_slice($_SESSION['easyshop']['checkout_done'], -5, 5, true); + } + + unset($_SESSION['easyshop']['checkout_nonce']); + } + + $_SESSION['easyshop']['last_order'] = $eintrag; + } + + // end bestellvorgang_vermerken + + + /** + * Ist zu dieser Order bereits Geld eingezogen? (24.08.2026) + * + * Entscheidet, ob ein wiederholter Bestell-POST auf den offenen PayPal-Vorgang + * zurueckgefuehrt wird oder als neuer Einkauf durchlaeuft. + * + * @param int $order_id + * @return bool + */ + private function paypal_order_is_paid($order_id) { + $order_id = (int)$order_id; + if ($order_id <= 0) { + return false; + } + + $result = $this->base_object->db->query(" + SELECT + paypal_transaction_id + FROM + orders + WHERE + id = ".$this->base_object->db->real_escape_string($order_id)." + LIMIT 1 + "); + + if ($result && $result->num_rows > 0) { + $row = $result->fetch_object(); + return !empty($row->paypal_transaction_id); + } + + // Order nicht gefunden -> nicht blockieren, normal weiterbestellen lassen. + return false; + } + + // end paypal_order_is_paid + + private function change_order($param) { + $path = $_SERVER['DOCUMENT_ROOT'].'/core/paymentmethods/'; + $module_name = ucfirst($param).'PaymentMethod'; + $file_name = strtolower($module_name).'.class.php'; + + if (file_exists($path.$file_name)) { + include_once('./core/paymentmethods/'.$file_name); + + $payment_method_module = new $module_name($this->base_object, $this->layout_object); + + return $payment_method_module->change_order(); + } + } + + // end change_order + + private function manage_guest_payment_method() { + $payment_method_object = new Payment_method($this->base_object); + /* temp: */ $this->shipping_area_id = 1; + $this->payment_methods = $payment_method_object->getMethodsForGroup($this->base_object->config->shopConfiguration['guest_order_customer_group'], $this->shoppingcart_data['price_sum_gross'], $this->shipping_area_id); + if (isset($_POST['payment_method']) && $_POST['payment_method'] > 0) { + $this->payment_method_id = $_POST['payment_method']; + } else { + $this->personal_data['payment_method'] = $this->payment_method_id = current($this->payment_methods)->id; + } + + $this->layout_object->assign('payment_methods', $this->payment_methods); + } + + // end manage_payment_method + + private function manage_guest_shipping_method() { + $shipping_method_object = new Deliverer($this->base_object); + + $shoppingcart_object = new Shoppingcart($this->base_object); + $this->shoppingcart_data = $shoppingcart_object->get_data(); + $this->shipping_methods = $shipping_method_object->getAllActive( + $this->shipping_area_id, $this->shoppingcart_data['standard_shipping_items'], $this->shoppingcart_data['bulk_goods'], $this->shoppingcart_data['item_weight'] + ); + + if (isset($_POST['shipping_method']) && $_POST['shipping_method'] > 0) { + $this->shipping_method_id = $_POST['shipping_method']; + } else { + $this->personal_data['shipping_method'] = $this->shipping_method_id = current($this->shipping_methods)->id; + } + $this->layout_object->assign('deliverers', $this->shipping_methods); + } + +// end manage_shipping_method +} + +// end website_cashdesk + ?> \ No newline at end of file