method, $this->getAvailableMethods()); if ($method) { return [ 'url' => $method['image'], 'name' => $method['name'], ]; } return null; } public function getGatewayUrl(): string { if (!isset($this->gatewayUrl)) { $this->processStep_1(false); } return $this->gatewayUrl; } public function processStep_1($redirectToGateway = true) { $apiContext = $this->getApiContext(); if (empty($this->method)) { $paymentData = $this->order->getData('payment_data'); if ($paymentData) { $this->loadPaymentInfo($paymentData); } } $paymentUid = uniqid($this->order->id.'_', true); $currencyContext = Contexts::get(CurrencyContext::class); $params = new \ThePay\ApiClient\Model\CreatePaymentParams( $amount = toDecimal($this->order->getRemainingPayment()) ->mul(DecimalConstants::hundred())->asInteger(), $currencyContext->getActiveId(), $paymentUid ); $params->setReturnUrl($this->getGenericPaymentUrl(2)); $params->setNotifUrl($this->getGenericPaymentUrl(10)); $params->setOrderId($this->order->order_no); // $params->setDescriptionForCustomer($this->order->order_no); // $params->setDescriptionForMerchant($this->order->order_no); $params->setCustomer(new CreatePaymentCustomer( $this->order->invoice_name, $this->order->invoice_surname, $this->order->invoice_email, $this->order->invoice_phone, ($this->order->invoice_country != '' && $this->order->invoice_city != '' && $this->order->invoice_zip != '' && $this->order->invoice_street != '') ? new \ThePay\ApiClient\Model\Address( $this->order->invoice_country, $this->order->invoice_city, $this->order->invoice_zip, $this->order->invoice_street ) : null )); foreach ($this->order->fetchItems() as $item) { if ($item['pieces'] < 1) { continue; } $tmpItem = [ 'type' => is_null($item['id_product']) ? 'delivery' : 'item', 'name' => $item['descr'], 'amount' => $item['total_price']['value_with_vat']->mul(DecimalConstants::hundred())->asInteger(), 'count' => (int) $item['pieces'], ]; if (isset($item['ean']) && EANValidator::checkEAN($item['ean'])) { $tmpItem['ean'] = $item['ean']; } $params->addItem( new CreatePaymentItem($tmpItem['type'], $tmpItem['name'], $tmpItem['amount'], $tmpItem['count'], $tmpItem['ean'] ?? null) ); } // check PaymentMethod beforehand: this should eliminate error "21 in not valid value" // when the order was created with the old ThePay and then the eshop switched to ThePay20 try { $tmp = new \ThePay\ApiClient\ValueObject\PaymentMethodCode($this->method); $method = $this->method; } catch (InvalidArgumentException $e) { $method = null; } $response = $apiContext->createPayment($params, $method); $this->gatewayUrl = $response->getPayUrl(); $this->createPayment( $paymentUid, Decimal::fromInteger($amount)->mul(Decimal::fromFloat(0.01))->asFloat(), ['paymentClass' => self::class] ); if ($redirectToGateway) { redirection($this->gatewayUrl); } } /** Return from gateway */ public function processStep_2() { $this->checkPaymentStatus(); } /** Webhook handler */ public function processStep_10() { $this->setIsNotification(true); $this->checkPaymentStatus(); $this->sendNotificationResponse(200, 'OK'); } private function checkPaymentStatus() { $paymentID = getVal('payment_uid', null, false); if (!$paymentID) { $this->error(translate('payment_id_missing', 'payment')); } $response = $this->getApiContext()->getPayment($paymentID); $thirdPartyPaymentState = $response->getState(); // https://dataapi21.docs.apiary.io/#paymentStatesEnum // determine kupshop unified payment state from third party payment state if (in_array($thirdPartyPaymentState, ['waiting_for_payment', 'waiting_for_confirmation'])) { $this->status = $unifiedState = Payment::STATUS_CREATED; } elseif (in_array($thirdPartyPaymentState, ['paid'])) { $this->status = $unifiedState = Payment::STATUS_FINISHED; } elseif (in_array($thirdPartyPaymentState, [ 'expired', 'preauth_cancelled', 'preauth_expired', 'partially_refunded', ]) ) { $this->status = $unifiedState = Payment::STATUS_STORNO; } else { logError(__FILE__, __LINE__, 'ThePay20 unexpected payment state "'.$thirdPartyPaymentState.'"'); $this->error(translate('payment_unexpected_status', 'payment')); } // change payment status if (!$this->setStatus($unifiedState, $paymentID)) { logError(__FILE__, __LINE__, 'ThePay20::updatePaymentStatus: setStatus failed!'); throw new \Exception('Set status failed'); } $this->setPaymentSubMethod($response->getPaymentMethod()); switch ($unifiedState) { case Payment::STATUS_FINISHED: $this->success(translate('paymentSuccess', 'payment')); break; case Payment::STATUS_STORNO: $this->info(translate('payment_storno', 'payment')); break; case Payment::STATUS_PENDING: case Payment::STATUS_CREATED: $this->info(translate('payment_waiting_for_confirmation', 'payment')); break; } } public function getApiContext(): ThePay\ApiClient\TheClient { if (!isset($this->apiContext)) { $languageContext = Contexts::get(LanguageContext::class); $apiContext = new \ThePay\ApiClient\TheClient(new \ThePay\ApiClient\TheConfig( $this->config['merchantId'], $this->config['projectId'], $this->config['apiPassword'], !empty($this->config['test']) ? 'https://demo.api.thepay.cz/' : 'https://api.thepay.cz/', !empty($this->config['test']) ? 'https://demo.gate.thepay.cz/' : 'https://gate.thepay.cz/', $this->getPreferredLanguage($this->config['language'] ?? $languageContext->getActiveId()) )); $this->apiContext = $apiContext; } return $this->apiContext; } public function getAvailableMethods() { $currencyContext = Contexts::get(CurrencyContext::class); $currency = $currencyContext->getActiveId(); $domainContext = Contexts::get(DomainContext::class); $domain = $domainContext->getActiveId(); $languageContext = Contexts::get(LanguageContext::class); $language = $languageContext->getActiveId(); $cacheKey = "thepay20-methods-{$currency}-{$domain}-{$language}"; $methods = getCache($cacheKey); if (!isset($methods) || $methods === false || !is_array($methods)) { $methods = $this->fetchAvailableMethods($currency); setCache($cacheKey, $methods); } return $methods; } public function fetchAvailableMethods($currency) { // check config presence if (empty($this->config['merchantId']) || empty($this->config['projectId']) || empty($this->config['apiPassword']) ) { addActivityLog( ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'ThePay: Chybí údaje pro požadovaný jazyk v nastavení eshopu, případně využijte omezení platby/dopravy, aby se způsob doručení v tomto jazyce nenabízel.', [ ...ActivityLog::addObjectData([$this->getId() => $this->getName()], 'deliveryPayment'), ...['context-languageID' => Contexts::get(LanguageContext::class)->getActiveId()], ] ); return []; } $overwriteMethodNames = $this->config['overwriteMethodNames'] ?? []; $methods = []; try { $preferredLang = $this->getPreferredLanguage( Contexts::get(LanguageContext::class)->getActiveId() ); $overrideNamePerLang = [ 'de' => ['card' => 'Kartenzahlung'], // preferred lang falls back to 'de' for 'at' too 'pl' => ['card' => 'Płatność kartą'], ]; foreach ($this->getApiContext()->getActivePaymentMethods(new PaymentMethodFilter([$currency], [], []), new LanguageCode($preferredLang)) as $method) { $methods[$method->getCode()] = [ 'name' => $overwriteMethodNames[$method->getCode()] ?? $overrideNamePerLang[$preferredLang][$method->getCode()] ?? $method->getTitle(), 'id' => $method->getCode(), 'image' => $method->getImageUrl()->getValue(), ]; } } catch (ApiException|MissingExtensionException|\RuntimeException $e) { addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'Chyba ThePay při načítání platebních metod: '.$e->getMessage()); $sentry = getRaven(); $sentry->captureException($e); } return $methods; } public function hasOnlinePayment() { return true; } public function doReturnPayment(array $payment, float $amount) { $amountCents = (int) floor($amount * -100); // musí být integer v centech $full = (($payment['price'] + $amount) == 0); try { $this->getApiContext()->createPaymentRefund($payment['payment_data']['session'], $amountCents, 'vrácení '.(($full) ? 'platby' : 'přeplatku')); $result = ['id' => $payment['payment_data']['session']]; return $result; } catch (ApiException|InvalidArgumentException|RuntimeException $e) { $message = translate('returnFailed', 'orderPayment'); $message .= translate('returnFailedMessage', 'orderPayment').$e->getMessage(); throw new PaymentException($message); } return true; } /** Check paid orders */ public function checkPaidOrders() { if (getVal('test', $this->config)) { return false; } if (empty($this->config['merchantId']) || empty($this->config['projectId']) || empty($this->config['apiPassword'])) { return false; } $context = $this->getApiContext(); $orderPayments = sqlQueryBuilder()->select('op.id, op.id_order, op.payment_data') ->from('order_payments', 'op') ->where(\Query\Operator::inIntArray([ static::STATUS_CREATED, static::STATUS_PENDING, static::STATUS_UNKNOWN, ], 'op.status')) ->andWhere('op.date > (DATE_SUB(CURDATE(), INTERVAL 1 MONTH))') ->execute(); foreach ($orderPayments as $orderPayment) { $paymentData = json_decode($orderPayment['payment_data'], true); if ($paymentData['paymentClass'] !== 'ThePay20' || empty($paymentData['session'])) { continue; } $thirdPartyPaymentState = false; try { $response = $context->getPayment($paymentData['session']); $thirdPartyPaymentState = $response->getState(); // https://dataapi21.docs.apiary.io/#paymentStatesEnum } catch (ApiException|InvalidArgumentException $e) { addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'ThePay error: '.$e->getMessage()); continue; } if ($thirdPartyPaymentState === 'paid') { $this->setOrder($orderPayment['id_order']); try { $this->setStatus(Payment::STATUS_FINISHED, $paymentData['session']); } catch (\KupShop\KupShopBundle\Exception\RedirectException $e) { throw $e; } catch (Exception $e) { getRaven()->captureException($e); } } elseif ($thirdPartyPaymentState === 'expired' || $thirdPartyPaymentState === 'partially_refunded' || $thirdPartyPaymentState === 'preauth_cancelled') { $this->setOrder($orderPayment['id_order']); try { $this->setStatus(Payment::STATUS_STORNO, $paymentData['session']); } catch (\KupShop\KupShopBundle\Exception\RedirectException $e) { throw $e; } catch (Exception $e) { getRaven()->captureException($e); } } } return 0; } private function getPreferredLanguage(string $language): string { $thePaySupportedLanguages = [ 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az', 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce', 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee', 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr', 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr', 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ii', 'ik', 'io', 'is', 'it', 'iu', 'ja', 'jv', 'ka', 'kg', 'ki', 'kj', 'kk', 'kl', 'km', 'kn', 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la', 'lb', 'lg', 'li', 'ln', 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk', 'ml', 'mn', 'mo', 'mr', 'ms', 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng', 'nl', 'nn', 'no', 'nr', 'nv', 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa', 'pi', 'pl', 'ps', 'pt', 'qu', 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 'ty', 'ug', 'uk', 'ur', 'uz', 've', 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', 'yo', 'za', 'zh', 'zu', ]; $preferredLang = $language; if (!in_array($preferredLang, $thePaySupportedLanguages)) { if ($preferredLang === 'at') { $preferredLang = 'de'; // use german language for at - austria } else { $preferredLang = 'en'; // if not supported fallback to english } } return $preferredLang; } }