diff --git a/.gitignore b/.gitignore index 31e3ac6..63a1736 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ vendor/ .idea +*.log +*.cache diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..ac73e83 --- /dev/null +++ b/.htaccess @@ -0,0 +1,9 @@ + + + Require all granted + + + Order Allow,Deny + Allow from all + + diff --git a/composer.json b/composer.json index 91b7605..ce26ce2 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "scripts": { "phpcbf": "phpcbf --standard=PSR12 --encoding=utf8 -p ./version", "php-cs-fixer": "php-cs-fixer fix", - "fix": "phpcbf --standard=PSR12 --encoding=utf8 -p ./version; php-cs-fixer fix" + "fix": "php-cs-fixer fix; phpcbf --standard=PSR12 --encoding=utf8 -p ./version" }, "require": { "ext-json": "*", diff --git a/info.xml b/info.xml index c5e96da..59ca7b3 100644 --- a/info.xml +++ b/info.xml @@ -45,9 +45,15 @@ 204.sql - 2021-08-10 + 2021-08-25 205.sql + + 2022-01-11 + + + 2022-06-10 + 75_bestellungInDb.php 131_globalinclude.php @@ -57,6 +63,13 @@ 181_sync.php 210_storno.php + + + applePay.js + 5 + body + + Bestellungen @@ -1102,7 +1115,7 @@ cVorname} {$Kunde->cNachname},
vielen Dank für Ihre Bestellung bei {$Einstellungen.global.global_shopname}.

Leider konnten wir für die Bestellung {$oPluginMail->Bestellung->cBestellNr} bisher keinen Zahlungseingang feststellen.

Sollte es bei der Zahlung zu Problemen gekommen sein, können Sie diese über den folgenden Link einfach erneut versuchen.

Zu zahlender Betrag: {$oPluginMail->Amount}

Jetzt bezahlen: {$oPluginMail->PayURL}

{includeMailTemplate template=footer type=html}]]>
- cVorname} {$Kunde->cNachname}, vielen Dank für Ihre Bestellung bei {$Einstellungen.global.global_shopname}. diff --git a/version/100/adminmenu/info.php b/version/100/adminmenu/info.php index cbffae6..f6e4e4a 100644 --- a/version/100/adminmenu/info.php +++ b/version/100/adminmenu/info.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/200/class/Checkout/Payment/Address.php b/version/200/class/Checkout/Payment/Address.php index c23e53e..468a08a 100644 --- a/version/200/class/Checkout/Payment/Address.php +++ b/version/200/class/Checkout/Payment/Address.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/200/class/ExclusiveLock.php b/version/200/class/ExclusiveLock.php index 8a2cd2a..1d596e4 100644 --- a/version/200/class/ExclusiveLock.php +++ b/version/200/class/ExclusiveLock.php @@ -1,6 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': diff --git a/version/200/class/Shipment.php b/version/200/class/Shipment.php index c01090a..31e6729 100644 --- a/version/200/class/Shipment.php +++ b/version/200/class/Shipment.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/201/class/Checkout/Payment/Address.php b/version/201/class/Checkout/Payment/Address.php index c23e53e..468a08a 100644 --- a/version/201/class/Checkout/Payment/Address.php +++ b/version/201/class/Checkout/Payment/Address.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/201/class/ExclusiveLock.php b/version/201/class/ExclusiveLock.php index 8a2cd2a..1d596e4 100644 --- a/version/201/class/ExclusiveLock.php +++ b/version/201/class/ExclusiveLock.php @@ -1,6 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': diff --git a/version/201/class/Shipment.php b/version/201/class/Shipment.php index c01090a..31e6729 100644 --- a/version/201/class/Shipment.php +++ b/version/201/class/Shipment.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/202/class/Checkout/Payment/Address.php b/version/202/class/Checkout/Payment/Address.php index c23e53e..468a08a 100644 --- a/version/202/class/Checkout/Payment/Address.php +++ b/version/202/class/Checkout/Payment/Address.php @@ -1,6 +1,7 @@ getBestellung()->oKunde->nRegistriert && ( $customer = $this->getCustomer( - array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' - ) + array_key_exists('mollie_create_customer', $_SESSION['cPost_arr'] ?: []) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y' + ) ) && isset($customer) ) { diff --git a/version/202/class/ExclusiveLock.php b/version/202/class/ExclusiveLock.php index 8a2cd2a..1d596e4 100644 --- a/version/202/class/ExclusiveLock.php +++ b/version/202/class/ExclusiveLock.php @@ -1,6 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': diff --git a/version/202/class/Shipment.php b/version/202/class/Shipment.php index c01090a..31e6729 100644 --- a/version/202/class/Shipment.php +++ b/version/202/class/Shipment.php @@ -1,6 +1,7 @@ nRegistriert && ( $customer = $this->getCustomer( - array_key_exists( - 'mollie_create_customer', - $_SESSION['cPost_arr'] ?: [] - ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', - $oKunde - ) + array_key_exists( + 'mollie_create_customer', + $_SESSION['cPost_arr'] ?: [] + ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', + $oKunde + ) ) && isset($customer) ) { diff --git a/version/203/class/Checkout/AbstractResource.php b/version/203/class/Checkout/AbstractResource.php index c49adb7..7c4439e 100644 --- a/version/203/class/Checkout/AbstractResource.php +++ b/version/203/class/Checkout/AbstractResource.php @@ -1,6 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': diff --git a/version/203/class/Shipment.php b/version/203/class/Shipment.php index b829f0a..31d16a3 100644 --- a/version/203/class/Shipment.php +++ b/version/203/class/Shipment.php @@ -1,6 +1,7 @@ nRegistriert && ( $customer = $this->getCustomer( - array_key_exists( - 'mollie_create_customer', - $_SESSION['cPost_arr'] ?: [] - ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', - $oKunde - ) + array_key_exists( + 'mollie_create_customer', + $_SESSION['cPost_arr'] ?: [] + ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', + $oKunde + ) ) && isset($customer) ) { diff --git a/version/204/class/Checkout/AbstractResource.php b/version/204/class/Checkout/AbstractResource.php index c49adb7..7c4439e 100644 --- a/version/204/class/Checkout/AbstractResource.php +++ b/version/204/class/Checkout/AbstractResource.php @@ -1,6 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': diff --git a/version/204/class/Shipment.php b/version/204/class/Shipment.php index c807e7f..a22a8d3 100644 --- a/version/204/class/Shipment.php +++ b/version/204/class/Shipment.php @@ -1,6 +1,7 @@ title = substr(trim(($address->cAnrede === 'm' ? Shop::Lang()->get('mr') : Shop::Lang()->get('mrs')) . ' ' . $address->cTitel) ?: null, 0 , 20); + $resource->title = substr(trim(($address->cAnrede === 'm' ? Shop::Lang()->get('mr') : Shop::Lang()->get('mrs')) . ' ' . $address->cTitel) ?: null, 0, 20); $resource->givenName = $address->cVorname; $resource->familyName = $address->cNachname; $resource->email = $address->cMail ?: null; diff --git a/version/205/class/Checkout/Order/OrderLine.php b/version/205/class/Checkout/Order/OrderLine.php index 878d8da..757edf7 100644 --- a/version/205/class/Checkout/Order/OrderLine.php +++ b/version/205/class/Checkout/Order/OrderLine.php @@ -1,7 +1,7 @@ cType))) { + if (([$type, $id] = explode(':', $todo->cType))) { try { switch ($type) { case 'webhook': @@ -109,7 +109,6 @@ public static function run($limit = 10) */ private static function getOpen($limit) { - // TODO: DOKU! if (!defined('MOLLIE_HOOK_DELAY')) { define('MOLLIE_HOOK_DELAY', 3); } @@ -167,6 +166,7 @@ protected static function handleHook($hook, QueueModel $todo) switch ($hook) { case HOOK_BESTELLUNGEN_XML_BESTELLSTATUS: if ((int)$data['kBestellung']) { + // TODO: #158 What happens when API requests fail? $checkout = AbstractCheckout::fromBestellung($data['kBestellung']); $status = array_key_exists('status', $data) ? (int)$data['status'] : 0; @@ -212,7 +212,7 @@ protected static function handleHook($hook, QueueModel $todo) $result = $e->getMessage() . "\n" . $e->getFile() . ':' . $e->getLine() . "\n" . $e->getTraceAsString(); } } else { - $result = sprintf('Unerwarteter Mollie Status "%s" für %s', $checkout->getMollie()->status, $checkout->getBestellung()->cBestellNr); + $result = sprintf('Unerwarteter Mollie Status "%s" für %s', $checkout->getMollie()->status, $checkout->getBestellung()->cBestellNr); } } else { $result = 'Nothing to do.'; diff --git a/version/205/class/Shipment.php b/version/205/class/Shipment.php index 3cf7a91..a22a8d3 100644 --- a/version/205/class/Shipment.php +++ b/version/205/class/Shipment.php @@ -1,7 +1,7 @@ lock()) { - // TODO: Doku! - AbstractCheckout::sendReminders(); Queue::storno((int)Helper::getSetting('autoStorno')); diff --git a/version/205/frontend/132_headPostGet.php b/version/205/frontend/132_headPostGet.php index a8b44e7..9562324 100644 --- a/version/205/frontend/132_headPostGet.php +++ b/version/205/frontend/132_headPostGet.php @@ -1,7 +1,7 @@ assign('defaultTabbertab', Helper::getAdminmenu('Info') + Helper::getAdminmenu('Support')); + Helper::selfupdate(); + } + + $svgQuery = http_build_query([ + 'p' => Helper::oPlugin()->cPluginID, + 'v' => Helper::oPlugin()->nVersion, + 's' => defined('APPLICATION_VERSION') ? APPLICATION_VERSION : JTL_VERSION, + 'b' => defined('JTL_MINOR_VERSION') ? JTL_MINOR_VERSION : '0', + 'd' => Helper::getDomain(), + 'm' => base64_encode(Helper::getMasterMail(true)), + 'php' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION . PHP_EXTRA_VERSION, + ]); + + echo ""; + echo "
" . + "
" . + " " . + " Lizenz Informationen" . + ' ' . + '
' . + "
" . + " " . + " Update Informationen" . + ' ' . + '
' . + "
" . + " " . + " Plugin informationen" . + ' ' . + '
' . + '
'; + + try { + $latestRelease = Helper::getLatestRelease(array_key_exists('update', $_REQUEST)); + if ((int)Helper::oPlugin()->nVersion < (int)$latestRelease->version) { + Shop::Smarty()->assign('update', $latestRelease); + } + } catch (Exception $e) { + } + + Shop::Smarty()->display(Helper::oPlugin()->cAdminmenuPfad . '/tpl/info.tpl'); + + if (file_exists(__DIR__ . '/_addon.php')) { + try { + include __DIR__ . '/_addon.php'; + } catch (Exception $e) { + } + } +} catch (Exception $e) { + echo "
Fehler: {$e->getMessage()}
"; + Helper::logExc($e); +} diff --git a/version/206/adminmenu/orders.php b/version/206/adminmenu/orders.php new file mode 100644 index 0000000..68197ed --- /dev/null +++ b/version/206/adminmenu/orders.php @@ -0,0 +1,245 @@ +getModel()->kID)) { + Helper::addAlert('Zahlungserinnerung wurde verschickt.', 'success', 'orders'); + } else { + Helper::addAlert('Es ist ein Fehler aufgetreten, prüfe den Log.', 'danger', 'orders'); + } + } else { + Helper::addAlert('Bestellung konnte nicht geladen werden.', 'danger', 'orders'); + } + + + break; + + case 'fetchable': + if (array_key_exists('kBestellung', $_REQUEST) && ($checkout = AbstractCheckout::fromBestellung((int)$_REQUEST['kBestellung']))) { + if (AbstractCheckout::makeFetchable($checkout->getBestellung(), $checkout->getModel())) { + Helper::addAlert('Bestellung kann jetzt von der WAWI abgeholt werden.', 'success', 'orders'); + } else { + Helper::addAlert('Es ist ein Fehler aufgetreten, prüfe den Log.', 'danger', 'orders'); + } + } else { + Helper::addAlert('Bestellung konnte nicht geladen werden.', 'danger', 'orders'); + } + + break; + + case 'export': + try { + $export = []; + + $from = new DateTime($_REQUEST['from']); + $to = new DateTime($_REQUEST['to']); + + $orders = Shop::DB()->executeQueryPrepared('SELECT * FROM xplugin_ws_mollie_payments WHERE kBestellung > 0 AND dCreatedAt >= :From AND dCreatedAt <= :To ORDER BY dCreatedAt', [ + ':From' => $from->format('Y-m-d'), + ':To' => $to->format('Y-m-d'), + ], 2); + + + header('Content-Type: application/csv'); + header('Content-Disposition: attachment; filename=mollie-' . $from->format('Ymd') . '-' . $to->format('Ymd') . '.csv'); + header('Pragma: no-cache'); + + $out = fopen('php://output', 'wb'); + + + fputcsv($out, [ + 'kBestellung', + 'OrderID', + 'Status (mollie)', + 'BestellNr', + 'Status (JTL)', + 'Mode', + 'OriginalOrderNumber', + 'Currency', + 'Amount', + 'Method', + 'PaymentID', + 'Created' + ]); + + + foreach ($orders as $order) { + $order = new ws_mollie\Model\Payment($order); + $checkout = AbstractCheckout::fromModel($order); + + $tmp = [ + 'kBestellung' => $order->kBestellung, + 'cOrderId' => $order->kID, + 'cStatus' => $checkout->getMollie() ? $checkout->getMollie()->status : $order->cStatus, + 'cBestellNr' => $checkout->getBestellung() ? $checkout->getBestellung()->cBestellNr : $order->cOrderNumber, + 'nStatus' => $checkout->getBestellung() ? $checkout->getBestellung()->cStatus : 0, + 'cMode' => $order->cMode, + 'cOriginalOrderNumber' => $checkout->getMollie() && isset($checkout->getMollie()->metadata->originalOrderNumber) ? $checkout->getMollie()->metadata->originalOrderNumber : '', + 'cCurrency' => $order->cCurrency, + 'fAmount' => $order->fAmount, + 'cMethod' => $order->cMethod, + 'cPaymentId' => $order->cTransactionId, + 'dCreated' => $order->dCreatedAt, + ]; + + try { + if ($checkout->getMollie() && $checkout->getMollie()->resource === 'order') { + foreach ($checkout->getMollie()->payments() as $payment) { + if ($payment->status === PaymentStatus::STATUS_PAID) { + $tmp['cPaymentId'] = $payment->id; + } + } + } + } catch (Exception $e) { + } + fputcsv($out, $tmp); + + $export[] = $tmp; + } + + fclose($out); + exit(); + } catch (Exception $e) { + Helper::addAlert('Fehler:' . $e->getMessage(), 'danger', 'orders'); + } + + break; + + case 'refund': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angegeben!', 'danger', 'orders'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($refund = $checkout::refund($checkout)) { + Helper::addAlert(sprintf('Bestellung wurde zurückerstattet (%s).', $refund->id), 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'cancel': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($checkout::cancel($checkout)) { + Helper::addAlert('Bestellung wurde abgebrochen.', 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'capture': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($shipmentId = OrderCheckout::capture($checkout)) { + Helper::addAlert(sprintf('Zahlung erfolgreich erfasst/versandt (%s).', $shipmentId), 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'order': + order : + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + + if ($checkout instanceof OrderCheckout) { + Shop::Smarty()->assign('shipments', $checkout->getShipments()); + } + + Shop::Smarty()->assign('payment', $checkout->getModel()) + ->assign('oBestellung', $checkout->getBestellung()) + ->assign('order', $checkout->getMollie()) + ->assign('checkout', $checkout) + ->assign('logs', $checkout->getLogs()); + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/order.tpl'); + + return; + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } + + break; + } + } + + // Mollie::fixZahlungsarten(); + + $checkouts = []; + $payments = Shop::DB()->executeQueryPrepared('SELECT * FROM xplugin_ws_mollie_payments WHERE kBestellung IS NOT NULL ORDER BY dCreatedAt DESC LIMIT 1000;', [], 2); + foreach ($payments as $i => $payment) { + $payment = new Payment($payment); + + try { + $checkouts[$payment->kBestellung] = AbstractCheckout::fromModel($payment, false); + } catch (Exception $e) { + //Helper::addAlert($e->getMessage(), 'danger', 'orders'); + } + } + + Shop::Smarty()->assign('payments', $payments) + ->assign('checkouts', $checkouts) + ->assign('admRoot', str_replace('http:', '', $oPlugin->cAdminmenuPfadURL)) + ->assign('hasAPIKey', trim(Helper::getSetting('api_key')) !== ''); + + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/orders.tpl'); +} catch (Exception $e) { + echo "
" . + "{$e->getMessage()}
" . + "
{$e->getFile()}:{$e->getLine()}
{$e->getTraceAsString()}
" . + '
'; + Helper::logExc($e); +} diff --git a/version/206/adminmenu/paymentmethods.php b/version/206/adminmenu/paymentmethods.php new file mode 100644 index 0000000..49c2851 --- /dev/null +++ b/version/206/adminmenu/paymentmethods.php @@ -0,0 +1,103 @@ +setApiKey(Helper::getSetting('api_key')); + + $profile = $mollie->profiles->get('me'); + + $za = filter_input(INPUT_GET, 'za', FILTER_VALIDATE_BOOLEAN); + $active = filter_input(INPUT_GET, 'active', FILTER_VALIDATE_BOOLEAN); + $amount = filter_input(INPUT_GET, 'amount', FILTER_VALIDATE_FLOAT) ?: null; + $locale = filter_input(INPUT_GET, 'locale', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-zA-Z]{2}_[a-zA-Z]{2}$/']]) ?: AbstractCheckout::getLocale(); + $currency = filter_input(INPUT_GET, 'currency', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-zA-Z]{3}$/']]) ?: 'EUR'; + + + if ($za) { + Shop::Smarty()->assign('defaultTabbertab', Helper::getAdminmenu('Zahlungsarten')); + } + + $params = [ + 'include' => 'pricing,issuers', + 'locale' => $locale + ]; + if ($amount && $currency) { + $params['amount'] = ['value' => number_format($amount, 2, '.', ''), 'currency' => $currency]; + if ($active) { + $params['includeWallets'] = 'applepay'; + //$params['resource'] = 'orders'; + } + } + + $allMethods = []; + if ($active) { + $_allMethods = $mollie->methods->allActive($params); + } else { + $_allMethods = $mollie->methods->allAvailable($params); + } + + $sessionLife = (int)ini_get('session.gc_maxlifetime'); + + /** @var Method $method */ + foreach ($_allMethods as $method) { + $id = $method->id === 'creditcard' ? 'kreditkarte' : $method->id; + $key = "kPlugin_{$oPlugin->kPlugin}_mollie$id"; + + $class = null; + $shop = null; + $oClass = null; + + if (array_key_exists($key, $oPlugin->oPluginZahlungsKlasseAssoc_arr) && !in_array($id, ['voucher', 'giftcard', 'directdebit'], true)) { + $class = $oPlugin->oPluginZahlungsKlasseAssoc_arr[$key]; + include_once $oPlugin->cPluginPfad . 'paymentmethod/' . $class->cClassPfad; + /** @var JTLMollie $oClass */ + $oClass = new $class->cClassName($id); + } + if (array_key_exists($key, $oPlugin->oPluginZahlungsmethodeAssoc_arr)) { + $shop = $oPlugin->oPluginZahlungsmethodeAssoc_arr[$key]; + } + + $maxExpiryDays = $oClass ? $oClass->getExpiryDays() : null; + $allMethods[$method->id] = (object)[ + 'mollie' => $method, + 'class' => $class, + 'allowPreOrder' => $oClass ? $oClass::ALLOW_PAYMENT_BEFORE_ORDER : false, + 'allowAutoStorno' => $oClass ? $oClass::ALLOW_AUTO_STORNO : false, + 'oClass' => $oClass, + 'shop' => $shop, + 'maxExpiryDays' => $oClass ? $maxExpiryDays : null, + 'warning' => $oClass && ($maxExpiryDays * 24 * 60 * 60) > $sessionLife, + 'session' => round($sessionLife / 60 / 60, 2) . 'h' + ]; + } + + Shop::Smarty()->assign('profile', $profile) + ->assign('currencies', AbstractCheckout::getCurrencies()) + ->assign('locales', AbstractCheckout::getLocales()) + ->assign('allMethods', $allMethods) + ->assign('settings', $oPlugin->oPluginEinstellungAssoc_arr); + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/paymentmethods.tpl'); +} catch (Exception $e) { + echo "
{$e->getMessage()}
"; + Helper::logExc($e); +} diff --git a/version/206/adminmenu/tpl/_addon.tpl b/version/206/adminmenu/tpl/_addon.tpl new file mode 100644 index 0000000..6539757 --- /dev/null +++ b/version/206/adminmenu/tpl/_addon.tpl @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/version/206/adminmenu/tpl/info.tpl b/version/206/adminmenu/tpl/info.tpl new file mode 100644 index 0000000..2d41c95 --- /dev/null +++ b/version/206/adminmenu/tpl/info.tpl @@ -0,0 +1,114 @@ +
+
+
+ + + +
+
+ + + +
+ + {if isset($update)} +
+ +
+

Update auf Version {$update->version} verfügbar!

+
+
+
+
Version:
+
{$update->version}
+
+
+
Erschienen:
+
{$update->create_date}
+
+
+
Changelog:
+
+ +
+
+ +
+
+ +
+
+
+ {else} +
+ + + +
+ {/if} + +
+
+
+ + + +
+
+ + + +
+ +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+{if file_exists("{$smarty['current_dir']}/_addon.tpl")} + {include file="{$smarty['current_dir']}/_addon.tpl"} +{/if} +{if isset($oPlugin)} + +{/if} diff --git a/version/206/adminmenu/tpl/mollie-account-erstellen.png b/version/206/adminmenu/tpl/mollie-account-erstellen.png new file mode 100644 index 0000000..fc18192 Binary files /dev/null and b/version/206/adminmenu/tpl/mollie-account-erstellen.png differ diff --git a/version/206/adminmenu/tpl/order.tpl b/version/206/adminmenu/tpl/order.tpl new file mode 100644 index 0000000..aad5810 --- /dev/null +++ b/version/206/adminmenu/tpl/order.tpl @@ -0,0 +1,344 @@ + +

+ « + Bestellung: {$oBestellung->cBestellNr} - + {if $oBestellung->cStatus|intval == 1} + OFFEN + {elseif $oBestellung->cStatus|intval == 2} + IN BEARBEITUNG + {elseif $oBestellung->cStatus|intval == 3} + BEZAHLT + {elseif $oBestellung->cStatus|intval == 4} + VERSANDT + {elseif $oBestellung->cStatus|intval == 5} + TEILVERSANDT + {elseif $oBestellung->cStatus|intval == -1} + STORNO + {else} + n/a + {/if} +

+ +{ws_mollie\Helper::showAlerts('orders')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mollie ID: + + {$payment->kID} + + + Mode:{$order->mode}Status: + {$order->status} + {if $order->amountRefunded && $order->amountRefunded->value == $order->amount->value} + (total refund) + {elseif $order->amountRefunded && $order->amountRefunded->value > 0} + (partly refund) + {/if} +
Betrag:{$order->amount->value|number_format:2:',':''} {$order->amount->currency}Captured:{if $order->amountCaptured}{$order->amountCaptured->value|number_format:2:',':''} {$order->amountCaptured->currency}{else}-{/if}Refunded:{if $order->amountRefunded}{$order->amountRefunded->value|number_format:2:',':''} {$order->amountRefunded->currency}{else}-{/if} +
Method:{$order->method}Locale:{$order->locale}Erstellt:{"d. M Y H:i:s"|date:{$order->createdAt|strtotime}}
Kunde: + {if isset($order->customerId)} + ID: + {$order->customerId} +
+ {/if} + {if isset($order->billingAddress, $order->billingAddress->organizationName)} + {$order->billingAddress->organizationName} + {elseif isset($order->billingAddress)} + {$order->billingAddress->title} {$order->billingAddress->givenName} {$order->billingAddress->familyName} + {/if} +
Mollie Checkout: + {if $order->getCheckoutURL()} + mollie.com/... + {else} + n/a + {/if} + + +
+{if $order->id|strpos:"ord_" !== false && $order->payments()->count > 0} +

Zahlungen

+ + + + + + + + + + + + + + {foreach from=$order->payments() item=payment} + + + + + + + + + + + {/foreach} +
IDStatusMethodeAmountSettlementRefundedRemainingDetails
{$payment->id}{$payment->status}{$payment->method}{$payment->amount->value} {$payment->amount->currency} + {if $payment->settlementAmount} + {$payment->settlementAmount->value} {$payment->settlementAmount->currency} + {else}-{/if} + + {if $payment->amountRefunded} + {$payment->amountRefunded->value} {$payment->amountRefunded->currency} + {else}-{/if} + + {if $payment->amountRemaining} + {$payment->amountRemaining->value} {$payment->amountRemaining->currency} + {else}-{/if} + +
    + {foreach from=$payment->details item=value key=key} +
  • {$key}: {if $value|is_scalar}{$value}{else}{$value|json_encode}{/if}
  • + {/foreach} +
+
+{/if} + +
+ {if ($order->status === 'authorized' || $order->status === 'shipping') && $oBestellung->cStatus|intval >= 3} + + Zahlung erfassen1 + + {/if} + {if !$order->amountRefunded || ($order->amount->value > $order->amountRefunded->value)} + Rückerstatten2 + + {/if} + {if $order->isCancelable} + Abbrechen3 + + {/if} +
+ +{if $order->id|strpos:"ord_" !== false} +

Positionen:

+ + + + + + + + + + + + + + + + + {assign var="vat" value=0} + {assign var="netto" value=0} + {assign var="brutto" value=0} + {foreach from=$order->lines item=line} + + {assign var="vat" value=$vat+$line->vatAmount->value} + {assign var="netto" value=$netto+$line->totalAmount->value-$line->vatAmount->value} + {assign var="brutto" value=$brutto+$line->totalAmount->value} + + + + + + + + + + + + + {/foreach} + + + + + + + + + + +
StatusSKUNameTypAnzahlMwStSteuerNettoBrutto 
+ {if $line->status == 'created'} + erstellt + {elseif $line->status == 'pending'} + austehend + {elseif $line->status == 'paid'} + bezahlt + {elseif $line->status == 'authorized'} + autorisiert + {elseif $line->status == 'shipping'} + versendet + {elseif $line->status == 'completed'} + abgeschlossen + {elseif $line->status == 'expired'} + abgelaufen + {elseif $line->status == 'canceled'} + abgebrochen + {else} + Unbekannt: {$line->status} + {/if} + {$line->sku}{$line->name|utf8_decode}{$line->type}{$line->quantity}{$line->vatRate|floatval}%{$line->vatAmount->value|number_format:2:',':''} {$line->vatAmount->currency}{($line->totalAmount->value - $line->vatAmount->value)|number_format:2:',':''} {$line->vatAmount->currency}{$line->totalAmount->value|number_format:2:',':''} {$line->totalAmount->currency} + {*if $line->quantity > $line->quantityShipped} + + + + {/if} + {if $line->quantity > $line->quantityRefunded} + + + + {/if} + {if $line->isCancelable} + + + + {/if*} + {*$line|var_dump*} +
{$vat|number_format:2:',':''} {$order->amount->currency}{$netto|number_format:2:',':''} {$order->amount->currency}{$brutto|number_format:2:',':''} {$order->amount->currency} 
+
+ 1 = Bestellung wird bei Mollie als versandt markiert. WAWI wird nicht informiert.
+ 2 = Bezahlter Betrag wird dem Kunden rckerstattet. WAWI wird nicht informiert.
+ 3 = Bestellung wird bei Mollie storniert. WAWI wird nicht informiert.
+
+{/if} + +{if isset($shipments) && $shipments|count} +

Shipmets

+ + + + + + + + + + + {foreach from=$shipments item=shipment} + + + + + + + {/foreach} + +
Lieferschein Nr.Mollie IDCarrierCode
{$shipment->getLieferschein()->getLieferscheinNr()}{$shipment->getShipment()->id} + {if isset($shipment->getShipment()->tracking)} + {$shipment->getShipment()->tracking->carrier} + {else} + - + {/if} + {if isset($shipment->getShipment()->tracking)} + {if isset($shipment->getShipment()->tracking->url)} + + {$shipment->getShipment()->tracking->code} + + {else} + {$shipment->getShipment()->tracking->code} + {/if} + {else} + - + {/if} +
+{/if} + +

Log

+ + {foreach from=$logs item=log} + + + + + + + {/foreach} +
+ {if $log->nLevel == 1} + Fehler + {elseif $log->nLevel == 2} + Hinweis + {elseif $log->nLevel == 3} + Debug + {else} + unknown {$log->nLevel} + {/if} + {$log->cModulId} +
+ {$log->cLog} +
+
{$log->dDatum}
+ + diff --git a/version/206/adminmenu/tpl/orders.tpl b/version/206/adminmenu/tpl/orders.tpl new file mode 100644 index 0000000..a499f32 --- /dev/null +++ b/version/206/adminmenu/tpl/orders.tpl @@ -0,0 +1,160 @@ +{ws_mollie\Helper::showAlerts('orders')} + +{if $hasAPIKey == false} + + Jetzt kostenlos Mollie Account eröffnen! + +{else} + + + + + + + + + + + + + + + {foreach from=$checkouts item=checkout} + + + + + + + + + + + + + + + {/foreach} + +
BestellNr.IDMollie StatusJTL StatusBetragMethodeErstellt 
+ {if $checkout->getModel()->bSynced == false && $checkout->getBestellung()->cAbgeholt === 'Y'} + * + {/if} + {$checkout->getModel()->cOrderNumber} + {if $checkout->getModel()->cMode == 'test'} + TEST + {/if} + {if $checkout->getModel()->bLockTimeout} + LOCK TIMEOUT + {/if} + {if $checkout->getModel()->dReminder && $checkout->getModel()->dReminder !== '0000-00-00 00:00:00'} + getModel()->dReminder|strtotime}}"> + {/if} + + {$checkout->getModel()->kID} + + {if $checkout->getModel()->cStatus == 'created' || $checkout->getModel()->cStatus == 'open'} + erstellt + {elseif $checkout->getModel()->cStatus == 'pending'} + austehend + {elseif $checkout->getModel()->cStatus == 'paid'} + bezahlt + {elseif $checkout->getModel()->cStatus == 'authorized'} + autorisiert + {elseif $checkout->getModel()->cStatus == 'shipping'} + versendet + {elseif $checkout->getModel()->cStatus == 'completed'} + abgeschlossen + {elseif $checkout->getModel()->cStatus == 'expired'} + abgelaufen + {elseif $checkout->getModel()->cStatus == 'canceled'} + abgebrochen + {else} + Unbekannt: {$checkout->getModel()->cStatus} + {/if} + {if $checkout->getModel()->fAmountRefunded && $checkout->getModel()->fAmountRefunded == $checkout->getModel()->fAmount} + (total refund) + {elseif $checkout->getModel()->fAmountRefunded && $checkout->getModel()->fAmountRefunded > 0} + (partly refund) + {/if} + + + {if $checkout->getBestellung()->cStatus|intval == 1} + OFFEN + {elseif $checkout->getBestellung()->cStatus|intval == 2} + IN BEARBEITUNG + {elseif $checkout->getBestellung()->cStatus|intval == 3} + BEZAHLT + {elseif $checkout->getBestellung()->cStatus|intval == 4} + VERSANDT + {elseif $checkout->getBestellung()->cStatus|intval == 5} + TEILVERSANDT + {elseif $checkout->getBestellung()->cStatus|intval == -1} + STORNO + {else} + n/a + {/if} + {$checkout->getModel()->fAmount|number_format:2:',':''} {$checkout->getModel()->cCurrency}{$checkout->getModel()->cMethod}{"d. M Y H:i"|date:{$checkout->getModel()->dCreatedAt|strtotime}} +
+ +
+ {if $checkout->getModel()->bSynced == false && $checkout->getBestellung()->cAbgeholt === 'Y'} + + {/if} + {if $checkout->remindable()} + + {/if} +
+
+
+ {if $payments|count > 900} +
Hier werden nur die letzten 1000 Ergebnisse angezeigt.
+ {/if} + + +
+
+

Export:

+
+ +
+ + + + + +
+
+ +{/if} + diff --git a/version/206/adminmenu/tpl/paymentmethods.tpl b/version/206/adminmenu/tpl/paymentmethods.tpl new file mode 100644 index 0000000..7821af5 --- /dev/null +++ b/version/206/adminmenu/tpl/paymentmethods.tpl @@ -0,0 +1,148 @@ +

Account Status

+ + + + + + + {if $profile->review} + + + {/if} + {if $profile->_links->checkoutPreviewUrl->href} + + {/if} + + +
Mode:{$profile->mode}Status:{$profile->status}Review:{$profile->review->status} + Checkout + Preview + + Mollie Dashboard +
+
+
+
+ + +
+ + + +
+
+ + +
+
+ + +
+
+ + + reset +
+
+
+
+{if $allMethods && $allMethods|count} + + + + + + + + + + + + {foreach from=$allMethods item=method} + + + + + + + + {/foreach} + +
BildName / IDInfoPreiseLimits
{$method->mollie->description|utf8_decode} + {if $method->mollie->status === 'activated'} + + {else} + + {/if} + {$method->mollie->description|utf8_decode}
+ {$method->mollie->id} +
+ {if $method->shop && $method->oClass} + {if intval($method->shop->nWaehrendBestellung) === 1 && !$method->allowPreOrder} +
Zahlung VOR Bestellabschluss nicht unterstützt!
+ {else} +
+ Bestellabschluss: + {if intval($method->shop->nWaehrendBestellung) === 1} + NACH Zahlung + {else} + VOR Zahlung + {/if} +
+ {/if} + + {if intval($settings.autoStorno) > 0} +
+ Unbez. Bestellung stornieren: + {if $method->allowAutoStorno} +
auto
+ {else} +
manual
+ {/if} +
+ {/if} +
+ Gültigkeit: + {$method->maxExpiryDays} Tage +
+ {else} + Derzeit nicht unterstützt. + {/if} +
+
    + {foreach from=$method->mollie->pricing item=price} +
  • + {$price->description|utf8_decode}: {$price->fixed->value} {$price->fixed->currency} + {if $price->variable > 0.0} + + {$price->variable}% + {/if} +
  • + {/foreach} +
+
+ Min: {if $method->mollie->minimumAmount}{$method->mollie->minimumAmount->value} {$method->mollie->minimumAmount->currency}{else}n/a{/if} +
+ Max: {if $method->mollie->maximumAmount}{$method->mollie->maximumAmount->value} {$method->mollie->maximumAmount->currency}{else}n/a{/if} +
+{else} +
Es konnten keine Methoden abgerufen werden.
+{/if} \ No newline at end of file diff --git a/version/206/class/API.php b/version/206/class/API.php new file mode 100644 index 0000000..3532455 --- /dev/null +++ b/version/206/class/API.php @@ -0,0 +1,77 @@ +test = $test === null ? self::getMode() : $test; + } + + /** + * @return bool + */ + public static function getMode() + { + require_once PFAD_ROOT . PFAD_ADMIN . PFAD_INCLUDES . 'benutzerverwaltung_inc.php'; + + return self::Plugin()->oPluginEinstellungAssoc_arr['testAsAdmin'] === 'Y' && Shop::isAdmin() && self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key']; + } + + /** + * @throws ApiException + * @throws IncompatiblePlatform + * @return MollieApiClient + */ + public function Client() + { + if (!$this->client) { + $this->client = new MollieApiClient(/*new Client([ + RequestOptions::VERIFY => CaBundle::getBundledCaBundlePath(), + RequestOptions::TIMEOUT => 60 + ])*/ + ); + $this->client->setApiKey($this->isTest() ? self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key'] : self::Plugin()->oPluginEinstellungAssoc_arr['api_key']) + ->addVersionString('JTL-Shop/' . JTL_VERSION . JTL_MINOR_VERSION) + ->addVersionString('ws_mollie/' . self::Plugin()->nVersion); + } + + return $this->client; + } + + /** + * @return bool + */ + public function isTest() + { + return $this->test; + } +} diff --git a/version/206/class/Checkout/AbstractCheckout.php b/version/206/class/Checkout/AbstractCheckout.php new file mode 100644 index 0000000..8a31700 --- /dev/null +++ b/version/206/class/Checkout/AbstractCheckout.php @@ -0,0 +1,1214 @@ + ['lang' => 'de', 'country' => ['DE', 'AT', 'CH']], + 'fre' => ['lang' => 'fr', 'country' => ['BE', 'FR']], + 'dut' => ['lang' => 'nl', 'country' => ['BE', 'NL']], + 'spa' => ['lang' => 'es', 'country' => ['ES']], + 'ita' => ['lang' => 'it', 'country' => ['IT']], + 'pol' => ['lang' => 'pl', 'country' => ['PL']], + 'hun' => ['lang' => 'hu', 'country' => ['HU']], + 'por' => ['lang' => 'pt', 'country' => ['PT']], + 'nor' => ['lang' => 'nb', 'country' => ['NO']], + 'swe' => ['lang' => 'sv', 'country' => ['SE']], + 'fin' => ['lang' => 'fi', 'country' => ['FI']], + 'dan' => ['lang' => 'da', 'country' => ['DK']], + 'ice' => ['lang' => 'is', 'country' => ['IS']], + 'eng' => ['lang' => 'en', 'country' => ['GB', 'US']], + ]; + /** + * @var null|\Mollie\Api\Resources\Customer + */ + protected $customer; + /** + * @var string + */ + private $hash; + /** + * @var API + */ + private $api; + /** + * @var JTLMollie + */ + private $paymentMethod; + /** + * @var Bestellung + */ + private $oBestellung; + /** + * @var Payment + */ + private $model; + + /** + * AbstractCheckout constructor. + * @param Bestellung $oBestellung + * @param null $api + */ + public function __construct(Bestellung $oBestellung, $api = null) + { + $this->api = $api; + $this->oBestellung = $oBestellung; + } + + /** + * @param int $kBestellung + * @param bool $checkZA + * @return bool + */ + public static function isMollie($kBestellung, $checkZA = false) + { + if ($checkZA) { + $res = Shop::DB()->executeQueryPrepared('SELECT * FROM tzahlungsart WHERE cModulId LIKE :cModulId AND kZahlungsart = :kZahlungsart', [ + ':kZahlungsart' => $kBestellung, + ':cModulId' => 'kPlugin_' . self::Plugin()->kPlugin . '_%' + ], 1); + + return (bool)$res; + } + + return ($res = Shop::DB()->executeQueryPrepared('SELECT kId FROM xplugin_ws_mollie_payments WHERE kBestellung = :kBestellung;', [ + ':kBestellung' => $kBestellung, + ], 1)) && $res->kId; + } + + public static function finalizeOrder($sessionHash, $id, $test = false) + { + try { + if ($paymentSession = Shop::DB()->select('tzahlungsession', 'cZahlungsID', $sessionHash)) { + if (session_id() !== $paymentSession->cSID) { + session_destroy(); + session_id($paymentSession->cSID); + $session = Session::getInstance(true, true); + } else { + $session = Session::getInstance(false); + } + + if ( + (!isset($paymentSession->nBezahlt) || !$paymentSession->nBezahlt) + && (!isset($paymentSession->kBestellung) || !$paymentSession->kBestellung) + && isset($_SESSION['Warenkorb']->PositionenArr) + && count($_SESSION['Warenkorb']->PositionenArr) + ) { + $paymentSession->cNotifyID = $id; + $paymentSession->dNotify = 'now()'; + Shop::DB()->update('tzahlungsession', 'cZahlungsID', $sessionHash, $paymentSession); + + $api = new API($test); + if (strpos($id, 'tr_') === 0) { + $mollie = $api->Client()->payments->get($id); + } else { + $mollie = $api->Client()->orders->get($id, ['embed' => 'payments']); + } + + if (in_array($mollie->status, [OrderStatus::STATUS_PENDING, OrderStatus::STATUS_AUTHORIZED, OrderStatus::STATUS_PAID], true)) { + require_once PFAD_ROOT . PFAD_INCLUDES . 'bestellabschluss_inc.php'; + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + $order = finalisiereBestellung(); + $session->cleanUp(); + $paymentSession->nBezahlt = 1; + $paymentSession->dZeitBezahlt = 'now()'; + } else { + throw new Exception('Mollie Status invalid: ' . $mollie->status . '\n' . print_r([$sessionHash, $id], 1)); + } + + if ($order->kBestellung) { + $paymentSession->kBestellung = $order->kBestellung; + Shop::DB()->update('tzahlungsession', 'cZahlungsID', $sessionHash, $paymentSession); + + try { + $checkout = self::fromID($id, false, $order); + } catch (Exception $e) { + if (strpos($id, 'tr_') === 0) { + $checkoutClass = PaymentCheckout::class; + } else { + $checkoutClass = OrderCheckout::class; + } + $checkout = new $checkoutClass($order, $api); + } + $checkout->setMollie($mollie)->updateModel()->saveModel(); + $checkout->updateOrderNumber(); + $checkout->handleNotification($sessionHash); + } + } else { + Jtllog::writeLog(sprintf('PaymentSession bereits bezahlt: %s - ID: %s => Queue', $sessionHash, $id), JTLLOG_LEVEL_NOTICE); + Queue::saveToQueue($id, $id, 'webhook'); + } + } else { + Jtllog::writeLog(sprintf('PaymentSession nicht gefunden: %s - ID: %s => Queue', $sessionHash, $id), JTLLOG_LEVEL_NOTICE); + Queue::saveToQueue($id, $id, 'webhook'); + } + } catch (Exception $e) { + Helper::logExc($e); + } + } + + /** + * @param string $id + * @param bool $bFill + * @param null|Bestellung $order + * @throws RuntimeException + * @return OrderCheckout|PaymentCheckout + */ + public static function fromID($id, $bFill = true, Bestellung $order = null) + { + if (($model = Payment::fromID($id))) { + return static::fromModel($model, $bFill, $order); + } + + throw new RuntimeException(sprintf('Error loading Order: %s', $id)); + } + + /** + * @param Payment $model + * @param bool $bFill + * @param null|Bestellung $order + * @return OrderCheckout|PaymentCheckout + */ + public static function fromModel($model, $bFill = true, Bestellung $order = null) + { + if (!$model) { + throw new RuntimeException(sprintf('Error loading Order for Model: %s', print_r($model, 1))); + } + + $oBestellung = $order; + if (!$order) { + $oBestellung = new Bestellung($model->kBestellung, $bFill); + if (!$oBestellung->kBestellung) { + throw new RuntimeException(sprintf('Error loading Bestellung: %s', $model->kBestellung)); + } + } + + if (strpos($model->kID, 'tr_') !== false) { + $self = new PaymentCheckout($oBestellung); + } else { + $self = new OrderCheckout($oBestellung); + } + + $self->setModel($model); + + return $self; + } + + /** + * @return bool + */ + public function saveModel() + { + return $this->getModel()->save(); + } + + /** + * @return Payment + */ + public function getModel() + { + if (!$this->model) { + $this->model = Payment::fromID($this->oBestellung->kBestellung, 'kBestellung'); + } + + return $this->model; + } + + /** + * @param $model + * @return $this + */ + protected function setModel($model) + { + if (!$this->model) { + $this->model = $model; + } else { + throw new RuntimeException('Model already set.'); + } + + return $this; + } + + /** + * @param null $hash + * @throws Exception + */ + public function handleNotification($hash = null) + { + if (!$this->getHash()) { + $this->getModel()->cHash = $hash; + } + + $this->updateModel()->saveModel(); + if (!$this->getBestellung()->dBezahltDatum || $this->getBestellung()->dBezahltDatum === '0000-00-00') { + if ($incoming = $this->getIncomingPayment()) { + $this->PaymentMethod()->addIncomingPayment($this->getBestellung(), $incoming); + + $this->PaymentMethod()->setOrderStatusToPaid($this->getBestellung()); + static::makeFetchable($this->getBestellung(), $this->getModel()); + $this->PaymentMethod()->deletePaymentHash($this->getHash()); + $this->Log(sprintf("Checkout::handleNotification: Bestellung '%s' als bezahlt markiert: %.2f %s", $this->getBestellung()->cBestellNr, (float)$incoming->fBetrag, $incoming->cISO)); + + $oZahlungsart = Shop::DB()->selectSingleRow('tzahlungsart', 'cModulId', $this->PaymentMethod()->moduleID); + if ($oZahlungsart && (int)$oZahlungsart->nMailSenden === 1) { + require_once PFAD_ROOT . 'includes/mailTools.php'; + $this->PaymentMethod()->sendConfirmationMail($this->getBestellung()); + } + if (!$this->completlyPaid()) { + $this->Log(sprintf("Checkout::handleNotification: Bestellung '%s': nicht komplett bezahlt: %.2f %s", $this->getBestellung()->cBestellNr, (float)$incoming->fBetrag, $incoming->cISO), LOGLEVEL_ERROR); + } + } + } + } + + /** + * @return string + */ + public function getHash() + { + if ($this->getModel()->cHash) { + return $this->getModel()->cHash; + } + if (!$this->hash) { + $this->hash = $this->PaymentMethod()->generateHash($this->oBestellung); + } + + return $this->hash; + } + + /** + * @return JTLMollie + */ + public function PaymentMethod() + { + if (!$this->paymentMethod) { + include_once PFAD_ROOT . PFAD_INCLUDES . 'modules/PaymentMethod.class.php'; + if ($this->getBestellung()->Zahlungsart && strpos($this->getBestellung()->Zahlungsart->cModulId, "kPlugin_{$this::Plugin()->kPlugin}_") !== false) { + try { + $this->paymentMethod = PaymentMethod::create($this->getBestellung()->Zahlungsart->cModulId); + } catch (Exception $e) { + $this->paymentMethod = PaymentMethod::create("kPlugin_{$this::Plugin()->kPlugin}_mollie"); + } + } else { + $this->paymentMethod = PaymentMethod::create("kPlugin_{$this::Plugin()->kPlugin}_mollie"); + } + } + + return $this->paymentMethod; + } + + /** + * @return Bestellung + */ + public function getBestellung() + { + if (!$this->oBestellung && $this->getModel()->kBestellung) { + $this->oBestellung = new Bestellung($this->getModel()->kBestellung, true); + } + + return $this->oBestellung; + } + + /** + * @return $this + */ + public function updateModel() + { + if ($this->getMollie()) { + $this->getModel()->kID = $this->getMollie()->id; + $this->getModel()->cLocale = $this->getMollie()->locale; + $this->getModel()->fAmount = (float)$this->getMollie()->amount->value; + $this->getModel()->cMethod = $this->getMollie()->method; + $this->getModel()->cCurrency = $this->getMollie()->amount->currency; + $this->getModel()->cStatus = $this->getMollie()->status; + if ($this->getMollie()->amountRefunded) { + $this->getModel()->fAmountRefunded = $this->getMollie()->amountRefunded->value; + } + if ($this->getMollie()->amountCaptured) { + $this->getModel()->fAmountCaptured = $this->getMollie()->amountCaptured->value; + } + $this->getModel()->cMode = $this->getMollie()->mode ?: null; + $this->getModel()->cRedirectURL = $this->getMollie()->redirectUrl; + $this->getModel()->cWebhookURL = $this->getMollie()->webhookUrl; + $this->getModel()->cCheckoutURL = $this->getMollie()->getCheckoutUrl(); + } + + $this->getModel()->kBestellung = $this->getBestellung()->kBestellung; + $this->getModel()->cOrderNumber = $this->getBestellung()->cBestellNr; + $this->getModel()->cHash = trim($this->getHash(), '_'); + + $this->getModel()->bSynced = $this->getModel()->bSynced !== null ? $this->getModel()->bSynced : Helper::getSetting('onlyPaid') !== 'Y'; + + return $this; + } + + /** + * @param false $force + * @return null|\Mollie\Api\Resources\Payment|Order + */ + abstract public function getMollie($force = false); + + /** + * @return stdClass + */ + abstract public function getIncomingPayment(); + + /** + * @param Bestellung $oBestellung + * @param Payment $model + * @return bool + */ + public static function makeFetchable(Bestellung $oBestellung, Payment $model) + { + // TODO: force ? + if ($oBestellung->cAbgeholt === 'Y' && !$model->bSynced) { + Shop::DB()->update('tbestellung', 'kBestellung', $oBestellung->kBestellung, (object)['cAbgeholt' => 'N']); + } + $model->bSynced = true; + + try { + return $model->save(); + } catch (Exception $e) { + Jtllog::writeLog(sprintf('Fehler beim speichern des Models: %s / Bestellung: %s', $model->kID, $oBestellung->cBestellNr)); + } + + return false; + } + + public function Log($msg, $level = LOGLEVEL_NOTICE) + { + try { + $data = ''; + if ($this->getBestellung()) { + $data .= '#' . $this->getBestellung()->kBestellung; + } + if ($this->getMollie()) { + $data .= '$' . $this->getMollie()->id; + } + ZahlungsLog::add($this->PaymentMethod()->moduleID, '[' . microtime(true) . ' - ' . $_SERVER['PHP_SELF'] . '] ' . $msg, $data, $level); + } catch (Exception $e) { + Jtllog::writeLog('Mollie Log failed: ' . $e->getMessage() . '; Previous Log: ' . print_r([$msg, $level, $data], 1)); + } + + return $this; + } + + /** + * @return bool + */ + public function completlyPaid() + { + if ( + $row = Shop::DB()->executeQueryPrepared('SELECT SUM(fBetrag) as fBetragSumme FROM tzahlungseingang WHERE kBestellung = :kBestellung', [ + ':kBestellung' => $this->getBestellung()->kBestellung + ], 1) + ) { + return $row->fBetragSumme >= round($this->getBestellung()->fGesamtsumme * $this->getBestellung()->fWaehrungsFaktor, 2); + } + + return false; + } + + /** + * @param $kBestellung + * * @throws RuntimeException + * @return OrderCheckout|PaymentCheckout + */ + public static function fromBestellung($kBestellung) + { + if ($model = Payment::fromID($kBestellung, 'kBestellung')) { + return static::fromModel($model); + } + + throw new RuntimeException(sprintf('Error loading Order for Bestellung: %s', $kBestellung)); + } + + public static function sendReminders() + { + $reminder = (int)self::Plugin()->oPluginEinstellungAssoc_arr['reminder']; + + if (!$reminder) { + return; + } + + $sql = 'SELECT p.kID FROM xplugin_ws_mollie_payments p JOIN tbestellung b ON b.kBestellung = p.kBestellung ' + . "WHERE (p.dReminder IS NULL OR p.dReminder = '0000-00-00 00:00:00') " + . 'AND p.dCreatedAt < NOW() - INTERVAL :d MINUTE AND p.dCreatedAt > NOW() - INTERVAL 7 DAY ' + . "AND p.cStatus IN ('created','open', 'expired', 'failed', 'canceled') AND NOT b.cStatus = '-1'"; + + $remindables = Shop::DB()->executeQueryPrepared($sql, [ + ':d' => $reminder + ], 2); + foreach ($remindables as $remindable) { + try { + self::sendReminder($remindable->kID); + } catch (Exception $e) { + Jtllog::writeLog('AbstractCheckout::sendReminders: ' . $e->getMessage()); + } + } + } + + /** + * @param $kID + * @return bool + */ + public static function sendReminder($kID) + { + $checkout = self::fromID($kID); + $return = true; + + if (!$checkout->getBestellung()->kBestellung || (int)$checkout->getBestellung()->cStatus > BESTELLUNG_STATUS_IN_BEARBEITUNG || (int)$checkout->getBestellung()->cStatus < 0) { + return $return; + } + + + try { + $repayURL = Shop::getURL() . '/?m_pay=' . md5($checkout->getModel()->kID . '-' . $checkout->getBestellung()->kBestellung); + + $data = new stdClass(); + $data->tkunde = new Kunde($checkout->getBestellung()->oKunde->kKunde); + if ($data->tkunde->kKunde) { + $data->Bestellung = $checkout->getBestellung(); + $data->PayURL = $repayURL; + $data->Amount = gibPreisStringLocalized($checkout->getModel()->fAmount, $checkout->getBestellung()->Waehrung); //Preise::getLocalizedPriceString($order->getAmount(), Currency::fromISO($order->getCurrency()), false); + + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + + $mail = new stdClass(); + $mail->toEmail = $data->tkunde->cMail; + $mail->toName = trim((isset($data->tKunde->cVorname) + ? $data->tKunde->cVorname + : '') . ' ' . ( + isset($data->tKunde->cNachname) + ? $data->tKunde->cNachname + : '' + )) ?: $mail->toEmail; + $data->mail = $mail; + if (!($sentMail = sendeMail('kPlugin_' . self::Plugin()->kPlugin . '_zahlungserinnerung', $data))) { + $checkout->Log(sprintf("Zahlungserinnerung konnte nicht versand werden: %s\n%s", isset($sentMail->cFehler) ?: print_r($sentMail, 1), print_r($data, 1)), LOGLEVEL_ERROR); + $return = false; + } else { + $checkout->Log(sprintf('Zahlungserinnerung für %s verschickt.', $checkout->getBestellung()->cBestellNr)); + } + } else { + $checkout->Log("Kunde '{$checkout->getBestellung()->oKunde->kKunde}' nicht gefunden.", LOGLEVEL_ERROR); + } + } catch (Exception $e) { + $checkout->Log(sprintf('AbstractCheckout::sendReminder: Zahlungserinnerung für %s fehlgeschlagen: %s', $checkout->getBestellung()->cBestellNr, $e->getMessage())); + $return = false; + } + $checkout->getModel()->dReminder = date('Y-m-d H:i:s'); + $checkout->getModel()->save(); + + return $return; + } + + /** + * @param AbstractCheckout $checkout + * @throws ApiException + * @return BaseResource|Refund + */ + public static function refund(self $checkout) + { + if ($checkout->getMollie()->resource === 'order') { + /** @var Order $order */ + $order = $checkout->getMollie(); + if (in_array($order->status, [OrderStatus::STATUS_CANCELED, OrderStatus::STATUS_EXPIRED, OrderStatus::STATUS_CREATED], true)) { + throw new RuntimeException('Bestellung kann derzeit nicht zurückerstattet werden.'); + } + $refund = $order->refundAll(); + $checkout->Log(sprintf('Bestellung wurde manuell zurückerstattet: %s', $refund->id)); + + return $refund; + } + if ($checkout->getMollie()->resource === 'payment') { + /** @var \Mollie\Api\Resources\Payment $payment */ + $payment = $checkout->getMollie(); + if (in_array($payment->status, [PaymentStatus::STATUS_CANCELED, PaymentStatus::STATUS_EXPIRED, PaymentStatus::STATUS_OPEN], true)) { + throw new RuntimeException('Zahlung kann derzeit nicht zurückerstattet werden.'); + } + $refund = $checkout->API()->Client()->payments->refund($checkout->getMollie(), ['amount' => $checkout->getMollie()->amount]); + $checkout->Log(sprintf('Zahlung wurde manuell zurückerstattet: %s', $refund->id)); + + return $refund; + } + + throw new RuntimeException(sprintf('Unbekannte Resource: %s', $checkout->getMollie()->resource)); + } + + /** + * @return API + */ + public function API() + { + if (!$this->api) { + if ($this->getModel()->kID) { + $this->api = new API($this->getModel()->cMode === 'test'); + } else { + $this->api = new API(API::getMode()); + } + } + + return $this->api; + } + + /** + * @param $checkout + * @throws ApiException + * @return \Mollie\Api\Resources\Payment|Order + */ + public static function cancel($checkout) + { + if ($checkout instanceof OrderCheckout) { + return OrderCheckout::cancel($checkout); + } + if ($checkout instanceof PaymentCheckout) { + return PaymentCheckout::cancel($checkout); + } + + throw new RuntimeException('AbstractCheckout::cancel: Invalid Checkout!'); + } + + public static function getLocales() + { + $locales = [ + 'en_US', + 'nl_NL', + 'nl_BE', + 'fr_FR', + 'fr_BE', + 'de_DE', + 'de_AT', + 'de_CH', + 'es_ES', + 'ca_ES', + 'pt_PT', + 'it_IT', + 'nb_NO', + 'sv_SE', + 'fi_FI', + 'da_DK', + 'is_IS', + 'hu_HU', + 'pl_PL', + 'lv_LV', + 'lt_LT',]; + + $laender = []; + $shopLaender = Shop::DB()->executeQuery('SELECT cLaender FROM tversandart', 2); + foreach ($shopLaender as $sL) { + $laender = array_merge(explode(' ', $sL->cLaender)); + } + $laender = array_unique($laender); + + $result = []; + $shopSprachen = Shop::DB()->executeQuery('SELECT * FROM tsprache', 2); + foreach ($shopSprachen as $sS) { + foreach ($laender as $land) { + $result[] = static::getLocale($sS->cISO, $land); + } + } + + return array_intersect(array_unique($result), $locales); + } + + public static function getLocale($cISOSprache = null, $country = null) + { + if ($cISOSprache === null) { + $cISOSprache = gibStandardsprache()->cISO; + } + if (array_key_exists($cISOSprache, self::$localeLangs)) { + $locale = self::$localeLangs[$cISOSprache]['lang']; + if ($country && is_array(self::$localeLangs[$cISOSprache]['country']) && in_array($country, self::$localeLangs[$cISOSprache]['country'], true)) { + $locale .= '_' . strtoupper($country); + } else { + $locale .= '_' . self::$localeLangs[$cISOSprache]['country'][0]; + } + + return $locale; + } + + return self::Plugin()->oPluginEinstellungAssoc_arr['fallbackLocale']; + } + + public static function getCurrencies() + { + $currencies = ['AED' => 'AED - United Arab Emirates dirham', + 'AFN' => 'AFN - Afghan afghani', + 'ALL' => 'ALL - Albanian lek', + 'AMD' => 'AMD - Armenian dram', + 'ANG' => 'ANG - Netherlands Antillean guilder', + 'AOA' => 'AOA - Angolan kwanza', + 'ARS' => 'ARS - Argentine peso', + 'AUD' => 'AUD - Australian dollar', + 'AWG' => 'AWG - Aruban florin', + 'AZN' => 'AZN - Azerbaijani manat', + 'BAM' => 'BAM - Bosnia and Herzegovina convertible mark', + 'BBD' => 'BBD - Barbados dollar', + 'BDT' => 'BDT - Bangladeshi taka', + 'BGN' => 'BGN - Bulgarian lev', + 'BHD' => 'BHD - Bahraini dinar', + 'BIF' => 'BIF - Burundian franc', + 'BMD' => 'BMD - Bermudian dollar', + 'BND' => 'BND - Brunei dollar', + 'BOB' => 'BOB - Boliviano', + 'BRL' => 'BRL - Brazilian real', + 'BSD' => 'BSD - Bahamian dollar', + 'BTN' => 'BTN - Bhutanese ngultrum', + 'BWP' => 'BWP - Botswana pula', + 'BYN' => 'BYN - Belarusian ruble', + 'BZD' => 'BZD - Belize dollar', + 'CAD' => 'CAD - Canadian dollar', + 'CDF' => 'CDF - Congolese franc', + 'CHF' => 'CHF - Swiss franc', + 'CLP' => 'CLP - Chilean peso', + 'CNY' => 'CNY - Renminbi (Chinese) yuan', + 'COP' => 'COP - Colombian peso', + 'COU' => 'COU - Unidad de Valor Real (UVR)', + 'CRC' => 'CRC - Costa Rican colon', + 'CUC' => 'CUC - Cuban convertible peso', + 'CUP' => 'CUP - Cuban peso', + 'CVE' => 'CVE - Cape Verde escudo', + 'CZK' => 'CZK - Czech koruna', + 'DJF' => 'DJF - Djiboutian franc', + 'DKK' => 'DKK - Danish krone', + 'DOP' => 'DOP - Dominican peso', + 'DZD' => 'DZD - Algerian dinar', + 'EGP' => 'EGP - Egyptian pound', + 'ERN' => 'ERN - Eritrean nakfa', + 'ETB' => 'ETB - Ethiopian birr', + 'EUR' => 'EUR - Euro', + 'FJD' => 'FJD - Fiji dollar', + 'FKP' => 'FKP - Falkland Islands pound', + 'GBP' => 'GBP - Pound sterling', + 'GEL' => 'GEL - Georgian lari', + 'GHS' => 'GHS - Ghanaian cedi', + 'GIP' => 'GIP - Gibraltar pound', + 'GMD' => 'GMD - Gambian dalasi', + 'GNF' => 'GNF - Guinean franc', + 'GTQ' => 'GTQ - Guatemalan quetzal', + 'GYD' => 'GYD - Guyanese dollar', + 'HKD' => 'HKD - Hong Kong dollar', + 'HNL' => 'HNL - Honduran lempira', + 'HRK' => 'HRK - Croatian kuna', + 'HTG' => 'HTG - Haitian gourde', + 'HUF' => 'HUF - Hungarian forint', + 'IDR' => 'IDR - Indonesian rupiah', + 'ILS' => 'ILS - Israeli new shekel', + 'INR' => 'INR - Indian rupee', + 'IQD' => 'IQD - Iraqi dinar', + 'IRR' => 'IRR - Iranian rial', + 'ISK' => 'ISK - Icelandic krÛna', + 'JMD' => 'JMD - Jamaican dollar', + 'JOD' => 'JOD - Jordanian dinar', + 'JPY' => 'JPY - Japanese yen', + 'KES' => 'KES - Kenyan shilling', + 'KGS' => 'KGS - Kyrgyzstani som', + 'KHR' => 'KHR - Cambodian riel', + 'KMF' => 'KMF - Comoro franc', + 'KPW' => 'KPW - North Korean won', + 'KRW' => 'KRW - South Korean won', + 'KWD' => 'KWD - Kuwaiti dinar', + 'KYD' => 'KYD - Cayman Islands dollar', + 'KZT' => 'KZT - Kazakhstani tenge', + 'LAK' => 'LAK - Lao kip', + 'LBP' => 'LBP - Lebanese pound', + 'LKR' => 'LKR - Sri Lankan rupee', + 'LRD' => 'LRD - Liberian dollar', + 'LSL' => 'LSL - Lesotho loti', + 'LYD' => 'LYD - Libyan dinar', + 'MAD' => 'MAD - Moroccan dirham', + 'MDL' => 'MDL - Moldovan leu', + 'MGA' => 'MGA - Malagasy ariary', + 'MKD' => 'MKD - Macedonian denar', + 'MMK' => 'MMK - Myanmar kyat', + 'MNT' => 'MNT - Mongolian t?gr?g', + 'MOP' => 'MOP - Macanese pataca', + 'MRU' => 'MRU - Mauritanian ouguiya', + 'MUR' => 'MUR - Mauritian rupee', + 'MVR' => 'MVR - Maldivian rufiyaa', + 'MWK' => 'MWK - Malawian kwacha', + 'MXN' => 'MXN - Mexican peso', + 'MXV' => 'MXV - Mexican Unidad de Inversion (UDI)', + 'MYR' => 'MYR - Malaysian ringgit', + 'MZN' => 'MZN - Mozambican metical', + 'NAD' => 'NAD - Namibian dollar', + 'NGN' => 'NGN - Nigerian naira', + 'NIO' => 'NIO - Nicaraguan cÛrdoba', + 'NOK' => 'NOK - Norwegian krone', + 'NPR' => 'NPR - Nepalese rupee', + 'NZD' => 'NZD - New Zealand dollar', + 'OMR' => 'OMR - Omani rial', + 'PAB' => 'PAB - Panamanian balboa', + 'PEN' => 'PEN - Peruvian sol', + 'PGK' => 'PGK - Papua New Guinean kina', + 'PHP' => 'PHP - Philippine peso', + 'PKR' => 'PKR - Pakistani rupee', + 'PLN' => 'PLN - Polish z?oty', + 'PYG' => 'PYG - Paraguayan guaranÌ', + 'QAR' => 'QAR - Qatari riyal', + 'RON' => 'RON - Romanian leu', + 'RSD' => 'RSD - Serbian dinar', + 'RUB' => 'RUB - Russian ruble', + 'RWF' => 'RWF - Rwandan franc', + 'SAR' => 'SAR - Saudi riyal', + 'SBD' => 'SBD - Solomon Islands dollar', + 'SCR' => 'SCR - Seychelles rupee', + 'SDG' => 'SDG - Sudanese pound', + 'SEK' => 'SEK - Swedish krona/kronor', + 'SGD' => 'SGD - Singapore dollar', + 'SHP' => 'SHP - Saint Helena pound', + 'SLL' => 'SLL - Sierra Leonean leone', + 'SOS' => 'SOS - Somali shilling', + 'SRD' => 'SRD - Surinamese dollar', + 'SSP' => 'SSP - South Sudanese pound', + 'STN' => 'STN - S?o TomÈ and PrÌncipe dobra', + 'SVC' => 'SVC - Salvadoran colÛn', + 'SYP' => 'SYP - Syrian pound', + 'SZL' => 'SZL - Swazi lilangeni', + 'THB' => 'THB - Thai baht', + 'TJS' => 'TJS - Tajikistani somoni', + 'TMT' => 'TMT - Turkmenistan manat', + 'TND' => 'TND - Tunisian dinar', + 'TOP' => 'TOP - Tongan pa?anga', + 'TRY' => 'TRY - Turkish lira', + 'TTD' => 'TTD - Trinidad and Tobago dollar', + 'TWD' => 'TWD - New Taiwan dollar', + 'TZS' => 'TZS - Tanzanian shilling', + 'UAH' => 'UAH - Ukrainian hryvnia', + 'UGX' => 'UGX - Ugandan shilling', + 'USD' => 'USD - United States dollar', + 'UYI' => 'UYI - Uruguay Peso en Unidades Indexadas', + 'UYU' => 'UYU - Uruguayan peso', + 'UYW' => 'UYW - Unidad previsional', + 'UZS' => 'UZS - Uzbekistan som', + 'VES' => 'VES - Venezuelan bolÌvar soberano', + 'VND' => 'VND - Vietnamese ??ng', + 'VUV' => 'VUV - Vanuatu vatu', + 'WST' => 'WST - Samoan tala', + 'YER' => 'YER - Yemeni rial', + 'ZAR' => 'ZAR - South African rand', + 'ZMW' => 'ZMW - Zambian kwacha', + 'ZWL' => 'ZWL - Zimbabwean dollar']; + + $shopCurrencies = Shop::DB()->executeQuery('SELECT * FROM twaehrung', 2); + + $result = []; + + foreach ($shopCurrencies as $sC) { + if (array_key_exists($sC->cISO, $currencies)) { + $result[$sC->cISO] = $currencies[$sC->cISO]; + } + } + + return $result; + } + + public function loadRequest(&$options = []) + { + $oKunde = !$this->getBestellung()->oKunde && $this->PaymentMethod()->duringCheckout ? $_SESSION['Kunde'] : $this->getBestellung()->oKunde; + if ($this->getBestellung()) { + if ( + $oKunde->nRegistriert + && ( + $customer = $this->getCustomer( + array_key_exists( + 'mollie_create_customer', + $_SESSION['cPost_arr'] ?: [] + ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', + $oKunde + ) + ) + && isset($customer) + ) { + $options['customerId'] = $customer->id; + } + $this->amount = Amount::factory($this->getBestellung()->fGesamtsummeKundenwaehrung, $this->getBestellung()->Waehrung->cISO, true); + $this->redirectUrl = $this->PaymentMethod()->duringCheckout ? Shop::getURL() . '/bestellabschluss.php?' . http_build_query(['hash' => $this->getHash()]) : $this->PaymentMethod()->getReturnURL($this->getBestellung()); + $this->metadata = [ + 'kBestellung' => $this->getBestellung()->kBestellung ?: $this->getBestellung()->cBestellNr, + 'kKunde' => $this->getBestellung()->kKunde, + 'kKundengruppe' => Session::getInstance()->CustomerGroup()->kKundengruppe, + 'cHash' => $this->getHash(), + ]; + } + + $this->locale = self::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer()->cLand); + $this->webhookUrl = Shop::getURL(true) . '/?' . http_build_query([ + 'mollie' => 1, + 'hash' => $this->getHash(), + 'test' => $this->API()->isTest() ?: null, + ]); + + $pm = $this->PaymentMethod(); + $isPayAgain = strpos($_SERVER['PHP_SELF'], 'bestellab_again') !== false; + if ($pm::METHOD !== '' && (self::Plugin()->oPluginEinstellungAssoc_arr['resetMethod'] !== 'Y' || !$isPayAgain)) { + $this->method = $pm::METHOD; + } + } + + /** + * @todo: Kunde wieder löschbar machen ?! + * @param mixed $createOrUpdate + * @param null|mixed $oKunde + * @return null|\Mollie\Api\Resources\Customer + */ + public function getCustomer($createOrUpdate = false, $oKunde = null) + { + if (!$oKunde) { + $oKunde = $this->getBestellung()->oKunde; + } + + if (!$this->customer) { + $customerModel = Customer::fromID($oKunde->kKunde, 'kKunde'); + if ($customerModel->customerId) { + try { + $this->customer = $this->API()->Client()->customers->get($customerModel->customerId); + } catch (ApiException $e) { + $this->Log(sprintf('Fehler beim laden des Mollie Customers %s (kKunde: %d): %s', $customerModel->customerId, $customerModel->kKunde, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + if ($createOrUpdate) { + $customer = [ + 'name' => utf8_encode(trim($oKunde->cVorname . ' ' . $oKunde->cNachname)), + 'email' => utf8_encode($oKunde->cMail), + 'locale' => self::getLocale($_SESSION['cISOSprache'], $oKunde->cLand), + 'metadata' => (object)[ + 'kKunde' => $oKunde->kKunde, + 'kKundengruppe' => $oKunde->kKundengruppe, + 'cKundenNr' => utf8_encode($oKunde->cKundenNr), + ], + ]; + + if ($this->customer) { // UPDATE + $this->customer->name = $customer['name']; + $this->customer->email = $customer['email']; + $this->customer->locale = $customer['locale']; + $this->customer->metadata = $customer['metadata']; + + try { + $this->customer->update(); + } catch (Exception $e) { + $this->Log(sprintf("Fehler beim aktualisieren des Mollie Customers %s: %s\n%s", $this->customer->id, $e->getMessage(), print_r($customer, 1)), LOGLEVEL_ERROR); + } + } else { // create + try { + $this->customer = $this->API()->Client()->customers->create($customer); + $customerModel->kKunde = $oKunde->kKunde; + $customerModel->customerId = $this->customer->id; + $customerModel->save(); + $this->Log(sprintf("Customer '%s' für Kunde %s (%d) bei Mollie angelegt.", $this->customer->id, $this->customer->name, $this->getBestellung()->kKunde)); + } catch (Exception $e) { + $this->Log(sprintf("Fehler beim anlegen eines Mollie Customers: %s\n%s", $e->getMessage(), print_r($customer, 1)), LOGLEVEL_ERROR); + } + } + } + } + + return $this->customer; + } + + /** + * Storno Order + */ + public function storno() + { + if (in_array((int)$this->getBestellung()->cStatus, [BESTELLUNG_STATUS_OFFEN, BESTELLUNG_STATUS_IN_BEARBEITUNG], true)) { + $log = []; + + $conf = Shop::getSettings([CONF_GLOBAL, CONF_TRUSTEDSHOPS]); + $nArtikelAnzeigefilter = (int)$conf['global']['artikel_artikelanzeigefilter']; + + foreach ($this->getBestellung()->Positionen as $pos) { + if ($pos->kArtikel && $pos->Artikel && $pos->Artikel->cLagerBeachten === 'Y') { + $log[] = sprintf('Reset stock of "%s" by %d', $pos->Artikel->cArtNr, -1 * $pos->nAnzahl); + self::aktualisiereLagerbestand($pos->Artikel, -1 * $pos->nAnzahl, $pos->WarenkorbPosEigenschaftArr, $nArtikelAnzeigefilter); + } + } + $log[] = sprintf("Cancel order '%s'.", $this->getBestellung()->cBestellNr); + + if (Shop::DB()->executeQueryPrepared('UPDATE tbestellung SET cAbgeholt = \'Y\', cStatus = :cStatus WHERE kBestellung = :kBestellung', [':cStatus' => '-1', ':kBestellung' => $this->getBestellung()->kBestellung], 3)) { + $this->Log(implode('\n', $log)); + } + } + } + + protected static function aktualisiereLagerbestand($Artikel, $nAnzahl, $WarenkorbPosEigenschaftArr, $nArtikelAnzeigefilter = 1) + { + $artikelBestand = (float)$Artikel->fLagerbestand; + + if (isset($Artikel->cLagerBeachten) && $Artikel->cLagerBeachten === 'Y') { + if ( + $Artikel->cLagerVariation === 'Y' && is_array($WarenkorbPosEigenschaftArr) && count($WarenkorbPosEigenschaftArr) > 0 + ) { + foreach ($WarenkorbPosEigenschaftArr as $eWert) { + $EigenschaftWert = new EigenschaftWert($eWert->kEigenschaftWert); + if ($EigenschaftWert->fPackeinheit === .0) { + $EigenschaftWert->fPackeinheit = 1; + } + Shop::DB()->query( + 'UPDATE teigenschaftwert + SET fLagerbestand = fLagerbestand - ' . ($nAnzahl * $EigenschaftWert->fPackeinheit) . ' + WHERE kEigenschaftWert = ' . (int)$eWert->kEigenschaftWert, + 4 + ); + } + } elseif ($Artikel->fPackeinheit > 0) { + // Stückliste + if ($Artikel->kStueckliste > 0) { + $artikelBestand = self::aktualisiereStuecklistenLagerbestand($Artikel, $nAnzahl); + } else { + Shop::DB()->query( + 'UPDATE tartikel + SET fLagerbestand = IF (fLagerbestand >= ' . ($nAnzahl * $Artikel->fPackeinheit) . ', + (fLagerbestand - ' . ($nAnzahl * $Artikel->fPackeinheit) . '), fLagerbestand) + WHERE kArtikel = ' . (int)$Artikel->kArtikel, + 4 + ); + $tmpArtikel = Shop::DB()->select('tartikel', 'kArtikel', (int)$Artikel->kArtikel, null, null, null, null, false, 'fLagerbestand'); + if ($tmpArtikel !== null) { + $artikelBestand = (float)$tmpArtikel->fLagerbestand; + } + // Stücklisten Komponente + if (ArtikelHelper::isStuecklisteKomponente($Artikel->kArtikel)) { + self::aktualisiereKomponenteLagerbestand($Artikel->kArtikel, $artikelBestand, isset($Artikel->cLagerKleinerNull) && $Artikel->cLagerKleinerNull === 'Y'); + } + } + // Aktualisiere Merkmale in tartikelmerkmal vom Vaterartikel + if ($Artikel->kVaterArtikel > 0) { + Artikel::beachteVarikombiMerkmalLagerbestand($Artikel->kVaterArtikel, $nArtikelAnzeigefilter); + } + } + } + + return $artikelBestand; + } + + protected static function aktualisiereStuecklistenLagerbestand($oStueckListeArtikel, $nAnzahl) + { + $nAnzahl = (float)$nAnzahl; + $kStueckListe = (int)$oStueckListeArtikel->kStueckliste; + $bestandAlt = (float)$oStueckListeArtikel->fLagerbestand; + $bestandNeu = $bestandAlt; + $bestandUeberverkauf = $bestandAlt; + + if ($nAnzahl > 0) { + // Gibt es lagerrelevante Komponenten in der Stückliste? + $oKomponente_arr = Shop::DB()->query( + "SELECT tstueckliste.kArtikel, tstueckliste.fAnzahl + FROM tstueckliste + JOIN tartikel + ON tartikel.kArtikel = tstueckliste.kArtikel + WHERE tstueckliste.kStueckliste = $kStueckListe + AND tartikel.cLagerBeachten = 'Y'", + 2 + ); + + if (is_array($oKomponente_arr) && count($oKomponente_arr) > 0) { + // wenn ja, dann wird für diese auch der Bestand aktualisiert + $options = Artikel::getDefaultOptions(); + + $options->nKeineSichtbarkeitBeachten = 1; + + foreach ($oKomponente_arr as $oKomponente) { + $tmpArtikel = new Artikel(); + $tmpArtikel->fuelleArtikel($oKomponente->kArtikel, $options); + + $komponenteBestand = floor(self::aktualisiereLagerbestand($tmpArtikel, $nAnzahl * $oKomponente->fAnzahl, null) / $oKomponente->fAnzahl); + + if ($komponenteBestand < $bestandNeu && $tmpArtikel->cLagerKleinerNull !== 'Y') { + // Neuer Bestand ist der Kleinste Komponententbestand aller Artikel ohne Überverkauf + $bestandNeu = $komponenteBestand; + } elseif ($komponenteBestand < $bestandUeberverkauf) { + // Für Komponenten mit Überverkauf wird der kleinste Bestand ermittelt. + $bestandUeberverkauf = $komponenteBestand; + } + } + } + + // Ist der alte gleich dem neuen Bestand? + if ($bestandAlt === $bestandNeu) { + // Es sind keine lagerrelevanten Komponenten vorhanden, die den Bestand der Stückliste herabsetzen. + if ($bestandUeberverkauf === $bestandNeu) { + // Es gibt auch keine Komponenten mit Überverkäufen, die den Bestand verringern, deshalb wird + // der Bestand des Stücklistenartikels anhand des Verkaufs verringert + $bestandNeu = $bestandNeu - $nAnzahl * $oStueckListeArtikel->fPackeinheit; + } else { + // Da keine lagerrelevanten Komponenten vorhanden sind, wird der kleinste Bestand der + // Komponentent mit Überverkauf verwendet. + $bestandNeu = $bestandUeberverkauf; + } + + Shop::DB()->update('tartikel', 'kArtikel', (int)$oStueckListeArtikel->kArtikel, (object)[ + 'fLagerbestand' => $bestandNeu, + ]); + } + // Kein Lagerbestands-Update für die Stückliste notwendig! Dies erfolgte bereits über die Komponentenabfrage und + // die dortige Lagerbestandsaktualisierung! + } + + return $bestandNeu; + } + + protected static function aktualisiereKomponenteLagerbestand($kKomponenteArtikel, $fLagerbestand, $bLagerKleinerNull) + { + $kKomponenteArtikel = (int)$kKomponenteArtikel; + $fLagerbestand = (float)$fLagerbestand; + + $oStueckliste_arr = Shop::DB()->query( + "SELECT tstueckliste.kStueckliste, tstueckliste.fAnzahl, + tartikel.kArtikel, tartikel.fLagerbestand, tartikel.cLagerKleinerNull + FROM tstueckliste + JOIN tartikel + ON tartikel.kStueckliste = tstueckliste.kStueckliste + WHERE tstueckliste.kArtikel = $kKomponenteArtikel + AND tartikel.cLagerBeachten = 'Y'", + 2 + ); + + if (is_array($oStueckliste_arr) && count($oStueckliste_arr) > 0) { + foreach ($oStueckliste_arr as $oStueckliste) { + // Ist der aktuelle Bestand der Stückliste größer als dies mit dem Bestand der Komponente möglich wäre? + $maxAnzahl = floor($fLagerbestand / $oStueckliste->fAnzahl); + if ($maxAnzahl < (float)$oStueckliste->fLagerbestand && (!$bLagerKleinerNull || $oStueckliste->cLagerKleinerNull === 'Y')) { + // wenn ja, dann den Bestand der Stückliste entsprechend verringern, aber nur wenn die Komponente nicht + // überberkaufbar ist oder die gesamte Stückliste Überverkäufe zulässt + Shop::DB()->update('tartikel', 'kArtikel', (int)$oStueckliste->kArtikel, (object)[ + 'fLagerbestand' => $maxAnzahl, + ]); + } + } + } + } + + /** + * @return null|array|bool|int|object + */ + public function getLogs() + { + return Shop::DB()->executeQueryPrepared('SELECT * FROM tzahlungslog WHERE cLogData LIKE :kBestellung OR cLogData LIKE :cBestellNr OR cLogData LIKE :MollieID ORDER BY dDatum DESC, cLog DESC', [ + ':kBestellung' => '%#' . ($this->getBestellung()->kBestellung ?: '##') . '%', + ':cBestellNr' => '%§' . ($this->getBestellung()->cBestellNr ?: '§§') . '%', + ':MollieID' => '%$' . ($this->getMollie()->id ?: '$$') . '%', + ], 2); + } + + /** + * @return bool + */ + public function remindable() + { + return (int)$this->getBestellung()->cStatus !== BESTELLUNG_STATUS_STORNO && !in_array($this->getModel()->cStatus, [PaymentStatus::STATUS_PAID, PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PENDING, OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_SHIPPING], true); + } + + /** + * @return string + */ + public function LogData() + { + $data = ''; + if ($this->getBestellung()->kBestellung) { + $data .= '#' . $this->getBestellung()->kBestellung; + } + if ($this->getMollie()) { + $data .= '$' . $this->getMollie()->id; + } + + return $data; + } + + abstract public function cancelOrRefund($force = false); + + /** + * @param array $paymentOptions + * @return Order|Payment + */ + abstract public function create(array $paymentOptions = []); + + /** + * @return string + */ + public function getRepayURL() + { + return Shop::getURL(true) . '/?m_pay=' . md5($this->getModel()->kID . '-' . $this->getBestellung()->kBestellung); + } + + public function getDescription() + { + $descTemplate = trim(Helper::getSetting('paymentDescTpl')) ?: 'Order {orderNumber}'; + $oKunde = $this->getBestellung()->oKunde ?: $_SESSION['Kunde']; + + return str_replace([ + '{orderNumber}', + '{storeName}', + '{customer.firstname}', + '{customer.lastname}', + '{customer.company}', + ], [ + $this->getBestellung()->cBestellNr, + Shop::getSettings([CONF_GLOBAL])['global']['global_shopname'], + $oKunde->cVorname, + $oKunde->cNachname, + $oKunde->cFirma + + ], $descTemplate); + } + + abstract protected function updateOrderNumber(); + + /** + * @param Bestellung $oBestellung + * @return $this + */ + protected function setBestellung(Bestellung $oBestellung) + { + $this->oBestellung = $oBestellung; + + return $this; + } + + /** + * @param \Mollie\Api\Resources\Payment|Order $model + * @return self + */ + abstract protected function setMollie($model); +} diff --git a/version/206/class/Checkout/AbstractResource.php b/version/206/class/Checkout/AbstractResource.php new file mode 100644 index 0000000..7c4439e --- /dev/null +++ b/version/206/class/Checkout/AbstractResource.php @@ -0,0 +1,18 @@ +title = substr(trim(($address->cAnrede === 'm' ? Shop::Lang()->get('mr') : Shop::Lang()->get('mrs')) . ' ' . $address->cTitel) ?: null, 0, 20); + $resource->givenName = $address->cVorname; + $resource->familyName = $address->cNachname; + $resource->email = $address->cMail ?: null; + + if ($organizationName = trim($address->cFirma)) { + $resource->organizationName = $organizationName; + } + + // Validity-Check + // TODO: Phone, with E.164 check + // TODO: Is Email-Format Check needed? + if (!$resource->givenName || !$resource->familyName || !$resource->email) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['givenName', 'familyName', 'email'], $resource); + } + + return $resource; + } +} diff --git a/version/206/class/Checkout/Order/OrderLine.php b/version/206/class/Checkout/Order/OrderLine.php new file mode 100644 index 0000000..757edf7 --- /dev/null +++ b/version/206/class/Checkout/Order/OrderLine.php @@ -0,0 +1,237 @@ +totalAmount->value; + } + if (abs($sum - (float)$amount->value) > 0) { + $diff = (round((float)$amount->value - $sum, 2)); + if ($diff !== 0.0) { + $line = new self(); + $line->type = $diff > 0 ? OrderLineType::TYPE_SURCHARGE : OrderLineType::TYPE_DISCOUNT; + // TODO: Translation needed? + $line->name = 'Rundungsausgleich'; + $line->quantity = 1; + $line->unitPrice = Amount::factory($diff, $currency); + $line->totalAmount = Amount::factory($diff, $currency); + $line->vatRate = '0.00'; + $line->vatAmount = Amount::factory(0, $currency); + + return $line; + } + } + + return null; + } + + /** + * @param Bestellung $oBestellung + * @return OrderLine + */ + public static function getCredit(Bestellung $oBestellung) + { + $line = new self(); + $line->type = OrderLineType::TYPE_STORE_CREDIT; + $line->name = 'Guthaben'; + $line->quantity = 1; + // TODO: check currency of Guthaben + $line->unitPrice = Amount::factory($oBestellung->fGuthaben, $oBestellung->Waehrung->cISO); + $line->totalAmount = $line->unitPrice; + $line->vatRate = '0.00'; + $line->vatAmount = Amount::factory(0, $oBestellung->Waehrung->cISO); + + return $line; + } + + /** + * @param stdClass|WarenkorbPos $oPosition + * @param null $currency + * @return OrderLine + */ + public static function factory($oPosition, $currency = null) + { + if (!$oPosition) { + throw new RuntimeException('$oPosition invalid:', print_r($oPosition, 1)); + } + + $resource = new static(); + + $resource->fill($oPosition, $currency); + + // Validity Check + if ( + !$resource->name || !$resource->quantity || !$resource->unitPrice || !$resource->totalAmount + || !$resource->vatRate || !$resource->vatAmount + ) { + throw ResourceValidityException::trigger( + ResourceValidityException::ERROR_REQUIRED, + ['name', 'quantity', 'unitPrice', 'totalAmount', 'vatRate', 'vatAmount'], + $resource + ); + } + + return $resource; + } + + /** + * @param stdClass|WarenkorbPos $oPosition + * @param null|stdClass $currency + * @return $this + * @todo Setting for Fraction handling needed? + */ + protected function fill($oPosition, $currency = null) + { + if (!$currency) { + $currency = Amount::FallbackCurrency(); + } + + $isKupon = (int)$oPosition->nPosTyp === (int)C_WARENKORBPOS_TYP_KUPON; + $isFrac = fmod($oPosition->nAnzahl, 1) !== 0.0; + + // Kupon? set vatRate to 0 and adjust netto + $vatRate = $isKupon ? 0 : (float)$oPosition->fMwSt / 100; + $netto = $isKupon ? round($oPosition->fPreis * (1 + $vatRate), 4) : round($oPosition->fPreis, 4); + + // Fraction? transform, as it were 1, and set quantity to 1 + $netto = round(($isFrac ? $netto * (float)$oPosition->nAnzahl : $netto) * $currency->fFaktor, 4); + $this->quantity = $isFrac ? 1 : (int)$oPosition->nAnzahl; + + // Fraction? include quantity and unit in name + $this->name = $isFrac ? sprintf('%s (%.2f %s)', $oPosition->cName, (float)$oPosition->nAnzahl, $oPosition->cEinheit) : $oPosition->cName; + if (!$this->name) { + $this->name = '(null)'; + } + $this->mapType($oPosition->nPosTyp, $netto > 0); + + //$unitPriceNetto = round(($currency->fFaktor * $netto), 4); + + $this->unitPrice = Amount::factory(round($netto * (1 + $vatRate), 2), $currency->cISO, false); + $this->totalAmount = Amount::factory(round($this->quantity * $this->unitPrice->value, 2), $currency->cISO, false); + + $this->vatRate = number_format($vatRate * 100, 2); + $this->vatAmount = Amount::factory(round($this->totalAmount->value - ($this->totalAmount->value / (1 + $vatRate)), 2), $currency->cISO, false); + + $metadata = []; + + // Is Artikel ? + if (isset($oPosition->Artikel)) { + $this->sku = $oPosition->Artikel->cArtNr; + $metadata['kArtikel'] = $oPosition->kArtikel; + if ($oPosition->cUnique !== '') { + $metadata['cUnique'] = utf8_encode($oPosition->cUnique); + } + } + + if (isset($oPosition->WarenkorbPosEigenschaftArr) && is_array($oPosition->WarenkorbPosEigenschaftArr) && count($oPosition->WarenkorbPosEigenschaftArr)) { + $metadata['properties'] = []; + /** @var WarenkorbPosEigenschaft $warenkorbPosEigenschaft */ + foreach ($oPosition->WarenkorbPosEigenschaftArr as $warenkorbPosEigenschaft) { + $metadata['properties'][] = [ + 'kEigenschaft' => $warenkorbPosEigenschaft->kEigenschaft, + 'kEigenschaftWert' => $warenkorbPosEigenschaft->kEigenschaftWert, + 'name' => utf8_encode($warenkorbPosEigenschaft->cEigenschaftName), + 'value' => utf8_encode($warenkorbPosEigenschaft->cEigenschaftWertName), + ]; + if (strlen(json_encode($metadata)) > 1000) { + array_pop($metadata['properties']); + + break; + } + } + } + if (json_encode($metadata) !== false) { + $this->metadata = $metadata; + } + + return $this; + } + + /** + * @param $nPosTyp + * @param $positive + * @return OrderLine + */ + protected function mapType($nPosTyp, $positive) + { + switch ($nPosTyp) { + case C_WARENKORBPOS_TYP_ARTIKEL: + case C_WARENKORBPOS_TYP_GRATISGESCHENK: + // TODO: digital / Download Artikel? + $this->type = OrderLineType::TYPE_PHYSICAL; + + return $this; + + case C_WARENKORBPOS_TYP_VERSANDPOS: + $this->type = OrderLineType::TYPE_SHIPPING_FEE; + + return $this; + + case C_WARENKORBPOS_TYP_GUTSCHEIN: + case C_WARENKORBPOS_TYP_KUPON: + case C_WARENKORBPOS_TYP_NEUKUNDENKUPON: + $this->type = OrderLineType::TYPE_DISCOUNT; + + return $this; + + case C_WARENKORBPOS_TYP_VERPACKUNG: + case C_WARENKORBPOS_TYP_VERSANDZUSCHLAG: + case C_WARENKORBPOS_TYP_ZAHLUNGSART: + case C_WARENKORBPOS_TYP_VERSAND_ARTIKELABHAENGIG: + case C_WARENKORBPOS_TYP_NACHNAHMEGEBUEHR: + $this->type = OrderLineType::TYPE_SURCHARGE; + + return $this; + + default: + $this->type = $positive ? OrderLineType::TYPE_SURCHARGE : OrderLineType::TYPE_DISCOUNT; + + return $this; + } + } +} diff --git a/version/206/class/Checkout/OrderCheckout.php b/version/206/class/Checkout/OrderCheckout.php new file mode 100644 index 0000000..1d61b6d --- /dev/null +++ b/version/206/class/Checkout/OrderCheckout.php @@ -0,0 +1,406 @@ +getMollie()->status !== OrderStatus::STATUS_AUTHORIZED && $checkout->getMollie()->status !== OrderStatus::STATUS_SHIPPING) { + throw new RuntimeException('Nur autorisierte Zahlungen können erfasst werden!'); + } + $shipment = $checkout->API()->Client()->shipments->createFor($checkout->getMollie(), ['lines' => []]); + $checkout->Log(sprintf('Bestellung wurde manuell erfasst/versandt: %s', $shipment->id)); + + return $shipment->id; + } + + /** + * @param false $force + * @return null|Order + */ + public function getMollie($force = false) + { + if ($force || (!$this->order && $this->getModel()->kID)) { + try { + $this->order = $this->API()->Client()->orders->get($this->getModel()->kID, ['embed' => 'payments,shipments,refunds']); + } catch (Exception $e) { + throw new RuntimeException(sprintf('Mollie-Order \'%s\' konnte nicht geladen werden: %s', $this->getModel()->kID, $e->getMessage())); + } + } + + return $this->order; + } + + /** + * @param OrderCheckout $checkout + * @throws ApiException + * @return Order + */ + public static function cancel($checkout) + { + if (!$checkout->getMollie() || !$checkout->getMollie()->isCancelable) { + throw new RuntimeException('Bestellung kann nicht abgebrochen werden.'); + } + $order = $checkout->getMollie()->cancel(); + $checkout->Log('Bestellung wurde manuell abgebrochen.'); + + return $order; + } + + /** + * @throws Exception + * @return array + */ + public function getShipments() + { + $shipments = []; + $lieferschien_arr = Shop::DB()->executeQueryPrepared('SELECT kLieferschein FROM tlieferschein WHERE kInetBestellung = :kBestellung', [ + ':kBestellung' => $this->getBestellung()->kBestellung + ], 2); + + foreach ($lieferschien_arr as $lieferschein) { + $shipments[] = new Shipment($lieferschein->kLieferschein, $this); + } + + return $shipments; + } + + /** + * @param mixed $force + * @throws ApiException + * @return string + */ + public function cancelOrRefund($force = false) + { + if (!$this->getMollie()) { + throw new RuntimeException('Mollie-Order konnte nicht geladen werden: ' . $this->getModel()->kID); + } + if ($force || (int)$this->getBestellung()->cStatus === BESTELLUNG_STATUS_STORNO) { + if ($this->getMollie()->isCancelable) { + $res = $this->getMollie()->cancel(); + $result = 'Order cancelled, Status: ' . $res->status; + } else { + $res = $this->getMollie()->refundAll(); + $result = 'Order Refund initiiert, Status: ' . $res->status; + } + $this->PaymentMethod()->Log('OrderCheckout::cancelOrRefund: ' . $result, $this->LogData()); + + return $result; + } + + throw new RuntimeException('Bestellung ist derzeit nicht storniert, Status: ' . $this->getBestellung()->cStatus); + } + + /** + * @param array $paymentOptions + * @return Order + */ + public function create(array $paymentOptions = []) + { + if ($this->getModel()->kID) { + try { + $this->order = $this->API()->Client()->orders->get($this->getModel()->kID, ['embed' => 'payments']); + if (in_array($this->order->status, [OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_PAID, OrderStatus::STATUS_AUTHORIZED, OrderStatus::STATUS_PENDING], true)) { + $this->handleNotification(); + + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + if ($this->order->status === OrderStatus::STATUS_CREATED) { + if ($this->order->payments()) { + /** @var Payment $payment */ + foreach ($this->order->payments() as $payment) { + if ($payment->status === PaymentStatus::STATUS_OPEN) { + $this->setPayment($payment); + + break; + } + } + } + if (!$this->getPayment()) { + $this->setPayment($this->API()->Client()->orderPayments->createForId($this->getModel()->kID, $paymentOptions)); + } + $this->updateModel()->saveModel(); + + return $this->getMollie(true); + } + } catch (RuntimeException $e) { + throw $e; + } catch (Exception $e) { + $this->Log(sprintf("OrderCheckout::create: Letzte Order '%s' konnte nicht geladen werden: %s, versuche neue zu erstellen.", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + try { + $req = $this->loadRequest($paymentOptions)->jsonSerialize(); + $this->order = $this->API()->Client()->orders->create($req); + $this->Log(sprintf("Order für '%s' wurde erfolgreich angelegt: %s", $this->getBestellung()->cBestellNr, $this->order->id)); + $this->updateModel()->saveModel(); + + return $this->order; + } catch (Exception $e) { + $this->Log(sprintf("OrderCheckout::create: Neue Order '%s' konnte nicht erstellt werden: %s.", $this->getBestellung()->cBestellNr, $e->getMessage()), LOGLEVEL_ERROR); + + throw new RuntimeException(sprintf("Order für '%s' konnte nicht angelegt werden: %s", $this->getBestellung()->cBestellNr, $e->getMessage())); + } + } + + /** + * @param mixed $search + * @return null|Payment + */ + public function getPayment($search = false) + { + if (!$this->_payment && $search && $this->getMollie()) { + foreach ($this->getMollie()->payments() as $payment) { + if ( + in_array($payment->status, [ + PaymentStatus::STATUS_AUTHORIZED, + PaymentStatus::STATUS_PAID, + PaymentStatus::STATUS_PENDING, + ], true) + ) { + $this->_payment = $payment; + + break; + } + } + } + + return $this->_payment; + } + + /** + * @param $payment + * @return $this + */ + public function setPayment($payment) + { + $this->_payment = $payment; + + return $this; + } + + /** + * @return $this|OrderCheckout + */ + public function updateModel() + { + parent::updateModel(); + + if (!$this->getPayment() && $this->getMollie() && $this->getMollie()->payments()) { + /** @var Payment $payment */ + foreach ($this->getMollie()->payments() as $payment) { + if (in_array($payment->status, [PaymentStatus::STATUS_OPEN, PaymentStatus::STATUS_PENDING, PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], true)) { + $this->setPayment($payment); + + break; + } + } + } + if ($this->getPayment()) { + $this->getModel()->cTransactionId = $this->getPayment()->id; + } + if ($this->getMollie()) { + $this->getModel()->cCheckoutURL = $this->getMollie()->getCheckoutUrl(); + $this->getModel()->cWebhookURL = $this->getMollie()->webhookUrl; + $this->getModel()->cRedirectURL = $this->getMollie()->redirectUrl; + } + + return $this; + } + + /** + * @param array $options + * @return $this|OrderCheckout + */ + public function loadRequest(&$options = []) + { + parent::loadRequest($options); + + $this->orderNumber = $this->getBestellung()->cBestellNr; + + $this->billingAddress = Address::factory($this->getBestellung()->oRechnungsadresse); + if ($this->getBestellung()->Lieferadresse !== null) { + if (!$this->getBestellung()->Lieferadresse->cMail) { + $this->getBestellung()->Lieferadresse->cMail = $this->getBestellung()->oRechnungsadresse->cMail; + } + $this->shippingAddress = Address::factory($this->getBestellung()->Lieferadresse); + } + + if ( + !empty(Session::getInstance()->Customer()->dGeburtstag) + && Session::getInstance()->Customer()->dGeburtstag !== '0000-00-00' + && preg_match('/^\d{4}-\d{2}-\d{2}$/', trim(Session::getInstance()->Customer()->dGeburtstag)) + ) { + $this->consumerDateOfBirth = trim(Session::getInstance()->Customer()->dGeburtstag); + } + + $lines = []; + foreach ($this->getBestellung()->Positionen as $oPosition) { + $lines[] = OrderLine::factory($oPosition, $this->getBestellung()->Waehrung); + } + + if ($this->getBestellung()->GuthabenNutzen && $this->getBestellung()->fGuthaben > 0) { + $lines[] = OrderLine::getCredit($this->getBestellung()); + } + + if ($comp = OrderLine::getRoundingCompensation($lines, $this->amount, $this->getBestellung()->Waehrung->cISO)) { + $lines[] = $comp; + } + $this->lines = $lines; + + if ($dueDays = $this->PaymentMethod()->getExpiryDays()) { + try { + // #145 + //$max = $this->method && strpos($this->method, 'klarna') !== false ? 28 : 100; + $date = new DateTime(sprintf('+%d DAYS', $dueDays), new DateTimeZone('UTC')); + $this->expiresAt = $date->format('Y-m-d'); + } catch (Exception $e) { + $this->Log($e->getMessage(), LOGLEVEL_ERROR); + } + } + + $this->payment = $options; + + return $this; + } + + /** + * @return null|object + */ + public function getIncomingPayment() + { + if (!$this->getMollie(true)) { + return null; + } + + $cHinweis = sprintf('%s / %s', $this->getMollie()->id, $this->getPayment(true)->id); + if (Helper::getSetting('wawiPaymentID') === 'ord') { + $cHinweis = $this->getMollie()->id; + } elseif (Helper::getSetting('wawiPaymentID') === 'tr') { + $cHinweis = $this->getPayment(true)->id; + } + + + /** @var Payment $payment */ + /** @noinspection NullPointerExceptionInspection */ + foreach ($this->getMollie()->payments() as $payment) { + if ( + in_array( + $payment->status, + [PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], + true + ) + ) { + $this->setPayment($payment); + $data = (object)[ + 'fBetrag' => (float)$payment->amount->value, + 'cISO' => $payment->amount->currency, + 'cZahler' => $payment->details->paypalPayerId ?: $payment->customerId, + 'cHinweis' => $payment->details->paypalReference ?: $cHinweis, + ]; + if (isset($payment->details, $payment->details->paypalFee)) { + $data->fZahlungsgebuehr = $payment->details->paypalFee->value; + } + + return $data; + } + } + + return null; + } + + /** + * @return $this + */ + protected function updateOrderNumber() + { + try { + if ($this->getMollie()) { + $this->getMollie()->orderNumber = $this->getBestellung()->cBestellNr; + $this->getMollie()->webhookUrl = Shop::getURL() . '/?mollie=1'; + $this->getMollie()->update(); + } + if ($this->getModel()->cTransactionId) { + $this->API()->Client()->payments->update($this->getModel()->cTransactionId, [ + 'description' => $this->getDescription() + ]); + } + } catch (Exception $e) { + $this->Log('OrderCheckout::updateOrderNumber:' . $e->getMessage(), LOGLEVEL_ERROR); + } + + return $this; + } + + /** + * @param Order $model + * @return $this|AbstractCheckout + */ + protected function setMollie($model) + { + if ($model instanceof Order) { + $this->order = $model; + } + + return $this; + } +} diff --git a/version/206/class/Checkout/Payment/Address.php b/version/206/class/Checkout/Payment/Address.php new file mode 100644 index 0000000..468a08a --- /dev/null +++ b/version/206/class/Checkout/Payment/Address.php @@ -0,0 +1,53 @@ +streetAndNumber = $address->cStrasse . ' ' . $address->cHausnummer; + $resource->postalCode = $address->cPLZ; + $resource->city = $address->cOrt; + $resource->country = $address->cLand; + + if ( + isset($address->cAdressZusatz) + && trim($address->cAdressZusatz) !== '' + ) { + $resource->streetAdditional = trim($address->cAdressZusatz); + } + + // Validity-Check + // TODO: Check for valid Country Code? + // TODO: Check PostalCode requirement Country? + if (!$resource->streetAndNumber || !$resource->city || !$resource->country) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['streetAndNumber', 'city', 'country'], $resource); + } + + return $resource; + } +} diff --git a/version/206/class/Checkout/Payment/Amount.php b/version/206/class/Checkout/Payment/Amount.php new file mode 100644 index 0000000..c11c64c --- /dev/null +++ b/version/206/class/Checkout/Payment/Amount.php @@ -0,0 +1,75 @@ + true [5 Rappen Rounding]) + * @return Amount + */ + public static function factory($value, $currency = null, $useRounding = false) + { + if (!$currency) { + $currency = static::FallbackCurrency()->cISO; + } + + $resource = new static(); + + $resource->currency = $currency; + //$resource->value = number_format(round($useRounding ? $resource->round($value * (float)$currency->fFaktor) : $value * (float)$currency->fFaktor, 2), 2, '.', ''); + $resource->value = number_format(round($useRounding ? $resource->round($value) : $value, 2), 2, '.', ''); + + // Validity Check + // TODO: Check ISO Code? + // TODO: Check Value + if (!$resource->currency || !$resource->value) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['currency', 'value'], $resource); + } + + return $resource; + } + + /** + * @return stdClass + */ + public static function FallbackCurrency() + { + return isset($_SESSION['Waehrung']) ? $_SESSION['Waehrung'] : Shop::DB()->select('twaehrung', 'cStandard', 'Y'); + } + + /** + * Check if 5 Rappen rounding is necessary + * + * @param mixed $value + * @return float + */ + protected function round($value) + { + $conf = Shop::getSettings([CONF_KAUFABWICKLUNG]); + if (isset($conf['kaufabwicklung']['bestellabschluss_runden5']) && (int)$conf['kaufabwicklung']['bestellabschluss_runden5'] === 1) { + $value = round($value * 20) / 20; + } + + return $value; + } +} diff --git a/version/206/class/Checkout/PaymentCheckout.php b/version/206/class/Checkout/PaymentCheckout.php new file mode 100644 index 0000000..568be4e --- /dev/null +++ b/version/206/class/Checkout/PaymentCheckout.php @@ -0,0 +1,235 @@ +getMollie()->isCancelable) { + throw new RuntimeException('Zahlung kann nicht abgebrochen werden.'); + } + $payment = $checkout->API()->Client()->payments->cancel($checkout->getMollie()->id); + $checkout->Log('Zahlung wurde manuell abgebrochen.'); + + return $payment; + } + + /** + * @param false $force + * @return null|Payment + */ + public function getMollie($force = false) + { + if ($force || (!$this->getPayment() && $this->getModel()->kID)) { + try { + $this->setPayment($this->API()->Client()->payments->get($this->getModel()->kID, ['embed' => 'refunds'])); + } catch (Exception $e) { + throw new RuntimeException('Mollie-Payment konnte nicht geladen werden: ' . $e->getMessage()); + } + } + + return $this->getPayment(); + } + + /** + * @return null|Payment + */ + public function getPayment() + { + return $this->_payment; + } + + /** + * @param $payment + * @return $this + */ + public function setPayment($payment) + { + $this->_payment = $payment; + + return $this; + } + + /** + * @param mixed $force + * @throws ApiException + * @throws IncompatiblePlatform + * @return string + */ + public function cancelOrRefund($force = false) + { + if (!$this->getMollie()) { + throw new RuntimeException('Mollie-Order konnte nicht geladen werden: ' . $this->getModel()->kID); + } + if ($force || (int)$this->getBestellung()->cStatus === BESTELLUNG_STATUS_STORNO) { + if ($this->getMollie()->isCancelable) { + $res = $this->API()->Client()->payments->cancel($this->getMollie()->id); + $result = 'Payment cancelled, Status: ' . $res->status; + } else { + $res = $this->API()->Client()->payments->refund($this->getMollie(), ['amount' => $this->getMollie()->amount]); + $result = 'Payment Refund initiiert, Status: ' . $res->status; + } + $this->PaymentMethod()->Log('PaymentCheckout::cancelOrRefund: ' . $result, $this->LogData()); + + return $result; + } + + throw new RuntimeException('Bestellung ist derzeit nicht storniert, Status: ' . $this->getBestellung()->cStatus); + } + + /** + * @param array $paymentOptions + * @return Payment + */ + public function create(array $paymentOptions = []) + { + if ($this->getModel()->kID) { + try { + $this->setPayment($this->API()->Client()->payments->get($this->getModel()->kID)); + if ($this->getPayment()->status === PaymentStatus::STATUS_PAID) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + if ($this->getPayment()->status === PaymentStatus::STATUS_OPEN) { + $this->updateModel()->saveModel(); + + return $this->getPayment(); + } + } catch (RuntimeException $e) { + //$this->Log(sprintf("PaymentCheckout::create: Letzte Transaktion '%s' konnte nicht geladen werden: %s", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + throw $e; + } catch (Exception $e) { + $this->Log(sprintf("PaymentCheckout::create: Letzte Transaktion '%s' konnte nicht geladen werden: %s, versuche neue zu erstellen.", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + try { + $req = $this->loadRequest($paymentOptions)->jsonSerialize(); + $this->setPayment($this->API()->Client()->payments->create($req)); + $this->Log(sprintf("Payment für '%s' wurde erfolgreich angelegt: %s", $this->getBestellung()->cBestellNr, $this->getPayment()->id)); + $this->updateModel()->saveModel(); + + return $this->getPayment(); + } catch (Exception $e) { + $this->Log(sprintf("PaymentCheckout::create: Neue Transaktion für '%s' konnte nicht erstellt werden: %s.", $this->getBestellung()->cBestellNr, $e->getMessage()), LOGLEVEL_ERROR); + + throw new RuntimeException(sprintf('Mollie-Payment \'%s\' konnte nicht geladen werden: %s', $this->getBestellung()->cBestellNr, $e->getMessage())); + } + } + + /** + * @param array $options + * @return $this|PaymentCheckout + */ + public function loadRequest(&$options = []) + { + parent::loadRequest($options); + + foreach ($options as $key => $value) { + $this->$key = $value; + } + + + $this->description = $this->getDescription(); + + return $this; + } + + + + /** + * @return null|object + */ + public function getIncomingPayment() + { + if (!$this->getMollie()) { + return null; + } + + if (in_array($this->getMollie()->status, [PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], true)) { + $data = []; + $data['fBetrag'] = (float)$this->getMollie()->amount->value; + $data['cISO'] = $this->getMollie()->amount->currency; + $data['cZahler'] = $this->getMollie()->details->paypalPayerId ?: $this->getMollie()->customerId; + $data['cHinweis'] = $this->getMollie()->details->paypalReference ?: $this->getMollie()->id; + if (isset($this->getMollie()->details, $this->getMollie()->details->paypalFee)) { + $data['fZahlungsgebuehr'] = $this->getMollie()->details->paypalFee->value; + } + + return (object)$data; + } + + return null; + } + + /** + * @param Payment $model + * @return $this|AbstractCheckout + */ + protected function setMollie($model) + { + if ($model instanceof Payment) { + $this->setPayment($model); + } + + return $this; + } + + /** + * @return $this + */ + protected function updateOrderNumber() + { + try { + if ($this->getMollie()) { + $this->getMollie()->description = $this->getDescription(); + $this->getMollie()->webhookUrl = Shop::getURL() . '/?mollie=1'; + $this->getMollie()->update(); + } + } catch (Exception $e) { + $this->Log('OrderCheckout::updateOrderNumber:' . $e->getMessage(), LOGLEVEL_ERROR); + } + + return $this; + } +} diff --git a/version/206/class/ExclusiveLock.php b/version/206/class/ExclusiveLock.php new file mode 100644 index 0000000..c7b834d --- /dev/null +++ b/version/206/class/ExclusiveLock.php @@ -0,0 +1,70 @@ +key = $key; + $this->path = rtrim(realpath($path), '/') . '/'; + if (!is_dir($path) || !is_writable($path)) { + throw new RuntimeException("Lock Path '$path' doesn't exist, or is not writable!"); + } + //create a new resource or get exisitng with same key + $this->file = fopen($this->path . "$key.lockfile", 'wb+'); + } + + + public function __destruct() + { + if ($this->own === true) { + $this->unlock(); + } + } + + /** @noinspection ForgottenDebugOutputInspection */ + public function unlock() + { + $key = $this->key; + if ($this->own === true) { + if (!flock($this->file, LOCK_UN)) { //failed + error_log("ExclusiveLock::lock FAILED to release lock [$key]"); + + return false; + } + fwrite($this->file, 'Unlocked - ' . microtime(true) . "\n"); + fflush($this->file); + $this->own = false; + } else { + error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller"); + } + + return true; // success + } + + public function lock() + { + if (!flock($this->file, LOCK_EX | LOCK_NB)) { //failed + return false; + } + fwrite($this->file, 'Locked - ' . microtime(true) . "\n"); + fflush($this->file); + + $this->own = true; + + return true; // success + } +} diff --git a/version/206/class/Helper.php b/version/206/class/Helper.php new file mode 100644 index 0000000..b03874f --- /dev/null +++ b/version/206/class/Helper.php @@ -0,0 +1,363 @@ +short_url != '' ? $release->short_url : $release->full_url; + $filename = basename($release->full_url); + $tmpDir = PFAD_ROOT . PFAD_COMPILEDIR; + $pluginsDir = PFAD_ROOT . PFAD_PLUGIN; + + // 1. PRE-CHECKS + if (file_exists($pluginsDir . self::oPlugin()->cVerzeichnis . '/.git') && is_dir($pluginsDir . self::oPlugin()->cVerzeichnis . '/.git')) { + throw new Exception('Pluginordner enthält ein GIT Repository, kein Update möglich!'); + } + + if (!function_exists('curl_exec')) { + throw new Exception('cURL ist nicht verfügbar!!'); + } + if (!is_writable($tmpDir)) { + throw new Exception("Temporäres Verzeichnis_'{$tmpDir}' ist nicht beschreibbar!"); + } + if (!is_writable($pluginsDir . self::oPlugin()->cVerzeichnis)) { + throw new Exception("Plugin Verzeichnis_'" . $pluginsDir . self::oPlugin()->cVerzeichnis . "' ist nicht beschreibbar!"); + } + if (file_exists($tmpDir . $filename) && !unlink($tmpDir . $filename)) { + throw new Exception("Temporäre Datei '" . $tmpDir . $filename . "' konnte nicht gelöscht werden!"); + } + + // 2. DOWNLOAD + $fp = fopen($tmpDir . $filename, 'w+'); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_TIMEOUT, 50); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + fclose($fp); + if ($info['http_code'] !== 200) { + throw new Exception("Unerwarteter Status Code '" . $info['http_code'] . "'!"); + } + if ($info['download_content_length'] <= 0) { + throw new Exception("Unerwartete Downloadgröße '" . $info['download_content_length'] . "'!"); + } + + // 3. UNZIP + require_once PFAD_ROOT . PFAD_PCLZIP . 'pclzip.lib.php'; + $zip = new PclZip($tmpDir . $filename); + $content = $zip->listContent(); + + if (!is_array($content) || !isset($content[0]['filename']) || strpos($content[0]['filename'], '.') !== false) { + throw new Exception('Das Zip-Archiv ist leider ungültig!'); + } + $unzipPath = PFAD_ROOT . PFAD_PLUGIN; + $res = $zip->extract(PCLZIP_OPT_PATH, $unzipPath, PCLZIP_OPT_REPLACE_NEWER); + if ($res !== 0) { + header('Location: ' . Shop::getURL() . DIRECTORY_SEPARATOR . PFAD_ADMIN . 'pluginverwaltung.php', true); + } else { + throw new Exception('Entpacken fehlgeschlagen: ' . $zip->errorCode()); + } + } + + /** + * @param bool $force + * @throws Exception + * @return mixed + */ + public static function getLatestRelease($force = false) + { + $lastCheck = (int)self::getSetting(__NAMESPACE__ . '_upd'); + $lastRelease = file_exists(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd') ? file_get_contents(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd') : false; + if ($force || !$lastCheck || !$lastRelease || ($lastCheck + 12 * 60 * 60) < time()) { + $curl = curl_init('https://api.dash.bar/release/' . __NAMESPACE__); + @curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + @curl_setopt($curl, CURLOPT_TIMEOUT, 5); + @curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($curl, CURLOPT_HEADER, 0); + @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + @curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + $data = curl_exec($curl); + $statusCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); + @curl_close($curl); + if ($statusCode !== 200) { + throw new Exception(__NAMESPACE__ . ': Could not fetch release info: ' . $statusCode); + } + $json = json_decode($data); + if (json_last_error() || $json->status != 'ok') { + throw new Exception(__NAMESPACE__ . ': Could not decode release info: ' . $data); + } + self::setSetting(__NAMESPACE__ . '_upd', time()); + file_put_contents(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd', json_encode($json->data)); + + return $json->data; + } + + return json_decode($lastRelease); + } + + /** + * Register PSR-4 autoloader + * Licence-Check + * @return bool + */ + public static function init() + { + ini_set('xdebug.default_enable', defined('WS_XDEBUG_ENABLED')); + + return self::autoload(); + } + + /** + * @var stdClass[] + */ + public static $alerts = []; + + /** + * Usage: + * + * Helper::addAlert('Success Message', 'success', 'namespace'); + * + * @param $content + * @param $type + * @param $namespace + */ + public static function addAlert($content, $type, $namespace) + { + if (!array_key_exists($namespace, self::$alerts)) { + self::$alerts[$namespace] = new stdClass(); + } + + self::$alerts[$namespace]->{$type . '_' . microtime(true)} = $content; + } + + /** + * Usage in Smarty: + * + * {ws_mollie\Helper::showAlerts('namespace')} + * + * @param $namespace + * @throws SmartyException + * @return string + */ + public static function showAlerts($namespace) + { + if (array_key_exists($namespace, self::$alerts) && file_exists(self::oPlugin()->cAdminmenuPfad . '../tpl/_alerts.tpl')) { + Shop::Smarty()->assign('alerts', self::$alerts[$namespace]); + + return Shop::Smarty()->fetch(self::oPlugin()->cAdminmenuPfad . '../tpl/_alerts.tpl'); + } + + return ''; + } + + /** + * Sets a Plugin Setting and saves it to the DB + * + * @param $name + * @param $value + * @return int + */ + public static function setSetting($name, $value) + { + $setting = new stdClass(); + $setting->kPlugin = self::oPlugin()->kPlugin; + $setting->cName = $name; + $setting->cWert = $value; + + if (array_key_exists($name, self::oPlugin()->oPluginEinstellungAssoc_arr)) { + $return = Shop::DB()->updateRow('tplugineinstellungen', ['kPlugin', 'cName'], [$setting->kPlugin, $setting->cName], $setting); + } else { + $return = Shop::DB()->insertRow('tplugineinstellungen', $setting); + } + self::oPlugin()->oPluginEinstellungAssoc_arr[$name] = $value; + self::oPlugin(true); // invalidate cache + + return $return; + } + + /** + * Get Plugin Object + * + * @param bool $force disable Cache + * @return null|Plugin + */ + public static function oPlugin($force = false) + { + if ($force === true) { + self::$oPlugin = new Plugin(self::oPlugin()->kPlugin, true); + } elseif (null === self::$oPlugin) { + self::$oPlugin = Plugin::getPluginById(__NAMESPACE__); + } + + return self::$oPlugin; + } + + /** + * get a Plugin setting + * + * @param $name + * @return null|mixed + */ + public static function getSetting($name) + { + if (array_key_exists($name, self::oPlugin()->oPluginEinstellungAssoc_arr ?: [])) { + return self::oPlugin()->oPluginEinstellungAssoc_arr[$name]; + } + + return null; + } + + /** + * Get Domain frpm URL_SHOP without www. + * + * @param string $url + * @return string + */ + public static function getDomain($url = URL_SHOP) + { + $matches = []; + @preg_match("/^((http(s)?):\/\/)?(www\.)?([a-zA-Z0-9-\.]+)(\/.*)?$/i", $url, $matches); + + return strtolower(isset($matches[5]) ? $matches[5] : $url); + } + + /** + * @param bool $e + * @return mixed + */ + public static function getMasterMail($e = false) + { + $settings = Shop::getSettings([CONF_EMAILS]); + $mail = trim($settings['emails']['email_master_absender']); + if ($e === true && $mail != '') { + $mail = base64_encode($mail); + $eMail = ''; + foreach (str_split($mail) as $c) { + $eMail .= chr(ord($c) ^ 0x00100110); + } + + return base64_encode($eMail); + } + + return $mail; + } + + /** + * @param Exception $exc + * @param bool $trace + * @return void + */ + public static function logExc(Exception $exc, $trace = true) + { + Jtllog::writeLog(__NAMESPACE__ . ': ' . $exc->getMessage() . ($trace ? ' - ' . $exc->getTraceAsString() : '')); + } + + /** + * Checks if admin session is loaded + * + * @return bool + */ + public static function isAdminBackend() + { + return session_name() === 'eSIdAdm'; + } + + /** + * Returns kAdminmenu ID for given Title, used for Tabswitching + * + * @param $name string CustomLink Title + * @return int + */ + public static function getAdminmenu($name) + { + $kPluginAdminMenu = 0; + foreach (self::oPlugin()->oPluginAdminMenu_arr as $adminmenu) { + if (strtolower($adminmenu->cName) == strtolower($name)) { + $kPluginAdminMenu = $adminmenu->kPluginAdminMenu; + + break; + } + } + + return $kPluginAdminMenu; + } + } + } + +} diff --git a/version/206/class/Hook/AbstractHook.php b/version/206/class/Hook/AbstractHook.php new file mode 100644 index 0000000..561e401 --- /dev/null +++ b/version/206/class/Hook/AbstractHook.php @@ -0,0 +1,15 @@ +kPlugin)) + && array_key_exists($key, $_SESSION) && !array_key_exists('Zahlungsart', $_SESSION) + ) { + unset($_SESSION[$key]); + } + + if (!array_key_exists('ws_mollie_applepay_available', $_SESSION)) { + // TODO DOKU + if (defined('MOLLIE_APPLEPAY_TPL') && MOLLIE_APPLEPAY_TPL) { + Shop::Smarty()->assign('applePayCheckURL', json_encode(self::Plugin()->cFrontendPfadURLSSL . 'applepay.php')); + pq('body')->append(Shop::Smarty()->fetch(self::Plugin()->cFrontendPfad . 'tpl/applepay.tpl')); + } else { + $checkUrl = self::Plugin()->cFrontendPfadURLSSL . 'applepay.php'; + pq('head')->append(""); + } + } + } + + /** + * @return bool + */ + public static function isAvailable() + { + if (array_key_exists('ws_mollie_applepay_available', $_SESSION)) { + return $_SESSION['ws_mollie_applepay_available']; + } + + return false; + } + + /** + * @param $status bool + */ + public static function setAvailable($status) + { + $_SESSION['ws_mollie_applepay_available'] = $status; + } +} diff --git a/version/206/class/Hook/Checkbox.php b/version/206/class/Hook/Checkbox.php new file mode 100644 index 0000000..87c4334 --- /dev/null +++ b/version/206/class/Hook/Checkbox.php @@ -0,0 +1,60 @@ +cModulId, 'kPlugin_' . self::Plugin()->kPlugin . '_') === false) { + return; + } + + if (array_key_exists('nAnzeigeOrt', $args_arr) && $args_arr['nAnzeigeOrt'] === CHECKBOX_ORT_BESTELLABSCHLUSS && Session::getInstance()->Customer()->nRegistriert) { + $mCustomer = Customer::fromID(Session::getInstance()->Customer()->kKunde, 'kKunde'); + + if ($mCustomer->customerId) { + return; + } + + $checkbox = new \CheckBox(); + $checkbox->kLink = 0; + $checkbox->kCheckBox = -1; + $checkbox->kCheckBoxFunktion = 0; + $checkbox->cName = 'MOLLIE SAVE CUSTOMER'; + $checkbox->cKundengruppe = ';1;'; + $checkbox->cAnzeigeOrt = ';2;'; + $checkbox->nAktiv = 1; + $checkbox->nPflicht = 0; + $checkbox->nLogging = 0; + $checkbox->nSort = 999; + $checkbox->dErstellt = date('Y-m-d H:i:s'); + $checkbox->oCheckBoxSprache_arr = []; + + $langs = gibAlleSprachen(); + foreach ($langs as $lang) { + $checkbox->oCheckBoxSprache_arr[$lang->kSprache] = (object)[ + 'cText' => self::Plugin()->oPluginSprachvariableAssoc_arr['checkboxText'], + 'cBeschreibung' => self::Plugin()->oPluginSprachvariableAssoc_arr['checkboxDescr'], + 'kSprache' => $lang->kSprache, + 'kCheckbox' => -1 + ]; + } + + $checkbox->kKundengruppe_arr = [Session::getInstance()->Customer()->kKundengruppe]; + $checkbox->kAnzeigeOrt_arr = [CHECKBOX_ORT_BESTELLABSCHLUSS]; + $checkbox->cID = 'mollie_create_customer'; + $checkbox->cLink = ''; + + $args_arr['oCheckBox_arr'][] = $checkbox; + } + } +} diff --git a/version/206/class/Hook/Queue.php b/version/206/class/Hook/Queue.php new file mode 100644 index 0000000..06149b7 --- /dev/null +++ b/version/206/class/Hook/Queue.php @@ -0,0 +1,140 @@ +nWaehrendBestellung === 0 + && $args_arr['oBestellung']->fGesamtsumme > 0 + && self::Plugin()->oPluginEinstellungAssoc_arr['onlyPaid'] === 'Y' + && AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kZahlungsart, true) + ) { + $args_arr['oBestellung']->cAbgeholt = 'Y'; + Jtllog::writeLog('Switch cAbgeholt for kBestellung: ' . print_r($args_arr['oBestellung']->kBestellung, 1), JTLLOG_LEVEL_NOTICE); + } + } + + /** + * @param array $args_arr + */ + public static function xmlBestellStatus(array $args_arr) + { + if (AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kBestellung)) { + self::saveToQueue(HOOK_BESTELLUNGEN_XML_BESTELLSTATUS . ':' . (int)$args_arr['oBestellung']->kBestellung, [ + 'kBestellung' => $args_arr['oBestellung']->kBestellung, + 'status' => (int)$args_arr['status'] + ]); + } + } + + /** + * @param $hook + * @param $args_arr + * @param string $type + * @return bool + */ + public static function saveToQueue($hook, $args_arr, $type = 'hook') + { + $mQueue = new QueueModel(); + $mQueue->cType = $type . ':' . $hook; + $mQueue->cData = serialize($args_arr); + + try { + return $mQueue->save(); + } catch (Exception $e) { + Jtllog::writeLog('mollie::saveToQueue: ' . $e->getMessage() . ' - ' . print_r($args_arr, 1)); + + return false; + } + } + + /** + * @param array $args_arr + */ + public static function xmlBearbeiteStorno(array $args_arr) + { + if (AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kBestellung)) { + self::saveToQueue(HOOK_BESTELLUNGEN_XML_BEARBEITESTORNO . ':' . $args_arr['oBestellung']->kBestellung, ['kBestellung' => $args_arr['oBestellung']->kBestellung]); + } + } + + /** + * + */ + public static function headPostGet() + { + if (array_key_exists('mollie', $_REQUEST) && (int)$_REQUEST['mollie'] === 1 && array_key_exists('id', $_REQUEST)) { + if (array_key_exists('hash', $_REQUEST) && $hash = trim(StringHandler::htmlentities(StringHandler::filterXSS($_REQUEST['hash'])), '_')) { + AbstractCheckout::finalizeOrder($hash, $_REQUEST['id'], array_key_exists('test', $_REQUEST)); + } else { + self::saveToQueue($_REQUEST['id'], $_REQUEST['id'], 'webhook'); + } + exit(); + } + if (array_key_exists('m_pay', $_REQUEST)) { + try { + $raw = Shop::DB()->executeQueryPrepared('SELECT kID FROM `xplugin_ws_mollie_payments` WHERE dReminder IS NOT NULL AND MD5(CONCAT(kID, "-", kBestellung)) = :md5', [ + ':md5' => $_REQUEST['m_pay'] + ], 1); + + if (!$raw) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errOrderNotFound']); + } + + if (strpos($raw->cOrderId, 'tr_') === 0) { + $checkout = PaymentCheckout::fromID($raw->kID); + } else { + $checkout = OrderCheckout::fromID($raw->kID); + } + $checkout->getMollie(true); + $checkout->updateModel()->saveModel(); + + if ( + ($checkout->getBestellung()->dBezahltDatum !== null && $checkout->getBestellung()->dBezahltDatum !== '0000-00-00') + || in_array($checkout->getModel()->cStatus, ['completed', 'paid', 'authorized', 'pending']) + ) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + + $options = []; + if (self::Plugin()->oPluginEinstellungAssoc_arr['resetMethod'] !== 'Y') { + $options['method'] = $checkout->getModel()->cMethod; + } + + $mollie = $checkout->create($options); // Order::repayOrder($orderModel->getOrderId(), $options, $api); + $url = $mollie->getCheckoutUrl(); + + header('Location: ' . $url); + exit(); + } catch (RuntimeException $e) { + // TODO Workaround? + //$alertHelper = Shop::Container()->getAlertService(); + //$alertHelper->addAlert(Alert::TYPE_ERROR, $e->getMessage(), 'mollie_repay', ['dismissable' => true]); + } catch (Exception $e) { + Jtllog::writeLog('mollie:repay:error: ' . $e->getMessage() . "\n" . print_r($_REQUEST, 1)); + } + } + } +} diff --git a/version/206/class/Model/AbstractModel.php b/version/206/class/Model/AbstractModel.php new file mode 100644 index 0000000..38a4ab5 --- /dev/null +++ b/version/206/class/Model/AbstractModel.php @@ -0,0 +1,110 @@ +data = $data; + if (!$data) { + $this->new = true; + } + } + + /** + * @param $id + * @param string $col + * @param false $failIfNotExists + * @return static + */ + public static function fromID($id, $col = 'kID', $failIfNotExists = false) + { + if ( + $payment = Shop::DB()->executeQueryPrepared( + 'SELECT * FROM ' . static::TABLE . " WHERE `$col` = :id", + [':id' => $id], + 1 + ) + ) { + return new static($payment); + } + if ($failIfNotExists) { + throw new RuntimeException(sprintf('Model %s in %s nicht gefunden!', $id, static::TABLE)); + } + + return new static(); + } + + /** + * @return null|mixed|stdClass + */ + public function jsonSerialize() + { + return $this->data; + } + + public function __get($name) + { + if (isset($this->data->$name)) { + return $this->data->$name; + } + + return null; + } + + public function __set($name, $value) + { + if (!$this->data) { + $this->data = new stdClass(); + } + $this->data->$name = $value; + } + + public function __isset($name) + { + return isset($this->data->$name); + } + + /** + * @return bool + */ + public function save() + { + if (!$this->data) { + throw new RuntimeException('No Data to save!'); + } + + if ($this->new) { + Shop::DB()->insertRow(static::TABLE, $this->data); + $this->new = false; + + return true; + } + Shop::DB()->updateRow(static::TABLE, static::PRIMARY, $this->data->{static::PRIMARY}, $this->data); + + return true; + } +} diff --git a/version/206/class/Model/Customer.php b/version/206/class/Model/Customer.php new file mode 100644 index 0000000..3d02409 --- /dev/null +++ b/version/206/class/Model/Customer.php @@ -0,0 +1,21 @@ +dCreatedAt) { + $this->dCreatedAt = date('Y-m-d H:i:s'); + } + if ($this->new) { + $this->dReminder = self::NULL; + } + + return parent::save(); + } +} diff --git a/version/206/class/Model/Queue.php b/version/206/class/Model/Queue.php new file mode 100644 index 0000000..1509e50 --- /dev/null +++ b/version/206/class/Model/Queue.php @@ -0,0 +1,46 @@ +cResult = $result; + $this->dDone = $date ?: date('Y-m-d H:i:s'); + + return $this->save(); + } + + public function save() + { + if (!$this->dCreated) { + $this->dCreated = date('Y-m-d H:i:s'); + } + $this->dModified = date('Y-m-d H:i:s'); + + return parent::save(); + } +} diff --git a/version/206/class/Model/Shipment.php b/version/206/class/Model/Shipment.php new file mode 100644 index 0000000..91d93be --- /dev/null +++ b/version/206/class/Model/Shipment.php @@ -0,0 +1,29 @@ +executeQueryPrepared( + 'SELECT p.kBestellung, b.cStatus FROM xplugin_ws_mollie_payments p ' + . 'JOIN tbestellung b ON b.kBestellung = p.kBestellung ' + . "WHERE b.cAbgeholt = 'Y' AND NOT p.bSynced AND b.cStatus IN ('1', '2') AND p.dCreatedAt < NOW() - INTERVAL :d HOUR", + [':d' => $delay], + 2 + ); + + foreach ($open as $o) { + try { + $checkout = AbstractCheckout::fromBestellung($o->kBestellung); + $pm = $checkout->PaymentMethod(); + if ($pm::ALLOW_AUTO_STORNO && $pm::METHOD === $checkout->getMollie()->method) { + if ($checkout->getBestellung()->cAbgeholt === 'Y' && (bool)$checkout->getModel()->bSynced === false) { + if (!in_array($checkout->getMollie()->status, [OrderStatus::STATUS_PAID, OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_AUTHORIZED], true)) { + $checkout->storno(); + } else { + $checkout->Log(sprintf('AutoStorno: Bestellung bezahlt? %s - Method: %s', $checkout->getMollie()->status, $checkout->getMollie()->method), LOGLEVEL_ERROR); + } + } else { + $checkout->Log('AutoStorno: bereits zur WAWI synchronisiert.', LOGLEVEL_ERROR); + } + } + } catch (Exception $e) { + Helper::logExc($e); + } + } + + return true; + } + + /** + * @param int $limit + */ + public static function run($limit = 10) + { + foreach (self::getOpen($limit) as $todo) { + if (!self::lock($todo)) { + Jtllog::writeLog(sprintf('%s already locked since %s', $todo->kId, $todo->bLock ?: 'just now')); + + continue; + } + + if ((list($type, $id) = explode(':', $todo->cType))) { + try { + switch ($type) { + case 'webhook': + self::handleWebhook($id, $todo); + + break; + + case 'hook': + self::handleHook((int)$id, $todo); + + break; + } + } catch (Exception $e) { + Jtllog::writeLog($e->getMessage() . " ($type, $id)"); + $todo->done("{$e->getMessage()}\n{$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}"); + } + } + + self::unlock($todo); + } + } + + /** + * @param $limit + * @return Generator|QueueModel[] + * @noinspection PhpReturnDocTypeMismatchInspection + * @noinspection SqlResolve + */ + private static function getOpen($limit) + { + if (!defined('MOLLIE_HOOK_DELAY')) { + define('MOLLIE_HOOK_DELAY', 3); + } + $open = Shop::DB()->executeQueryPrepared(sprintf("SELECT * FROM %s WHERE (dDone IS NULL OR dDone = '0000-00-00 00:00:00') AND `bLock` IS NULL AND (cType LIKE 'webhook:%%' OR (cType LIKE 'hook:%%') AND dCreated < DATE_SUB(NOW(), INTERVAL " . (int)MOLLIE_HOOK_DELAY . ' MINUTE)) ORDER BY dCreated DESC LIMIT 0, :LIMIT;', QueueModel::TABLE), [ + ':LIMIT' => $limit + ], 2); + + foreach ($open as $_raw) { + yield new QueueModel($_raw); + } + } + + /** + * @param $todo + * @return bool + * @noinspection SqlResolve + */ + protected static function lock($todo) + { + return $todo->kId && Shop::DB()->executeQueryPrepared(sprintf('UPDATE %s SET `bLock` = NOW() WHERE `bLock` IS NULL AND kId = :kId', QueueModel::TABLE), [ + 'kId' => $todo->kId + ], 3) >= 1; + } + + /** + * @param $id + * @param QueueModel $todo + * @throws Exception + * @return bool + */ + protected static function handleWebhook($id, QueueModel $todo) + { + $checkout = AbstractCheckout::fromID($id); + if ($checkout->getBestellung()->kBestellung && $checkout->PaymentMethod()) { + $checkout->handleNotification(); + + return $todo->done('Status: ' . $checkout->getMollie()->status); + } + + throw new RuntimeException("Bestellung oder Zahlungsart konnte nicht geladen werden: $id"); + } + + /** + * @param $hook + * @param QueueModel $todo + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return bool + */ + protected static function handleHook($hook, QueueModel $todo) + { + $data = unserialize($todo->cData); + if (array_key_exists('kBestellung', $data)) { + switch ($hook) { + case HOOK_BESTELLUNGEN_XML_BESTELLSTATUS: + if ((int)$data['kBestellung']) { + // TODO: #158 What happens when API requests fail? + $checkout = AbstractCheckout::fromBestellung($data['kBestellung']); + + $status = array_key_exists('status', $data) ? (int)$data['status'] : 0; + $result = ''; + if (!$status || $status < BESTELLUNG_STATUS_VERSANDT) { + return $todo->done("Bestellung noch nicht versendet: {$checkout->getBestellung()->cStatus}"); + } + if (!count($checkout->getBestellung()->oLieferschein_arr)) { + $todo->dCreated = date('Y-m-d H:i:s', strtotime('+3 MINUTES')); + $todo->cResult = 'Noch keine Lieferscheine, delay...'; + + return $todo->save(); + } + + /** @var $method JTLMollie */ + if ( + (strpos($checkout->getModel()->kID, 'tr_') === false) + && $checkout->PaymentMethod() + && $checkout->getMollie() + ) { + /** @var OrderCheckout $checkout */ + $checkout->handleNotification(); + if ($checkout->getMollie()->status === OrderStatus::STATUS_COMPLETED) { + $result = 'Mollie Status already ' . $checkout->getMollie()->status; + } elseif ($checkout->getMollie()->isCreated() || $checkout->getMollie()->isPaid() || $checkout->getMollie()->isAuthorized() || $checkout->getMollie()->isShipping() || $checkout->getMollie()->isPending()) { + try { + if ($shipments = Shipment::syncBestellung($checkout)) { + foreach ($shipments as $shipment) { + if (is_string($shipment)) { + $checkout->Log("Shipping-Error: $shipment"); + $result .= "Shipping-Error: $shipment;\n"; + } else { + $checkout->Log("Order shipped: {$shipment->id}"); + $result .= "Order shipped: $shipment->id;\n"; + } + } + } else { + $result = 'No Shipments ready!'; + } + } catch (RuntimeException $e) { + $result = $e->getMessage(); + } catch (Exception $e) { + $result = $e->getMessage() . "\n" . $e->getFile() . ':' . $e->getLine() . "\n" . $e->getTraceAsString(); + } + } else { + $result = sprintf('Unerwarteter Mollie Status "%s" für %s', $checkout->getMollie()->status, $checkout->getBestellung()->cBestellNr); + } + } else { + $result = 'Nothing to do.'; + } + $checkout->Log('Queue::handleHook: ' . $result); + } else { + $result = 'kBestellung missing'; + } + + return $todo->done($result); + + case HOOK_BESTELLUNGEN_XML_BEARBEITESTORNO: + if (self::Plugin()->oPluginEinstellungAssoc_arr['autoRefund'] !== 'Y') { + throw new RuntimeException('Auto-Refund disabled'); + } + + $checkout = AbstractCheckout::fromBestellung((int)$data['kBestellung']); + + return $todo->done($checkout->cancelOrRefund()); + } + } + + return false; + } + + /** + * @param $todo + * @return bool + */ + protected static function unlock($todo) + { + return $todo->kId && Shop::DB()->executeQueryPrepared(sprintf('UPDATE %s SET `bLock` = NULL WHERE kId = :kId OR bLock < DATE_SUB(NOW(), INTERVAL 15 MINUTE)', QueueModel::TABLE), [ + 'kId' => $todo->kId + ], 3) >= 1; + } +} diff --git a/version/206/class/Shipment.php b/version/206/class/Shipment.php new file mode 100644 index 0000000..a22a8d3 --- /dev/null +++ b/version/206/class/Shipment.php @@ -0,0 +1,325 @@ +kLieferschein = $kLieferschein; + if ($checkout) { + $this->checkout = $checkout; + } + + if (!$this->getLieferschein() || !$this->getLieferschein()->getLieferschein()) { + throw new RuntimeException('Lieferschein konnte nicht geladen werden'); + } + + if (!count($this->getLieferschein()->oVersand_arr)) { + throw new RuntimeException('Kein Versand gefunden!'); + } + + if (!$this->getCheckout()->getBestellung()->oKunde->nRegistriert) { + $this->isGuest = true; + } + } + + public function getLieferschein() + { + if (!$this->oLieferschein && $this->kLieferschein) { + $this->oLieferschein = new Lieferschein($this->kLieferschein); + } + + return $this->oLieferschein; + } + + /** + * @throws Exception + * @return OrderCheckout + */ + public function getCheckout() + { + if (!$this->checkout) { + //TODO evtl. load by lieferschien + throw new Exception('Should not happen, but it did!'); + } + + return $this->checkout; + } + + public static function syncBestellung(OrderCheckout $checkout) + { + $shipments = []; + if ($checkout->getBestellung()->kBestellung) { + $oKunde = $checkout->getBestellung()->oKunde ?: new Kunde($checkout->getBestellung()->kKunde); + + $shippingActive = Helper::getSetting('shippingActive'); + if ($shippingActive === 'N') { + throw new RuntimeException('Shipping deaktiviert'); + } + + if ($shippingActive === 'K' && !$oKunde->nRegistriert && (int)$checkout->getBestellung()->cStatus !== BESTELLUNG_STATUS_VERSANDT) { + throw new RuntimeException('Shipping für Gast-Bestellungen und Teilversand deaktiviert'); + } + + /** @var Lieferschein $oLieferschein */ + foreach ($checkout->getBestellung()->oLieferschein_arr as $oLieferschein) { + try { + $shipment = new self($oLieferschein->getLieferschein(), $checkout); + $mode = self::Plugin()->oPluginEinstellungAssoc_arr['shippingMode']; + switch ($mode) { + case 'A': + // ship directly + if (!$shipment->send() && !$shipment->getShipment()) { + throw new RuntimeException('Shipment konnte nicht gespeichert werden.'); + } + $shipments[] = $shipment->getShipment(); + + break; + + case 'B': + // only ship if complete shipping + if ($oKunde->nRegistriert || (int)$checkout->getBestellung()->cStatus === BESTELLUNG_STATUS_VERSANDT) { + if (!$shipment->send() && !$shipment->getShipment()) { + throw new RuntimeException('Shipment konnte nicht gespeichert werden.'); + } + $shipments[] = $shipment->getShipment(); + + break; + } + + throw new RuntimeException('Gastbestellung noch nicht komplett versendet!'); + } + } catch (RuntimeException $e) { + $shipments[] = $e->getMessage(); + } catch (Exception $e) { + $shipments[] = $e->getMessage(); + $checkout->Log("mollie: Shipment::syncBestellung (BestellNr. {$checkout->getBestellung()->cBestellNr}, Lieferschein: {$oLieferschein->getLieferscheinNr()}) - " . $e->getMessage(), LOGLEVEL_ERROR); + } + } + } + + return $shipments; + } + + /** + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return bool + */ + public function send() + { + if ($this->getShipment()) { + throw new RuntimeException('Lieferschien bereits an Mollie übertragen: ' . $this->getShipment()->id); + } + + if ($this->getCheckout()->getMollie(true)->status === OrderStatus::STATUS_COMPLETED) { + throw new RuntimeException('Bestellung bei Mollie bereits abgeschlossen!'); + } + + $api = $this->getCheckout()->API()->Client(); + $this->shipment = $api->shipments->createForId($this->checkout->getModel()->kID, $this->loadRequest()->jsonSerialize()); + + return $this->updateModel()->saveModel(); + } + + /** + * @param false $force + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return BaseResource|\Mollie\Api\Resources\Shipment + */ + public function getShipment($force = false) + { + if (($force || !$this->shipment) && $this->getModel() && $this->getModel()->cShipmentId) { + $this->shipment = $this->getCheckout()->API()->Client()->shipments->getForId($this->getModel()->cOrderId, $this->getModel()->cShipmentId); + } + + return $this->shipment; + } + + /** + * @throws Exception + * @return ShipmentModel + */ + public function getModel() + { + if (!$this->model && $this->kLieferschein) { + $this->model = ShipmentModel::fromID($this->kLieferschein, 'kLieferschein'); + + if (!$this->model->dCreated) { + $this->model->dCreated = date('Y-m-d H:i:s'); + } + $this->updateModel(); + } + + return $this->model; + } + + /** + * @throws Exception + * @return $this + */ + public function updateModel() + { + $this->getModel()->kLieferschein = $this->kLieferschein; + if ($this->getCheckout()) { + $this->getModel()->cOrderId = $this->getCheckout()->getModel()->kID; + $this->getModel()->kBestellung = $this->getCheckout()->getModel()->kBestellung; + } + if ($this->getShipment()) { + $this->getModel()->cShipmentId = $this->getShipment()->id; + $this->getModel()->cUrl = $this->getShipment()->getTrackingUrl() ?: ''; + } + if (isset($this->tracking)) { + $this->getModel()->cCarrier = $this->tracking['carrier'] ?: ''; + $this->getModel()->cCode = $this->tracking['code'] ?: ''; + } + + return $this; + } + + /** + * @param array $options + * @throws Exception + * @return $this + */ + public function loadRequest(&$options = []) + { + /** @var Versand $oVersand */ + $oVersand = $this->getLieferschein()->oVersand_arr[0]; + if ($oVersand->getIdentCode() && $oVersand->getLogistik()) { + $tracking = [ + 'carrier' => utf8_encode($oVersand->getLogistik()), + 'code' => utf8_encode($oVersand->getIdentCode()), + ]; + if ($oVersand->getLogistikVarUrl()) { + $tracking['url'] = utf8_encode($oVersand->getLogistikURL()); + } + $this->tracking = $tracking; + } + + // TODO: Wenn alle Lieferschiene in der WAWI erstellt wurden, aber nicht im Shop, kommt status 4. + if ($this->isGuest || (int)$this->getCheckout()->getBestellung()->cStatus === BESTELLUNG_STATUS_VERSANDT) { + $this->lines = []; + } else { + $this->lines = $this->getOrderLines(); + } + + return $this; + } + + /** + * @throws Exception + * @return array + */ + protected function getOrderLines() + { + $lines = []; + + if (!count($this->getLieferschein()->oLieferscheinPos_arr)) { + return $lines; + } + + // Bei Stücklisten, sonst gibt es mehrere OrderLines für die selbe ID + $shippedOrderLines = []; + + /** @var Lieferscheinpos $oLieferscheinPos */ + foreach ($this->getLieferschein()->oLieferscheinPos_arr as $oLieferscheinPos) { + $wkpos = Shop::DB()->executeQueryPrepared('SELECT * FROM twarenkorbpos WHERE kBestellpos = :kBestellpos', [ + ':kBestellpos' => $oLieferscheinPos->getBestellPos() + ], 1); + + /** @var OrderLine $orderLine */ + foreach ($this->getCheckout()->getMollie()->lines as $orderLine) { + if ($orderLine->sku === $wkpos->cArtNr && !in_array($orderLine->id, $shippedOrderLines, true)) { + if ($quantity = min($oLieferscheinPos->getAnzahl(), $orderLine->shippableQuantity)) { + $lines[] = [ + 'id' => $orderLine->id, + 'quantity' => $quantity + ]; + } + $shippedOrderLines[] = $orderLine->id; + + break; + } + } + } + + return $lines; + } + + /** + * @throws Exception + * @return bool + */ + public function saveModel() + { + return $this->getModel()->save(); + } +} diff --git a/version/206/class/Traits/Jsonable.php b/version/206/class/Traits/Jsonable.php new file mode 100644 index 0000000..8ba9b19 --- /dev/null +++ b/version/206/class/Traits/Jsonable.php @@ -0,0 +1,23 @@ +jsonSerialize(); + } + + public function jsonSerialize() + { + return array_filter(get_object_vars($this), static function ($value) { + return !($value === null || (is_string($value) && $value === '')); + }); + } +} diff --git a/version/206/class/Traits/Plugin.php b/version/206/class/Traits/Plugin.php new file mode 100644 index 0000000..08069cd --- /dev/null +++ b/version/206/class/Traits/Plugin.php @@ -0,0 +1,30 @@ +requestData) === false) { + throw new \RuntimeException(sprintf("JSON Encode Error: %s\n%s", json_last_error_msg(), print_r($this->requestData, 1))); + } + + return $this->requestData; + } + + public function __get($name) + { + if (!$this->requestData) { + $this->loadRequest(); + } + + return is_string($this->requestData[$name]) ? utf8_decode($this->requestData[$name]) : $this->requestData[$name]; + } + + public function __set($name, $value) + { + if (!$this->requestData) { + $this->requestData = []; + } + + $this->requestData[$name] = is_string($value) ? utf8_encode($value) : $value; + + return $this; + } + + /** + * @param array $options + * @return $this + */ + public function loadRequest(&$options = []) + { + return $this; + } + + + public function __serialize() + { + return $this->requestData ?: []; + } + + public function __isset($name) + { + return $this->requestData[$name] !== null; + } +} diff --git a/version/206/frontend/.htaccess b/version/206/frontend/.htaccess new file mode 100644 index 0000000..df6c074 --- /dev/null +++ b/version/206/frontend/.htaccess @@ -0,0 +1,9 @@ + + + Require all granted + + + Order Allow,Deny + Allow from all + + diff --git a/version/206/frontend/131_globalinclude.php b/version/206/frontend/131_globalinclude.php new file mode 100644 index 0000000..962b5ad --- /dev/null +++ b/version/206/frontend/131_globalinclude.php @@ -0,0 +1,61 @@ +select('tzahlungsession', 'cZahlungsID', $sessionHash); + if ($paymentSession && $paymentSession->kBestellung) { + $oBestellung = new Bestellung($paymentSession->kBestellung); + + if (Shop::getConfig([CONF_KAUFABWICKLUNG])['kaufabwicklung']['bestellabschluss_abschlussseite'] === 'A') { + $oZahlungsID = Shop::DB()->query( + ' + SELECT cId + FROM tbestellid + WHERE kBestellung = ' . (int)$paymentSession->kBestellung, + 1 + ); + if (is_object($oZahlungsID)) { + header('Location: ' . Shop::getURL() . '/bestellabschluss.php?i=' . $oZahlungsID->cId); + exit(); + } + } + $bestellstatus = Shop::DB()->select('tbestellstatus', 'kBestellung', (int)$paymentSession->kBestellung); + header('Location: ' . Shop::getURL() . '/status.php?uid=' . $bestellstatus->cUID); + exit(); + } + } + + ifndef('MOLLIE_REMINDER_PROP', 10); + if (mt_rand(1, MOLLIE_REMINDER_PROP) % MOLLIE_REMINDER_PROP === 0) { + $lock = new \ws_mollie\ExclusiveLock('mollie_reminder', PFAD_ROOT . PFAD_COMPILEDIR); + if ($lock->lock()) { + AbstractCheckout::sendReminders(); + Queue::storno((int)Helper::getSetting('autoStorno')); + + $lock->unlock(); + } + } +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/206/frontend/132_headPostGet.php b/version/206/frontend/132_headPostGet.php new file mode 100644 index 0000000..9562324 --- /dev/null +++ b/version/206/frontend/132_headPostGet.php @@ -0,0 +1,20 @@ +append( + << + /* MOLLIE CHECKOUT STYLES*/ + #fieldset-payment .form-group > div:hover, #checkout-shipping-payment .form-group > div:hover { + background-color: #eee; + color: black; + } + $selector label { + $border + } + + $selector label::after { + clear: both; + content: ' '; + display: block; + } + + $selector label span small { + line-height: 48px; + } + + $selector label img { + float: right; + } + +HTML + ); +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/206/frontend/180_checkbox.php b/version/206/frontend/180_checkbox.php new file mode 100644 index 0000000..530a9dc --- /dev/null +++ b/version/206/frontend/180_checkbox.php @@ -0,0 +1,21 @@ +oPluginEinstellungAssoc_arr['useCustomerAPI'] === 'C') { + \ws_mollie\Hook\Checkbox::execute($args_arr); + } +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/206/frontend/181_sync.php b/version/206/frontend/181_sync.php new file mode 100644 index 0000000..46fcf31 --- /dev/null +++ b/version/206/frontend/181_sync.php @@ -0,0 +1,18 @@ + diff --git a/version/206/frontend/tpl/applepay.tpl b/version/206/frontend/tpl/applepay.tpl new file mode 100644 index 0000000..57c2e75 --- /dev/null +++ b/version/206/frontend/tpl/applepay.tpl @@ -0,0 +1,19 @@ + + diff --git a/version/206/paymentmethod/JTLMollie.php b/version/206/paymentmethod/JTLMollie.php new file mode 100644 index 0000000..ee887a8 --- /dev/null +++ b/version/206/paymentmethod/JTLMollie.php @@ -0,0 +1,306 @@ +cModulId = 'kPlugin_' . self::Plugin()->kPlugin . '_mollie' . $moduleID; + } + + /** + * @param Bestellung $order + * @return PaymentMethod + */ + public function setOrderStatusToPaid($order) + { + // If paid already, do nothing + if ((int)$order->cStatus >= BESTELLUNG_STATUS_BEZAHLT) { + return $this; + } + + return parent::setOrderStatusToPaid($order); + } + + + /** + * @param $kBestellung + * @param bool $redirect + * @return bool|string + */ + public static function getOrderCompletedRedirect($kBestellung, $redirect = true) + { + $mode = Shopsetting::getInstance()->getValue(CONF_KAUFABWICKLUNG, 'bestellabschluss_abschlussseite'); + + $bestellid = Shop::DB()->select('tbestellid ', 'kBestellung', (int)$kBestellung); + $url = Shop::getURL() . '/bestellabschluss.php?i=' . $bestellid->cId; + + + if ($mode === 'S' || !$bestellid) { // Statusseite + $bestellstatus = Shop::DB()->select('tbestellstatus', 'kBestellung', (int)$kBestellung); + $url = Shop::getURL() . '/status.php?uid=' . $bestellstatus->cUID; + } + + if ($redirect) { + if (!headers_sent()) { + header('Location: ' . $url); + } + echo "redirect ..."; + exit(); + } + + return $url; + } + + + /** + * Prepares everything so that the Customer can start the Payment Process. + * Tells Template Engine. + * + * @param Bestellung $order + * @return void + */ + public function preparePaymentProcess($order) + { + parent::preparePaymentProcess($order); + + try { + if ($this->duringCheckout && !static::ALLOW_PAYMENT_BEFORE_ORDER) { + $this->Log(sprintf('Zahlung vor Bestellabschluss nicht unterstützt (%s)!', $order->cBestellNr), sprintf('#%s', $order->kBestellung), LOGLEVEL_ERROR); + + return; + } + + $payable = (float)$order->fGesamtsumme > 0; + if (!$payable) { + $this->Log(sprintf("Bestellung '%s': Gesamtsumme %.2f, keine Zahlung notwendig!", $order->cBestellNr, $order->fGesamtsumme), sprintf('#%s', $order->kBestellung)); + require_once PFAD_ROOT . PFAD_INCLUDES . 'bestellabschluss_inc.php'; + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + $finalized = finalisiereBestellung(); + Session::getInstance()->cleanUp(); + self::getOrderCompletedRedirect($finalized->kBestellung); + + return; + } + + $paymentOptions = []; + + $api = array_key_exists($this->moduleID . '_api', self::Plugin()->oPluginEinstellungAssoc_arr) ? self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_api'] : 'order'; + + $paymentOptions = array_merge($paymentOptions, $this->getPaymentOptions($order, $api)); + + if ($api === 'payment') { + $checkout = new PaymentCheckout($order); + $payment = $checkout->create($paymentOptions); + /** @noinspection NullPointerExceptionInspection */ + $url = $payment->getCheckoutUrl(); + } else { + $checkout = new OrderCheckout($order); + $mOrder = $checkout->create($paymentOptions); + /** @noinspection NullPointerExceptionInspection */ + $url = $mOrder->getCheckoutUrl(); + } + + ifndef('MOLLIE_REDIRECT_DELAY', 3); + $checkoutMode = self::Plugin()->oPluginEinstellungAssoc_arr['checkoutMode']; + Shop::Smarty()->assign('redirect', $url) + ->assign('checkoutMode', $checkoutMode); + if ($checkoutMode === 'Y' && !headers_sent()) { + header('Location: ' . $url); + } + } catch (Exception $e) { + $this->Log('mollie::preparePaymentProcess: ' . $e->getMessage() . ' - ' . print_r(['cBestellNr' => $order->cBestellNr], 1), '#' . $order->kBestellung, LOGLEVEL_ERROR); + Shop::Smarty()->assign('oMollieException', $e) + ->assign('tryAgain', Shop::getURL() . '/bestellab_again.php?kBestellung=' . $order->kBestellung); + } + } + + public function Log($msg, $data = null, $level = LOGLEVEL_NOTICE) + { + ZahlungsLog::add($this->moduleID, '[' . microtime(true) . ' - ' . $_SERVER['PHP_SELF'] . '] ' . $msg, $data, $level); + + return $this; + } + + public function getPaymentOptions(Bestellung $order, $apiType) + { + return []; + } + + /** + * @param Bestellung $order + * @param string $hash + * @param array $args + */ + public function handleNotification($order, $hash, $args) + { + parent::handleNotification($order, $hash, $args); + + try { + $orderId = $args['id']; + $checkout = null; + if (strpos($orderId, 'tr_') === 0) { + $checkout = new PaymentCheckout($order); + } else { + $checkout = new OrderCheckout($order); + } + $checkout->handleNotification($hash); + } catch (Exception $e) { + $checkout->Log(sprintf("mollie::handleNotification: Fehler bei Bestellung '%s': %s\n%s", $order->cBestellNr, $e->getMessage(), print_r($args, 1)), LOGLEVEL_ERROR); + } + } + + /** + * @return bool + */ + public function canPayAgain() + { + return true; + } + + /** + * determines, if the payment method can be selected in the checkout process + * + * @return bool + */ + public function isSelectable() + { + if (API::getMode()) { + $selectable = trim(self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key']) !== ''; + } else { + $selectable = trim(self::Plugin()->oPluginEinstellungAssoc_arr['api_key']) !== ''; + if (!$selectable) { + Jtllog::writeLog('Live API Key missing!'); + } + } + if ($selectable) { + try { + $locale = AbstractCheckout::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer()->cLand); + $amount = Session::getInstance()->Basket()->gibGesamtsummeWarenExt([ + C_WARENKORBPOS_TYP_ARTIKEL, + C_WARENKORBPOS_TYP_KUPON, + C_WARENKORBPOS_TYP_GUTSCHEIN, + C_WARENKORBPOS_TYP_NEUKUNDENKUPON, + ], true) * Session::getInstance()->Currency()->fFaktor; + if ($amount <= 0) { + $amount = 0.01; + } + $selectable = self::isMethodPossible( + static::METHOD, + $locale, + Session::getInstance()->Customer()->cLand, + Session::getInstance()->Currency()->cISO, + $amount + ); + } catch (Exception $e) { + Helper::logExc($e); + $selectable = false; + } + } + + return $selectable && parent::isSelectable(); + } + + /** + * @param $method + * @param $locale + * @param $billingCountry + * @param $currency + * @param $amount + * @throws ApiException + * @return bool + */ + protected static function isMethodPossible($method, $locale, $billingCountry, $currency, $amount) + { + $api = new API(API::getMode()); + + if (!array_key_exists('mollie_possibleMethods', $_SESSION)) { + $_SESSION['mollie_possibleMethods'] = []; + } + + $key = md5(serialize([$locale, $billingCountry, $currency, $amount])); + if (!array_key_exists($key, $_SESSION['mollie_possibleMethods'])) { + $active = $api->Client()->methods->allActive([ + 'locale' => $locale, + 'amount' => [ + 'currency' => $currency, + 'value' => number_format($amount, 2, '.', '') + ], + 'billingCountry' => $billingCountry, + 'resource' => 'orders', + 'includeWallets' => 'applepay', + ]); + foreach ($active as $a) { + $_SESSION['mollie_possibleMethods'][$key][] = (object)['id' => $a->id]; + } + } + + if ($method !== '') { + foreach ($_SESSION['mollie_possibleMethods'][$key] as $m) { + if ($m->id === $method) { + return true; + } + } + } else { + return true; + } + + return false; + } + + /** + * @param array $args_arr + * @return bool + */ + public function isValidIntern($args_arr = []) + { + return $this->duringCheckout + ? static::ALLOW_PAYMENT_BEFORE_ORDER && parent::isValidIntern($args_arr) + : parent::isValidIntern($args_arr); + } + + /** + * @return int + */ + public function getExpiryDays() + { + return (int)min(abs((int)Helper::oPlugin()->oPluginEinstellungAssoc_arr[$this->cModulId . '_dueDays']), static::MAX_EXPIRY_DAYS) ?: static::MAX_EXPIRY_DAYS; + } +} diff --git a/version/206/paymentmethod/JTLMollieApplePay.php b/version/206/paymentmethod/JTLMollieApplePay.php new file mode 100644 index 0000000..ca0b375 --- /dev/null +++ b/version/206/paymentmethod/JTLMollieApplePay.php @@ -0,0 +1,20 @@ +oRechnungsadresse->cMail; + $paymentOptions['locale'] = AbstractCheckout::getLocale($_SESSION['cISOSprache'], $order->oRechnungsadresse->cLand); + } + + $dueDays = $this->getExpiryDays(); + if ($dueDays > 3) { + $paymentOptions['dueDate'] = date('Y-m-d', strtotime("+$dueDays DAYS")); + } + + return $paymentOptions; + } +} diff --git a/version/206/paymentmethod/JTLMollieBelfius.php b/version/206/paymentmethod/JTLMollieBelfius.php new file mode 100644 index 0000000..b7cac71 --- /dev/null +++ b/version/206/paymentmethod/JTLMollieBelfius.php @@ -0,0 +1,17 @@ +clearToken(); + } + + protected function clearToken() + { + $this->unsetCache(self::CACHE_TOKEN) + ->unsetCache(self::CACHE_TOKEN_TIMESTAMP); + + return true; + } + + public function handleAdditional($aPost_arr) + { + $components = self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_components']; + $profileId = self::Plugin()->oPluginEinstellungAssoc_arr['profileId']; + + if ($components === 'N' || !$profileId || trim($profileId) === '') { + return parent::handleAdditional($aPost_arr); + } + + $cleared = false; + if (array_key_exists('clear', $aPost_arr) && (int)$aPost_arr['clear']) { + $cleared = $this->clearToken(); + } + + if ($components === 'S' && array_key_exists('skip', $aPost_arr) && (int)$aPost_arr['skip']) { + return parent::handleAdditional($aPost_arr); + } + + try { + $trustBadge = (bool)self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_loadTrust']; + $locale = AbstractCheckout::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer() ? Session::getInstance()->Customer()->cLand : null); + $mode = API::getMode(); + $errorMessage = json_encode(self::Plugin()->oPluginSprachvariableAssoc_arr['mcErrorMessage']); + } catch (Exception $e) { + Jtllog::writeLog($e->getMessage() . "\n" . print_r(['e' => $e], 1)); + + return parent::handleAdditional($aPost_arr); + } + + if (!$cleared && array_key_exists('cardToken', $aPost_arr) && ($token = trim($aPost_arr['cardToken']))) { + return $this->setToken($token) && parent::handleAdditional($aPost_arr); + } + + $token = false; + if (($ctTS = (int)$this->getCache(self::CACHE_TOKEN_TIMESTAMP)) && $ctTS > time()) { + $token = $this->getCache(self::CACHE_TOKEN); + } + + Shop::Smarty()->assign('profileId', $profileId) + ->assign('trustBadge', $trustBadge ? self::Plugin()->cFrontendPfadURLSSL . 'img/trust_' . $_SESSION['cISOSprache'] . '.png' : false) + ->assign('components', $components) + ->assign('locale', $locale ?: 'de_DE') + ->assign('token', $token ?: false) + ->assign('testMode', $mode ?: false) + ->assign('errorMessage', $errorMessage ?: null) + ->assign('mollieLang', self::Plugin()->oPluginSprachvariableAssoc_arr); + + return false; + } + + protected function setToken($token) + { + $this->addCache(self::CACHE_TOKEN, $token) + ->addCache(self::CACHE_TOKEN_TIMESTAMP, time() + 3600); + + return true; + } + + public function getPaymentOptions(Bestellung $order, $apiType) + { + $paymentOptions = []; + + if ($apiType === 'payment') { + if ($order->Lieferadresse !== null) { + if (!$order->Lieferadresse->cMail) { + $order->Lieferadresse->cMail = $order->oRechnungsadresse->cMail; + } + $paymentOptions['shippingAddress'] = Address::factory($order->Lieferadresse); + } + + $paymentOptions['billingAddress'] = Address::factory($order->oRechnungsadresse); + } + if ((int)$this->getCache(self::CACHE_TOKEN_TIMESTAMP) > time() && ($token = trim($this->getCache(self::CACHE_TOKEN)))) { + $paymentOptions['cardToken'] = $token; + } + + return $paymentOptions; + } +} diff --git a/version/206/paymentmethod/JTLMollieEPS.php b/version/206/paymentmethod/JTLMollieEPS.php new file mode 100644 index 0000000..5120b55 --- /dev/null +++ b/version/206/paymentmethod/JTLMollieEPS.php @@ -0,0 +1,17 @@ + substr($order->cBestellNr, 0, 13)]; + } +} diff --git a/version/206/paymentmethod/JTLMollieKlarnaPayLater.php b/version/206/paymentmethod/JTLMollieKlarnaPayLater.php new file mode 100644 index 0000000..3474053 --- /dev/null +++ b/version/206/paymentmethod/JTLMollieKlarnaPayLater.php @@ -0,0 +1,24 @@ +Lieferadresse !== null) { + if (!$order->Lieferadresse->cMail) { + $order->Lieferadresse->cMail = $order->oRechnungsadresse->cMail; + } + $paymentOptions['shippingAddress'] = Address::factory($order->Lieferadresse); + } + } + + + return $paymentOptions; + } +} diff --git a/version/206/paymentmethod/JTLMolliePaysafecard.php b/version/206/paymentmethod/JTLMolliePaysafecard.php new file mode 100644 index 0000000..29be769 --- /dev/null +++ b/version/206/paymentmethod/JTLMolliePaysafecard.php @@ -0,0 +1,23 @@ + $order->oKunde->kKunde] : []; + } +} diff --git a/version/206/paymentmethod/JTLMolliePrzelewy24.php b/version/206/paymentmethod/JTLMolliePrzelewy24.php new file mode 100644 index 0000000..62a55d5 --- /dev/null +++ b/version/206/paymentmethod/JTLMolliePrzelewy24.php @@ -0,0 +1,23 @@ + $order->oRechnungsadresse->cMail] : []; + } +} diff --git a/version/206/paymentmethod/JTLMollieSofort.php b/version/206/paymentmethod/JTLMollieSofort.php new file mode 100644 index 0000000..892adc3 --- /dev/null +++ b/version/206/paymentmethod/JTLMollieSofort.php @@ -0,0 +1,18 @@ + + {$oMollieException->getMessage()} + + +{/if} + +{if isset($redirect) && $redirect != ''} + + {if $checkoutMode == 'D'} + + {/if} +{/if} + diff --git a/version/206/paymentmethod/tpl/mollieComponents.tpl b/version/206/paymentmethod/tpl/mollieComponents.tpl new file mode 100644 index 0000000..987abf1 --- /dev/null +++ b/version/206/paymentmethod/tpl/mollieComponents.tpl @@ -0,0 +1,148 @@ +

{$mollieLang.cctitle}

+ +
+ +
+ + {if $token !== false} + +
+
+ {$mollieLang.clearDescr} +
+
+ +
+
+ + {if $trustBadge} +
+ PCI-DSS SAQ-A compliant +
+ {/if} + + {else} + + +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+ + {if $trustBadge} +
+ PCI-DSS SAQ-A compliant +
+ {/if} + {if $components == 'S'} + + {/if} +
+ +{/if} + + + + + + \ No newline at end of file diff --git a/version/206/tpl/_alerts.tpl b/version/206/tpl/_alerts.tpl new file mode 100644 index 0000000..5279fa3 --- /dev/null +++ b/version/206/tpl/_alerts.tpl @@ -0,0 +1,10 @@ +{if $alerts} + {assign var=alert_arr value=$alerts|get_object_vars} + {if $alert_arr|count} +
+ {foreach from=$alert_arr item=alert key=id} +
{$alert}
+ {/foreach} +
+ {/if} +{/if} diff --git a/version/207/adminmenu/info.php b/version/207/adminmenu/info.php new file mode 100644 index 0000000..4ef2525 --- /dev/null +++ b/version/207/adminmenu/info.php @@ -0,0 +1,68 @@ +assign('defaultTabbertab', Helper::getAdminmenu('Info') + Helper::getAdminmenu('Support')); + Helper::selfupdate(); + } + + $svgQuery = http_build_query([ + 'p' => Helper::oPlugin()->cPluginID, + 'v' => Helper::oPlugin()->nVersion, + 's' => defined('APPLICATION_VERSION') ? APPLICATION_VERSION : JTL_VERSION, + 'b' => defined('JTL_MINOR_VERSION') ? JTL_MINOR_VERSION : '0', + 'd' => Helper::getDomain(), + 'm' => base64_encode(Helper::getMasterMail(true)), + 'php' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION . PHP_EXTRA_VERSION, + ]); + + echo ""; + echo "
" . + "
" . + " " . + " Lizenz Informationen" . + ' ' . + '
' . + "
" . + " " . + " Update Informationen" . + ' ' . + '
' . + "
" . + " " . + " Plugin informationen" . + ' ' . + '
' . + '
'; + + try { + $latestRelease = Helper::getLatestRelease(array_key_exists('update', $_REQUEST)); + if ((int)Helper::oPlugin()->nVersion < (int)$latestRelease->version) { + Shop::Smarty()->assign('update', $latestRelease); + } + } catch (Exception $e) { + } + + Shop::Smarty()->display(Helper::oPlugin()->cAdminmenuPfad . '/tpl/info.tpl'); + + if (file_exists(__DIR__ . '/_addon.php')) { + try { + include __DIR__ . '/_addon.php'; + } catch (Exception $e) { + } + } +} catch (Exception $e) { + echo "
Fehler: {$e->getMessage()}
"; + Helper::logExc($e); +} diff --git a/version/207/adminmenu/orders.php b/version/207/adminmenu/orders.php new file mode 100644 index 0000000..68197ed --- /dev/null +++ b/version/207/adminmenu/orders.php @@ -0,0 +1,245 @@ +getModel()->kID)) { + Helper::addAlert('Zahlungserinnerung wurde verschickt.', 'success', 'orders'); + } else { + Helper::addAlert('Es ist ein Fehler aufgetreten, prüfe den Log.', 'danger', 'orders'); + } + } else { + Helper::addAlert('Bestellung konnte nicht geladen werden.', 'danger', 'orders'); + } + + + break; + + case 'fetchable': + if (array_key_exists('kBestellung', $_REQUEST) && ($checkout = AbstractCheckout::fromBestellung((int)$_REQUEST['kBestellung']))) { + if (AbstractCheckout::makeFetchable($checkout->getBestellung(), $checkout->getModel())) { + Helper::addAlert('Bestellung kann jetzt von der WAWI abgeholt werden.', 'success', 'orders'); + } else { + Helper::addAlert('Es ist ein Fehler aufgetreten, prüfe den Log.', 'danger', 'orders'); + } + } else { + Helper::addAlert('Bestellung konnte nicht geladen werden.', 'danger', 'orders'); + } + + break; + + case 'export': + try { + $export = []; + + $from = new DateTime($_REQUEST['from']); + $to = new DateTime($_REQUEST['to']); + + $orders = Shop::DB()->executeQueryPrepared('SELECT * FROM xplugin_ws_mollie_payments WHERE kBestellung > 0 AND dCreatedAt >= :From AND dCreatedAt <= :To ORDER BY dCreatedAt', [ + ':From' => $from->format('Y-m-d'), + ':To' => $to->format('Y-m-d'), + ], 2); + + + header('Content-Type: application/csv'); + header('Content-Disposition: attachment; filename=mollie-' . $from->format('Ymd') . '-' . $to->format('Ymd') . '.csv'); + header('Pragma: no-cache'); + + $out = fopen('php://output', 'wb'); + + + fputcsv($out, [ + 'kBestellung', + 'OrderID', + 'Status (mollie)', + 'BestellNr', + 'Status (JTL)', + 'Mode', + 'OriginalOrderNumber', + 'Currency', + 'Amount', + 'Method', + 'PaymentID', + 'Created' + ]); + + + foreach ($orders as $order) { + $order = new ws_mollie\Model\Payment($order); + $checkout = AbstractCheckout::fromModel($order); + + $tmp = [ + 'kBestellung' => $order->kBestellung, + 'cOrderId' => $order->kID, + 'cStatus' => $checkout->getMollie() ? $checkout->getMollie()->status : $order->cStatus, + 'cBestellNr' => $checkout->getBestellung() ? $checkout->getBestellung()->cBestellNr : $order->cOrderNumber, + 'nStatus' => $checkout->getBestellung() ? $checkout->getBestellung()->cStatus : 0, + 'cMode' => $order->cMode, + 'cOriginalOrderNumber' => $checkout->getMollie() && isset($checkout->getMollie()->metadata->originalOrderNumber) ? $checkout->getMollie()->metadata->originalOrderNumber : '', + 'cCurrency' => $order->cCurrency, + 'fAmount' => $order->fAmount, + 'cMethod' => $order->cMethod, + 'cPaymentId' => $order->cTransactionId, + 'dCreated' => $order->dCreatedAt, + ]; + + try { + if ($checkout->getMollie() && $checkout->getMollie()->resource === 'order') { + foreach ($checkout->getMollie()->payments() as $payment) { + if ($payment->status === PaymentStatus::STATUS_PAID) { + $tmp['cPaymentId'] = $payment->id; + } + } + } + } catch (Exception $e) { + } + fputcsv($out, $tmp); + + $export[] = $tmp; + } + + fclose($out); + exit(); + } catch (Exception $e) { + Helper::addAlert('Fehler:' . $e->getMessage(), 'danger', 'orders'); + } + + break; + + case 'refund': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angegeben!', 'danger', 'orders'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($refund = $checkout::refund($checkout)) { + Helper::addAlert(sprintf('Bestellung wurde zurückerstattet (%s).', $refund->id), 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'cancel': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($checkout::cancel($checkout)) { + Helper::addAlert('Bestellung wurde abgebrochen.', 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'capture': + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + if ($shipmentId = OrderCheckout::capture($checkout)) { + Helper::addAlert(sprintf('Zahlung erfolgreich erfasst/versandt (%s).', $shipmentId), 'success', 'orders'); + } + goto order; + } catch (InvalidArgumentException $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + goto order; + } + + break; + + case 'order': + order : + try { + if (!array_key_exists('id', $_REQUEST)) { + throw new InvalidArgumentException('Keine ID angeben!'); + } + + $checkout = AbstractCheckout::fromID($_REQUEST['id']); + + if ($checkout instanceof OrderCheckout) { + Shop::Smarty()->assign('shipments', $checkout->getShipments()); + } + + Shop::Smarty()->assign('payment', $checkout->getModel()) + ->assign('oBestellung', $checkout->getBestellung()) + ->assign('order', $checkout->getMollie()) + ->assign('checkout', $checkout) + ->assign('logs', $checkout->getLogs()); + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/order.tpl'); + + return; + } catch (Exception $e) { + Helper::addAlert('Fehler: ' . $e->getMessage(), 'danger', 'orders'); + } + + break; + } + } + + // Mollie::fixZahlungsarten(); + + $checkouts = []; + $payments = Shop::DB()->executeQueryPrepared('SELECT * FROM xplugin_ws_mollie_payments WHERE kBestellung IS NOT NULL ORDER BY dCreatedAt DESC LIMIT 1000;', [], 2); + foreach ($payments as $i => $payment) { + $payment = new Payment($payment); + + try { + $checkouts[$payment->kBestellung] = AbstractCheckout::fromModel($payment, false); + } catch (Exception $e) { + //Helper::addAlert($e->getMessage(), 'danger', 'orders'); + } + } + + Shop::Smarty()->assign('payments', $payments) + ->assign('checkouts', $checkouts) + ->assign('admRoot', str_replace('http:', '', $oPlugin->cAdminmenuPfadURL)) + ->assign('hasAPIKey', trim(Helper::getSetting('api_key')) !== ''); + + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/orders.tpl'); +} catch (Exception $e) { + echo "
" . + "{$e->getMessage()}
" . + "
{$e->getFile()}:{$e->getLine()}
{$e->getTraceAsString()}
" . + '
'; + Helper::logExc($e); +} diff --git a/version/207/adminmenu/paymentmethods.php b/version/207/adminmenu/paymentmethods.php new file mode 100644 index 0000000..49c2851 --- /dev/null +++ b/version/207/adminmenu/paymentmethods.php @@ -0,0 +1,103 @@ +setApiKey(Helper::getSetting('api_key')); + + $profile = $mollie->profiles->get('me'); + + $za = filter_input(INPUT_GET, 'za', FILTER_VALIDATE_BOOLEAN); + $active = filter_input(INPUT_GET, 'active', FILTER_VALIDATE_BOOLEAN); + $amount = filter_input(INPUT_GET, 'amount', FILTER_VALIDATE_FLOAT) ?: null; + $locale = filter_input(INPUT_GET, 'locale', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-zA-Z]{2}_[a-zA-Z]{2}$/']]) ?: AbstractCheckout::getLocale(); + $currency = filter_input(INPUT_GET, 'currency', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[a-zA-Z]{3}$/']]) ?: 'EUR'; + + + if ($za) { + Shop::Smarty()->assign('defaultTabbertab', Helper::getAdminmenu('Zahlungsarten')); + } + + $params = [ + 'include' => 'pricing,issuers', + 'locale' => $locale + ]; + if ($amount && $currency) { + $params['amount'] = ['value' => number_format($amount, 2, '.', ''), 'currency' => $currency]; + if ($active) { + $params['includeWallets'] = 'applepay'; + //$params['resource'] = 'orders'; + } + } + + $allMethods = []; + if ($active) { + $_allMethods = $mollie->methods->allActive($params); + } else { + $_allMethods = $mollie->methods->allAvailable($params); + } + + $sessionLife = (int)ini_get('session.gc_maxlifetime'); + + /** @var Method $method */ + foreach ($_allMethods as $method) { + $id = $method->id === 'creditcard' ? 'kreditkarte' : $method->id; + $key = "kPlugin_{$oPlugin->kPlugin}_mollie$id"; + + $class = null; + $shop = null; + $oClass = null; + + if (array_key_exists($key, $oPlugin->oPluginZahlungsKlasseAssoc_arr) && !in_array($id, ['voucher', 'giftcard', 'directdebit'], true)) { + $class = $oPlugin->oPluginZahlungsKlasseAssoc_arr[$key]; + include_once $oPlugin->cPluginPfad . 'paymentmethod/' . $class->cClassPfad; + /** @var JTLMollie $oClass */ + $oClass = new $class->cClassName($id); + } + if (array_key_exists($key, $oPlugin->oPluginZahlungsmethodeAssoc_arr)) { + $shop = $oPlugin->oPluginZahlungsmethodeAssoc_arr[$key]; + } + + $maxExpiryDays = $oClass ? $oClass->getExpiryDays() : null; + $allMethods[$method->id] = (object)[ + 'mollie' => $method, + 'class' => $class, + 'allowPreOrder' => $oClass ? $oClass::ALLOW_PAYMENT_BEFORE_ORDER : false, + 'allowAutoStorno' => $oClass ? $oClass::ALLOW_AUTO_STORNO : false, + 'oClass' => $oClass, + 'shop' => $shop, + 'maxExpiryDays' => $oClass ? $maxExpiryDays : null, + 'warning' => $oClass && ($maxExpiryDays * 24 * 60 * 60) > $sessionLife, + 'session' => round($sessionLife / 60 / 60, 2) . 'h' + ]; + } + + Shop::Smarty()->assign('profile', $profile) + ->assign('currencies', AbstractCheckout::getCurrencies()) + ->assign('locales', AbstractCheckout::getLocales()) + ->assign('allMethods', $allMethods) + ->assign('settings', $oPlugin->oPluginEinstellungAssoc_arr); + Shop::Smarty()->display($oPlugin->cAdminmenuPfad . '/tpl/paymentmethods.tpl'); +} catch (Exception $e) { + echo "
{$e->getMessage()}
"; + Helper::logExc($e); +} diff --git a/version/207/adminmenu/tpl/_addon.tpl b/version/207/adminmenu/tpl/_addon.tpl new file mode 100644 index 0000000..6539757 --- /dev/null +++ b/version/207/adminmenu/tpl/_addon.tpl @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/version/207/adminmenu/tpl/info.tpl b/version/207/adminmenu/tpl/info.tpl new file mode 100644 index 0000000..2d41c95 --- /dev/null +++ b/version/207/adminmenu/tpl/info.tpl @@ -0,0 +1,114 @@ +
+
+
+ + + +
+
+ + + +
+ + {if isset($update)} +
+ +
+

Update auf Version {$update->version} verfügbar!

+
+
+
+
Version:
+
{$update->version}
+
+
+
Erschienen:
+
{$update->create_date}
+
+
+
Changelog:
+
+ +
+
+ +
+
+ +
+
+
+ {else} +
+ + + +
+ {/if} + +
+
+
+ + + +
+
+ + + +
+ +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+{if file_exists("{$smarty['current_dir']}/_addon.tpl")} + {include file="{$smarty['current_dir']}/_addon.tpl"} +{/if} +{if isset($oPlugin)} + +{/if} diff --git a/version/207/adminmenu/tpl/mollie-account-erstellen.png b/version/207/adminmenu/tpl/mollie-account-erstellen.png new file mode 100644 index 0000000..fc18192 Binary files /dev/null and b/version/207/adminmenu/tpl/mollie-account-erstellen.png differ diff --git a/version/207/adminmenu/tpl/order.tpl b/version/207/adminmenu/tpl/order.tpl new file mode 100644 index 0000000..aad5810 --- /dev/null +++ b/version/207/adminmenu/tpl/order.tpl @@ -0,0 +1,344 @@ + +

+ « + Bestellung: {$oBestellung->cBestellNr} - + {if $oBestellung->cStatus|intval == 1} + OFFEN + {elseif $oBestellung->cStatus|intval == 2} + IN BEARBEITUNG + {elseif $oBestellung->cStatus|intval == 3} + BEZAHLT + {elseif $oBestellung->cStatus|intval == 4} + VERSANDT + {elseif $oBestellung->cStatus|intval == 5} + TEILVERSANDT + {elseif $oBestellung->cStatus|intval == -1} + STORNO + {else} + n/a + {/if} +

+ +{ws_mollie\Helper::showAlerts('orders')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Mollie ID: + + {$payment->kID} + + + Mode:{$order->mode}Status: + {$order->status} + {if $order->amountRefunded && $order->amountRefunded->value == $order->amount->value} + (total refund) + {elseif $order->amountRefunded && $order->amountRefunded->value > 0} + (partly refund) + {/if} +
Betrag:{$order->amount->value|number_format:2:',':''} {$order->amount->currency}Captured:{if $order->amountCaptured}{$order->amountCaptured->value|number_format:2:',':''} {$order->amountCaptured->currency}{else}-{/if}Refunded:{if $order->amountRefunded}{$order->amountRefunded->value|number_format:2:',':''} {$order->amountRefunded->currency}{else}-{/if} +
Method:{$order->method}Locale:{$order->locale}Erstellt:{"d. M Y H:i:s"|date:{$order->createdAt|strtotime}}
Kunde: + {if isset($order->customerId)} + ID: + {$order->customerId} +
+ {/if} + {if isset($order->billingAddress, $order->billingAddress->organizationName)} + {$order->billingAddress->organizationName} + {elseif isset($order->billingAddress)} + {$order->billingAddress->title} {$order->billingAddress->givenName} {$order->billingAddress->familyName} + {/if} +
Mollie Checkout: + {if $order->getCheckoutURL()} + mollie.com/... + {else} + n/a + {/if} + + +
+{if $order->id|strpos:"ord_" !== false && $order->payments()->count > 0} +

Zahlungen

+ + + + + + + + + + + + + + {foreach from=$order->payments() item=payment} + + + + + + + + + + + {/foreach} +
IDStatusMethodeAmountSettlementRefundedRemainingDetails
{$payment->id}{$payment->status}{$payment->method}{$payment->amount->value} {$payment->amount->currency} + {if $payment->settlementAmount} + {$payment->settlementAmount->value} {$payment->settlementAmount->currency} + {else}-{/if} + + {if $payment->amountRefunded} + {$payment->amountRefunded->value} {$payment->amountRefunded->currency} + {else}-{/if} + + {if $payment->amountRemaining} + {$payment->amountRemaining->value} {$payment->amountRemaining->currency} + {else}-{/if} + +
    + {foreach from=$payment->details item=value key=key} +
  • {$key}: {if $value|is_scalar}{$value}{else}{$value|json_encode}{/if}
  • + {/foreach} +
+
+{/if} + +
+ {if ($order->status === 'authorized' || $order->status === 'shipping') && $oBestellung->cStatus|intval >= 3} + + Zahlung erfassen1 + + {/if} + {if !$order->amountRefunded || ($order->amount->value > $order->amountRefunded->value)} + Rückerstatten2 + + {/if} + {if $order->isCancelable} + Abbrechen3 + + {/if} +
+ +{if $order->id|strpos:"ord_" !== false} +

Positionen:

+ + + + + + + + + + + + + + + + + {assign var="vat" value=0} + {assign var="netto" value=0} + {assign var="brutto" value=0} + {foreach from=$order->lines item=line} + + {assign var="vat" value=$vat+$line->vatAmount->value} + {assign var="netto" value=$netto+$line->totalAmount->value-$line->vatAmount->value} + {assign var="brutto" value=$brutto+$line->totalAmount->value} + + + + + + + + + + + + + {/foreach} + + + + + + + + + + +
StatusSKUNameTypAnzahlMwStSteuerNettoBrutto 
+ {if $line->status == 'created'} + erstellt + {elseif $line->status == 'pending'} + austehend + {elseif $line->status == 'paid'} + bezahlt + {elseif $line->status == 'authorized'} + autorisiert + {elseif $line->status == 'shipping'} + versendet + {elseif $line->status == 'completed'} + abgeschlossen + {elseif $line->status == 'expired'} + abgelaufen + {elseif $line->status == 'canceled'} + abgebrochen + {else} + Unbekannt: {$line->status} + {/if} + {$line->sku}{$line->name|utf8_decode}{$line->type}{$line->quantity}{$line->vatRate|floatval}%{$line->vatAmount->value|number_format:2:',':''} {$line->vatAmount->currency}{($line->totalAmount->value - $line->vatAmount->value)|number_format:2:',':''} {$line->vatAmount->currency}{$line->totalAmount->value|number_format:2:',':''} {$line->totalAmount->currency} + {*if $line->quantity > $line->quantityShipped} + + + + {/if} + {if $line->quantity > $line->quantityRefunded} + + + + {/if} + {if $line->isCancelable} + + + + {/if*} + {*$line|var_dump*} +
{$vat|number_format:2:',':''} {$order->amount->currency}{$netto|number_format:2:',':''} {$order->amount->currency}{$brutto|number_format:2:',':''} {$order->amount->currency} 
+
+ 1 = Bestellung wird bei Mollie als versandt markiert. WAWI wird nicht informiert.
+ 2 = Bezahlter Betrag wird dem Kunden rckerstattet. WAWI wird nicht informiert.
+ 3 = Bestellung wird bei Mollie storniert. WAWI wird nicht informiert.
+
+{/if} + +{if isset($shipments) && $shipments|count} +

Shipmets

+ + + + + + + + + + + {foreach from=$shipments item=shipment} + + + + + + + {/foreach} + +
Lieferschein Nr.Mollie IDCarrierCode
{$shipment->getLieferschein()->getLieferscheinNr()}{$shipment->getShipment()->id} + {if isset($shipment->getShipment()->tracking)} + {$shipment->getShipment()->tracking->carrier} + {else} + - + {/if} + {if isset($shipment->getShipment()->tracking)} + {if isset($shipment->getShipment()->tracking->url)} + + {$shipment->getShipment()->tracking->code} + + {else} + {$shipment->getShipment()->tracking->code} + {/if} + {else} + - + {/if} +
+{/if} + +

Log

+ + {foreach from=$logs item=log} + + + + + + + {/foreach} +
+ {if $log->nLevel == 1} + Fehler + {elseif $log->nLevel == 2} + Hinweis + {elseif $log->nLevel == 3} + Debug + {else} + unknown {$log->nLevel} + {/if} + {$log->cModulId} +
+ {$log->cLog} +
+
{$log->dDatum}
+ + diff --git a/version/207/adminmenu/tpl/orders.tpl b/version/207/adminmenu/tpl/orders.tpl new file mode 100644 index 0000000..a499f32 --- /dev/null +++ b/version/207/adminmenu/tpl/orders.tpl @@ -0,0 +1,160 @@ +{ws_mollie\Helper::showAlerts('orders')} + +{if $hasAPIKey == false} + + Jetzt kostenlos Mollie Account eröffnen! + +{else} + + + + + + + + + + + + + + + {foreach from=$checkouts item=checkout} + + + + + + + + + + + + + + + {/foreach} + +
BestellNr.IDMollie StatusJTL StatusBetragMethodeErstellt 
+ {if $checkout->getModel()->bSynced == false && $checkout->getBestellung()->cAbgeholt === 'Y'} + * + {/if} + {$checkout->getModel()->cOrderNumber} + {if $checkout->getModel()->cMode == 'test'} + TEST + {/if} + {if $checkout->getModel()->bLockTimeout} + LOCK TIMEOUT + {/if} + {if $checkout->getModel()->dReminder && $checkout->getModel()->dReminder !== '0000-00-00 00:00:00'} + getModel()->dReminder|strtotime}}"> + {/if} + + {$checkout->getModel()->kID} + + {if $checkout->getModel()->cStatus == 'created' || $checkout->getModel()->cStatus == 'open'} + erstellt + {elseif $checkout->getModel()->cStatus == 'pending'} + austehend + {elseif $checkout->getModel()->cStatus == 'paid'} + bezahlt + {elseif $checkout->getModel()->cStatus == 'authorized'} + autorisiert + {elseif $checkout->getModel()->cStatus == 'shipping'} + versendet + {elseif $checkout->getModel()->cStatus == 'completed'} + abgeschlossen + {elseif $checkout->getModel()->cStatus == 'expired'} + abgelaufen + {elseif $checkout->getModel()->cStatus == 'canceled'} + abgebrochen + {else} + Unbekannt: {$checkout->getModel()->cStatus} + {/if} + {if $checkout->getModel()->fAmountRefunded && $checkout->getModel()->fAmountRefunded == $checkout->getModel()->fAmount} + (total refund) + {elseif $checkout->getModel()->fAmountRefunded && $checkout->getModel()->fAmountRefunded > 0} + (partly refund) + {/if} + + + {if $checkout->getBestellung()->cStatus|intval == 1} + OFFEN + {elseif $checkout->getBestellung()->cStatus|intval == 2} + IN BEARBEITUNG + {elseif $checkout->getBestellung()->cStatus|intval == 3} + BEZAHLT + {elseif $checkout->getBestellung()->cStatus|intval == 4} + VERSANDT + {elseif $checkout->getBestellung()->cStatus|intval == 5} + TEILVERSANDT + {elseif $checkout->getBestellung()->cStatus|intval == -1} + STORNO + {else} + n/a + {/if} + {$checkout->getModel()->fAmount|number_format:2:',':''} {$checkout->getModel()->cCurrency}{$checkout->getModel()->cMethod}{"d. M Y H:i"|date:{$checkout->getModel()->dCreatedAt|strtotime}} +
+ +
+ {if $checkout->getModel()->bSynced == false && $checkout->getBestellung()->cAbgeholt === 'Y'} + + {/if} + {if $checkout->remindable()} + + {/if} +
+
+
+ {if $payments|count > 900} +
Hier werden nur die letzten 1000 Ergebnisse angezeigt.
+ {/if} + + +
+
+

Export:

+
+ +
+ + + + + +
+
+ +{/if} + diff --git a/version/207/adminmenu/tpl/paymentmethods.tpl b/version/207/adminmenu/tpl/paymentmethods.tpl new file mode 100644 index 0000000..7821af5 --- /dev/null +++ b/version/207/adminmenu/tpl/paymentmethods.tpl @@ -0,0 +1,148 @@ +

Account Status

+ + + + + + + {if $profile->review} + + + {/if} + {if $profile->_links->checkoutPreviewUrl->href} + + {/if} + + +
Mode:{$profile->mode}Status:{$profile->status}Review:{$profile->review->status} + Checkout + Preview + + Mollie Dashboard +
+
+
+
+ + +
+ + + +
+
+ + +
+
+ + +
+
+ + + reset +
+
+
+
+{if $allMethods && $allMethods|count} + + + + + + + + + + + + {foreach from=$allMethods item=method} + + + + + + + + {/foreach} + +
BildName / IDInfoPreiseLimits
{$method->mollie->description|utf8_decode} + {if $method->mollie->status === 'activated'} + + {else} + + {/if} + {$method->mollie->description|utf8_decode}
+ {$method->mollie->id} +
+ {if $method->shop && $method->oClass} + {if intval($method->shop->nWaehrendBestellung) === 1 && !$method->allowPreOrder} +
Zahlung VOR Bestellabschluss nicht unterstützt!
+ {else} +
+ Bestellabschluss: + {if intval($method->shop->nWaehrendBestellung) === 1} + NACH Zahlung + {else} + VOR Zahlung + {/if} +
+ {/if} + + {if intval($settings.autoStorno) > 0} +
+ Unbez. Bestellung stornieren: + {if $method->allowAutoStorno} +
auto
+ {else} +
manual
+ {/if} +
+ {/if} +
+ Gültigkeit: + {$method->maxExpiryDays} Tage +
+ {else} + Derzeit nicht unterstützt. + {/if} +
+
    + {foreach from=$method->mollie->pricing item=price} +
  • + {$price->description|utf8_decode}: {$price->fixed->value} {$price->fixed->currency} + {if $price->variable > 0.0} + + {$price->variable}% + {/if} +
  • + {/foreach} +
+
+ Min: {if $method->mollie->minimumAmount}{$method->mollie->minimumAmount->value} {$method->mollie->minimumAmount->currency}{else}n/a{/if} +
+ Max: {if $method->mollie->maximumAmount}{$method->mollie->maximumAmount->value} {$method->mollie->maximumAmount->currency}{else}n/a{/if} +
+{else} +
Es konnten keine Methoden abgerufen werden.
+{/if} \ No newline at end of file diff --git a/version/207/class/API.php b/version/207/class/API.php new file mode 100644 index 0000000..3532455 --- /dev/null +++ b/version/207/class/API.php @@ -0,0 +1,77 @@ +test = $test === null ? self::getMode() : $test; + } + + /** + * @return bool + */ + public static function getMode() + { + require_once PFAD_ROOT . PFAD_ADMIN . PFAD_INCLUDES . 'benutzerverwaltung_inc.php'; + + return self::Plugin()->oPluginEinstellungAssoc_arr['testAsAdmin'] === 'Y' && Shop::isAdmin() && self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key']; + } + + /** + * @throws ApiException + * @throws IncompatiblePlatform + * @return MollieApiClient + */ + public function Client() + { + if (!$this->client) { + $this->client = new MollieApiClient(/*new Client([ + RequestOptions::VERIFY => CaBundle::getBundledCaBundlePath(), + RequestOptions::TIMEOUT => 60 + ])*/ + ); + $this->client->setApiKey($this->isTest() ? self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key'] : self::Plugin()->oPluginEinstellungAssoc_arr['api_key']) + ->addVersionString('JTL-Shop/' . JTL_VERSION . JTL_MINOR_VERSION) + ->addVersionString('ws_mollie/' . self::Plugin()->nVersion); + } + + return $this->client; + } + + /** + * @return bool + */ + public function isTest() + { + return $this->test; + } +} diff --git a/version/207/class/Checkout/AbstractCheckout.php b/version/207/class/Checkout/AbstractCheckout.php new file mode 100644 index 0000000..8a31700 --- /dev/null +++ b/version/207/class/Checkout/AbstractCheckout.php @@ -0,0 +1,1214 @@ + ['lang' => 'de', 'country' => ['DE', 'AT', 'CH']], + 'fre' => ['lang' => 'fr', 'country' => ['BE', 'FR']], + 'dut' => ['lang' => 'nl', 'country' => ['BE', 'NL']], + 'spa' => ['lang' => 'es', 'country' => ['ES']], + 'ita' => ['lang' => 'it', 'country' => ['IT']], + 'pol' => ['lang' => 'pl', 'country' => ['PL']], + 'hun' => ['lang' => 'hu', 'country' => ['HU']], + 'por' => ['lang' => 'pt', 'country' => ['PT']], + 'nor' => ['lang' => 'nb', 'country' => ['NO']], + 'swe' => ['lang' => 'sv', 'country' => ['SE']], + 'fin' => ['lang' => 'fi', 'country' => ['FI']], + 'dan' => ['lang' => 'da', 'country' => ['DK']], + 'ice' => ['lang' => 'is', 'country' => ['IS']], + 'eng' => ['lang' => 'en', 'country' => ['GB', 'US']], + ]; + /** + * @var null|\Mollie\Api\Resources\Customer + */ + protected $customer; + /** + * @var string + */ + private $hash; + /** + * @var API + */ + private $api; + /** + * @var JTLMollie + */ + private $paymentMethod; + /** + * @var Bestellung + */ + private $oBestellung; + /** + * @var Payment + */ + private $model; + + /** + * AbstractCheckout constructor. + * @param Bestellung $oBestellung + * @param null $api + */ + public function __construct(Bestellung $oBestellung, $api = null) + { + $this->api = $api; + $this->oBestellung = $oBestellung; + } + + /** + * @param int $kBestellung + * @param bool $checkZA + * @return bool + */ + public static function isMollie($kBestellung, $checkZA = false) + { + if ($checkZA) { + $res = Shop::DB()->executeQueryPrepared('SELECT * FROM tzahlungsart WHERE cModulId LIKE :cModulId AND kZahlungsart = :kZahlungsart', [ + ':kZahlungsart' => $kBestellung, + ':cModulId' => 'kPlugin_' . self::Plugin()->kPlugin . '_%' + ], 1); + + return (bool)$res; + } + + return ($res = Shop::DB()->executeQueryPrepared('SELECT kId FROM xplugin_ws_mollie_payments WHERE kBestellung = :kBestellung;', [ + ':kBestellung' => $kBestellung, + ], 1)) && $res->kId; + } + + public static function finalizeOrder($sessionHash, $id, $test = false) + { + try { + if ($paymentSession = Shop::DB()->select('tzahlungsession', 'cZahlungsID', $sessionHash)) { + if (session_id() !== $paymentSession->cSID) { + session_destroy(); + session_id($paymentSession->cSID); + $session = Session::getInstance(true, true); + } else { + $session = Session::getInstance(false); + } + + if ( + (!isset($paymentSession->nBezahlt) || !$paymentSession->nBezahlt) + && (!isset($paymentSession->kBestellung) || !$paymentSession->kBestellung) + && isset($_SESSION['Warenkorb']->PositionenArr) + && count($_SESSION['Warenkorb']->PositionenArr) + ) { + $paymentSession->cNotifyID = $id; + $paymentSession->dNotify = 'now()'; + Shop::DB()->update('tzahlungsession', 'cZahlungsID', $sessionHash, $paymentSession); + + $api = new API($test); + if (strpos($id, 'tr_') === 0) { + $mollie = $api->Client()->payments->get($id); + } else { + $mollie = $api->Client()->orders->get($id, ['embed' => 'payments']); + } + + if (in_array($mollie->status, [OrderStatus::STATUS_PENDING, OrderStatus::STATUS_AUTHORIZED, OrderStatus::STATUS_PAID], true)) { + require_once PFAD_ROOT . PFAD_INCLUDES . 'bestellabschluss_inc.php'; + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + $order = finalisiereBestellung(); + $session->cleanUp(); + $paymentSession->nBezahlt = 1; + $paymentSession->dZeitBezahlt = 'now()'; + } else { + throw new Exception('Mollie Status invalid: ' . $mollie->status . '\n' . print_r([$sessionHash, $id], 1)); + } + + if ($order->kBestellung) { + $paymentSession->kBestellung = $order->kBestellung; + Shop::DB()->update('tzahlungsession', 'cZahlungsID', $sessionHash, $paymentSession); + + try { + $checkout = self::fromID($id, false, $order); + } catch (Exception $e) { + if (strpos($id, 'tr_') === 0) { + $checkoutClass = PaymentCheckout::class; + } else { + $checkoutClass = OrderCheckout::class; + } + $checkout = new $checkoutClass($order, $api); + } + $checkout->setMollie($mollie)->updateModel()->saveModel(); + $checkout->updateOrderNumber(); + $checkout->handleNotification($sessionHash); + } + } else { + Jtllog::writeLog(sprintf('PaymentSession bereits bezahlt: %s - ID: %s => Queue', $sessionHash, $id), JTLLOG_LEVEL_NOTICE); + Queue::saveToQueue($id, $id, 'webhook'); + } + } else { + Jtllog::writeLog(sprintf('PaymentSession nicht gefunden: %s - ID: %s => Queue', $sessionHash, $id), JTLLOG_LEVEL_NOTICE); + Queue::saveToQueue($id, $id, 'webhook'); + } + } catch (Exception $e) { + Helper::logExc($e); + } + } + + /** + * @param string $id + * @param bool $bFill + * @param null|Bestellung $order + * @throws RuntimeException + * @return OrderCheckout|PaymentCheckout + */ + public static function fromID($id, $bFill = true, Bestellung $order = null) + { + if (($model = Payment::fromID($id))) { + return static::fromModel($model, $bFill, $order); + } + + throw new RuntimeException(sprintf('Error loading Order: %s', $id)); + } + + /** + * @param Payment $model + * @param bool $bFill + * @param null|Bestellung $order + * @return OrderCheckout|PaymentCheckout + */ + public static function fromModel($model, $bFill = true, Bestellung $order = null) + { + if (!$model) { + throw new RuntimeException(sprintf('Error loading Order for Model: %s', print_r($model, 1))); + } + + $oBestellung = $order; + if (!$order) { + $oBestellung = new Bestellung($model->kBestellung, $bFill); + if (!$oBestellung->kBestellung) { + throw new RuntimeException(sprintf('Error loading Bestellung: %s', $model->kBestellung)); + } + } + + if (strpos($model->kID, 'tr_') !== false) { + $self = new PaymentCheckout($oBestellung); + } else { + $self = new OrderCheckout($oBestellung); + } + + $self->setModel($model); + + return $self; + } + + /** + * @return bool + */ + public function saveModel() + { + return $this->getModel()->save(); + } + + /** + * @return Payment + */ + public function getModel() + { + if (!$this->model) { + $this->model = Payment::fromID($this->oBestellung->kBestellung, 'kBestellung'); + } + + return $this->model; + } + + /** + * @param $model + * @return $this + */ + protected function setModel($model) + { + if (!$this->model) { + $this->model = $model; + } else { + throw new RuntimeException('Model already set.'); + } + + return $this; + } + + /** + * @param null $hash + * @throws Exception + */ + public function handleNotification($hash = null) + { + if (!$this->getHash()) { + $this->getModel()->cHash = $hash; + } + + $this->updateModel()->saveModel(); + if (!$this->getBestellung()->dBezahltDatum || $this->getBestellung()->dBezahltDatum === '0000-00-00') { + if ($incoming = $this->getIncomingPayment()) { + $this->PaymentMethod()->addIncomingPayment($this->getBestellung(), $incoming); + + $this->PaymentMethod()->setOrderStatusToPaid($this->getBestellung()); + static::makeFetchable($this->getBestellung(), $this->getModel()); + $this->PaymentMethod()->deletePaymentHash($this->getHash()); + $this->Log(sprintf("Checkout::handleNotification: Bestellung '%s' als bezahlt markiert: %.2f %s", $this->getBestellung()->cBestellNr, (float)$incoming->fBetrag, $incoming->cISO)); + + $oZahlungsart = Shop::DB()->selectSingleRow('tzahlungsart', 'cModulId', $this->PaymentMethod()->moduleID); + if ($oZahlungsart && (int)$oZahlungsart->nMailSenden === 1) { + require_once PFAD_ROOT . 'includes/mailTools.php'; + $this->PaymentMethod()->sendConfirmationMail($this->getBestellung()); + } + if (!$this->completlyPaid()) { + $this->Log(sprintf("Checkout::handleNotification: Bestellung '%s': nicht komplett bezahlt: %.2f %s", $this->getBestellung()->cBestellNr, (float)$incoming->fBetrag, $incoming->cISO), LOGLEVEL_ERROR); + } + } + } + } + + /** + * @return string + */ + public function getHash() + { + if ($this->getModel()->cHash) { + return $this->getModel()->cHash; + } + if (!$this->hash) { + $this->hash = $this->PaymentMethod()->generateHash($this->oBestellung); + } + + return $this->hash; + } + + /** + * @return JTLMollie + */ + public function PaymentMethod() + { + if (!$this->paymentMethod) { + include_once PFAD_ROOT . PFAD_INCLUDES . 'modules/PaymentMethod.class.php'; + if ($this->getBestellung()->Zahlungsart && strpos($this->getBestellung()->Zahlungsart->cModulId, "kPlugin_{$this::Plugin()->kPlugin}_") !== false) { + try { + $this->paymentMethod = PaymentMethod::create($this->getBestellung()->Zahlungsart->cModulId); + } catch (Exception $e) { + $this->paymentMethod = PaymentMethod::create("kPlugin_{$this::Plugin()->kPlugin}_mollie"); + } + } else { + $this->paymentMethod = PaymentMethod::create("kPlugin_{$this::Plugin()->kPlugin}_mollie"); + } + } + + return $this->paymentMethod; + } + + /** + * @return Bestellung + */ + public function getBestellung() + { + if (!$this->oBestellung && $this->getModel()->kBestellung) { + $this->oBestellung = new Bestellung($this->getModel()->kBestellung, true); + } + + return $this->oBestellung; + } + + /** + * @return $this + */ + public function updateModel() + { + if ($this->getMollie()) { + $this->getModel()->kID = $this->getMollie()->id; + $this->getModel()->cLocale = $this->getMollie()->locale; + $this->getModel()->fAmount = (float)$this->getMollie()->amount->value; + $this->getModel()->cMethod = $this->getMollie()->method; + $this->getModel()->cCurrency = $this->getMollie()->amount->currency; + $this->getModel()->cStatus = $this->getMollie()->status; + if ($this->getMollie()->amountRefunded) { + $this->getModel()->fAmountRefunded = $this->getMollie()->amountRefunded->value; + } + if ($this->getMollie()->amountCaptured) { + $this->getModel()->fAmountCaptured = $this->getMollie()->amountCaptured->value; + } + $this->getModel()->cMode = $this->getMollie()->mode ?: null; + $this->getModel()->cRedirectURL = $this->getMollie()->redirectUrl; + $this->getModel()->cWebhookURL = $this->getMollie()->webhookUrl; + $this->getModel()->cCheckoutURL = $this->getMollie()->getCheckoutUrl(); + } + + $this->getModel()->kBestellung = $this->getBestellung()->kBestellung; + $this->getModel()->cOrderNumber = $this->getBestellung()->cBestellNr; + $this->getModel()->cHash = trim($this->getHash(), '_'); + + $this->getModel()->bSynced = $this->getModel()->bSynced !== null ? $this->getModel()->bSynced : Helper::getSetting('onlyPaid') !== 'Y'; + + return $this; + } + + /** + * @param false $force + * @return null|\Mollie\Api\Resources\Payment|Order + */ + abstract public function getMollie($force = false); + + /** + * @return stdClass + */ + abstract public function getIncomingPayment(); + + /** + * @param Bestellung $oBestellung + * @param Payment $model + * @return bool + */ + public static function makeFetchable(Bestellung $oBestellung, Payment $model) + { + // TODO: force ? + if ($oBestellung->cAbgeholt === 'Y' && !$model->bSynced) { + Shop::DB()->update('tbestellung', 'kBestellung', $oBestellung->kBestellung, (object)['cAbgeholt' => 'N']); + } + $model->bSynced = true; + + try { + return $model->save(); + } catch (Exception $e) { + Jtllog::writeLog(sprintf('Fehler beim speichern des Models: %s / Bestellung: %s', $model->kID, $oBestellung->cBestellNr)); + } + + return false; + } + + public function Log($msg, $level = LOGLEVEL_NOTICE) + { + try { + $data = ''; + if ($this->getBestellung()) { + $data .= '#' . $this->getBestellung()->kBestellung; + } + if ($this->getMollie()) { + $data .= '$' . $this->getMollie()->id; + } + ZahlungsLog::add($this->PaymentMethod()->moduleID, '[' . microtime(true) . ' - ' . $_SERVER['PHP_SELF'] . '] ' . $msg, $data, $level); + } catch (Exception $e) { + Jtllog::writeLog('Mollie Log failed: ' . $e->getMessage() . '; Previous Log: ' . print_r([$msg, $level, $data], 1)); + } + + return $this; + } + + /** + * @return bool + */ + public function completlyPaid() + { + if ( + $row = Shop::DB()->executeQueryPrepared('SELECT SUM(fBetrag) as fBetragSumme FROM tzahlungseingang WHERE kBestellung = :kBestellung', [ + ':kBestellung' => $this->getBestellung()->kBestellung + ], 1) + ) { + return $row->fBetragSumme >= round($this->getBestellung()->fGesamtsumme * $this->getBestellung()->fWaehrungsFaktor, 2); + } + + return false; + } + + /** + * @param $kBestellung + * * @throws RuntimeException + * @return OrderCheckout|PaymentCheckout + */ + public static function fromBestellung($kBestellung) + { + if ($model = Payment::fromID($kBestellung, 'kBestellung')) { + return static::fromModel($model); + } + + throw new RuntimeException(sprintf('Error loading Order for Bestellung: %s', $kBestellung)); + } + + public static function sendReminders() + { + $reminder = (int)self::Plugin()->oPluginEinstellungAssoc_arr['reminder']; + + if (!$reminder) { + return; + } + + $sql = 'SELECT p.kID FROM xplugin_ws_mollie_payments p JOIN tbestellung b ON b.kBestellung = p.kBestellung ' + . "WHERE (p.dReminder IS NULL OR p.dReminder = '0000-00-00 00:00:00') " + . 'AND p.dCreatedAt < NOW() - INTERVAL :d MINUTE AND p.dCreatedAt > NOW() - INTERVAL 7 DAY ' + . "AND p.cStatus IN ('created','open', 'expired', 'failed', 'canceled') AND NOT b.cStatus = '-1'"; + + $remindables = Shop::DB()->executeQueryPrepared($sql, [ + ':d' => $reminder + ], 2); + foreach ($remindables as $remindable) { + try { + self::sendReminder($remindable->kID); + } catch (Exception $e) { + Jtllog::writeLog('AbstractCheckout::sendReminders: ' . $e->getMessage()); + } + } + } + + /** + * @param $kID + * @return bool + */ + public static function sendReminder($kID) + { + $checkout = self::fromID($kID); + $return = true; + + if (!$checkout->getBestellung()->kBestellung || (int)$checkout->getBestellung()->cStatus > BESTELLUNG_STATUS_IN_BEARBEITUNG || (int)$checkout->getBestellung()->cStatus < 0) { + return $return; + } + + + try { + $repayURL = Shop::getURL() . '/?m_pay=' . md5($checkout->getModel()->kID . '-' . $checkout->getBestellung()->kBestellung); + + $data = new stdClass(); + $data->tkunde = new Kunde($checkout->getBestellung()->oKunde->kKunde); + if ($data->tkunde->kKunde) { + $data->Bestellung = $checkout->getBestellung(); + $data->PayURL = $repayURL; + $data->Amount = gibPreisStringLocalized($checkout->getModel()->fAmount, $checkout->getBestellung()->Waehrung); //Preise::getLocalizedPriceString($order->getAmount(), Currency::fromISO($order->getCurrency()), false); + + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + + $mail = new stdClass(); + $mail->toEmail = $data->tkunde->cMail; + $mail->toName = trim((isset($data->tKunde->cVorname) + ? $data->tKunde->cVorname + : '') . ' ' . ( + isset($data->tKunde->cNachname) + ? $data->tKunde->cNachname + : '' + )) ?: $mail->toEmail; + $data->mail = $mail; + if (!($sentMail = sendeMail('kPlugin_' . self::Plugin()->kPlugin . '_zahlungserinnerung', $data))) { + $checkout->Log(sprintf("Zahlungserinnerung konnte nicht versand werden: %s\n%s", isset($sentMail->cFehler) ?: print_r($sentMail, 1), print_r($data, 1)), LOGLEVEL_ERROR); + $return = false; + } else { + $checkout->Log(sprintf('Zahlungserinnerung für %s verschickt.', $checkout->getBestellung()->cBestellNr)); + } + } else { + $checkout->Log("Kunde '{$checkout->getBestellung()->oKunde->kKunde}' nicht gefunden.", LOGLEVEL_ERROR); + } + } catch (Exception $e) { + $checkout->Log(sprintf('AbstractCheckout::sendReminder: Zahlungserinnerung für %s fehlgeschlagen: %s', $checkout->getBestellung()->cBestellNr, $e->getMessage())); + $return = false; + } + $checkout->getModel()->dReminder = date('Y-m-d H:i:s'); + $checkout->getModel()->save(); + + return $return; + } + + /** + * @param AbstractCheckout $checkout + * @throws ApiException + * @return BaseResource|Refund + */ + public static function refund(self $checkout) + { + if ($checkout->getMollie()->resource === 'order') { + /** @var Order $order */ + $order = $checkout->getMollie(); + if (in_array($order->status, [OrderStatus::STATUS_CANCELED, OrderStatus::STATUS_EXPIRED, OrderStatus::STATUS_CREATED], true)) { + throw new RuntimeException('Bestellung kann derzeit nicht zurückerstattet werden.'); + } + $refund = $order->refundAll(); + $checkout->Log(sprintf('Bestellung wurde manuell zurückerstattet: %s', $refund->id)); + + return $refund; + } + if ($checkout->getMollie()->resource === 'payment') { + /** @var \Mollie\Api\Resources\Payment $payment */ + $payment = $checkout->getMollie(); + if (in_array($payment->status, [PaymentStatus::STATUS_CANCELED, PaymentStatus::STATUS_EXPIRED, PaymentStatus::STATUS_OPEN], true)) { + throw new RuntimeException('Zahlung kann derzeit nicht zurückerstattet werden.'); + } + $refund = $checkout->API()->Client()->payments->refund($checkout->getMollie(), ['amount' => $checkout->getMollie()->amount]); + $checkout->Log(sprintf('Zahlung wurde manuell zurückerstattet: %s', $refund->id)); + + return $refund; + } + + throw new RuntimeException(sprintf('Unbekannte Resource: %s', $checkout->getMollie()->resource)); + } + + /** + * @return API + */ + public function API() + { + if (!$this->api) { + if ($this->getModel()->kID) { + $this->api = new API($this->getModel()->cMode === 'test'); + } else { + $this->api = new API(API::getMode()); + } + } + + return $this->api; + } + + /** + * @param $checkout + * @throws ApiException + * @return \Mollie\Api\Resources\Payment|Order + */ + public static function cancel($checkout) + { + if ($checkout instanceof OrderCheckout) { + return OrderCheckout::cancel($checkout); + } + if ($checkout instanceof PaymentCheckout) { + return PaymentCheckout::cancel($checkout); + } + + throw new RuntimeException('AbstractCheckout::cancel: Invalid Checkout!'); + } + + public static function getLocales() + { + $locales = [ + 'en_US', + 'nl_NL', + 'nl_BE', + 'fr_FR', + 'fr_BE', + 'de_DE', + 'de_AT', + 'de_CH', + 'es_ES', + 'ca_ES', + 'pt_PT', + 'it_IT', + 'nb_NO', + 'sv_SE', + 'fi_FI', + 'da_DK', + 'is_IS', + 'hu_HU', + 'pl_PL', + 'lv_LV', + 'lt_LT',]; + + $laender = []; + $shopLaender = Shop::DB()->executeQuery('SELECT cLaender FROM tversandart', 2); + foreach ($shopLaender as $sL) { + $laender = array_merge(explode(' ', $sL->cLaender)); + } + $laender = array_unique($laender); + + $result = []; + $shopSprachen = Shop::DB()->executeQuery('SELECT * FROM tsprache', 2); + foreach ($shopSprachen as $sS) { + foreach ($laender as $land) { + $result[] = static::getLocale($sS->cISO, $land); + } + } + + return array_intersect(array_unique($result), $locales); + } + + public static function getLocale($cISOSprache = null, $country = null) + { + if ($cISOSprache === null) { + $cISOSprache = gibStandardsprache()->cISO; + } + if (array_key_exists($cISOSprache, self::$localeLangs)) { + $locale = self::$localeLangs[$cISOSprache]['lang']; + if ($country && is_array(self::$localeLangs[$cISOSprache]['country']) && in_array($country, self::$localeLangs[$cISOSprache]['country'], true)) { + $locale .= '_' . strtoupper($country); + } else { + $locale .= '_' . self::$localeLangs[$cISOSprache]['country'][0]; + } + + return $locale; + } + + return self::Plugin()->oPluginEinstellungAssoc_arr['fallbackLocale']; + } + + public static function getCurrencies() + { + $currencies = ['AED' => 'AED - United Arab Emirates dirham', + 'AFN' => 'AFN - Afghan afghani', + 'ALL' => 'ALL - Albanian lek', + 'AMD' => 'AMD - Armenian dram', + 'ANG' => 'ANG - Netherlands Antillean guilder', + 'AOA' => 'AOA - Angolan kwanza', + 'ARS' => 'ARS - Argentine peso', + 'AUD' => 'AUD - Australian dollar', + 'AWG' => 'AWG - Aruban florin', + 'AZN' => 'AZN - Azerbaijani manat', + 'BAM' => 'BAM - Bosnia and Herzegovina convertible mark', + 'BBD' => 'BBD - Barbados dollar', + 'BDT' => 'BDT - Bangladeshi taka', + 'BGN' => 'BGN - Bulgarian lev', + 'BHD' => 'BHD - Bahraini dinar', + 'BIF' => 'BIF - Burundian franc', + 'BMD' => 'BMD - Bermudian dollar', + 'BND' => 'BND - Brunei dollar', + 'BOB' => 'BOB - Boliviano', + 'BRL' => 'BRL - Brazilian real', + 'BSD' => 'BSD - Bahamian dollar', + 'BTN' => 'BTN - Bhutanese ngultrum', + 'BWP' => 'BWP - Botswana pula', + 'BYN' => 'BYN - Belarusian ruble', + 'BZD' => 'BZD - Belize dollar', + 'CAD' => 'CAD - Canadian dollar', + 'CDF' => 'CDF - Congolese franc', + 'CHF' => 'CHF - Swiss franc', + 'CLP' => 'CLP - Chilean peso', + 'CNY' => 'CNY - Renminbi (Chinese) yuan', + 'COP' => 'COP - Colombian peso', + 'COU' => 'COU - Unidad de Valor Real (UVR)', + 'CRC' => 'CRC - Costa Rican colon', + 'CUC' => 'CUC - Cuban convertible peso', + 'CUP' => 'CUP - Cuban peso', + 'CVE' => 'CVE - Cape Verde escudo', + 'CZK' => 'CZK - Czech koruna', + 'DJF' => 'DJF - Djiboutian franc', + 'DKK' => 'DKK - Danish krone', + 'DOP' => 'DOP - Dominican peso', + 'DZD' => 'DZD - Algerian dinar', + 'EGP' => 'EGP - Egyptian pound', + 'ERN' => 'ERN - Eritrean nakfa', + 'ETB' => 'ETB - Ethiopian birr', + 'EUR' => 'EUR - Euro', + 'FJD' => 'FJD - Fiji dollar', + 'FKP' => 'FKP - Falkland Islands pound', + 'GBP' => 'GBP - Pound sterling', + 'GEL' => 'GEL - Georgian lari', + 'GHS' => 'GHS - Ghanaian cedi', + 'GIP' => 'GIP - Gibraltar pound', + 'GMD' => 'GMD - Gambian dalasi', + 'GNF' => 'GNF - Guinean franc', + 'GTQ' => 'GTQ - Guatemalan quetzal', + 'GYD' => 'GYD - Guyanese dollar', + 'HKD' => 'HKD - Hong Kong dollar', + 'HNL' => 'HNL - Honduran lempira', + 'HRK' => 'HRK - Croatian kuna', + 'HTG' => 'HTG - Haitian gourde', + 'HUF' => 'HUF - Hungarian forint', + 'IDR' => 'IDR - Indonesian rupiah', + 'ILS' => 'ILS - Israeli new shekel', + 'INR' => 'INR - Indian rupee', + 'IQD' => 'IQD - Iraqi dinar', + 'IRR' => 'IRR - Iranian rial', + 'ISK' => 'ISK - Icelandic krÛna', + 'JMD' => 'JMD - Jamaican dollar', + 'JOD' => 'JOD - Jordanian dinar', + 'JPY' => 'JPY - Japanese yen', + 'KES' => 'KES - Kenyan shilling', + 'KGS' => 'KGS - Kyrgyzstani som', + 'KHR' => 'KHR - Cambodian riel', + 'KMF' => 'KMF - Comoro franc', + 'KPW' => 'KPW - North Korean won', + 'KRW' => 'KRW - South Korean won', + 'KWD' => 'KWD - Kuwaiti dinar', + 'KYD' => 'KYD - Cayman Islands dollar', + 'KZT' => 'KZT - Kazakhstani tenge', + 'LAK' => 'LAK - Lao kip', + 'LBP' => 'LBP - Lebanese pound', + 'LKR' => 'LKR - Sri Lankan rupee', + 'LRD' => 'LRD - Liberian dollar', + 'LSL' => 'LSL - Lesotho loti', + 'LYD' => 'LYD - Libyan dinar', + 'MAD' => 'MAD - Moroccan dirham', + 'MDL' => 'MDL - Moldovan leu', + 'MGA' => 'MGA - Malagasy ariary', + 'MKD' => 'MKD - Macedonian denar', + 'MMK' => 'MMK - Myanmar kyat', + 'MNT' => 'MNT - Mongolian t?gr?g', + 'MOP' => 'MOP - Macanese pataca', + 'MRU' => 'MRU - Mauritanian ouguiya', + 'MUR' => 'MUR - Mauritian rupee', + 'MVR' => 'MVR - Maldivian rufiyaa', + 'MWK' => 'MWK - Malawian kwacha', + 'MXN' => 'MXN - Mexican peso', + 'MXV' => 'MXV - Mexican Unidad de Inversion (UDI)', + 'MYR' => 'MYR - Malaysian ringgit', + 'MZN' => 'MZN - Mozambican metical', + 'NAD' => 'NAD - Namibian dollar', + 'NGN' => 'NGN - Nigerian naira', + 'NIO' => 'NIO - Nicaraguan cÛrdoba', + 'NOK' => 'NOK - Norwegian krone', + 'NPR' => 'NPR - Nepalese rupee', + 'NZD' => 'NZD - New Zealand dollar', + 'OMR' => 'OMR - Omani rial', + 'PAB' => 'PAB - Panamanian balboa', + 'PEN' => 'PEN - Peruvian sol', + 'PGK' => 'PGK - Papua New Guinean kina', + 'PHP' => 'PHP - Philippine peso', + 'PKR' => 'PKR - Pakistani rupee', + 'PLN' => 'PLN - Polish z?oty', + 'PYG' => 'PYG - Paraguayan guaranÌ', + 'QAR' => 'QAR - Qatari riyal', + 'RON' => 'RON - Romanian leu', + 'RSD' => 'RSD - Serbian dinar', + 'RUB' => 'RUB - Russian ruble', + 'RWF' => 'RWF - Rwandan franc', + 'SAR' => 'SAR - Saudi riyal', + 'SBD' => 'SBD - Solomon Islands dollar', + 'SCR' => 'SCR - Seychelles rupee', + 'SDG' => 'SDG - Sudanese pound', + 'SEK' => 'SEK - Swedish krona/kronor', + 'SGD' => 'SGD - Singapore dollar', + 'SHP' => 'SHP - Saint Helena pound', + 'SLL' => 'SLL - Sierra Leonean leone', + 'SOS' => 'SOS - Somali shilling', + 'SRD' => 'SRD - Surinamese dollar', + 'SSP' => 'SSP - South Sudanese pound', + 'STN' => 'STN - S?o TomÈ and PrÌncipe dobra', + 'SVC' => 'SVC - Salvadoran colÛn', + 'SYP' => 'SYP - Syrian pound', + 'SZL' => 'SZL - Swazi lilangeni', + 'THB' => 'THB - Thai baht', + 'TJS' => 'TJS - Tajikistani somoni', + 'TMT' => 'TMT - Turkmenistan manat', + 'TND' => 'TND - Tunisian dinar', + 'TOP' => 'TOP - Tongan pa?anga', + 'TRY' => 'TRY - Turkish lira', + 'TTD' => 'TTD - Trinidad and Tobago dollar', + 'TWD' => 'TWD - New Taiwan dollar', + 'TZS' => 'TZS - Tanzanian shilling', + 'UAH' => 'UAH - Ukrainian hryvnia', + 'UGX' => 'UGX - Ugandan shilling', + 'USD' => 'USD - United States dollar', + 'UYI' => 'UYI - Uruguay Peso en Unidades Indexadas', + 'UYU' => 'UYU - Uruguayan peso', + 'UYW' => 'UYW - Unidad previsional', + 'UZS' => 'UZS - Uzbekistan som', + 'VES' => 'VES - Venezuelan bolÌvar soberano', + 'VND' => 'VND - Vietnamese ??ng', + 'VUV' => 'VUV - Vanuatu vatu', + 'WST' => 'WST - Samoan tala', + 'YER' => 'YER - Yemeni rial', + 'ZAR' => 'ZAR - South African rand', + 'ZMW' => 'ZMW - Zambian kwacha', + 'ZWL' => 'ZWL - Zimbabwean dollar']; + + $shopCurrencies = Shop::DB()->executeQuery('SELECT * FROM twaehrung', 2); + + $result = []; + + foreach ($shopCurrencies as $sC) { + if (array_key_exists($sC->cISO, $currencies)) { + $result[$sC->cISO] = $currencies[$sC->cISO]; + } + } + + return $result; + } + + public function loadRequest(&$options = []) + { + $oKunde = !$this->getBestellung()->oKunde && $this->PaymentMethod()->duringCheckout ? $_SESSION['Kunde'] : $this->getBestellung()->oKunde; + if ($this->getBestellung()) { + if ( + $oKunde->nRegistriert + && ( + $customer = $this->getCustomer( + array_key_exists( + 'mollie_create_customer', + $_SESSION['cPost_arr'] ?: [] + ) && $_SESSION['cPost_arr']['mollie_create_customer'] === 'Y', + $oKunde + ) + ) + && isset($customer) + ) { + $options['customerId'] = $customer->id; + } + $this->amount = Amount::factory($this->getBestellung()->fGesamtsummeKundenwaehrung, $this->getBestellung()->Waehrung->cISO, true); + $this->redirectUrl = $this->PaymentMethod()->duringCheckout ? Shop::getURL() . '/bestellabschluss.php?' . http_build_query(['hash' => $this->getHash()]) : $this->PaymentMethod()->getReturnURL($this->getBestellung()); + $this->metadata = [ + 'kBestellung' => $this->getBestellung()->kBestellung ?: $this->getBestellung()->cBestellNr, + 'kKunde' => $this->getBestellung()->kKunde, + 'kKundengruppe' => Session::getInstance()->CustomerGroup()->kKundengruppe, + 'cHash' => $this->getHash(), + ]; + } + + $this->locale = self::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer()->cLand); + $this->webhookUrl = Shop::getURL(true) . '/?' . http_build_query([ + 'mollie' => 1, + 'hash' => $this->getHash(), + 'test' => $this->API()->isTest() ?: null, + ]); + + $pm = $this->PaymentMethod(); + $isPayAgain = strpos($_SERVER['PHP_SELF'], 'bestellab_again') !== false; + if ($pm::METHOD !== '' && (self::Plugin()->oPluginEinstellungAssoc_arr['resetMethod'] !== 'Y' || !$isPayAgain)) { + $this->method = $pm::METHOD; + } + } + + /** + * @todo: Kunde wieder löschbar machen ?! + * @param mixed $createOrUpdate + * @param null|mixed $oKunde + * @return null|\Mollie\Api\Resources\Customer + */ + public function getCustomer($createOrUpdate = false, $oKunde = null) + { + if (!$oKunde) { + $oKunde = $this->getBestellung()->oKunde; + } + + if (!$this->customer) { + $customerModel = Customer::fromID($oKunde->kKunde, 'kKunde'); + if ($customerModel->customerId) { + try { + $this->customer = $this->API()->Client()->customers->get($customerModel->customerId); + } catch (ApiException $e) { + $this->Log(sprintf('Fehler beim laden des Mollie Customers %s (kKunde: %d): %s', $customerModel->customerId, $customerModel->kKunde, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + if ($createOrUpdate) { + $customer = [ + 'name' => utf8_encode(trim($oKunde->cVorname . ' ' . $oKunde->cNachname)), + 'email' => utf8_encode($oKunde->cMail), + 'locale' => self::getLocale($_SESSION['cISOSprache'], $oKunde->cLand), + 'metadata' => (object)[ + 'kKunde' => $oKunde->kKunde, + 'kKundengruppe' => $oKunde->kKundengruppe, + 'cKundenNr' => utf8_encode($oKunde->cKundenNr), + ], + ]; + + if ($this->customer) { // UPDATE + $this->customer->name = $customer['name']; + $this->customer->email = $customer['email']; + $this->customer->locale = $customer['locale']; + $this->customer->metadata = $customer['metadata']; + + try { + $this->customer->update(); + } catch (Exception $e) { + $this->Log(sprintf("Fehler beim aktualisieren des Mollie Customers %s: %s\n%s", $this->customer->id, $e->getMessage(), print_r($customer, 1)), LOGLEVEL_ERROR); + } + } else { // create + try { + $this->customer = $this->API()->Client()->customers->create($customer); + $customerModel->kKunde = $oKunde->kKunde; + $customerModel->customerId = $this->customer->id; + $customerModel->save(); + $this->Log(sprintf("Customer '%s' für Kunde %s (%d) bei Mollie angelegt.", $this->customer->id, $this->customer->name, $this->getBestellung()->kKunde)); + } catch (Exception $e) { + $this->Log(sprintf("Fehler beim anlegen eines Mollie Customers: %s\n%s", $e->getMessage(), print_r($customer, 1)), LOGLEVEL_ERROR); + } + } + } + } + + return $this->customer; + } + + /** + * Storno Order + */ + public function storno() + { + if (in_array((int)$this->getBestellung()->cStatus, [BESTELLUNG_STATUS_OFFEN, BESTELLUNG_STATUS_IN_BEARBEITUNG], true)) { + $log = []; + + $conf = Shop::getSettings([CONF_GLOBAL, CONF_TRUSTEDSHOPS]); + $nArtikelAnzeigefilter = (int)$conf['global']['artikel_artikelanzeigefilter']; + + foreach ($this->getBestellung()->Positionen as $pos) { + if ($pos->kArtikel && $pos->Artikel && $pos->Artikel->cLagerBeachten === 'Y') { + $log[] = sprintf('Reset stock of "%s" by %d', $pos->Artikel->cArtNr, -1 * $pos->nAnzahl); + self::aktualisiereLagerbestand($pos->Artikel, -1 * $pos->nAnzahl, $pos->WarenkorbPosEigenschaftArr, $nArtikelAnzeigefilter); + } + } + $log[] = sprintf("Cancel order '%s'.", $this->getBestellung()->cBestellNr); + + if (Shop::DB()->executeQueryPrepared('UPDATE tbestellung SET cAbgeholt = \'Y\', cStatus = :cStatus WHERE kBestellung = :kBestellung', [':cStatus' => '-1', ':kBestellung' => $this->getBestellung()->kBestellung], 3)) { + $this->Log(implode('\n', $log)); + } + } + } + + protected static function aktualisiereLagerbestand($Artikel, $nAnzahl, $WarenkorbPosEigenschaftArr, $nArtikelAnzeigefilter = 1) + { + $artikelBestand = (float)$Artikel->fLagerbestand; + + if (isset($Artikel->cLagerBeachten) && $Artikel->cLagerBeachten === 'Y') { + if ( + $Artikel->cLagerVariation === 'Y' && is_array($WarenkorbPosEigenschaftArr) && count($WarenkorbPosEigenschaftArr) > 0 + ) { + foreach ($WarenkorbPosEigenschaftArr as $eWert) { + $EigenschaftWert = new EigenschaftWert($eWert->kEigenschaftWert); + if ($EigenschaftWert->fPackeinheit === .0) { + $EigenschaftWert->fPackeinheit = 1; + } + Shop::DB()->query( + 'UPDATE teigenschaftwert + SET fLagerbestand = fLagerbestand - ' . ($nAnzahl * $EigenschaftWert->fPackeinheit) . ' + WHERE kEigenschaftWert = ' . (int)$eWert->kEigenschaftWert, + 4 + ); + } + } elseif ($Artikel->fPackeinheit > 0) { + // Stückliste + if ($Artikel->kStueckliste > 0) { + $artikelBestand = self::aktualisiereStuecklistenLagerbestand($Artikel, $nAnzahl); + } else { + Shop::DB()->query( + 'UPDATE tartikel + SET fLagerbestand = IF (fLagerbestand >= ' . ($nAnzahl * $Artikel->fPackeinheit) . ', + (fLagerbestand - ' . ($nAnzahl * $Artikel->fPackeinheit) . '), fLagerbestand) + WHERE kArtikel = ' . (int)$Artikel->kArtikel, + 4 + ); + $tmpArtikel = Shop::DB()->select('tartikel', 'kArtikel', (int)$Artikel->kArtikel, null, null, null, null, false, 'fLagerbestand'); + if ($tmpArtikel !== null) { + $artikelBestand = (float)$tmpArtikel->fLagerbestand; + } + // Stücklisten Komponente + if (ArtikelHelper::isStuecklisteKomponente($Artikel->kArtikel)) { + self::aktualisiereKomponenteLagerbestand($Artikel->kArtikel, $artikelBestand, isset($Artikel->cLagerKleinerNull) && $Artikel->cLagerKleinerNull === 'Y'); + } + } + // Aktualisiere Merkmale in tartikelmerkmal vom Vaterartikel + if ($Artikel->kVaterArtikel > 0) { + Artikel::beachteVarikombiMerkmalLagerbestand($Artikel->kVaterArtikel, $nArtikelAnzeigefilter); + } + } + } + + return $artikelBestand; + } + + protected static function aktualisiereStuecklistenLagerbestand($oStueckListeArtikel, $nAnzahl) + { + $nAnzahl = (float)$nAnzahl; + $kStueckListe = (int)$oStueckListeArtikel->kStueckliste; + $bestandAlt = (float)$oStueckListeArtikel->fLagerbestand; + $bestandNeu = $bestandAlt; + $bestandUeberverkauf = $bestandAlt; + + if ($nAnzahl > 0) { + // Gibt es lagerrelevante Komponenten in der Stückliste? + $oKomponente_arr = Shop::DB()->query( + "SELECT tstueckliste.kArtikel, tstueckliste.fAnzahl + FROM tstueckliste + JOIN tartikel + ON tartikel.kArtikel = tstueckliste.kArtikel + WHERE tstueckliste.kStueckliste = $kStueckListe + AND tartikel.cLagerBeachten = 'Y'", + 2 + ); + + if (is_array($oKomponente_arr) && count($oKomponente_arr) > 0) { + // wenn ja, dann wird für diese auch der Bestand aktualisiert + $options = Artikel::getDefaultOptions(); + + $options->nKeineSichtbarkeitBeachten = 1; + + foreach ($oKomponente_arr as $oKomponente) { + $tmpArtikel = new Artikel(); + $tmpArtikel->fuelleArtikel($oKomponente->kArtikel, $options); + + $komponenteBestand = floor(self::aktualisiereLagerbestand($tmpArtikel, $nAnzahl * $oKomponente->fAnzahl, null) / $oKomponente->fAnzahl); + + if ($komponenteBestand < $bestandNeu && $tmpArtikel->cLagerKleinerNull !== 'Y') { + // Neuer Bestand ist der Kleinste Komponententbestand aller Artikel ohne Überverkauf + $bestandNeu = $komponenteBestand; + } elseif ($komponenteBestand < $bestandUeberverkauf) { + // Für Komponenten mit Überverkauf wird der kleinste Bestand ermittelt. + $bestandUeberverkauf = $komponenteBestand; + } + } + } + + // Ist der alte gleich dem neuen Bestand? + if ($bestandAlt === $bestandNeu) { + // Es sind keine lagerrelevanten Komponenten vorhanden, die den Bestand der Stückliste herabsetzen. + if ($bestandUeberverkauf === $bestandNeu) { + // Es gibt auch keine Komponenten mit Überverkäufen, die den Bestand verringern, deshalb wird + // der Bestand des Stücklistenartikels anhand des Verkaufs verringert + $bestandNeu = $bestandNeu - $nAnzahl * $oStueckListeArtikel->fPackeinheit; + } else { + // Da keine lagerrelevanten Komponenten vorhanden sind, wird der kleinste Bestand der + // Komponentent mit Überverkauf verwendet. + $bestandNeu = $bestandUeberverkauf; + } + + Shop::DB()->update('tartikel', 'kArtikel', (int)$oStueckListeArtikel->kArtikel, (object)[ + 'fLagerbestand' => $bestandNeu, + ]); + } + // Kein Lagerbestands-Update für die Stückliste notwendig! Dies erfolgte bereits über die Komponentenabfrage und + // die dortige Lagerbestandsaktualisierung! + } + + return $bestandNeu; + } + + protected static function aktualisiereKomponenteLagerbestand($kKomponenteArtikel, $fLagerbestand, $bLagerKleinerNull) + { + $kKomponenteArtikel = (int)$kKomponenteArtikel; + $fLagerbestand = (float)$fLagerbestand; + + $oStueckliste_arr = Shop::DB()->query( + "SELECT tstueckliste.kStueckliste, tstueckliste.fAnzahl, + tartikel.kArtikel, tartikel.fLagerbestand, tartikel.cLagerKleinerNull + FROM tstueckliste + JOIN tartikel + ON tartikel.kStueckliste = tstueckliste.kStueckliste + WHERE tstueckliste.kArtikel = $kKomponenteArtikel + AND tartikel.cLagerBeachten = 'Y'", + 2 + ); + + if (is_array($oStueckliste_arr) && count($oStueckliste_arr) > 0) { + foreach ($oStueckliste_arr as $oStueckliste) { + // Ist der aktuelle Bestand der Stückliste größer als dies mit dem Bestand der Komponente möglich wäre? + $maxAnzahl = floor($fLagerbestand / $oStueckliste->fAnzahl); + if ($maxAnzahl < (float)$oStueckliste->fLagerbestand && (!$bLagerKleinerNull || $oStueckliste->cLagerKleinerNull === 'Y')) { + // wenn ja, dann den Bestand der Stückliste entsprechend verringern, aber nur wenn die Komponente nicht + // überberkaufbar ist oder die gesamte Stückliste Überverkäufe zulässt + Shop::DB()->update('tartikel', 'kArtikel', (int)$oStueckliste->kArtikel, (object)[ + 'fLagerbestand' => $maxAnzahl, + ]); + } + } + } + } + + /** + * @return null|array|bool|int|object + */ + public function getLogs() + { + return Shop::DB()->executeQueryPrepared('SELECT * FROM tzahlungslog WHERE cLogData LIKE :kBestellung OR cLogData LIKE :cBestellNr OR cLogData LIKE :MollieID ORDER BY dDatum DESC, cLog DESC', [ + ':kBestellung' => '%#' . ($this->getBestellung()->kBestellung ?: '##') . '%', + ':cBestellNr' => '%§' . ($this->getBestellung()->cBestellNr ?: '§§') . '%', + ':MollieID' => '%$' . ($this->getMollie()->id ?: '$$') . '%', + ], 2); + } + + /** + * @return bool + */ + public function remindable() + { + return (int)$this->getBestellung()->cStatus !== BESTELLUNG_STATUS_STORNO && !in_array($this->getModel()->cStatus, [PaymentStatus::STATUS_PAID, PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PENDING, OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_SHIPPING], true); + } + + /** + * @return string + */ + public function LogData() + { + $data = ''; + if ($this->getBestellung()->kBestellung) { + $data .= '#' . $this->getBestellung()->kBestellung; + } + if ($this->getMollie()) { + $data .= '$' . $this->getMollie()->id; + } + + return $data; + } + + abstract public function cancelOrRefund($force = false); + + /** + * @param array $paymentOptions + * @return Order|Payment + */ + abstract public function create(array $paymentOptions = []); + + /** + * @return string + */ + public function getRepayURL() + { + return Shop::getURL(true) . '/?m_pay=' . md5($this->getModel()->kID . '-' . $this->getBestellung()->kBestellung); + } + + public function getDescription() + { + $descTemplate = trim(Helper::getSetting('paymentDescTpl')) ?: 'Order {orderNumber}'; + $oKunde = $this->getBestellung()->oKunde ?: $_SESSION['Kunde']; + + return str_replace([ + '{orderNumber}', + '{storeName}', + '{customer.firstname}', + '{customer.lastname}', + '{customer.company}', + ], [ + $this->getBestellung()->cBestellNr, + Shop::getSettings([CONF_GLOBAL])['global']['global_shopname'], + $oKunde->cVorname, + $oKunde->cNachname, + $oKunde->cFirma + + ], $descTemplate); + } + + abstract protected function updateOrderNumber(); + + /** + * @param Bestellung $oBestellung + * @return $this + */ + protected function setBestellung(Bestellung $oBestellung) + { + $this->oBestellung = $oBestellung; + + return $this; + } + + /** + * @param \Mollie\Api\Resources\Payment|Order $model + * @return self + */ + abstract protected function setMollie($model); +} diff --git a/version/207/class/Checkout/AbstractResource.php b/version/207/class/Checkout/AbstractResource.php new file mode 100644 index 0000000..7c4439e --- /dev/null +++ b/version/207/class/Checkout/AbstractResource.php @@ -0,0 +1,18 @@ +title = html_entity_decode(substr(trim(($address->cAnrede === 'm' ? Shop::Lang()->get('mr') : Shop::Lang()->get('mrs')) . ' ' . $address->cTitel) ?: null, 0, 20)); + $resource->givenName = html_entity_decode($address->cVorname); + $resource->familyName = html_entity_decode($address->cNachname); + $resource->email = html_entity_decode($address->cMail) ?: null; + + if ($organizationName = trim($address->cFirma)) { + $resource->organizationName = html_entity_decode($organizationName); + } + + // Validity-Check + // TODO: Phone, with E.164 check + // TODO: Is Email-Format Check needed? + if (!$resource->givenName || !$resource->familyName || !$resource->email) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['givenName', 'familyName', 'email'], $resource); + } + + return $resource; + } +} diff --git a/version/207/class/Checkout/Order/OrderLine.php b/version/207/class/Checkout/Order/OrderLine.php new file mode 100644 index 0000000..757edf7 --- /dev/null +++ b/version/207/class/Checkout/Order/OrderLine.php @@ -0,0 +1,237 @@ +totalAmount->value; + } + if (abs($sum - (float)$amount->value) > 0) { + $diff = (round((float)$amount->value - $sum, 2)); + if ($diff !== 0.0) { + $line = new self(); + $line->type = $diff > 0 ? OrderLineType::TYPE_SURCHARGE : OrderLineType::TYPE_DISCOUNT; + // TODO: Translation needed? + $line->name = 'Rundungsausgleich'; + $line->quantity = 1; + $line->unitPrice = Amount::factory($diff, $currency); + $line->totalAmount = Amount::factory($diff, $currency); + $line->vatRate = '0.00'; + $line->vatAmount = Amount::factory(0, $currency); + + return $line; + } + } + + return null; + } + + /** + * @param Bestellung $oBestellung + * @return OrderLine + */ + public static function getCredit(Bestellung $oBestellung) + { + $line = new self(); + $line->type = OrderLineType::TYPE_STORE_CREDIT; + $line->name = 'Guthaben'; + $line->quantity = 1; + // TODO: check currency of Guthaben + $line->unitPrice = Amount::factory($oBestellung->fGuthaben, $oBestellung->Waehrung->cISO); + $line->totalAmount = $line->unitPrice; + $line->vatRate = '0.00'; + $line->vatAmount = Amount::factory(0, $oBestellung->Waehrung->cISO); + + return $line; + } + + /** + * @param stdClass|WarenkorbPos $oPosition + * @param null $currency + * @return OrderLine + */ + public static function factory($oPosition, $currency = null) + { + if (!$oPosition) { + throw new RuntimeException('$oPosition invalid:', print_r($oPosition, 1)); + } + + $resource = new static(); + + $resource->fill($oPosition, $currency); + + // Validity Check + if ( + !$resource->name || !$resource->quantity || !$resource->unitPrice || !$resource->totalAmount + || !$resource->vatRate || !$resource->vatAmount + ) { + throw ResourceValidityException::trigger( + ResourceValidityException::ERROR_REQUIRED, + ['name', 'quantity', 'unitPrice', 'totalAmount', 'vatRate', 'vatAmount'], + $resource + ); + } + + return $resource; + } + + /** + * @param stdClass|WarenkorbPos $oPosition + * @param null|stdClass $currency + * @return $this + * @todo Setting for Fraction handling needed? + */ + protected function fill($oPosition, $currency = null) + { + if (!$currency) { + $currency = Amount::FallbackCurrency(); + } + + $isKupon = (int)$oPosition->nPosTyp === (int)C_WARENKORBPOS_TYP_KUPON; + $isFrac = fmod($oPosition->nAnzahl, 1) !== 0.0; + + // Kupon? set vatRate to 0 and adjust netto + $vatRate = $isKupon ? 0 : (float)$oPosition->fMwSt / 100; + $netto = $isKupon ? round($oPosition->fPreis * (1 + $vatRate), 4) : round($oPosition->fPreis, 4); + + // Fraction? transform, as it were 1, and set quantity to 1 + $netto = round(($isFrac ? $netto * (float)$oPosition->nAnzahl : $netto) * $currency->fFaktor, 4); + $this->quantity = $isFrac ? 1 : (int)$oPosition->nAnzahl; + + // Fraction? include quantity and unit in name + $this->name = $isFrac ? sprintf('%s (%.2f %s)', $oPosition->cName, (float)$oPosition->nAnzahl, $oPosition->cEinheit) : $oPosition->cName; + if (!$this->name) { + $this->name = '(null)'; + } + $this->mapType($oPosition->nPosTyp, $netto > 0); + + //$unitPriceNetto = round(($currency->fFaktor * $netto), 4); + + $this->unitPrice = Amount::factory(round($netto * (1 + $vatRate), 2), $currency->cISO, false); + $this->totalAmount = Amount::factory(round($this->quantity * $this->unitPrice->value, 2), $currency->cISO, false); + + $this->vatRate = number_format($vatRate * 100, 2); + $this->vatAmount = Amount::factory(round($this->totalAmount->value - ($this->totalAmount->value / (1 + $vatRate)), 2), $currency->cISO, false); + + $metadata = []; + + // Is Artikel ? + if (isset($oPosition->Artikel)) { + $this->sku = $oPosition->Artikel->cArtNr; + $metadata['kArtikel'] = $oPosition->kArtikel; + if ($oPosition->cUnique !== '') { + $metadata['cUnique'] = utf8_encode($oPosition->cUnique); + } + } + + if (isset($oPosition->WarenkorbPosEigenschaftArr) && is_array($oPosition->WarenkorbPosEigenschaftArr) && count($oPosition->WarenkorbPosEigenschaftArr)) { + $metadata['properties'] = []; + /** @var WarenkorbPosEigenschaft $warenkorbPosEigenschaft */ + foreach ($oPosition->WarenkorbPosEigenschaftArr as $warenkorbPosEigenschaft) { + $metadata['properties'][] = [ + 'kEigenschaft' => $warenkorbPosEigenschaft->kEigenschaft, + 'kEigenschaftWert' => $warenkorbPosEigenschaft->kEigenschaftWert, + 'name' => utf8_encode($warenkorbPosEigenschaft->cEigenschaftName), + 'value' => utf8_encode($warenkorbPosEigenschaft->cEigenschaftWertName), + ]; + if (strlen(json_encode($metadata)) > 1000) { + array_pop($metadata['properties']); + + break; + } + } + } + if (json_encode($metadata) !== false) { + $this->metadata = $metadata; + } + + return $this; + } + + /** + * @param $nPosTyp + * @param $positive + * @return OrderLine + */ + protected function mapType($nPosTyp, $positive) + { + switch ($nPosTyp) { + case C_WARENKORBPOS_TYP_ARTIKEL: + case C_WARENKORBPOS_TYP_GRATISGESCHENK: + // TODO: digital / Download Artikel? + $this->type = OrderLineType::TYPE_PHYSICAL; + + return $this; + + case C_WARENKORBPOS_TYP_VERSANDPOS: + $this->type = OrderLineType::TYPE_SHIPPING_FEE; + + return $this; + + case C_WARENKORBPOS_TYP_GUTSCHEIN: + case C_WARENKORBPOS_TYP_KUPON: + case C_WARENKORBPOS_TYP_NEUKUNDENKUPON: + $this->type = OrderLineType::TYPE_DISCOUNT; + + return $this; + + case C_WARENKORBPOS_TYP_VERPACKUNG: + case C_WARENKORBPOS_TYP_VERSANDZUSCHLAG: + case C_WARENKORBPOS_TYP_ZAHLUNGSART: + case C_WARENKORBPOS_TYP_VERSAND_ARTIKELABHAENGIG: + case C_WARENKORBPOS_TYP_NACHNAHMEGEBUEHR: + $this->type = OrderLineType::TYPE_SURCHARGE; + + return $this; + + default: + $this->type = $positive ? OrderLineType::TYPE_SURCHARGE : OrderLineType::TYPE_DISCOUNT; + + return $this; + } + } +} diff --git a/version/207/class/Checkout/OrderCheckout.php b/version/207/class/Checkout/OrderCheckout.php new file mode 100644 index 0000000..1d61b6d --- /dev/null +++ b/version/207/class/Checkout/OrderCheckout.php @@ -0,0 +1,406 @@ +getMollie()->status !== OrderStatus::STATUS_AUTHORIZED && $checkout->getMollie()->status !== OrderStatus::STATUS_SHIPPING) { + throw new RuntimeException('Nur autorisierte Zahlungen können erfasst werden!'); + } + $shipment = $checkout->API()->Client()->shipments->createFor($checkout->getMollie(), ['lines' => []]); + $checkout->Log(sprintf('Bestellung wurde manuell erfasst/versandt: %s', $shipment->id)); + + return $shipment->id; + } + + /** + * @param false $force + * @return null|Order + */ + public function getMollie($force = false) + { + if ($force || (!$this->order && $this->getModel()->kID)) { + try { + $this->order = $this->API()->Client()->orders->get($this->getModel()->kID, ['embed' => 'payments,shipments,refunds']); + } catch (Exception $e) { + throw new RuntimeException(sprintf('Mollie-Order \'%s\' konnte nicht geladen werden: %s', $this->getModel()->kID, $e->getMessage())); + } + } + + return $this->order; + } + + /** + * @param OrderCheckout $checkout + * @throws ApiException + * @return Order + */ + public static function cancel($checkout) + { + if (!$checkout->getMollie() || !$checkout->getMollie()->isCancelable) { + throw new RuntimeException('Bestellung kann nicht abgebrochen werden.'); + } + $order = $checkout->getMollie()->cancel(); + $checkout->Log('Bestellung wurde manuell abgebrochen.'); + + return $order; + } + + /** + * @throws Exception + * @return array + */ + public function getShipments() + { + $shipments = []; + $lieferschien_arr = Shop::DB()->executeQueryPrepared('SELECT kLieferschein FROM tlieferschein WHERE kInetBestellung = :kBestellung', [ + ':kBestellung' => $this->getBestellung()->kBestellung + ], 2); + + foreach ($lieferschien_arr as $lieferschein) { + $shipments[] = new Shipment($lieferschein->kLieferschein, $this); + } + + return $shipments; + } + + /** + * @param mixed $force + * @throws ApiException + * @return string + */ + public function cancelOrRefund($force = false) + { + if (!$this->getMollie()) { + throw new RuntimeException('Mollie-Order konnte nicht geladen werden: ' . $this->getModel()->kID); + } + if ($force || (int)$this->getBestellung()->cStatus === BESTELLUNG_STATUS_STORNO) { + if ($this->getMollie()->isCancelable) { + $res = $this->getMollie()->cancel(); + $result = 'Order cancelled, Status: ' . $res->status; + } else { + $res = $this->getMollie()->refundAll(); + $result = 'Order Refund initiiert, Status: ' . $res->status; + } + $this->PaymentMethod()->Log('OrderCheckout::cancelOrRefund: ' . $result, $this->LogData()); + + return $result; + } + + throw new RuntimeException('Bestellung ist derzeit nicht storniert, Status: ' . $this->getBestellung()->cStatus); + } + + /** + * @param array $paymentOptions + * @return Order + */ + public function create(array $paymentOptions = []) + { + if ($this->getModel()->kID) { + try { + $this->order = $this->API()->Client()->orders->get($this->getModel()->kID, ['embed' => 'payments']); + if (in_array($this->order->status, [OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_PAID, OrderStatus::STATUS_AUTHORIZED, OrderStatus::STATUS_PENDING], true)) { + $this->handleNotification(); + + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + if ($this->order->status === OrderStatus::STATUS_CREATED) { + if ($this->order->payments()) { + /** @var Payment $payment */ + foreach ($this->order->payments() as $payment) { + if ($payment->status === PaymentStatus::STATUS_OPEN) { + $this->setPayment($payment); + + break; + } + } + } + if (!$this->getPayment()) { + $this->setPayment($this->API()->Client()->orderPayments->createForId($this->getModel()->kID, $paymentOptions)); + } + $this->updateModel()->saveModel(); + + return $this->getMollie(true); + } + } catch (RuntimeException $e) { + throw $e; + } catch (Exception $e) { + $this->Log(sprintf("OrderCheckout::create: Letzte Order '%s' konnte nicht geladen werden: %s, versuche neue zu erstellen.", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + try { + $req = $this->loadRequest($paymentOptions)->jsonSerialize(); + $this->order = $this->API()->Client()->orders->create($req); + $this->Log(sprintf("Order für '%s' wurde erfolgreich angelegt: %s", $this->getBestellung()->cBestellNr, $this->order->id)); + $this->updateModel()->saveModel(); + + return $this->order; + } catch (Exception $e) { + $this->Log(sprintf("OrderCheckout::create: Neue Order '%s' konnte nicht erstellt werden: %s.", $this->getBestellung()->cBestellNr, $e->getMessage()), LOGLEVEL_ERROR); + + throw new RuntimeException(sprintf("Order für '%s' konnte nicht angelegt werden: %s", $this->getBestellung()->cBestellNr, $e->getMessage())); + } + } + + /** + * @param mixed $search + * @return null|Payment + */ + public function getPayment($search = false) + { + if (!$this->_payment && $search && $this->getMollie()) { + foreach ($this->getMollie()->payments() as $payment) { + if ( + in_array($payment->status, [ + PaymentStatus::STATUS_AUTHORIZED, + PaymentStatus::STATUS_PAID, + PaymentStatus::STATUS_PENDING, + ], true) + ) { + $this->_payment = $payment; + + break; + } + } + } + + return $this->_payment; + } + + /** + * @param $payment + * @return $this + */ + public function setPayment($payment) + { + $this->_payment = $payment; + + return $this; + } + + /** + * @return $this|OrderCheckout + */ + public function updateModel() + { + parent::updateModel(); + + if (!$this->getPayment() && $this->getMollie() && $this->getMollie()->payments()) { + /** @var Payment $payment */ + foreach ($this->getMollie()->payments() as $payment) { + if (in_array($payment->status, [PaymentStatus::STATUS_OPEN, PaymentStatus::STATUS_PENDING, PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], true)) { + $this->setPayment($payment); + + break; + } + } + } + if ($this->getPayment()) { + $this->getModel()->cTransactionId = $this->getPayment()->id; + } + if ($this->getMollie()) { + $this->getModel()->cCheckoutURL = $this->getMollie()->getCheckoutUrl(); + $this->getModel()->cWebhookURL = $this->getMollie()->webhookUrl; + $this->getModel()->cRedirectURL = $this->getMollie()->redirectUrl; + } + + return $this; + } + + /** + * @param array $options + * @return $this|OrderCheckout + */ + public function loadRequest(&$options = []) + { + parent::loadRequest($options); + + $this->orderNumber = $this->getBestellung()->cBestellNr; + + $this->billingAddress = Address::factory($this->getBestellung()->oRechnungsadresse); + if ($this->getBestellung()->Lieferadresse !== null) { + if (!$this->getBestellung()->Lieferadresse->cMail) { + $this->getBestellung()->Lieferadresse->cMail = $this->getBestellung()->oRechnungsadresse->cMail; + } + $this->shippingAddress = Address::factory($this->getBestellung()->Lieferadresse); + } + + if ( + !empty(Session::getInstance()->Customer()->dGeburtstag) + && Session::getInstance()->Customer()->dGeburtstag !== '0000-00-00' + && preg_match('/^\d{4}-\d{2}-\d{2}$/', trim(Session::getInstance()->Customer()->dGeburtstag)) + ) { + $this->consumerDateOfBirth = trim(Session::getInstance()->Customer()->dGeburtstag); + } + + $lines = []; + foreach ($this->getBestellung()->Positionen as $oPosition) { + $lines[] = OrderLine::factory($oPosition, $this->getBestellung()->Waehrung); + } + + if ($this->getBestellung()->GuthabenNutzen && $this->getBestellung()->fGuthaben > 0) { + $lines[] = OrderLine::getCredit($this->getBestellung()); + } + + if ($comp = OrderLine::getRoundingCompensation($lines, $this->amount, $this->getBestellung()->Waehrung->cISO)) { + $lines[] = $comp; + } + $this->lines = $lines; + + if ($dueDays = $this->PaymentMethod()->getExpiryDays()) { + try { + // #145 + //$max = $this->method && strpos($this->method, 'klarna') !== false ? 28 : 100; + $date = new DateTime(sprintf('+%d DAYS', $dueDays), new DateTimeZone('UTC')); + $this->expiresAt = $date->format('Y-m-d'); + } catch (Exception $e) { + $this->Log($e->getMessage(), LOGLEVEL_ERROR); + } + } + + $this->payment = $options; + + return $this; + } + + /** + * @return null|object + */ + public function getIncomingPayment() + { + if (!$this->getMollie(true)) { + return null; + } + + $cHinweis = sprintf('%s / %s', $this->getMollie()->id, $this->getPayment(true)->id); + if (Helper::getSetting('wawiPaymentID') === 'ord') { + $cHinweis = $this->getMollie()->id; + } elseif (Helper::getSetting('wawiPaymentID') === 'tr') { + $cHinweis = $this->getPayment(true)->id; + } + + + /** @var Payment $payment */ + /** @noinspection NullPointerExceptionInspection */ + foreach ($this->getMollie()->payments() as $payment) { + if ( + in_array( + $payment->status, + [PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], + true + ) + ) { + $this->setPayment($payment); + $data = (object)[ + 'fBetrag' => (float)$payment->amount->value, + 'cISO' => $payment->amount->currency, + 'cZahler' => $payment->details->paypalPayerId ?: $payment->customerId, + 'cHinweis' => $payment->details->paypalReference ?: $cHinweis, + ]; + if (isset($payment->details, $payment->details->paypalFee)) { + $data->fZahlungsgebuehr = $payment->details->paypalFee->value; + } + + return $data; + } + } + + return null; + } + + /** + * @return $this + */ + protected function updateOrderNumber() + { + try { + if ($this->getMollie()) { + $this->getMollie()->orderNumber = $this->getBestellung()->cBestellNr; + $this->getMollie()->webhookUrl = Shop::getURL() . '/?mollie=1'; + $this->getMollie()->update(); + } + if ($this->getModel()->cTransactionId) { + $this->API()->Client()->payments->update($this->getModel()->cTransactionId, [ + 'description' => $this->getDescription() + ]); + } + } catch (Exception $e) { + $this->Log('OrderCheckout::updateOrderNumber:' . $e->getMessage(), LOGLEVEL_ERROR); + } + + return $this; + } + + /** + * @param Order $model + * @return $this|AbstractCheckout + */ + protected function setMollie($model) + { + if ($model instanceof Order) { + $this->order = $model; + } + + return $this; + } +} diff --git a/version/207/class/Checkout/Payment/Address.php b/version/207/class/Checkout/Payment/Address.php new file mode 100644 index 0000000..7e33875 --- /dev/null +++ b/version/207/class/Checkout/Payment/Address.php @@ -0,0 +1,53 @@ +streetAndNumber = html_entity_decode($address->cStrasse . ' ' . $address->cHausnummer); + $resource->postalCode = html_entity_decode($address->cPLZ); + $resource->city = html_entity_decode($address->cOrt); + $resource->country = html_entity_decode($address->cLand); + + if ( + isset($address->cAdressZusatz) + && trim($address->cAdressZusatz) !== '' + ) { + $resource->streetAdditional = html_entity_decode(trim($address->cAdressZusatz)); + } + + // Validity-Check + // TODO: Check for valid Country Code? + // TODO: Check PostalCode requirement Country? + if (!$resource->streetAndNumber || !$resource->city || !$resource->country) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['streetAndNumber', 'city', 'country'], $resource); + } + + return $resource; + } +} diff --git a/version/207/class/Checkout/Payment/Amount.php b/version/207/class/Checkout/Payment/Amount.php new file mode 100644 index 0000000..c11c64c --- /dev/null +++ b/version/207/class/Checkout/Payment/Amount.php @@ -0,0 +1,75 @@ + true [5 Rappen Rounding]) + * @return Amount + */ + public static function factory($value, $currency = null, $useRounding = false) + { + if (!$currency) { + $currency = static::FallbackCurrency()->cISO; + } + + $resource = new static(); + + $resource->currency = $currency; + //$resource->value = number_format(round($useRounding ? $resource->round($value * (float)$currency->fFaktor) : $value * (float)$currency->fFaktor, 2), 2, '.', ''); + $resource->value = number_format(round($useRounding ? $resource->round($value) : $value, 2), 2, '.', ''); + + // Validity Check + // TODO: Check ISO Code? + // TODO: Check Value + if (!$resource->currency || !$resource->value) { + throw ResourceValidityException::trigger(ResourceValidityException::ERROR_REQUIRED, ['currency', 'value'], $resource); + } + + return $resource; + } + + /** + * @return stdClass + */ + public static function FallbackCurrency() + { + return isset($_SESSION['Waehrung']) ? $_SESSION['Waehrung'] : Shop::DB()->select('twaehrung', 'cStandard', 'Y'); + } + + /** + * Check if 5 Rappen rounding is necessary + * + * @param mixed $value + * @return float + */ + protected function round($value) + { + $conf = Shop::getSettings([CONF_KAUFABWICKLUNG]); + if (isset($conf['kaufabwicklung']['bestellabschluss_runden5']) && (int)$conf['kaufabwicklung']['bestellabschluss_runden5'] === 1) { + $value = round($value * 20) / 20; + } + + return $value; + } +} diff --git a/version/207/class/Checkout/PaymentCheckout.php b/version/207/class/Checkout/PaymentCheckout.php new file mode 100644 index 0000000..568be4e --- /dev/null +++ b/version/207/class/Checkout/PaymentCheckout.php @@ -0,0 +1,235 @@ +getMollie()->isCancelable) { + throw new RuntimeException('Zahlung kann nicht abgebrochen werden.'); + } + $payment = $checkout->API()->Client()->payments->cancel($checkout->getMollie()->id); + $checkout->Log('Zahlung wurde manuell abgebrochen.'); + + return $payment; + } + + /** + * @param false $force + * @return null|Payment + */ + public function getMollie($force = false) + { + if ($force || (!$this->getPayment() && $this->getModel()->kID)) { + try { + $this->setPayment($this->API()->Client()->payments->get($this->getModel()->kID, ['embed' => 'refunds'])); + } catch (Exception $e) { + throw new RuntimeException('Mollie-Payment konnte nicht geladen werden: ' . $e->getMessage()); + } + } + + return $this->getPayment(); + } + + /** + * @return null|Payment + */ + public function getPayment() + { + return $this->_payment; + } + + /** + * @param $payment + * @return $this + */ + public function setPayment($payment) + { + $this->_payment = $payment; + + return $this; + } + + /** + * @param mixed $force + * @throws ApiException + * @throws IncompatiblePlatform + * @return string + */ + public function cancelOrRefund($force = false) + { + if (!$this->getMollie()) { + throw new RuntimeException('Mollie-Order konnte nicht geladen werden: ' . $this->getModel()->kID); + } + if ($force || (int)$this->getBestellung()->cStatus === BESTELLUNG_STATUS_STORNO) { + if ($this->getMollie()->isCancelable) { + $res = $this->API()->Client()->payments->cancel($this->getMollie()->id); + $result = 'Payment cancelled, Status: ' . $res->status; + } else { + $res = $this->API()->Client()->payments->refund($this->getMollie(), ['amount' => $this->getMollie()->amount]); + $result = 'Payment Refund initiiert, Status: ' . $res->status; + } + $this->PaymentMethod()->Log('PaymentCheckout::cancelOrRefund: ' . $result, $this->LogData()); + + return $result; + } + + throw new RuntimeException('Bestellung ist derzeit nicht storniert, Status: ' . $this->getBestellung()->cStatus); + } + + /** + * @param array $paymentOptions + * @return Payment + */ + public function create(array $paymentOptions = []) + { + if ($this->getModel()->kID) { + try { + $this->setPayment($this->API()->Client()->payments->get($this->getModel()->kID)); + if ($this->getPayment()->status === PaymentStatus::STATUS_PAID) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + if ($this->getPayment()->status === PaymentStatus::STATUS_OPEN) { + $this->updateModel()->saveModel(); + + return $this->getPayment(); + } + } catch (RuntimeException $e) { + //$this->Log(sprintf("PaymentCheckout::create: Letzte Transaktion '%s' konnte nicht geladen werden: %s", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + throw $e; + } catch (Exception $e) { + $this->Log(sprintf("PaymentCheckout::create: Letzte Transaktion '%s' konnte nicht geladen werden: %s, versuche neue zu erstellen.", $this->getModel()->kID, $e->getMessage()), LOGLEVEL_ERROR); + } + } + + try { + $req = $this->loadRequest($paymentOptions)->jsonSerialize(); + $this->setPayment($this->API()->Client()->payments->create($req)); + $this->Log(sprintf("Payment für '%s' wurde erfolgreich angelegt: %s", $this->getBestellung()->cBestellNr, $this->getPayment()->id)); + $this->updateModel()->saveModel(); + + return $this->getPayment(); + } catch (Exception $e) { + $this->Log(sprintf("PaymentCheckout::create: Neue Transaktion für '%s' konnte nicht erstellt werden: %s.", $this->getBestellung()->cBestellNr, $e->getMessage()), LOGLEVEL_ERROR); + + throw new RuntimeException(sprintf('Mollie-Payment \'%s\' konnte nicht geladen werden: %s', $this->getBestellung()->cBestellNr, $e->getMessage())); + } + } + + /** + * @param array $options + * @return $this|PaymentCheckout + */ + public function loadRequest(&$options = []) + { + parent::loadRequest($options); + + foreach ($options as $key => $value) { + $this->$key = $value; + } + + + $this->description = $this->getDescription(); + + return $this; + } + + + + /** + * @return null|object + */ + public function getIncomingPayment() + { + if (!$this->getMollie()) { + return null; + } + + if (in_array($this->getMollie()->status, [PaymentStatus::STATUS_AUTHORIZED, PaymentStatus::STATUS_PAID], true)) { + $data = []; + $data['fBetrag'] = (float)$this->getMollie()->amount->value; + $data['cISO'] = $this->getMollie()->amount->currency; + $data['cZahler'] = $this->getMollie()->details->paypalPayerId ?: $this->getMollie()->customerId; + $data['cHinweis'] = $this->getMollie()->details->paypalReference ?: $this->getMollie()->id; + if (isset($this->getMollie()->details, $this->getMollie()->details->paypalFee)) { + $data['fZahlungsgebuehr'] = $this->getMollie()->details->paypalFee->value; + } + + return (object)$data; + } + + return null; + } + + /** + * @param Payment $model + * @return $this|AbstractCheckout + */ + protected function setMollie($model) + { + if ($model instanceof Payment) { + $this->setPayment($model); + } + + return $this; + } + + /** + * @return $this + */ + protected function updateOrderNumber() + { + try { + if ($this->getMollie()) { + $this->getMollie()->description = $this->getDescription(); + $this->getMollie()->webhookUrl = Shop::getURL() . '/?mollie=1'; + $this->getMollie()->update(); + } + } catch (Exception $e) { + $this->Log('OrderCheckout::updateOrderNumber:' . $e->getMessage(), LOGLEVEL_ERROR); + } + + return $this; + } +} diff --git a/version/207/class/ExclusiveLock.php b/version/207/class/ExclusiveLock.php new file mode 100644 index 0000000..c7b834d --- /dev/null +++ b/version/207/class/ExclusiveLock.php @@ -0,0 +1,70 @@ +key = $key; + $this->path = rtrim(realpath($path), '/') . '/'; + if (!is_dir($path) || !is_writable($path)) { + throw new RuntimeException("Lock Path '$path' doesn't exist, or is not writable!"); + } + //create a new resource or get exisitng with same key + $this->file = fopen($this->path . "$key.lockfile", 'wb+'); + } + + + public function __destruct() + { + if ($this->own === true) { + $this->unlock(); + } + } + + /** @noinspection ForgottenDebugOutputInspection */ + public function unlock() + { + $key = $this->key; + if ($this->own === true) { + if (!flock($this->file, LOCK_UN)) { //failed + error_log("ExclusiveLock::lock FAILED to release lock [$key]"); + + return false; + } + fwrite($this->file, 'Unlocked - ' . microtime(true) . "\n"); + fflush($this->file); + $this->own = false; + } else { + error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller"); + } + + return true; // success + } + + public function lock() + { + if (!flock($this->file, LOCK_EX | LOCK_NB)) { //failed + return false; + } + fwrite($this->file, 'Locked - ' . microtime(true) . "\n"); + fflush($this->file); + + $this->own = true; + + return true; // success + } +} diff --git a/version/207/class/Helper.php b/version/207/class/Helper.php new file mode 100644 index 0000000..b03874f --- /dev/null +++ b/version/207/class/Helper.php @@ -0,0 +1,363 @@ +short_url != '' ? $release->short_url : $release->full_url; + $filename = basename($release->full_url); + $tmpDir = PFAD_ROOT . PFAD_COMPILEDIR; + $pluginsDir = PFAD_ROOT . PFAD_PLUGIN; + + // 1. PRE-CHECKS + if (file_exists($pluginsDir . self::oPlugin()->cVerzeichnis . '/.git') && is_dir($pluginsDir . self::oPlugin()->cVerzeichnis . '/.git')) { + throw new Exception('Pluginordner enthält ein GIT Repository, kein Update möglich!'); + } + + if (!function_exists('curl_exec')) { + throw new Exception('cURL ist nicht verfügbar!!'); + } + if (!is_writable($tmpDir)) { + throw new Exception("Temporäres Verzeichnis_'{$tmpDir}' ist nicht beschreibbar!"); + } + if (!is_writable($pluginsDir . self::oPlugin()->cVerzeichnis)) { + throw new Exception("Plugin Verzeichnis_'" . $pluginsDir . self::oPlugin()->cVerzeichnis . "' ist nicht beschreibbar!"); + } + if (file_exists($tmpDir . $filename) && !unlink($tmpDir . $filename)) { + throw new Exception("Temporäre Datei '" . $tmpDir . $filename . "' konnte nicht gelöscht werden!"); + } + + // 2. DOWNLOAD + $fp = fopen($tmpDir . $filename, 'w+'); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_TIMEOUT, 50); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + fclose($fp); + if ($info['http_code'] !== 200) { + throw new Exception("Unerwarteter Status Code '" . $info['http_code'] . "'!"); + } + if ($info['download_content_length'] <= 0) { + throw new Exception("Unerwartete Downloadgröße '" . $info['download_content_length'] . "'!"); + } + + // 3. UNZIP + require_once PFAD_ROOT . PFAD_PCLZIP . 'pclzip.lib.php'; + $zip = new PclZip($tmpDir . $filename); + $content = $zip->listContent(); + + if (!is_array($content) || !isset($content[0]['filename']) || strpos($content[0]['filename'], '.') !== false) { + throw new Exception('Das Zip-Archiv ist leider ungültig!'); + } + $unzipPath = PFAD_ROOT . PFAD_PLUGIN; + $res = $zip->extract(PCLZIP_OPT_PATH, $unzipPath, PCLZIP_OPT_REPLACE_NEWER); + if ($res !== 0) { + header('Location: ' . Shop::getURL() . DIRECTORY_SEPARATOR . PFAD_ADMIN . 'pluginverwaltung.php', true); + } else { + throw new Exception('Entpacken fehlgeschlagen: ' . $zip->errorCode()); + } + } + + /** + * @param bool $force + * @throws Exception + * @return mixed + */ + public static function getLatestRelease($force = false) + { + $lastCheck = (int)self::getSetting(__NAMESPACE__ . '_upd'); + $lastRelease = file_exists(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd') ? file_get_contents(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd') : false; + if ($force || !$lastCheck || !$lastRelease || ($lastCheck + 12 * 60 * 60) < time()) { + $curl = curl_init('https://api.dash.bar/release/' . __NAMESPACE__); + @curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + @curl_setopt($curl, CURLOPT_TIMEOUT, 5); + @curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + @curl_setopt($curl, CURLOPT_HEADER, 0); + @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + @curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + + $data = curl_exec($curl); + $statusCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); + @curl_close($curl); + if ($statusCode !== 200) { + throw new Exception(__NAMESPACE__ . ': Could not fetch release info: ' . $statusCode); + } + $json = json_decode($data); + if (json_last_error() || $json->status != 'ok') { + throw new Exception(__NAMESPACE__ . ': Could not decode release info: ' . $data); + } + self::setSetting(__NAMESPACE__ . '_upd', time()); + file_put_contents(PFAD_ROOT . PFAD_COMPILEDIR . __NAMESPACE__ . '_upd', json_encode($json->data)); + + return $json->data; + } + + return json_decode($lastRelease); + } + + /** + * Register PSR-4 autoloader + * Licence-Check + * @return bool + */ + public static function init() + { + ini_set('xdebug.default_enable', defined('WS_XDEBUG_ENABLED')); + + return self::autoload(); + } + + /** + * @var stdClass[] + */ + public static $alerts = []; + + /** + * Usage: + * + * Helper::addAlert('Success Message', 'success', 'namespace'); + * + * @param $content + * @param $type + * @param $namespace + */ + public static function addAlert($content, $type, $namespace) + { + if (!array_key_exists($namespace, self::$alerts)) { + self::$alerts[$namespace] = new stdClass(); + } + + self::$alerts[$namespace]->{$type . '_' . microtime(true)} = $content; + } + + /** + * Usage in Smarty: + * + * {ws_mollie\Helper::showAlerts('namespace')} + * + * @param $namespace + * @throws SmartyException + * @return string + */ + public static function showAlerts($namespace) + { + if (array_key_exists($namespace, self::$alerts) && file_exists(self::oPlugin()->cAdminmenuPfad . '../tpl/_alerts.tpl')) { + Shop::Smarty()->assign('alerts', self::$alerts[$namespace]); + + return Shop::Smarty()->fetch(self::oPlugin()->cAdminmenuPfad . '../tpl/_alerts.tpl'); + } + + return ''; + } + + /** + * Sets a Plugin Setting and saves it to the DB + * + * @param $name + * @param $value + * @return int + */ + public static function setSetting($name, $value) + { + $setting = new stdClass(); + $setting->kPlugin = self::oPlugin()->kPlugin; + $setting->cName = $name; + $setting->cWert = $value; + + if (array_key_exists($name, self::oPlugin()->oPluginEinstellungAssoc_arr)) { + $return = Shop::DB()->updateRow('tplugineinstellungen', ['kPlugin', 'cName'], [$setting->kPlugin, $setting->cName], $setting); + } else { + $return = Shop::DB()->insertRow('tplugineinstellungen', $setting); + } + self::oPlugin()->oPluginEinstellungAssoc_arr[$name] = $value; + self::oPlugin(true); // invalidate cache + + return $return; + } + + /** + * Get Plugin Object + * + * @param bool $force disable Cache + * @return null|Plugin + */ + public static function oPlugin($force = false) + { + if ($force === true) { + self::$oPlugin = new Plugin(self::oPlugin()->kPlugin, true); + } elseif (null === self::$oPlugin) { + self::$oPlugin = Plugin::getPluginById(__NAMESPACE__); + } + + return self::$oPlugin; + } + + /** + * get a Plugin setting + * + * @param $name + * @return null|mixed + */ + public static function getSetting($name) + { + if (array_key_exists($name, self::oPlugin()->oPluginEinstellungAssoc_arr ?: [])) { + return self::oPlugin()->oPluginEinstellungAssoc_arr[$name]; + } + + return null; + } + + /** + * Get Domain frpm URL_SHOP without www. + * + * @param string $url + * @return string + */ + public static function getDomain($url = URL_SHOP) + { + $matches = []; + @preg_match("/^((http(s)?):\/\/)?(www\.)?([a-zA-Z0-9-\.]+)(\/.*)?$/i", $url, $matches); + + return strtolower(isset($matches[5]) ? $matches[5] : $url); + } + + /** + * @param bool $e + * @return mixed + */ + public static function getMasterMail($e = false) + { + $settings = Shop::getSettings([CONF_EMAILS]); + $mail = trim($settings['emails']['email_master_absender']); + if ($e === true && $mail != '') { + $mail = base64_encode($mail); + $eMail = ''; + foreach (str_split($mail) as $c) { + $eMail .= chr(ord($c) ^ 0x00100110); + } + + return base64_encode($eMail); + } + + return $mail; + } + + /** + * @param Exception $exc + * @param bool $trace + * @return void + */ + public static function logExc(Exception $exc, $trace = true) + { + Jtllog::writeLog(__NAMESPACE__ . ': ' . $exc->getMessage() . ($trace ? ' - ' . $exc->getTraceAsString() : '')); + } + + /** + * Checks if admin session is loaded + * + * @return bool + */ + public static function isAdminBackend() + { + return session_name() === 'eSIdAdm'; + } + + /** + * Returns kAdminmenu ID for given Title, used for Tabswitching + * + * @param $name string CustomLink Title + * @return int + */ + public static function getAdminmenu($name) + { + $kPluginAdminMenu = 0; + foreach (self::oPlugin()->oPluginAdminMenu_arr as $adminmenu) { + if (strtolower($adminmenu->cName) == strtolower($name)) { + $kPluginAdminMenu = $adminmenu->kPluginAdminMenu; + + break; + } + } + + return $kPluginAdminMenu; + } + } + } + +} diff --git a/version/207/class/Hook/AbstractHook.php b/version/207/class/Hook/AbstractHook.php new file mode 100644 index 0000000..561e401 --- /dev/null +++ b/version/207/class/Hook/AbstractHook.php @@ -0,0 +1,15 @@ +kPlugin)) + && array_key_exists($key, $_SESSION) && !array_key_exists('Zahlungsart', $_SESSION) + ) { + unset($_SESSION[$key]); + } + + if (!array_key_exists('ws_mollie_applepay_available', $_SESSION)) { + // TODO DOKU + if (defined('MOLLIE_APPLEPAY_TPL') && MOLLIE_APPLEPAY_TPL) { + Shop::Smarty()->assign('applePayCheckURL', json_encode(self::Plugin()->cFrontendPfadURLSSL . 'applepay.php')); + pq('body')->append(Shop::Smarty()->fetch(self::Plugin()->cFrontendPfad . 'tpl/applepay.tpl')); + } else { + $checkUrl = self::Plugin()->cFrontendPfadURLSSL . 'applepay.php'; + pq('head')->append(""); + } + } + } + + /** + * @return bool + */ + public static function isAvailable() + { + if (array_key_exists('ws_mollie_applepay_available', $_SESSION)) { + return $_SESSION['ws_mollie_applepay_available']; + } + + return false; + } + + /** + * @param $status bool + */ + public static function setAvailable($status) + { + $_SESSION['ws_mollie_applepay_available'] = $status; + } +} diff --git a/version/207/class/Hook/Checkbox.php b/version/207/class/Hook/Checkbox.php new file mode 100644 index 0000000..87c4334 --- /dev/null +++ b/version/207/class/Hook/Checkbox.php @@ -0,0 +1,60 @@ +cModulId, 'kPlugin_' . self::Plugin()->kPlugin . '_') === false) { + return; + } + + if (array_key_exists('nAnzeigeOrt', $args_arr) && $args_arr['nAnzeigeOrt'] === CHECKBOX_ORT_BESTELLABSCHLUSS && Session::getInstance()->Customer()->nRegistriert) { + $mCustomer = Customer::fromID(Session::getInstance()->Customer()->kKunde, 'kKunde'); + + if ($mCustomer->customerId) { + return; + } + + $checkbox = new \CheckBox(); + $checkbox->kLink = 0; + $checkbox->kCheckBox = -1; + $checkbox->kCheckBoxFunktion = 0; + $checkbox->cName = 'MOLLIE SAVE CUSTOMER'; + $checkbox->cKundengruppe = ';1;'; + $checkbox->cAnzeigeOrt = ';2;'; + $checkbox->nAktiv = 1; + $checkbox->nPflicht = 0; + $checkbox->nLogging = 0; + $checkbox->nSort = 999; + $checkbox->dErstellt = date('Y-m-d H:i:s'); + $checkbox->oCheckBoxSprache_arr = []; + + $langs = gibAlleSprachen(); + foreach ($langs as $lang) { + $checkbox->oCheckBoxSprache_arr[$lang->kSprache] = (object)[ + 'cText' => self::Plugin()->oPluginSprachvariableAssoc_arr['checkboxText'], + 'cBeschreibung' => self::Plugin()->oPluginSprachvariableAssoc_arr['checkboxDescr'], + 'kSprache' => $lang->kSprache, + 'kCheckbox' => -1 + ]; + } + + $checkbox->kKundengruppe_arr = [Session::getInstance()->Customer()->kKundengruppe]; + $checkbox->kAnzeigeOrt_arr = [CHECKBOX_ORT_BESTELLABSCHLUSS]; + $checkbox->cID = 'mollie_create_customer'; + $checkbox->cLink = ''; + + $args_arr['oCheckBox_arr'][] = $checkbox; + } + } +} diff --git a/version/207/class/Hook/Queue.php b/version/207/class/Hook/Queue.php new file mode 100644 index 0000000..06149b7 --- /dev/null +++ b/version/207/class/Hook/Queue.php @@ -0,0 +1,140 @@ +nWaehrendBestellung === 0 + && $args_arr['oBestellung']->fGesamtsumme > 0 + && self::Plugin()->oPluginEinstellungAssoc_arr['onlyPaid'] === 'Y' + && AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kZahlungsart, true) + ) { + $args_arr['oBestellung']->cAbgeholt = 'Y'; + Jtllog::writeLog('Switch cAbgeholt for kBestellung: ' . print_r($args_arr['oBestellung']->kBestellung, 1), JTLLOG_LEVEL_NOTICE); + } + } + + /** + * @param array $args_arr + */ + public static function xmlBestellStatus(array $args_arr) + { + if (AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kBestellung)) { + self::saveToQueue(HOOK_BESTELLUNGEN_XML_BESTELLSTATUS . ':' . (int)$args_arr['oBestellung']->kBestellung, [ + 'kBestellung' => $args_arr['oBestellung']->kBestellung, + 'status' => (int)$args_arr['status'] + ]); + } + } + + /** + * @param $hook + * @param $args_arr + * @param string $type + * @return bool + */ + public static function saveToQueue($hook, $args_arr, $type = 'hook') + { + $mQueue = new QueueModel(); + $mQueue->cType = $type . ':' . $hook; + $mQueue->cData = serialize($args_arr); + + try { + return $mQueue->save(); + } catch (Exception $e) { + Jtllog::writeLog('mollie::saveToQueue: ' . $e->getMessage() . ' - ' . print_r($args_arr, 1)); + + return false; + } + } + + /** + * @param array $args_arr + */ + public static function xmlBearbeiteStorno(array $args_arr) + { + if (AbstractCheckout::isMollie((int)$args_arr['oBestellung']->kBestellung)) { + self::saveToQueue(HOOK_BESTELLUNGEN_XML_BEARBEITESTORNO . ':' . $args_arr['oBestellung']->kBestellung, ['kBestellung' => $args_arr['oBestellung']->kBestellung]); + } + } + + /** + * + */ + public static function headPostGet() + { + if (array_key_exists('mollie', $_REQUEST) && (int)$_REQUEST['mollie'] === 1 && array_key_exists('id', $_REQUEST)) { + if (array_key_exists('hash', $_REQUEST) && $hash = trim(StringHandler::htmlentities(StringHandler::filterXSS($_REQUEST['hash'])), '_')) { + AbstractCheckout::finalizeOrder($hash, $_REQUEST['id'], array_key_exists('test', $_REQUEST)); + } else { + self::saveToQueue($_REQUEST['id'], $_REQUEST['id'], 'webhook'); + } + exit(); + } + if (array_key_exists('m_pay', $_REQUEST)) { + try { + $raw = Shop::DB()->executeQueryPrepared('SELECT kID FROM `xplugin_ws_mollie_payments` WHERE dReminder IS NOT NULL AND MD5(CONCAT(kID, "-", kBestellung)) = :md5', [ + ':md5' => $_REQUEST['m_pay'] + ], 1); + + if (!$raw) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errOrderNotFound']); + } + + if (strpos($raw->cOrderId, 'tr_') === 0) { + $checkout = PaymentCheckout::fromID($raw->kID); + } else { + $checkout = OrderCheckout::fromID($raw->kID); + } + $checkout->getMollie(true); + $checkout->updateModel()->saveModel(); + + if ( + ($checkout->getBestellung()->dBezahltDatum !== null && $checkout->getBestellung()->dBezahltDatum !== '0000-00-00') + || in_array($checkout->getModel()->cStatus, ['completed', 'paid', 'authorized', 'pending']) + ) { + throw new RuntimeException(self::Plugin()->oPluginSprachvariableAssoc_arr['errAlreadyPaid']); + } + + $options = []; + if (self::Plugin()->oPluginEinstellungAssoc_arr['resetMethod'] !== 'Y') { + $options['method'] = $checkout->getModel()->cMethod; + } + + $mollie = $checkout->create($options); // Order::repayOrder($orderModel->getOrderId(), $options, $api); + $url = $mollie->getCheckoutUrl(); + + header('Location: ' . $url); + exit(); + } catch (RuntimeException $e) { + // TODO Workaround? + //$alertHelper = Shop::Container()->getAlertService(); + //$alertHelper->addAlert(Alert::TYPE_ERROR, $e->getMessage(), 'mollie_repay', ['dismissable' => true]); + } catch (Exception $e) { + Jtllog::writeLog('mollie:repay:error: ' . $e->getMessage() . "\n" . print_r($_REQUEST, 1)); + } + } + } +} diff --git a/version/207/class/Model/AbstractModel.php b/version/207/class/Model/AbstractModel.php new file mode 100644 index 0000000..38a4ab5 --- /dev/null +++ b/version/207/class/Model/AbstractModel.php @@ -0,0 +1,110 @@ +data = $data; + if (!$data) { + $this->new = true; + } + } + + /** + * @param $id + * @param string $col + * @param false $failIfNotExists + * @return static + */ + public static function fromID($id, $col = 'kID', $failIfNotExists = false) + { + if ( + $payment = Shop::DB()->executeQueryPrepared( + 'SELECT * FROM ' . static::TABLE . " WHERE `$col` = :id", + [':id' => $id], + 1 + ) + ) { + return new static($payment); + } + if ($failIfNotExists) { + throw new RuntimeException(sprintf('Model %s in %s nicht gefunden!', $id, static::TABLE)); + } + + return new static(); + } + + /** + * @return null|mixed|stdClass + */ + public function jsonSerialize() + { + return $this->data; + } + + public function __get($name) + { + if (isset($this->data->$name)) { + return $this->data->$name; + } + + return null; + } + + public function __set($name, $value) + { + if (!$this->data) { + $this->data = new stdClass(); + } + $this->data->$name = $value; + } + + public function __isset($name) + { + return isset($this->data->$name); + } + + /** + * @return bool + */ + public function save() + { + if (!$this->data) { + throw new RuntimeException('No Data to save!'); + } + + if ($this->new) { + Shop::DB()->insertRow(static::TABLE, $this->data); + $this->new = false; + + return true; + } + Shop::DB()->updateRow(static::TABLE, static::PRIMARY, $this->data->{static::PRIMARY}, $this->data); + + return true; + } +} diff --git a/version/207/class/Model/Customer.php b/version/207/class/Model/Customer.php new file mode 100644 index 0000000..3d02409 --- /dev/null +++ b/version/207/class/Model/Customer.php @@ -0,0 +1,21 @@ +dCreatedAt) { + $this->dCreatedAt = date('Y-m-d H:i:s'); + } + if ($this->new) { + $this->dReminder = self::NULL; + } + + return parent::save(); + } +} diff --git a/version/207/class/Model/Queue.php b/version/207/class/Model/Queue.php new file mode 100644 index 0000000..1509e50 --- /dev/null +++ b/version/207/class/Model/Queue.php @@ -0,0 +1,46 @@ +cResult = $result; + $this->dDone = $date ?: date('Y-m-d H:i:s'); + + return $this->save(); + } + + public function save() + { + if (!$this->dCreated) { + $this->dCreated = date('Y-m-d H:i:s'); + } + $this->dModified = date('Y-m-d H:i:s'); + + return parent::save(); + } +} diff --git a/version/207/class/Model/Shipment.php b/version/207/class/Model/Shipment.php new file mode 100644 index 0000000..91d93be --- /dev/null +++ b/version/207/class/Model/Shipment.php @@ -0,0 +1,29 @@ +executeQueryPrepared( + 'SELECT p.kBestellung, b.cStatus FROM xplugin_ws_mollie_payments p ' + . 'JOIN tbestellung b ON b.kBestellung = p.kBestellung ' + . "WHERE b.cAbgeholt = 'Y' AND NOT p.bSynced AND b.cStatus IN ('1', '2') AND p.dCreatedAt < NOW() - INTERVAL :d HOUR", + [':d' => $delay], + 2 + ); + + foreach ($open as $o) { + try { + $checkout = AbstractCheckout::fromBestellung($o->kBestellung); + $pm = $checkout->PaymentMethod(); + if ($pm::ALLOW_AUTO_STORNO && $pm::METHOD === $checkout->getMollie()->method) { + if ($checkout->getBestellung()->cAbgeholt === 'Y' && (bool)$checkout->getModel()->bSynced === false) { + if (!in_array($checkout->getMollie()->status, [OrderStatus::STATUS_PAID, OrderStatus::STATUS_COMPLETED, OrderStatus::STATUS_AUTHORIZED], true)) { + $checkout->storno(); + } else { + $checkout->Log(sprintf('AutoStorno: Bestellung bezahlt? %s - Method: %s', $checkout->getMollie()->status, $checkout->getMollie()->method), LOGLEVEL_ERROR); + } + } else { + $checkout->Log('AutoStorno: bereits zur WAWI synchronisiert.', LOGLEVEL_ERROR); + } + } + } catch (Exception $e) { + Helper::logExc($e); + } + } + + return true; + } + + /** + * @param int $limit + */ + public static function run($limit = 10) + { + foreach (self::getOpen($limit) as $todo) { + if (!self::lock($todo)) { + Jtllog::writeLog(sprintf('%s already locked since %s', $todo->kId, $todo->bLock ?: 'just now')); + + continue; + } + + if ((list($type, $id) = explode(':', $todo->cType))) { + try { + switch ($type) { + case 'webhook': + self::handleWebhook($id, $todo); + + break; + + case 'hook': + self::handleHook((int)$id, $todo); + + break; + } + } catch (Exception $e) { + Jtllog::writeLog($e->getMessage() . " ($type, $id)"); + $todo->done("{$e->getMessage()}\n{$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}"); + } + } + + self::unlock($todo); + } + } + + /** + * @param $limit + * @return Generator|QueueModel[] + * @noinspection PhpReturnDocTypeMismatchInspection + * @noinspection SqlResolve + */ + private static function getOpen($limit) + { + if (!defined('MOLLIE_HOOK_DELAY')) { + define('MOLLIE_HOOK_DELAY', 3); + } + $open = Shop::DB()->executeQueryPrepared(sprintf("SELECT * FROM %s WHERE (dDone IS NULL OR dDone = '0000-00-00 00:00:00') AND `bLock` IS NULL AND (cType LIKE 'webhook:%%' OR (cType LIKE 'hook:%%') AND dCreated < DATE_SUB(NOW(), INTERVAL " . (int)MOLLIE_HOOK_DELAY . ' MINUTE)) ORDER BY dCreated DESC LIMIT 0, :LIMIT;', QueueModel::TABLE), [ + ':LIMIT' => $limit + ], 2); + + foreach ($open as $_raw) { + yield new QueueModel($_raw); + } + } + + /** + * @param $todo + * @return bool + * @noinspection SqlResolve + */ + protected static function lock($todo) + { + return $todo->kId && Shop::DB()->executeQueryPrepared(sprintf('UPDATE %s SET `bLock` = NOW() WHERE `bLock` IS NULL AND kId = :kId', QueueModel::TABLE), [ + 'kId' => $todo->kId + ], 3) >= 1; + } + + /** + * @param $id + * @param QueueModel $todo + * @throws Exception + * @return bool + */ + protected static function handleWebhook($id, QueueModel $todo) + { + $checkout = AbstractCheckout::fromID($id); + if ($checkout->getBestellung()->kBestellung && $checkout->PaymentMethod()) { + $checkout->handleNotification(); + + return $todo->done('Status: ' . $checkout->getMollie()->status); + } + + throw new RuntimeException("Bestellung oder Zahlungsart konnte nicht geladen werden: $id"); + } + + /** + * @param $hook + * @param QueueModel $todo + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return bool + */ + protected static function handleHook($hook, QueueModel $todo) + { + $data = unserialize($todo->cData); + if (array_key_exists('kBestellung', $data)) { + switch ($hook) { + case HOOK_BESTELLUNGEN_XML_BESTELLSTATUS: + if ((int)$data['kBestellung']) { + // TODO: #158 What happens when API requests fail? + $checkout = AbstractCheckout::fromBestellung($data['kBestellung']); + + $status = array_key_exists('status', $data) ? (int)$data['status'] : 0; + $result = ''; + if (!$status || $status < BESTELLUNG_STATUS_VERSANDT) { + return $todo->done("Bestellung noch nicht versendet: {$checkout->getBestellung()->cStatus}"); + } + if (!count($checkout->getBestellung()->oLieferschein_arr)) { + $todo->dCreated = date('Y-m-d H:i:s', strtotime('+3 MINUTES')); + $todo->cResult = 'Noch keine Lieferscheine, delay...'; + + return $todo->save(); + } + + /** @var $method JTLMollie */ + if ( + (strpos($checkout->getModel()->kID, 'tr_') === false) + && $checkout->PaymentMethod() + && $checkout->getMollie() + ) { + /** @var OrderCheckout $checkout */ + $checkout->handleNotification(); + if ($checkout->getMollie()->status === OrderStatus::STATUS_COMPLETED) { + $result = 'Mollie Status already ' . $checkout->getMollie()->status; + } elseif ($checkout->getMollie()->isCreated() || $checkout->getMollie()->isPaid() || $checkout->getMollie()->isAuthorized() || $checkout->getMollie()->isShipping() || $checkout->getMollie()->isPending()) { + try { + if ($shipments = Shipment::syncBestellung($checkout)) { + foreach ($shipments as $shipment) { + if (is_string($shipment)) { + $checkout->Log("Shipping-Error: $shipment"); + $result .= "Shipping-Error: $shipment;\n"; + } else { + $checkout->Log("Order shipped: {$shipment->id}"); + $result .= "Order shipped: $shipment->id;\n"; + } + } + } else { + $result = 'No Shipments ready!'; + } + } catch (RuntimeException $e) { + $result = $e->getMessage(); + } catch (Exception $e) { + $result = $e->getMessage() . "\n" . $e->getFile() . ':' . $e->getLine() . "\n" . $e->getTraceAsString(); + } + } else { + $result = sprintf('Unerwarteter Mollie Status "%s" für %s', $checkout->getMollie()->status, $checkout->getBestellung()->cBestellNr); + } + } else { + $result = 'Nothing to do.'; + } + $checkout->Log('Queue::handleHook: ' . $result); + } else { + $result = 'kBestellung missing'; + } + + return $todo->done($result); + + case HOOK_BESTELLUNGEN_XML_BEARBEITESTORNO: + if (self::Plugin()->oPluginEinstellungAssoc_arr['autoRefund'] !== 'Y') { + throw new RuntimeException('Auto-Refund disabled'); + } + + $checkout = AbstractCheckout::fromBestellung((int)$data['kBestellung']); + + return $todo->done($checkout->cancelOrRefund()); + } + } + + return false; + } + + /** + * @param $todo + * @return bool + */ + protected static function unlock($todo) + { + return $todo->kId && Shop::DB()->executeQueryPrepared(sprintf('UPDATE %s SET `bLock` = NULL WHERE kId = :kId OR bLock < DATE_SUB(NOW(), INTERVAL 15 MINUTE)', QueueModel::TABLE), [ + 'kId' => $todo->kId + ], 3) >= 1; + } +} diff --git a/version/207/class/Shipment.php b/version/207/class/Shipment.php new file mode 100644 index 0000000..a22a8d3 --- /dev/null +++ b/version/207/class/Shipment.php @@ -0,0 +1,325 @@ +kLieferschein = $kLieferschein; + if ($checkout) { + $this->checkout = $checkout; + } + + if (!$this->getLieferschein() || !$this->getLieferschein()->getLieferschein()) { + throw new RuntimeException('Lieferschein konnte nicht geladen werden'); + } + + if (!count($this->getLieferschein()->oVersand_arr)) { + throw new RuntimeException('Kein Versand gefunden!'); + } + + if (!$this->getCheckout()->getBestellung()->oKunde->nRegistriert) { + $this->isGuest = true; + } + } + + public function getLieferschein() + { + if (!$this->oLieferschein && $this->kLieferschein) { + $this->oLieferschein = new Lieferschein($this->kLieferschein); + } + + return $this->oLieferschein; + } + + /** + * @throws Exception + * @return OrderCheckout + */ + public function getCheckout() + { + if (!$this->checkout) { + //TODO evtl. load by lieferschien + throw new Exception('Should not happen, but it did!'); + } + + return $this->checkout; + } + + public static function syncBestellung(OrderCheckout $checkout) + { + $shipments = []; + if ($checkout->getBestellung()->kBestellung) { + $oKunde = $checkout->getBestellung()->oKunde ?: new Kunde($checkout->getBestellung()->kKunde); + + $shippingActive = Helper::getSetting('shippingActive'); + if ($shippingActive === 'N') { + throw new RuntimeException('Shipping deaktiviert'); + } + + if ($shippingActive === 'K' && !$oKunde->nRegistriert && (int)$checkout->getBestellung()->cStatus !== BESTELLUNG_STATUS_VERSANDT) { + throw new RuntimeException('Shipping für Gast-Bestellungen und Teilversand deaktiviert'); + } + + /** @var Lieferschein $oLieferschein */ + foreach ($checkout->getBestellung()->oLieferschein_arr as $oLieferschein) { + try { + $shipment = new self($oLieferschein->getLieferschein(), $checkout); + $mode = self::Plugin()->oPluginEinstellungAssoc_arr['shippingMode']; + switch ($mode) { + case 'A': + // ship directly + if (!$shipment->send() && !$shipment->getShipment()) { + throw new RuntimeException('Shipment konnte nicht gespeichert werden.'); + } + $shipments[] = $shipment->getShipment(); + + break; + + case 'B': + // only ship if complete shipping + if ($oKunde->nRegistriert || (int)$checkout->getBestellung()->cStatus === BESTELLUNG_STATUS_VERSANDT) { + if (!$shipment->send() && !$shipment->getShipment()) { + throw new RuntimeException('Shipment konnte nicht gespeichert werden.'); + } + $shipments[] = $shipment->getShipment(); + + break; + } + + throw new RuntimeException('Gastbestellung noch nicht komplett versendet!'); + } + } catch (RuntimeException $e) { + $shipments[] = $e->getMessage(); + } catch (Exception $e) { + $shipments[] = $e->getMessage(); + $checkout->Log("mollie: Shipment::syncBestellung (BestellNr. {$checkout->getBestellung()->cBestellNr}, Lieferschein: {$oLieferschein->getLieferscheinNr()}) - " . $e->getMessage(), LOGLEVEL_ERROR); + } + } + } + + return $shipments; + } + + /** + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return bool + */ + public function send() + { + if ($this->getShipment()) { + throw new RuntimeException('Lieferschien bereits an Mollie übertragen: ' . $this->getShipment()->id); + } + + if ($this->getCheckout()->getMollie(true)->status === OrderStatus::STATUS_COMPLETED) { + throw new RuntimeException('Bestellung bei Mollie bereits abgeschlossen!'); + } + + $api = $this->getCheckout()->API()->Client(); + $this->shipment = $api->shipments->createForId($this->checkout->getModel()->kID, $this->loadRequest()->jsonSerialize()); + + return $this->updateModel()->saveModel(); + } + + /** + * @param false $force + * @throws ApiException + * @throws IncompatiblePlatform + * @throws Exception + * @return BaseResource|\Mollie\Api\Resources\Shipment + */ + public function getShipment($force = false) + { + if (($force || !$this->shipment) && $this->getModel() && $this->getModel()->cShipmentId) { + $this->shipment = $this->getCheckout()->API()->Client()->shipments->getForId($this->getModel()->cOrderId, $this->getModel()->cShipmentId); + } + + return $this->shipment; + } + + /** + * @throws Exception + * @return ShipmentModel + */ + public function getModel() + { + if (!$this->model && $this->kLieferschein) { + $this->model = ShipmentModel::fromID($this->kLieferschein, 'kLieferschein'); + + if (!$this->model->dCreated) { + $this->model->dCreated = date('Y-m-d H:i:s'); + } + $this->updateModel(); + } + + return $this->model; + } + + /** + * @throws Exception + * @return $this + */ + public function updateModel() + { + $this->getModel()->kLieferschein = $this->kLieferschein; + if ($this->getCheckout()) { + $this->getModel()->cOrderId = $this->getCheckout()->getModel()->kID; + $this->getModel()->kBestellung = $this->getCheckout()->getModel()->kBestellung; + } + if ($this->getShipment()) { + $this->getModel()->cShipmentId = $this->getShipment()->id; + $this->getModel()->cUrl = $this->getShipment()->getTrackingUrl() ?: ''; + } + if (isset($this->tracking)) { + $this->getModel()->cCarrier = $this->tracking['carrier'] ?: ''; + $this->getModel()->cCode = $this->tracking['code'] ?: ''; + } + + return $this; + } + + /** + * @param array $options + * @throws Exception + * @return $this + */ + public function loadRequest(&$options = []) + { + /** @var Versand $oVersand */ + $oVersand = $this->getLieferschein()->oVersand_arr[0]; + if ($oVersand->getIdentCode() && $oVersand->getLogistik()) { + $tracking = [ + 'carrier' => utf8_encode($oVersand->getLogistik()), + 'code' => utf8_encode($oVersand->getIdentCode()), + ]; + if ($oVersand->getLogistikVarUrl()) { + $tracking['url'] = utf8_encode($oVersand->getLogistikURL()); + } + $this->tracking = $tracking; + } + + // TODO: Wenn alle Lieferschiene in der WAWI erstellt wurden, aber nicht im Shop, kommt status 4. + if ($this->isGuest || (int)$this->getCheckout()->getBestellung()->cStatus === BESTELLUNG_STATUS_VERSANDT) { + $this->lines = []; + } else { + $this->lines = $this->getOrderLines(); + } + + return $this; + } + + /** + * @throws Exception + * @return array + */ + protected function getOrderLines() + { + $lines = []; + + if (!count($this->getLieferschein()->oLieferscheinPos_arr)) { + return $lines; + } + + // Bei Stücklisten, sonst gibt es mehrere OrderLines für die selbe ID + $shippedOrderLines = []; + + /** @var Lieferscheinpos $oLieferscheinPos */ + foreach ($this->getLieferschein()->oLieferscheinPos_arr as $oLieferscheinPos) { + $wkpos = Shop::DB()->executeQueryPrepared('SELECT * FROM twarenkorbpos WHERE kBestellpos = :kBestellpos', [ + ':kBestellpos' => $oLieferscheinPos->getBestellPos() + ], 1); + + /** @var OrderLine $orderLine */ + foreach ($this->getCheckout()->getMollie()->lines as $orderLine) { + if ($orderLine->sku === $wkpos->cArtNr && !in_array($orderLine->id, $shippedOrderLines, true)) { + if ($quantity = min($oLieferscheinPos->getAnzahl(), $orderLine->shippableQuantity)) { + $lines[] = [ + 'id' => $orderLine->id, + 'quantity' => $quantity + ]; + } + $shippedOrderLines[] = $orderLine->id; + + break; + } + } + } + + return $lines; + } + + /** + * @throws Exception + * @return bool + */ + public function saveModel() + { + return $this->getModel()->save(); + } +} diff --git a/version/207/class/Traits/Jsonable.php b/version/207/class/Traits/Jsonable.php new file mode 100644 index 0000000..8ba9b19 --- /dev/null +++ b/version/207/class/Traits/Jsonable.php @@ -0,0 +1,23 @@ +jsonSerialize(); + } + + public function jsonSerialize() + { + return array_filter(get_object_vars($this), static function ($value) { + return !($value === null || (is_string($value) && $value === '')); + }); + } +} diff --git a/version/207/class/Traits/Plugin.php b/version/207/class/Traits/Plugin.php new file mode 100644 index 0000000..08069cd --- /dev/null +++ b/version/207/class/Traits/Plugin.php @@ -0,0 +1,30 @@ +requestData) === false) { + throw new \RuntimeException(sprintf("JSON Encode Error: %s\n%s", json_last_error_msg(), print_r($this->requestData, 1))); + } + + return $this->requestData; + } + + public function __get($name) + { + if (!$this->requestData) { + $this->loadRequest(); + } + + return is_string($this->requestData[$name]) ? utf8_decode($this->requestData[$name]) : $this->requestData[$name]; + } + + public function __set($name, $value) + { + if (!$this->requestData) { + $this->requestData = []; + } + + $this->requestData[$name] = is_string($value) ? utf8_encode($value) : $value; + + return $this; + } + + /** + * @param array $options + * @return $this + */ + public function loadRequest(&$options = []) + { + return $this; + } + + + public function __serialize() + { + return $this->requestData ?: []; + } + + public function __isset($name) + { + return $this->requestData[$name] !== null; + } +} diff --git a/version/207/frontend/.htaccess b/version/207/frontend/.htaccess new file mode 100644 index 0000000..df6c074 --- /dev/null +++ b/version/207/frontend/.htaccess @@ -0,0 +1,9 @@ + + + Require all granted + + + Order Allow,Deny + Allow from all + + diff --git a/version/207/frontend/131_globalinclude.php b/version/207/frontend/131_globalinclude.php new file mode 100644 index 0000000..962b5ad --- /dev/null +++ b/version/207/frontend/131_globalinclude.php @@ -0,0 +1,61 @@ +select('tzahlungsession', 'cZahlungsID', $sessionHash); + if ($paymentSession && $paymentSession->kBestellung) { + $oBestellung = new Bestellung($paymentSession->kBestellung); + + if (Shop::getConfig([CONF_KAUFABWICKLUNG])['kaufabwicklung']['bestellabschluss_abschlussseite'] === 'A') { + $oZahlungsID = Shop::DB()->query( + ' + SELECT cId + FROM tbestellid + WHERE kBestellung = ' . (int)$paymentSession->kBestellung, + 1 + ); + if (is_object($oZahlungsID)) { + header('Location: ' . Shop::getURL() . '/bestellabschluss.php?i=' . $oZahlungsID->cId); + exit(); + } + } + $bestellstatus = Shop::DB()->select('tbestellstatus', 'kBestellung', (int)$paymentSession->kBestellung); + header('Location: ' . Shop::getURL() . '/status.php?uid=' . $bestellstatus->cUID); + exit(); + } + } + + ifndef('MOLLIE_REMINDER_PROP', 10); + if (mt_rand(1, MOLLIE_REMINDER_PROP) % MOLLIE_REMINDER_PROP === 0) { + $lock = new \ws_mollie\ExclusiveLock('mollie_reminder', PFAD_ROOT . PFAD_COMPILEDIR); + if ($lock->lock()) { + AbstractCheckout::sendReminders(); + Queue::storno((int)Helper::getSetting('autoStorno')); + + $lock->unlock(); + } + } +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/207/frontend/132_headPostGet.php b/version/207/frontend/132_headPostGet.php new file mode 100644 index 0000000..9562324 --- /dev/null +++ b/version/207/frontend/132_headPostGet.php @@ -0,0 +1,20 @@ +append( + << + /* MOLLIE CHECKOUT STYLES*/ + #fieldset-payment .form-group > div:hover, #checkout-shipping-payment .form-group > div:hover { + background-color: #eee; + color: black; + } + $selector label { + $border + } + + $selector label::after { + clear: both; + content: ' '; + display: block; + } + + $selector label span small { + line-height: 48px; + } + + $selector label img { + float: right; + } + +HTML + ); +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/207/frontend/180_checkbox.php b/version/207/frontend/180_checkbox.php new file mode 100644 index 0000000..530a9dc --- /dev/null +++ b/version/207/frontend/180_checkbox.php @@ -0,0 +1,21 @@ +oPluginEinstellungAssoc_arr['useCustomerAPI'] === 'C') { + \ws_mollie\Hook\Checkbox::execute($args_arr); + } +} catch (Exception $e) { + Helper::logExc($e); +} diff --git a/version/207/frontend/181_sync.php b/version/207/frontend/181_sync.php new file mode 100644 index 0000000..46fcf31 --- /dev/null +++ b/version/207/frontend/181_sync.php @@ -0,0 +1,18 @@ + diff --git a/version/207/frontend/tpl/applepay.tpl b/version/207/frontend/tpl/applepay.tpl new file mode 100644 index 0000000..57c2e75 --- /dev/null +++ b/version/207/frontend/tpl/applepay.tpl @@ -0,0 +1,19 @@ + + diff --git a/version/207/paymentmethod/JTLMollie.php b/version/207/paymentmethod/JTLMollie.php new file mode 100644 index 0000000..ee887a8 --- /dev/null +++ b/version/207/paymentmethod/JTLMollie.php @@ -0,0 +1,306 @@ +cModulId = 'kPlugin_' . self::Plugin()->kPlugin . '_mollie' . $moduleID; + } + + /** + * @param Bestellung $order + * @return PaymentMethod + */ + public function setOrderStatusToPaid($order) + { + // If paid already, do nothing + if ((int)$order->cStatus >= BESTELLUNG_STATUS_BEZAHLT) { + return $this; + } + + return parent::setOrderStatusToPaid($order); + } + + + /** + * @param $kBestellung + * @param bool $redirect + * @return bool|string + */ + public static function getOrderCompletedRedirect($kBestellung, $redirect = true) + { + $mode = Shopsetting::getInstance()->getValue(CONF_KAUFABWICKLUNG, 'bestellabschluss_abschlussseite'); + + $bestellid = Shop::DB()->select('tbestellid ', 'kBestellung', (int)$kBestellung); + $url = Shop::getURL() . '/bestellabschluss.php?i=' . $bestellid->cId; + + + if ($mode === 'S' || !$bestellid) { // Statusseite + $bestellstatus = Shop::DB()->select('tbestellstatus', 'kBestellung', (int)$kBestellung); + $url = Shop::getURL() . '/status.php?uid=' . $bestellstatus->cUID; + } + + if ($redirect) { + if (!headers_sent()) { + header('Location: ' . $url); + } + echo "redirect ..."; + exit(); + } + + return $url; + } + + + /** + * Prepares everything so that the Customer can start the Payment Process. + * Tells Template Engine. + * + * @param Bestellung $order + * @return void + */ + public function preparePaymentProcess($order) + { + parent::preparePaymentProcess($order); + + try { + if ($this->duringCheckout && !static::ALLOW_PAYMENT_BEFORE_ORDER) { + $this->Log(sprintf('Zahlung vor Bestellabschluss nicht unterstützt (%s)!', $order->cBestellNr), sprintf('#%s', $order->kBestellung), LOGLEVEL_ERROR); + + return; + } + + $payable = (float)$order->fGesamtsumme > 0; + if (!$payable) { + $this->Log(sprintf("Bestellung '%s': Gesamtsumme %.2f, keine Zahlung notwendig!", $order->cBestellNr, $order->fGesamtsumme), sprintf('#%s', $order->kBestellung)); + require_once PFAD_ROOT . PFAD_INCLUDES . 'bestellabschluss_inc.php'; + require_once PFAD_ROOT . PFAD_INCLUDES . 'mailTools.php'; + $finalized = finalisiereBestellung(); + Session::getInstance()->cleanUp(); + self::getOrderCompletedRedirect($finalized->kBestellung); + + return; + } + + $paymentOptions = []; + + $api = array_key_exists($this->moduleID . '_api', self::Plugin()->oPluginEinstellungAssoc_arr) ? self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_api'] : 'order'; + + $paymentOptions = array_merge($paymentOptions, $this->getPaymentOptions($order, $api)); + + if ($api === 'payment') { + $checkout = new PaymentCheckout($order); + $payment = $checkout->create($paymentOptions); + /** @noinspection NullPointerExceptionInspection */ + $url = $payment->getCheckoutUrl(); + } else { + $checkout = new OrderCheckout($order); + $mOrder = $checkout->create($paymentOptions); + /** @noinspection NullPointerExceptionInspection */ + $url = $mOrder->getCheckoutUrl(); + } + + ifndef('MOLLIE_REDIRECT_DELAY', 3); + $checkoutMode = self::Plugin()->oPluginEinstellungAssoc_arr['checkoutMode']; + Shop::Smarty()->assign('redirect', $url) + ->assign('checkoutMode', $checkoutMode); + if ($checkoutMode === 'Y' && !headers_sent()) { + header('Location: ' . $url); + } + } catch (Exception $e) { + $this->Log('mollie::preparePaymentProcess: ' . $e->getMessage() . ' - ' . print_r(['cBestellNr' => $order->cBestellNr], 1), '#' . $order->kBestellung, LOGLEVEL_ERROR); + Shop::Smarty()->assign('oMollieException', $e) + ->assign('tryAgain', Shop::getURL() . '/bestellab_again.php?kBestellung=' . $order->kBestellung); + } + } + + public function Log($msg, $data = null, $level = LOGLEVEL_NOTICE) + { + ZahlungsLog::add($this->moduleID, '[' . microtime(true) . ' - ' . $_SERVER['PHP_SELF'] . '] ' . $msg, $data, $level); + + return $this; + } + + public function getPaymentOptions(Bestellung $order, $apiType) + { + return []; + } + + /** + * @param Bestellung $order + * @param string $hash + * @param array $args + */ + public function handleNotification($order, $hash, $args) + { + parent::handleNotification($order, $hash, $args); + + try { + $orderId = $args['id']; + $checkout = null; + if (strpos($orderId, 'tr_') === 0) { + $checkout = new PaymentCheckout($order); + } else { + $checkout = new OrderCheckout($order); + } + $checkout->handleNotification($hash); + } catch (Exception $e) { + $checkout->Log(sprintf("mollie::handleNotification: Fehler bei Bestellung '%s': %s\n%s", $order->cBestellNr, $e->getMessage(), print_r($args, 1)), LOGLEVEL_ERROR); + } + } + + /** + * @return bool + */ + public function canPayAgain() + { + return true; + } + + /** + * determines, if the payment method can be selected in the checkout process + * + * @return bool + */ + public function isSelectable() + { + if (API::getMode()) { + $selectable = trim(self::Plugin()->oPluginEinstellungAssoc_arr['test_api_key']) !== ''; + } else { + $selectable = trim(self::Plugin()->oPluginEinstellungAssoc_arr['api_key']) !== ''; + if (!$selectable) { + Jtllog::writeLog('Live API Key missing!'); + } + } + if ($selectable) { + try { + $locale = AbstractCheckout::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer()->cLand); + $amount = Session::getInstance()->Basket()->gibGesamtsummeWarenExt([ + C_WARENKORBPOS_TYP_ARTIKEL, + C_WARENKORBPOS_TYP_KUPON, + C_WARENKORBPOS_TYP_GUTSCHEIN, + C_WARENKORBPOS_TYP_NEUKUNDENKUPON, + ], true) * Session::getInstance()->Currency()->fFaktor; + if ($amount <= 0) { + $amount = 0.01; + } + $selectable = self::isMethodPossible( + static::METHOD, + $locale, + Session::getInstance()->Customer()->cLand, + Session::getInstance()->Currency()->cISO, + $amount + ); + } catch (Exception $e) { + Helper::logExc($e); + $selectable = false; + } + } + + return $selectable && parent::isSelectable(); + } + + /** + * @param $method + * @param $locale + * @param $billingCountry + * @param $currency + * @param $amount + * @throws ApiException + * @return bool + */ + protected static function isMethodPossible($method, $locale, $billingCountry, $currency, $amount) + { + $api = new API(API::getMode()); + + if (!array_key_exists('mollie_possibleMethods', $_SESSION)) { + $_SESSION['mollie_possibleMethods'] = []; + } + + $key = md5(serialize([$locale, $billingCountry, $currency, $amount])); + if (!array_key_exists($key, $_SESSION['mollie_possibleMethods'])) { + $active = $api->Client()->methods->allActive([ + 'locale' => $locale, + 'amount' => [ + 'currency' => $currency, + 'value' => number_format($amount, 2, '.', '') + ], + 'billingCountry' => $billingCountry, + 'resource' => 'orders', + 'includeWallets' => 'applepay', + ]); + foreach ($active as $a) { + $_SESSION['mollie_possibleMethods'][$key][] = (object)['id' => $a->id]; + } + } + + if ($method !== '') { + foreach ($_SESSION['mollie_possibleMethods'][$key] as $m) { + if ($m->id === $method) { + return true; + } + } + } else { + return true; + } + + return false; + } + + /** + * @param array $args_arr + * @return bool + */ + public function isValidIntern($args_arr = []) + { + return $this->duringCheckout + ? static::ALLOW_PAYMENT_BEFORE_ORDER && parent::isValidIntern($args_arr) + : parent::isValidIntern($args_arr); + } + + /** + * @return int + */ + public function getExpiryDays() + { + return (int)min(abs((int)Helper::oPlugin()->oPluginEinstellungAssoc_arr[$this->cModulId . '_dueDays']), static::MAX_EXPIRY_DAYS) ?: static::MAX_EXPIRY_DAYS; + } +} diff --git a/version/207/paymentmethod/JTLMollieApplePay.php b/version/207/paymentmethod/JTLMollieApplePay.php new file mode 100644 index 0000000..ca0b375 --- /dev/null +++ b/version/207/paymentmethod/JTLMollieApplePay.php @@ -0,0 +1,20 @@ +oRechnungsadresse->cMail; + $paymentOptions['locale'] = AbstractCheckout::getLocale($_SESSION['cISOSprache'], $order->oRechnungsadresse->cLand); + } + + $dueDays = $this->getExpiryDays(); + if ($dueDays > 3) { + $paymentOptions['dueDate'] = date('Y-m-d', strtotime("+$dueDays DAYS")); + } + + return $paymentOptions; + } +} diff --git a/version/207/paymentmethod/JTLMollieBelfius.php b/version/207/paymentmethod/JTLMollieBelfius.php new file mode 100644 index 0000000..b7cac71 --- /dev/null +++ b/version/207/paymentmethod/JTLMollieBelfius.php @@ -0,0 +1,17 @@ +clearToken(); + } + + protected function clearToken() + { + $this->unsetCache(self::CACHE_TOKEN) + ->unsetCache(self::CACHE_TOKEN_TIMESTAMP); + + return true; + } + + public function handleAdditional($aPost_arr) + { + $components = self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_components']; + $profileId = self::Plugin()->oPluginEinstellungAssoc_arr['profileId']; + + if ($components === 'N' || !$profileId || trim($profileId) === '') { + return parent::handleAdditional($aPost_arr); + } + + $cleared = false; + if (array_key_exists('clear', $aPost_arr) && (int)$aPost_arr['clear']) { + $cleared = $this->clearToken(); + } + + if ($components === 'S' && array_key_exists('skip', $aPost_arr) && (int)$aPost_arr['skip']) { + return parent::handleAdditional($aPost_arr); + } + + try { + $trustBadge = (bool)self::Plugin()->oPluginEinstellungAssoc_arr[$this->moduleID . '_loadTrust']; + $locale = AbstractCheckout::getLocale($_SESSION['cISOSprache'], Session::getInstance()->Customer() ? Session::getInstance()->Customer()->cLand : null); + $mode = API::getMode(); + $errorMessage = json_encode(self::Plugin()->oPluginSprachvariableAssoc_arr['mcErrorMessage']); + } catch (Exception $e) { + Jtllog::writeLog($e->getMessage() . "\n" . print_r(['e' => $e], 1)); + + return parent::handleAdditional($aPost_arr); + } + + if (!$cleared && array_key_exists('cardToken', $aPost_arr) && ($token = trim($aPost_arr['cardToken']))) { + return $this->setToken($token) && parent::handleAdditional($aPost_arr); + } + + $token = false; + if (($ctTS = (int)$this->getCache(self::CACHE_TOKEN_TIMESTAMP)) && $ctTS > time()) { + $token = $this->getCache(self::CACHE_TOKEN); + } + + Shop::Smarty()->assign('profileId', $profileId) + ->assign('trustBadge', $trustBadge ? self::Plugin()->cFrontendPfadURLSSL . 'img/trust_' . $_SESSION['cISOSprache'] . '.png' : false) + ->assign('components', $components) + ->assign('locale', $locale ?: 'de_DE') + ->assign('token', $token ?: false) + ->assign('testMode', $mode ?: false) + ->assign('errorMessage', $errorMessage ?: null) + ->assign('mollieLang', self::Plugin()->oPluginSprachvariableAssoc_arr); + + return false; + } + + protected function setToken($token) + { + $this->addCache(self::CACHE_TOKEN, $token) + ->addCache(self::CACHE_TOKEN_TIMESTAMP, time() + 3600); + + return true; + } + + public function getPaymentOptions(Bestellung $order, $apiType) + { + $paymentOptions = []; + + if ($apiType === 'payment') { + if ($order->Lieferadresse !== null) { + if (!$order->Lieferadresse->cMail) { + $order->Lieferadresse->cMail = $order->oRechnungsadresse->cMail; + } + $paymentOptions['shippingAddress'] = Address::factory($order->Lieferadresse); + } + + $paymentOptions['billingAddress'] = Address::factory($order->oRechnungsadresse); + } + if ((int)$this->getCache(self::CACHE_TOKEN_TIMESTAMP) > time() && ($token = trim($this->getCache(self::CACHE_TOKEN)))) { + $paymentOptions['cardToken'] = $token; + } + + return $paymentOptions; + } +} diff --git a/version/207/paymentmethod/JTLMollieEPS.php b/version/207/paymentmethod/JTLMollieEPS.php new file mode 100644 index 0000000..5120b55 --- /dev/null +++ b/version/207/paymentmethod/JTLMollieEPS.php @@ -0,0 +1,17 @@ + substr($order->cBestellNr, 0, 13)]; + } +} diff --git a/version/207/paymentmethod/JTLMollieKlarnaPayLater.php b/version/207/paymentmethod/JTLMollieKlarnaPayLater.php new file mode 100644 index 0000000..3474053 --- /dev/null +++ b/version/207/paymentmethod/JTLMollieKlarnaPayLater.php @@ -0,0 +1,24 @@ +Lieferadresse !== null) { + if (!$order->Lieferadresse->cMail) { + $order->Lieferadresse->cMail = $order->oRechnungsadresse->cMail; + } + $paymentOptions['shippingAddress'] = Address::factory($order->Lieferadresse); + } + } + + + return $paymentOptions; + } +} diff --git a/version/207/paymentmethod/JTLMolliePaysafecard.php b/version/207/paymentmethod/JTLMolliePaysafecard.php new file mode 100644 index 0000000..29be769 --- /dev/null +++ b/version/207/paymentmethod/JTLMolliePaysafecard.php @@ -0,0 +1,23 @@ + $order->oKunde->kKunde] : []; + } +} diff --git a/version/207/paymentmethod/JTLMolliePrzelewy24.php b/version/207/paymentmethod/JTLMolliePrzelewy24.php new file mode 100644 index 0000000..62a55d5 --- /dev/null +++ b/version/207/paymentmethod/JTLMolliePrzelewy24.php @@ -0,0 +1,23 @@ + $order->oRechnungsadresse->cMail] : []; + } +} diff --git a/version/207/paymentmethod/JTLMollieSofort.php b/version/207/paymentmethod/JTLMollieSofort.php new file mode 100644 index 0000000..892adc3 --- /dev/null +++ b/version/207/paymentmethod/JTLMollieSofort.php @@ -0,0 +1,18 @@ + + {$oMollieException->getMessage()} + + +{/if} + +{if isset($redirect) && $redirect != ''} + + {if $checkoutMode == 'D'} + + {/if} +{/if} + diff --git a/version/207/paymentmethod/tpl/mollieComponents.tpl b/version/207/paymentmethod/tpl/mollieComponents.tpl new file mode 100644 index 0000000..987abf1 --- /dev/null +++ b/version/207/paymentmethod/tpl/mollieComponents.tpl @@ -0,0 +1,148 @@ +

{$mollieLang.cctitle}

+ +
+ +
+ + {if $token !== false} + +
+
+ {$mollieLang.clearDescr} +
+
+ +
+
+ + {if $trustBadge} +
+ PCI-DSS SAQ-A compliant +
+ {/if} + + {else} + + +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+ + {if $trustBadge} +
+ PCI-DSS SAQ-A compliant +
+ {/if} + {if $components == 'S'} + + {/if} +
+ +{/if} + + + + + + \ No newline at end of file diff --git a/version/207/tpl/_alerts.tpl b/version/207/tpl/_alerts.tpl new file mode 100644 index 0000000..5279fa3 --- /dev/null +++ b/version/207/tpl/_alerts.tpl @@ -0,0 +1,10 @@ +{if $alerts} + {assign var=alert_arr value=$alerts|get_object_vars} + {if $alert_arr|count} +
+ {foreach from=$alert_arr item=alert key=id} +
{$alert}
+ {/foreach} +
+ {/if} +{/if}