diff --git a/Bootstrap.php b/Bootstrap.php index f3df2ff..edd1d06 100644 --- a/Bootstrap.php +++ b/Bootstrap.php @@ -10,16 +10,22 @@ use JTL\Events\Dispatcher; use JTL\Exceptions\CircularReferenceException; use JTL\Exceptions\ServiceNotFoundException; +use JTL\Router\Router; +use Plugin\ws5_mollie\lib\CleanupCronJob; use Plugin\ws5_mollie\lib\Hook\ApplePay; use Plugin\ws5_mollie\lib\Hook\Checkbox; use Plugin\ws5_mollie\lib\Hook\IncompletePaymentHandler; use Plugin\ws5_mollie\lib\Hook\Queue; +use Plugin\ws5_mollie\lib\Hook\FrontendHook; use Plugin\ws5_mollie\lib\PluginHelper; +use JTL\Events\Event; require_once __DIR__ . '/vendor/autoload.php'; class Bootstrap extends \WS\JTL5\V1_0_16\Bootstrap { + private const CRON_TYPE = 'cronjob_mollie_cleanup'; + /** * @param Dispatcher $dispatcher * @throws CircularReferenceException @@ -29,7 +35,16 @@ public function boot(Dispatcher $dispatcher): void { parent::boot($dispatcher); + $dispatcher->listen(Event::GET_AVAILABLE_CRONJOBS, [$this, 'availableCronjobType']); + $dispatcher->listen(Event::MAP_CRONJOB_TYPE, static function (array &$args) { + if ($args['type'] === self::CRON_TYPE) { + $args['mapping'] = CleanupCronJob::class; + } + }); + + $this->listen(HOOK_SMARTY_OUTPUTFILTER, [ApplePay::class, 'execute']); + $this->listen(HOOK_SMARTY_OUTPUTFILTER, [FrontendHook::class, 'execute']); $this->listen(HOOK_BESTELLVORGANG_PAGE, [IncompletePaymentHandler::class, 'checkForIncompletePayment']); @@ -44,6 +59,69 @@ public function boot(Dispatcher $dispatcher): void if (PluginHelper::getSetting('useCustomerAPI') === 'C') { $this->listen(HOOK_CHECKBOX_CLASS_GETCHECKBOXFRONTEND, [Checkbox::class, 'execute']); } + + //routes + if (PluginHelper::getSetting('queue') === 'async') { + $this->listen(HOOK_ROUTER_PRE_DISPATCH, function ($args) { + /** @var Router $router */ + $router = $args['router']; + $router->addRoute('/' . self::getPlugin()->getPluginID() . '/queue', [\Plugin\ws5_mollie\lib\Queue::class, 'runAsynchronous'], null, ['POST']); + }); + } + } + + /** + * @return void + */ + private function addCleanupCron(): void + { + $isInstalled = $this->getDB()->executeQueryPrepared('SELECT * FROM tcron WHERE name = :name AND jobType = :jobType', + [ + ':name' => 'Mollie Queue Cleanup', + ':jobType' => self::CRON_TYPE + ], + 3) > 0; + + if (!$isInstalled) { + $job = new \stdClass(); + $job->name = 'Mollie Queue Cleanup'; + $job->jobType = self::CRON_TYPE; + $job->frequency = 1; + $job->startDate = 'NOW()'; + $job->startTime = '06:00:00'; + $this->getDB()->insert('tcron', $job); + } + } + + /** + * @param array $args + * @return void + */ + public function availableCronjobType(array &$args): void + { + if (!\in_array(self::CRON_TYPE, $args['jobs'], true)) { + $args['jobs'][] = self::CRON_TYPE; + } + } + + + /** + * @return void + */ + public function installed(): void + { + parent::installed(); + $this->addCleanupCron(); + } + + /** + * @param bool $deleteData + * @return void + */ + public function uninstalled(bool $deleteData = true): void + { + parent::uninstalled($deleteData); + $this->getDB()->delete('tcron', ['name', 'jobType'], ['Mollie Queue Cleanup', self::CRON_TYPE]); } /** @@ -55,6 +133,10 @@ public function updated($oldVersion, $newVersion): void { parent::updated($oldVersion, $newVersion); + if ($newVersion >= "1.9.0") { + $this->addCleanupCron(); + } + if (PluginHelper::isShopVersionEqualOrGreaterThan('5.3.0')) { \JTL\Update\DBMigrationHelper::migrateToInnoDButf8('xplugin_ws5_mollie_kunde'); // TODO: remove this code when min. shop version is 5.3 \JTL\Update\DBMigrationHelper::migrateToInnoDButf8('xplugin_ws5_mollie_orders'); // TODO: remove this code when min. shop version is 5.3 diff --git a/Migrations/Migration20240919161400.php b/Migrations/Migration20240919161400.php new file mode 100644 index 0000000..24176cf --- /dev/null +++ b/Migrations/Migration20240919161400.php @@ -0,0 +1,23 @@ +execute('CREATE INDEX idx_composite_with_order ON xplugin_ws5_mollie_queue (`dDone`, `bLock`, `cType`, `dCreated` DESC);'); + } + + public function down() + { + // No need to change since 'xplugin_ws5_mollie_orders' is removed in Migration where it is created, and we don't support downgrading of Plugins + } +} \ No newline at end of file diff --git a/frontend/hooks/131.php b/frontend/hooks/131.php index f395068..3ee8b33 100644 --- a/frontend/hooks/131.php +++ b/frontend/hooks/131.php @@ -24,9 +24,11 @@ require_once __DIR__ . '/../../vendor/autoload.php'; - ifndef('MOLLIE_QUEUE_MAX', 3); - /** @noinspection PhpUndefinedConstantInspection */ - Queue::run(MOLLIE_QUEUE_MAX); + if (PluginHelper::getSetting('queue') === 'sync') { + ifndef('MOLLIE_QUEUE_MAX', 3); + /** @noinspection PhpUndefinedConstantInspection */ + Queue::runSynchronous(MOLLIE_QUEUE_MAX); + } //TODO: remove this check in next version // forces v5.1 Shop to abschlussseite @@ -83,32 +85,10 @@ } } - - - - ifndef('MOLLIE_REMINDER_PROP', 10); - if (random_int(1, MOLLIE_REMINDER_PROP) % MOLLIE_REMINDER_PROP === 0) { - /** @noinspection PhpUndefinedConstantInspection */ - $lock = new ExclusiveLock('mollie_reminder', PFAD_ROOT . PFAD_COMPILEDIR); - if ($lock->lock()) { - AbstractCheckout::sendReminders(); - Queue::storno(PluginHelper::getSetting('autoStorno')); - } - } - - // TODO: DOKU - ifndef('MOLLIE_DISABLE_USER_CLEANUP', false); - - if (!MOLLIE_DISABLE_USER_CLEANUP) { - ifndef('MOLLIE_CLEANUP_PROP', 15); - /** @noinspection PhpUndefinedConstantInspection */ - if (MOLLIE_CLEANUP_PROP && random_int(1, MOLLIE_CLEANUP_PROP) % MOLLIE_CLEANUP_PROP === 0) { - QueueModel::cleanUp(); - } - } if (array_key_exists('mollie_cleanup_cron', $_REQUEST)) { exit((string) QueueModel::cleanUp()); } + } catch (Exception $e) { Shop::Container()->getLogService()->error($e->getMessage() . " (Trace: {$e->getTraceAsString()})"); } diff --git a/frontend/template/queue.tpl b/frontend/template/queue.tpl new file mode 100644 index 0000000..15aab94 --- /dev/null +++ b/frontend/template/queue.tpl @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/info.xml b/info.xml index 2694604..36bc9ef 100644 --- a/info.xml +++ b/info.xml @@ -8,7 +8,8 @@ 5.2.0 ws5_mollie 2023-02-13 - 1.9.0 + 1.9.1 + 689388c6-9f04-4648-b516-e67d96b0dc1d hooks/131.php diff --git a/lib/CleanupCronJob.php b/lib/CleanupCronJob.php new file mode 100644 index 0000000..f046e4d --- /dev/null +++ b/lib/CleanupCronJob.php @@ -0,0 +1,32 @@ +logger->debug('Mollie Queue Cleanup'); + ifndef('MOLLIE_DISABLE_USER_CLEANUP', false); + if (!MOLLIE_DISABLE_USER_CLEANUP) { + QueueModel::cleanUp(); + } + + PluginHelper::cleanupPaymentLogs(); + + } catch (\Exception $e) { + $this->logger->debug('Mollie Queue Exception: ' . $e->getMessage()); + } + + $this->setFinished(true); + return $this; + } +} diff --git a/lib/Hook/ApplePay.php b/lib/Hook/ApplePay.php index a8d9a60..2e8afb3 100644 --- a/lib/Hook/ApplePay.php +++ b/lib/Hook/ApplePay.php @@ -24,15 +24,8 @@ public static function execute($args_arr = []): void return; } - // Reset CreditCard-Token after Order! - if ( - ($key = sprintf('kPlugin_%d_creditcard', PluginHelper::getPlugin()->getID())) - && array_key_exists($key, $_SESSION) && !array_key_exists('Zahlungsart', $_SESSION) - ) { - unset($_SESSION[$key]); - } - - if (!array_key_exists('ws_mollie_applepay_available', $_SESSION)) { + // append applepay script + if (!array_key_exists('ws_mollie_applepay_available', $_SESSION) && self::isActive()) { pq('head')->append(""); } } catch (Exception $e) { @@ -58,4 +51,23 @@ public static function setAvailable(bool $status): void { $_SESSION['ws_mollie_applepay_available'] = $status; } + + /** + * @return bool + */ + public static function isActive(): bool + { + $kZahlunsgart = PluginHelper::getDB()->executeQueryPrepared('SELECT kZahlungsart FROM tzahlungsart WHERE cModulId = :cModulId', + [ + ':cModulId' => 'kPlugin_' . PluginHelper::getPlugin()->getID() . '_applepay' + ], 1)->kZahlungsart ?? null; + if ($kZahlunsgart > 0) { + return PluginHelper::getDB()->executeQueryPrepared('SELECT * FROM tversandartzahlungsart WHERE kZahlungsart = :kZahlungsart', + [ + ':kZahlungsart' => $kZahlunsgart + ], 3) > 0; + } else { + return false; + } + } } diff --git a/lib/Hook/FrontendHook.php b/lib/Hook/FrontendHook.php new file mode 100644 index 0000000..e8ead30 --- /dev/null +++ b/lib/Hook/FrontendHook.php @@ -0,0 +1,45 @@ +getID())) + && array_key_exists($key, $_SESSION) && !array_key_exists('Zahlungsart', $_SESSION) + ) { + unset($_SESSION[$key]); + } + + // append queue script + if (PluginHelper::getSetting('queue') === 'async') { + Shop::Smarty()->assign('wsQueueURL', Shop::getURL() . '/ws5_mollie/queue'); + pq('body')->append(Shop::Smarty()->fetch(PluginHelper::getPlugin()->getPaths()->getFrontendPath() . 'template/queue.tpl', false)); + } + } catch (Exception $e) { + } + } +} diff --git a/lib/PluginHelper.php b/lib/PluginHelper.php index 3984059..3b54664 100644 --- a/lib/PluginHelper.php +++ b/lib/PluginHelper.php @@ -11,4 +11,13 @@ class PluginHelper extends AbstractPluginHelper { protected static $pluginId = 'ws5_mollie'; + + public static function cleanupPaymentLogs(): void + { + self::getDB()->executeQueryPrepared('DELETE FROM tzahlungslog WHERE cModulId LIKE :cModulId AND dDatum < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 100000;', + [ + 'cModulId' => 'kPlugin_' . self::getPlugin()->getID() . '_%' + ], + 10); + } } diff --git a/lib/Queue.php b/lib/Queue.php index b67237b..b4cdb0e 100644 --- a/lib/Queue.php +++ b/lib/Queue.php @@ -12,6 +12,7 @@ use JTL\Exceptions\CircularReferenceException; use JTL\Exceptions\ServiceNotFoundException; use JTL\Shop; +use JTL\Helpers\Request; use Mollie\Api\Types\OrderStatus; use Plugin\ws5_mollie\lib\Checkout\AbstractCheckout; use Plugin\ws5_mollie\lib\Checkout\OrderCheckout; @@ -24,13 +25,75 @@ class Queue { use Plugins; + /** * @param int $limit * @throws CircularReferenceException * @throws ServiceNotFoundException */ - public static function run(int $limit = 10): void + public static function runSynchronous(int $limit = 10): void + { + /** @var QueueModel $todo */ + foreach (self::getOpen($limit) as $todo) { + if (!self::lock($todo)) { + continue; + } + + if (([$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) { + Shop::Container()->getLogService()->notice('Mollie Queue Fehler: ' . $e->getMessage() . " ($type, $id)"); + $todo->cError = "{$e->getMessage()}\n{$e->getFile()}:{$e->getLine()}\n{$e->getTraceAsString()}"; + $todo->done(); + } + } + + self::unlock($todo); + } + + ifndef('MOLLIE_REMINDER_PROP', 10); + if (random_int(1, MOLLIE_REMINDER_PROP) % MOLLIE_REMINDER_PROP === 0) { + /** @noinspection PhpUndefinedConstantInspection */ + $lock = new ExclusiveLock('mollie_reminder', PFAD_ROOT . PFAD_COMPILEDIR); + if ($lock->lock()) { + AbstractCheckout::sendReminders(); + Queue::storno(PluginHelper::getSetting('autoStorno')); + } + } + } + + + /** + * @throws CircularReferenceException + * @throws ServiceNotFoundException + */ + public static function runAsynchronous(): void { + ifndef('MOLLIE_QUEUE_MAX', 3); + + if ($_SERVER['REQUEST_METHOD'] === 'POST' && Request::isAjaxRequest()) { + $limit = MOLLIE_QUEUE_MAX; + } else { + $response = [ + "status" => "error", + "message" => "Invalid request" + ]; + + header('Content-Type: application/json'); + echo json_encode($response); + exit; + } + /** @var QueueModel $todo */ foreach (self::getOpen($limit) as $todo) { if (!self::lock($todo)) { @@ -58,8 +121,30 @@ public static function run(int $limit = 10): void self::unlock($todo); } + + ifndef('MOLLIE_REMINDER_PROP', 10); + if (random_int(1, MOLLIE_REMINDER_PROP) % MOLLIE_REMINDER_PROP === 0) { + /** @noinspection PhpUndefinedConstantInspection */ + $lock = new ExclusiveLock('mollie_reminder', PFAD_ROOT . PFAD_COMPILEDIR); + if ($lock->lock()) { + AbstractCheckout::sendReminders(); + Queue::storno(PluginHelper::getSetting('autoStorno')); + } + } + + $response = [ + "status" => "success", + "data" => [ + "message" => "Request was successful" + ] + ]; + + header('Content-Type: application/json'); + echo json_encode($response); + exit; } + /** * @param int $limit * @return Generator @@ -79,6 +164,7 @@ private static function getOpen(int $limit): Generator } } + /** * @param QueueModel $todo * @return bool diff --git a/pluginSettings.json b/pluginSettings.json index 1f02bd6..6964c1b 100644 --- a/pluginSettings.json +++ b/pluginSettings.json @@ -286,5 +286,17 @@ {"value": "A" , "label": "Nur Authorized"}, {"value": "N" , "label": "Keine"} ] + }, + { + "id": "queue", + "label": "Abarbeitung der Queue", + "description": "Soll die Queue schrittweise bei jedem Aufruf abgearbeitet werden (wie bisher), oder über asynchrone Aufrufe via Javascript auf der Shopseite?", + "settingType": "select", + "type": "string", + "defaultValue": "sync", + "selectItems": [ + {"value": "sync" , "label": "Synchron"}, + {"value": "async" , "label": "Asynchron"} + ] } ] \ No newline at end of file