From a734cba80a37ac82856bf60b2383c8c1a9ad237e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20G=C3=B6rtz?= Date: Wed, 12 Jun 2019 16:57:39 +0200 Subject: [PATCH 1/3] Change the flow of the payment with QuickPay A generated orderId and the QuickPay paymentId are now used as Shopware transactionId and paymentUniqueId. Writing the order to the database was moved to the successAction to prevent premature ending of the the ordering process. Once a payment id is received from Quickpay it is stored in the session until the order is finished. Identification of the order in the callbackAction is accordingly done by using the order_id and id of the received payment object. --- Components/QuickPayService.php | 70 ++++++++++++++++++------ Controllers/Frontend/QuickPay.php | 88 +++++++++++++++++++------------ 2 files changed, 108 insertions(+), 50 deletions(-) diff --git a/Components/QuickPayService.php b/Components/QuickPayService.php index ea4ef07..eb04193 100644 --- a/Components/QuickPayService.php +++ b/Components/QuickPayService.php @@ -2,6 +2,10 @@ namespace QuickPayPayment\Components; +use Exception; +use Shopware\Components\Random; +use function Shopware; + class QuickPayService { private $baseUrl = 'https://api.quickpay.net'; @@ -9,22 +13,58 @@ class QuickPayService const METHOD_POST = 'POST'; const METHOD_PUT = 'PUT'; const METHOD_GET = 'GET'; + const METHOD_PATCH = 'PATCH'; /** * Create payment * * @param $orderId - * @param $currency + * @param $parameters * @return mixed */ - public function createPayment($parameters) + public function createPayment($orderId, $parameters) { + $parameters['order_id'] = $orderId; + //Create payment $payment = $this->request(self::METHOD_POST, '/payments', $parameters); return $payment; } + /** + * Create payment + * + * @param $paymentId + * @param $parameters + * @return mixed + */ + public function updatePayment($paymentId, $parameters) + { + $resource = sprintf('/payments/%s', $paymentId); + + //Update payment + $payment = $this->request(self::METHOD_PATCH, $resource, $parameters); + + return $payment; + } + + /** + * Get payment information + * + * @param $paymentId + * @return mixed + */ + public function getPayment($paymentId) + { + $resource = sprintf('/payments/%s', $paymentId); + + //Get payment + $payment = $this->request(self::METHOD_GET, $resource); + + return $payment; + } + /** * Create payment link * @@ -87,14 +127,14 @@ private function request($method = self::METHOD_POST, $resource, $params = [], $ //Validate reponsecode if (! in_array($responseCode, [200, 201, 202])) { - throw new \Exception('Invalid gateway response ' . $result); + throw new Exception('Invalid gateway response ' . $result); } $response = json_decode($result); //Check for JSON errors if (! $response || (json_last_error() !== JSON_ERROR_NONE)) { - throw new \Exception('Invalid json response'); + throw new Exception('Invalid json response'); } return $response; @@ -124,18 +164,6 @@ private function getApiKey() return Shopware()->Config()->getByNamespace('QuickPayPayment', 'public_key'); } - /** - * Create payment token - * - * @param float $amount - * @param int $customerId - * @return string - */ - public function createPaymentToken($amount, $customerId) - { - return md5(implode('|', [$amount, $customerId])); - } - /** * Get language code * @@ -147,4 +175,14 @@ private function getLanguageCode() return substr($locale, 0, 2); } + + /** + * Creates a unique order id + * + * @return string + */ + public function createOrderId() + { + return Random::getAlphanumericString(20); + } } diff --git a/Controllers/Frontend/QuickPay.php b/Controllers/Frontend/QuickPay.php index 4ecd77d..76335d0 100644 --- a/Controllers/Frontend/QuickPay.php +++ b/Controllers/Frontend/QuickPay.php @@ -12,31 +12,28 @@ public function redirectAction() $service = $this->container->get('quickpay_payment.quickpay_service'); try { - $user = $this->getUser(); - $billing = $user['billingaddress']; - - $paymentId = $this->createPaymentUniqueId(); - $token = $service->createPaymentToken($this->getAmount(), $billing['customernumber']); - - //Save order and grab ordernumber - $orderNumber = $this->saveOrder($paymentId, $token, \Shopware\Models\Order\Status::PAYMENT_STATE_OPEN); - - //Save orderNumber to session - Shopware()->Session()->offsetSet('quickpay_order_id', $orderNumber); - Shopware()->Session()->offsetSet('quickpay_order_token', $token); - $paymentParameters = [ - 'order_id' => $orderNumber, 'currency' => $this->getCurrencyShortName(), - 'variables' => [ - 'payment_id' => $paymentId, - 'token' => $token - ], ]; - - //Create payment - $payment = $service->createPayment($paymentParameters); - + + //Save order and grab ordernumber + $paymentId = Shopware()->Session()->offsetGet('quickpay_payment_id'); + if(empty($paymentId)) + { + //Create new QuickPay payment + $orderId = $service->createOrderId(); + + $payment = $service->createPayment($orderId, $paymentParameters); + } + else + { + //Update existing QuickPay payment + $payment = $service->updatePayment($paymentId, $paymentParameters); + } + + // Save ID to session + Shopware()->Session()->offsetSet('quickpay_payment_id', $payment->id); + $user = $this->getUser(); $email = $user['additional']['user']['email']; @@ -49,14 +46,7 @@ public function redirectAction() $this->getCancelUrl(), $this->getCallbackUrl() ); - - $repository = Shopware()->Models()->getRepository(\Shopware\Models\Order\Order::class); - $order = $repository->findOneBy(array( - 'number' => $orderNumber - )); - $order->getAttribute()->setQuickpayPaymentLink($paymentLink); - Shopware()->Models()->flush($order->getAttribute()); - + $this->redirect($paymentLink); } catch (\Exception $e) { die($e->getMessage()); @@ -90,17 +80,17 @@ public function callbackAction() if (!$testmode && ($response->test_mode === true)) { //Set order as cancelled - $this->savePaymentStatus($response->variables->payment_id, $response->variables->token, \Shopware\Models\Order\Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED); + $this->savePaymentStatus($response->order_id, $response->id, \Shopware\Models\Order\Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED); Shopware()->PluginLogger()->info("Order attempted paid with testcard while testmode was disabled"); return; } //Set order as reserved - $this->savePaymentStatus($response->variables->payment_id, $response->variables->token, \Shopware\Models\Order\Status::PAYMENT_STATE_RESERVED); + $this->savePaymentStatus($response->order_id, $response->id, \Shopware\Models\Order\Status::PAYMENT_STATE_RESERVED); } } else { //Cancel order - $this->savePaymentStatus($response->variables->payment_id, $response->variables->token, \Shopware\Models\Order\Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED); + $this->savePaymentStatus($response->order_id, $response->id, \Shopware\Models\Order\Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED); Shopware()->PluginLogger()->info('Checksum mismatch'); } } @@ -111,6 +101,36 @@ public function callbackAction() */ public function successAction() { + /** @var \QuickPayPayment\Components\QuickPayService $service */ + $service = $this->container->get('quickpay_payment.quickpay_service'); + + $paymentId = Shopware()->Session()->offsetGet('quickpay_payment_id'); + + if(empty($paymentId)) + { + $this->redirect(['controller' => 'checkout', 'action' => 'confirm']); + return; + } + + $payment = $service->getPayment($paymentId); + if(empty($payment) || !isset($payment->order_id)) + { + $this->redirect(['controller' => 'checkout', 'action' => 'confirm']); + return; + } + + $orderNumber = $this->saveOrder($payment->order_id, $payment->id, \Shopware\Models\Order\Status::PAYMENT_STATE_OPEN); + + $repository = Shopware()->Models()->getRepository(\Shopware\Models\Order\Order::class); + $order = $repository->findOneBy(array( + 'number' => $orderNumber + )); + $order->getAttribute()->setQuickpayPaymentLink($payment->link->url); + Shopware()->Models()->flush($order->getAttribute()); + + //Remove ID from session + Shopware()->Session()->offsetUnset('quickpay_payment_id'); + //Redirect to finish $this->redirect(['controller' => 'checkout', 'action' => 'finish']); @@ -122,7 +142,7 @@ public function successAction() */ public function cancelAction() { - $this->redirect(['controller' => 'checkout', 'action' => 'cancel']); + $this->redirect(['controller' => 'checkout', 'action' => 'confirm']); } /** From ed2ef125c211c0dbfc6bbbe74e6561fa2abbca98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20G=C3=B6rtz?= Date: Mon, 17 Jun 2019 12:11:47 +0200 Subject: [PATCH 2/3] Added attribute creation to update function attribute fields for shopware tables are now created/updated when updating the plugin not only during installation. --- QuickPayPayment.php | 68 ++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/QuickPayPayment.php b/QuickPayPayment.php index 58a88bc..c72ddc5 100644 --- a/QuickPayPayment.php +++ b/QuickPayPayment.php @@ -6,6 +6,7 @@ use Shopware\Components\Plugin\Context\UninstallContext; use Shopware\Components\Plugin\Context\ActivateContext; use Shopware\Components\Plugin\Context\DeactivateContext; +use Shopware\Components\Plugin\Context\UpdateContext; use Shopware\Models\Payment\Payment; class QuickPayPayment extends Plugin @@ -33,19 +34,21 @@ public function install(InstallContext $context) ]; $installer->createOrUpdate($context->getPlugin(), $options); - - $crud = $this->container->get('shopware_attribute.crud_service'); - $crud->update('s_order_attributes', 'quickpay_payment_link', 'string', array( - 'displayInBackend' => true, - 'label' => 'QuickPay payment link' - ), null, false, 'NULL'); - - Shopware()->Models()->generateAttributeModels( - array('s_order_attributes') - ); - + + $this->createAttributes(); } + /** + * Update plugin + * + * @param UpdateContext $context + */ + public function update(UpdateContext $context) + { + $this->createAttributes(); + + } + /** * Uninstall plugin * @@ -55,14 +58,7 @@ public function uninstall(UninstallContext $context) { $this->setActiveFlag($context->getPlugin()->getPayments(), false); - $crud = $this->container->get('shopware_attribute.crud_service'); - try { - $crud->delete('s_order_attributes', 'quickpay_payment_link'); - } catch (\Exception $e) { - } - Shopware()->Models()->generateAttributeModels( - array('s_order_attributes') - ); + $this->removeAttributes(); } /** @@ -98,4 +94,38 @@ private function setActiveFlag($payments, $active) } $em->flush(); } + + /** + * Create or update all Attributes + * + */ + private function createAttributes() + { + + $crud = $this->container->get('shopware_attribute.crud_service'); + $crud->update('s_order_attributes', 'quickpay_payment_link', 'string', array( + 'displayInBackend' => true, + 'label' => 'QuickPay payment link' + ), null, false, 'NULL'); + + Shopware()->Models()->generateAttributeModels( + array('s_order_attributes') + ); + + } + + /** + * Remove all attributes + */ + private function removeAttributes() + { + $crud = $this->container->get('shopware_attribute.crud_service'); + try { + $crud->delete('s_order_attributes', 'quickpay_payment_link'); + } catch (\Exception $e) { + } + Shopware()->Models()->generateAttributeModels( + array('s_order_attributes') + ); + } } \ No newline at end of file From 0ef2339ee9f4c1731f67edf40278c57013c97f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20G=C3=B6rtz?= Date: Mon, 17 Jun 2019 13:47:28 +0200 Subject: [PATCH 3/3] Check payment acceptance when finishing order the accepted status of the QuickPay payment is checked when finishing the ordering process and the order state is set accordingly. This prevents a race condition between the callback and success action which could lead to the order state remaining OPEN instead of RESERVED --- Controllers/Frontend/QuickPay.php | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/Controllers/Frontend/QuickPay.php b/Controllers/Frontend/QuickPay.php index 76335d0..49218c1 100644 --- a/Controllers/Frontend/QuickPay.php +++ b/Controllers/Frontend/QuickPay.php @@ -73,11 +73,8 @@ public function callbackAction() //Check if payment is accepted if ($response->accepted === true) { - //Check is test mode is enabled - $testmode = Shopware()->Config()->getByNamespace('QuickPayPayment', 'testmode'); - //Cancel order if testmode is disabled and payment is test mode - if (!$testmode && ($response->test_mode === true)) { + if (!$this->checkTestMode($response)) { //Set order as cancelled $this->savePaymentStatus($response->order_id, $response->id, \Shopware\Models\Order\Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED); @@ -119,7 +116,13 @@ public function successAction() return; } - $orderNumber = $this->saveOrder($payment->order_id, $payment->id, \Shopware\Models\Order\Status::PAYMENT_STATE_OPEN); + $state = \Shopware\Models\Order\Status::PAYMENT_STATE_OPEN; + if($payment->accepted && $this->checkTestMode($payment)) + { + $state = \Shopware\Models\Order\Status::PAYMENT_STATE_RESERVED; + } + + $orderNumber = $this->saveOrder($payment->order_id, $payment->id, $state); $repository = Shopware()->Models()->getRepository(\Shopware\Models\Order\Order::class); $order = $repository->findOneBy(array( @@ -195,4 +198,24 @@ private function getCallbackUrl() public function getWhitelistedCSRFActions() { return ['callback']; } + + /** + * Check if the text_mode property of the payment matches the shop configuration + * + * @param mixed $payment + * @return boolean + */ + private function checkTestMode($payment) + { + //Check is test mode is enabled + $testmode = Shopware()->Config()->getByNamespace('QuickPayPayment', 'testmode'); + + //Check if test_mode property matches the configuration + if (!$testmode && ($payment->test_mode === true)) { + + return false; + } + + return true; + } } \ No newline at end of file