feat(payments): Anbieter-Gerüst core/payments mit PayPal Orders v2
- ProviderInterface/AbstractProvider: Vorgang mit Uhrzeit+Session (payment_transactions), Mail und Warenkorb erst nach Zahlung, idempotentes markPaid, Betragsabgleich, shop_event + PG payment_log - Secrets verschluesselt (libsodium) in MySQL, Schluessel ausserhalb Docroot - Cashdesk: ?payment_return/?payment_cancel, Pending-Riegel, zahlung_erst_spaeter() - Backend Zahlart-Editor: Einstellungen speichern (war ohne Empfaenger), Secret-Felder - payment_webhook.php (Standalone), cli.php (init-key, secret, reconcile, status) - PayPal-v2-Blaupause vom Juli entfernt
This commit is contained in:
@@ -41,6 +41,14 @@ class admin_payment_method_editor {
|
||||
$is_submitted = false;
|
||||
}
|
||||
|
||||
// 13.09.2026: Reiter "Einstellungen" speichern (Konfigurationsgruppe + Secrets).
|
||||
// Vorher hatte dieses Formular keinen Empfaenger -- es postete ins Leere.
|
||||
if (isset($_POST['payment_config_save']) && $has_id) {
|
||||
$this->save_provider_config((int)$_POST['id']);
|
||||
$is_submitted = false;
|
||||
$_GET['id'] = (int)$_POST['id'];
|
||||
}
|
||||
|
||||
// decide what CRUD method to call and call it in ListAndEdit
|
||||
// create
|
||||
$payment_method_object = new Payment_method($this->base_object);
|
||||
@@ -60,6 +68,7 @@ class admin_payment_method_editor {
|
||||
$config_data = $this->config->get_config_elements($payment_method_data->configuration_group_id);
|
||||
$this->layout_object->assign('config_data', $config_data);
|
||||
}
|
||||
$this->assign_provider($payment_method_data);
|
||||
}// read
|
||||
elseif($has_id && !$is_submitted && $_GET['mod'] != 'delete') {
|
||||
// template variable name
|
||||
@@ -72,6 +81,7 @@ class admin_payment_method_editor {
|
||||
$config_data = $this->config->get_config_elements($payment_method_data->configuration_group_id);
|
||||
$this->layout_object->assign('config_data', $config_data);
|
||||
}
|
||||
$this->assign_provider($payment_method_data);
|
||||
} // update
|
||||
elseif($has_id && $is_submitted && $_GET['mod'] != 'delete') {
|
||||
$payment_method_object->id = $_POST['id'];
|
||||
@@ -92,6 +102,7 @@ class admin_payment_method_editor {
|
||||
$config_data = $this->config->get_config_elements($payment_method_data->configuration_group_id);
|
||||
$this->layout_object->assign('config_data', $config_data);
|
||||
}
|
||||
$this->assign_provider($payment_method_data);
|
||||
} // delete
|
||||
elseif($has_id && isset($_GET['mod']) && $_GET['mod'] == 'delete'){
|
||||
$payment_method_object->id = $_GET['id'];
|
||||
@@ -103,6 +114,76 @@ class admin_payment_method_editor {
|
||||
|
||||
return $this->layout_object->fetch('admin_payment_method_editor.tpl');
|
||||
}
|
||||
|
||||
/**
|
||||
* 13.09.2026 -- Zahlungsanbieter aus core/payments: Secret-Felder fuers Template.
|
||||
* Klartext wird nie ausgeliefert, nur "hinterlegt seit".
|
||||
*/
|
||||
private function assign_provider($payment_method_data) {
|
||||
if (!is_object($payment_method_data) || empty($payment_method_data->module)) {
|
||||
return;
|
||||
}
|
||||
include_once './core/payments/Registry.php';
|
||||
if (!\Payments\Registry::has((string)$payment_method_data->module)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$provider = \Payments\Registry::get((string)$payment_method_data->module, $this->base_object);
|
||||
} catch (Throwable $e) {
|
||||
$this->layout_object->assign('provider_error', $e->getMessage());
|
||||
return;
|
||||
}
|
||||
if (!$provider) {
|
||||
return;
|
||||
}
|
||||
$key_ok = \Payments\Secrets::keyAvailable();
|
||||
$status = $key_ok ? \Payments\Secrets::status($this->base_object->db, $provider->module()) : array();
|
||||
$fields = array();
|
||||
foreach ($provider->secretFields() as $name => $label) {
|
||||
$fields[] = array(
|
||||
'name' => $name,
|
||||
'label' => $label,
|
||||
'since' => isset($status[$name]) ? $status[$name] : '',
|
||||
);
|
||||
}
|
||||
$this->layout_object->assign('provider_label', $provider->label());
|
||||
$this->layout_object->assign('provider_module', $provider->module());
|
||||
$this->layout_object->assign('provider_key_ok', $key_ok ? 1 : 0);
|
||||
$this->layout_object->assign('provider_key_path', \Payments\Secrets::keyPath());
|
||||
$this->layout_object->assign('provider_secrets', $fields);
|
||||
$this->layout_object->assign('provider_webhook_url', rtrim(MAIN_URL, '/').'/payment_webhook.php?p='.$provider->module());
|
||||
}
|
||||
|
||||
/** Einstellungen (configuration-Gruppe) und Secrets aus dem Reiter "Einstellungen" speichern. */
|
||||
private function save_provider_config($payment_method_id) {
|
||||
$pm_object = new Payment_method($this->base_object);
|
||||
$pm_object->id = $payment_method_id;
|
||||
$pm = $pm_object->get_data();
|
||||
if (!$pm) {
|
||||
return;
|
||||
}
|
||||
if (!empty($pm->configuration_group_id) && isset($_POST['form_field']) && is_array($_POST['form_field'])) {
|
||||
$clean = array();
|
||||
foreach ($_POST['form_field'] as $k => $v) {
|
||||
$clean[$k] = $this->base_object->db->real_escape_string((string)$v);
|
||||
}
|
||||
$this->config->update($pm->configuration_group_id, $clean);
|
||||
}
|
||||
if (!empty($pm->module) && isset($_POST['secret_field']) && is_array($_POST['secret_field'])) {
|
||||
include_once './core/payments/Registry.php';
|
||||
if (\Payments\Registry::has((string)$pm->module) && \Payments\Secrets::keyAvailable()) {
|
||||
$provider = \Payments\Registry::get((string)$pm->module, $this->base_object);
|
||||
$allowed = $provider ? $provider->secretFields() : array();
|
||||
foreach ($_POST['secret_field'] as $name => $value) {
|
||||
$value = trim((string)$value);
|
||||
if ($value === '' || !isset($allowed[$name])) {
|
||||
continue; // leer = unveraendert lassen
|
||||
}
|
||||
\Payments\Secrets::set($this->base_object->db, $provider->module(), (string)$name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ include_once('./core/customer.class.php');
|
||||
include_once('./core/customeraddress.class.php');
|
||||
include_once('./core/shoppingcart.class.php');
|
||||
include_once('./core/payment_method.class.php');
|
||||
include_once('./core/payments/Registry.php'); // 13.09.2026: Zahlungsanbieter mit Weiterleitung (PayPal v2, ...)
|
||||
include_once('./core/deliverer.class.php'); // should be called shipping methods
|
||||
include_once('./core/order.class.php');
|
||||
include_once('./core/cs_ticket.class.php');
|
||||
@@ -70,6 +71,15 @@ class website_cashdesk {
|
||||
return $this->layout_object->_fetch('content_cash_desk/completed.tpl');
|
||||
}
|
||||
|
||||
// 13.09.2026 -- Rueckkehr von einem Zahlungsanbieter aus core/payments
|
||||
// (?payment_return=<modul> bzw. ?payment_cancel=<modul>). 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();
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -224,6 +234,16 @@ class website_cashdesk {
|
||||
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');
|
||||
|
||||
@@ -265,6 +285,21 @@ class website_cashdesk {
|
||||
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) {
|
||||
@@ -313,7 +348,7 @@ class website_cashdesk {
|
||||
|
||||
return $this->layout_object->_fetch('content_cash_desk.tpl');
|
||||
|
||||
} else if ($_GET['token']) {
|
||||
} else if (isset($_GET['token']) && $_GET['token']) {
|
||||
return $this->change_order('paypal');
|
||||
} else {
|
||||
// login
|
||||
@@ -326,6 +361,93 @@ class website_cashdesk {
|
||||
|
||||
// end run
|
||||
|
||||
/**
|
||||
* 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=<modul> | ?payment_cancel=<modul>).
|
||||
* 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);
|
||||
|
||||
@@ -842,7 +964,7 @@ class website_cashdesk {
|
||||
? strtolower((string)$this->payment_methods[$this->payment_method_id]->module)
|
||||
: '';
|
||||
|
||||
if ($mail_modul !== 'paypal') {
|
||||
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);
|
||||
@@ -874,8 +996,8 @@ class website_cashdesk {
|
||||
'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' => $mail_modul === 'paypal'
|
||||
? 'PayPal: noch nicht bezahlt' : 'sofort verbindlich'
|
||||
'detail' => $this->zahlung_erst_spaeter($mail_modul)
|
||||
? $mail_modul.': noch nicht bezahlt' : 'sofort verbindlich'
|
||||
));
|
||||
}
|
||||
|
||||
@@ -917,7 +1039,7 @@ class website_cashdesk {
|
||||
? strtolower((string)$this->payment_methods[$this->payment_method_id]->module)
|
||||
: '';
|
||||
|
||||
if ($payment_module !== 'paypal') {
|
||||
if (!$this->zahlung_erst_spaeter($payment_module)) {
|
||||
$shoppingcart_object = new Shoppingcart($this->base_object);
|
||||
$shoppingcart_object->clear(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user