1, aby to na nasazené betě fungovalo. * Jinak v košíku neuvidíte žádné jejich platby - gopay odmítne spojení ostrého gopay serveru s testovacími údaji. * A Nedivit se, na lokale ať je cokoliv nastavené, přetluče to config_db, kde jsou testovací údaje z KOZA. * * !!!! POZOR !!!! */ class GoPay extends Payment { use PaymentOrderSubMethodTrait; public const BAD_STATE_CODE = '303'; public static $name = 'GoPay platební brána'; public static bool $canAutoReturn = true; public $template = 'payment.GoPay.tpl'; protected $templateCart = 'payment.GoPay.cart.tpl'; protected $templateOrderView = 'payment.GoPay.orderView.tpl'; public $class = 'GoPay'; public $method; protected $pay_method = Payment::METHOD_ONLINE; private $apiContext; private $gatewayUrl; public static function getSettingsConfiguration(): array { return [ 'fields' => [ 'goid' => [ 'title' => 'GoID', 'type' => 'text', ], 'clientId' => [ 'title' => 'ClientID', 'type' => 'text', ], 'clientSecret' => [ 'title' => 'ClientSecret', 'type' => 'text', ], 'test' => [ 'title' => 'Testovací režim', 'type' => 'toggle', ], ], ]; } /** Get payment icon url and name. * @return array|null */ public function getIcon() { $method = getVal($this->method, $this->getAvailableMethods()); if ($method) { return [ 'url' => $method['image'], 'name' => $method['name'], ]; } return null; } public function getPaymentUrl(int $step = 1): string { return path('kupshop_ordering_payment_payment', [ 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => $step, 'class' => $this->class, ], \Symfony\Component\Routing\Router::ABSOLUTE_URL); } public function getGatewayUrl(): string { if (!isset($this->gatewayUrl)) { $this->processStep_1(false, false); } return $this->gatewayUrl; } /* Payment steps */ public function processStep_1($enableWaitRedirect = true, $enableRedirects = true) { $apiContext = $this->getApiContext(); if (empty($this->method)) { $paymentData = $this->order->getData('payment_data'); if (!$paymentData) { $paymentData = ['method' => 'PAYMENT_CARD']; } $this->loadPaymentInfo($paymentData); } $tmpMethod = explode('__', $this->method); $defaultPaymentInstrument = $tmpMethod[0]; $defaultSwift = isset($tmpMethod[1]) ? $tmpMethod[1] : null; $availableInstruments = []; $availableSwifts = []; // test gopay (maybe live too?) might return swifts that triggers error 111 // 'BACXCZPP', 'AGBACZPP', 'AIRACZPP', 'EQBKCZPP', 'CITICZPX', 'INGBCZPP', 'EXPNCZPP', 'OBKLCZ2X', 'VBOECZ2X', 'SUBACZPP' // maybe check against $usableSwifts = array_values( // (new ReflectionClass(\GoPay\Definition\Payment\BankSwiftCode::class)) // ->getConstants() // ); foreach ($this->getAvailableMethods() as $methodID => $method) { $tmpMethod = explode('__', $methodID); if (!in_array($tmpMethod[0], $availableInstruments)) { $availableInstruments[] = $tmpMethod[0]; } if (isset($tmpMethod[1]) && !in_array($tmpMethod[1], $availableSwifts)) { $availableSwifts[] = $tmpMethod[1]; } } $rawPaymentRequest = [ 'payer' => [ 'default_payment_instrument' => $defaultPaymentInstrument, 'default_swift' => $defaultSwift, 'contact' => [ 'first_name' => $this->order->invoice_name, 'last_name' => $this->order->invoice_surname, 'email' => $this->order->invoice_email, 'phone_number' => $this->order->invoice_phone, 'city' => $this->order->invoice_city, 'street' => $this->order->invoice_street, 'postal_code' => $this->order->invoice_zip, 'country_code' => (new \League\ISO3166\ISO3166())->alpha2( !empty($this->order->invoice_country) ? $this->order->invoice_country : (!empty($this->order->delivery_country) ? $this->order->delivery_country : 'CZ') )['alpha3'], ], ], 'amount' => $amount = toDecimal($this->order->getRemainingPayment())->mul(DecimalConstants::hundred())->asFloat(), 'currency' => $this->order->getCurrency(), 'order_number' => $this->getOrderNumber(), 'items' => [], 'callback' => [ 'return_url' => $this->getPaymentUrl(2), 'notification_url' => $this->getPaymentUrl(10), ], ]; foreach ($this->order->fetchItems() as $item) { $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']; } if (isset($item['product'])) { $tmpItem['product_url'] = createScriptURL([ 'absolute' => true, 's' => 'product', 'IDproduct' => $item['id_product'], 'TITLE' => $item['product']->title, ]).(!empty($item['id_variation']) ? '#'.$item['id_variation'] : ''); } $rawPaymentRequest['items'][] = $tmpItem; } $response = $apiContext->createPayment($rawPaymentRequest); if ($response->hasSucceed()) { $this->createPayment( $response->json['id'], Decimal::fromInteger($response->json['amount']) ->mul(Decimal::fromFloat(0.01))->asFloat(), ['paymentClass' => self::class] ); $this->gatewayUrl = $response->json['gw_url']; } else { $jsonErrors = json_decode($response->rawBody, true); $errorMessage = []; foreach ($jsonErrors['errors'] ?? [] as $error) { $errorMessage[] = $error['message']; } addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'GoPay chyba: '.implode(', ', $errorMessage).' Objednávka '.$this->order->order_no, ['order' => "Kód objednávky {$this->order->order_no} (ID: {$this->order->id})", 'request' => json_encode($rawPaymentRequest), 'response' => (string) $response]); $this->step(-3, 'storno'); } } /** * GoPay return from gateway. */ public function processStep_2() { $this->checkPaymentStatus(); } /** * Waiting for approval - current payment status is created. */ public function processStep_3() { $this->checkPaymentStatus(false); $this->info(translate('payment_waiting_for_confirmation', 'payment')); } /** * Change GoPay payment method. */ public function processStep_4() { $kupshopPayment = $this->getPendingPayment(); if ($kupshopPayment) { // storno pending payment so new one can be created $this->setStatus(Payment::STATUS_STORNO, $kupshopPayment['decoded_data']->session); } $this->step(1, ''); } /** * GoPay webhook handler. */ public function processStep_10() { $this->setIsNotification(true); $this->checkPaymentStatus(); $this->sendNotificationResponse(200, 'OK'); } private function checkPaymentStatus( $enableWaitRedirect = true, $goPayPaymentID = false, $enableStepOne = false, ) { if (!$goPayPaymentID) { $goPayPaymentID = getVal('id', null, false); } if (!$goPayPaymentID) { $this->error(translate('payment_id_missing', 'payment')); } $response = $this->getApiContext()->getStatus($goPayPaymentID); if (!$response->hasSucceed()) { $this->error(translate('payment_status_check_error', 'payment')); return; } $goPayPaymentState = $response->json['state']; // determine kupshop unified payment state from GoPay payment state if (in_array($goPayPaymentState, [ 'CREATED', 'AUTHORIZED', ]) ) { $this->status = $unifiedState = Payment::STATUS_CREATED; if ($enableStepOne) { $this->gatewayUrl = $response->json['gw_url']; $this->step = 1; return; } } elseif ($goPayPaymentState === 'PAYMENT_METHOD_CHOSEN') { $this->status = $unifiedState = Payment::STATUS_PENDING; // customer might have returned from the gateway without success/failure // set gatewayUrl so it can be used in order detail $this->gatewayUrl = $response->json['gw_url']; } elseif (in_array($goPayPaymentState, ['PAID'])) { $this->status = $unifiedState = Payment::STATUS_FINISHED; } elseif (in_array($goPayPaymentState, [ 'CANCELED', 'TIMEOUTED', 'REFUNDED', 'PARTIALLY_REFUNDED', ]) ) { $this->status = $unifiedState = Payment::STATUS_STORNO; } else { logError(__FILE__, __LINE__, 'GoPay unexpected payment state "'.$goPayPaymentState.'"'); $this->error(translate('payment_unexpected_status', 'payment')); return; } // change payment status if (!$this->setStatus($unifiedState, $goPayPaymentID)) { logError(__FILE__, __LINE__, 'PayPal::updatePaymentStatus: setStatus failed!'); throw new \Exception('Set status failed'); } $paymentInstrument = $response->json['payment_instrument'] ?? null; if ($paymentInstrument && $paymentInstrument != ($response->json['payer']['default_payment_instrument'] ?? null)) { $this->setPaymentSubMethod($paymentInstrument); } 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: if ($enableWaitRedirect) { $this->step(3, 'wait', ['id' => $goPayPaymentID]); } break; } } /** * @return GoPay\Payments */ public function getApiContext() { if (!isset($this->apiContext)) { if (isset($this->config['language'])) { $language = $this->config['language']; } else { $languageContext = ServiceContainer::getService(LanguageContext::class); // see \GoPay\Definition\Language $availableLangs = [ 'cs' => 'CS', 'en' => 'EN', 'sk' => 'SK', 'de' => 'DE', 'ru' => 'RU', 'pl' => 'PL', 'hu' => 'HU', 'fr' => 'FR', 'ro' => 'RO', 'bg' => 'BG', 'hr' => 'HR', 'it' => 'IT', 'es' => 'ES', 'at' => 'DE', ]; $language = $availableLangs[$languageContext->getActiveId()] ?? \GoPay\Definition\Language::CZECH; } $apiContext = GoPay\payments([ 'goid' => $this->config['goid'], 'clientId' => $this->config['clientId'], 'clientSecret' => $this->config['clientSecret'], 'isProductionMode' => empty($this->config['test']), 'scope' => GoPay\Definition\TokenScope::ALL, 'language' => $language, 'timeout' => isset($this->config['timeout']) ? $this->config['timeout'] : 30, 'gatewayUrl' => empty($this->config['test']) ? 'https://gate.gopay.cz/api/' : 'https://gw.sandbox.gopay.com/api/', ]); $this->apiContext = $apiContext; } return $this->apiContext; } public function getAvailableMethods() { $currencyContext = ServiceContainer::getService(CurrencyContext::class); $currency = $currencyContext->getActiveId(); $domainContext = ServiceContainer::getService(DomainContext::class); $domain = $domainContext->getActiveId(); $languageContext = ServiceContainer::getService(LanguageContext::class); $language = $languageContext->getActiveId(); $cacheKey = "gopay-methods-{$currency}-{$domain}-{$language}"; if (!($methods = getCache($cacheKey))) { $methods = $this->fetchAvailableMethods($currency); setCache($cacheKey, $methods); } return $methods; } public function fetchAvailableMethods($currency) { /** * Sračka GoPay vrací rychlý převody společně s offline platbama (obyč převod, kde se jen napíše číslo účtu a vs) * To jestli je metoda rychlej převod nebo offline se rozlišuje přes "isOnline" * My zobrazujeme na eshopu jen ty isOnline=true, protože jinak tam je 15 zbytečnejch metod úplně k prdu * Metoda "Rychlý bankovní převod" je kec, jsou tam tedy i pomalý offline převody * Když nejsou povolený offline převody, můžu "Rychlý bankovní převod" smazat, není k ničemu, protože online se zobrazí samostatně * V Adminu to maj úplně dementní. Nedaj se vypínat jednotlivý metody, jen jestli je daná banka online nebo offline. * Naprosto nepoužitelnou metody GoPay která nejde vypnout se pokouším hodit dozadu, stejně tak "Offline" bankovní převody, protože jsou k ničemu. */ $apiContext = $this->getApiContext(); $response = $apiContext->getPaymentInstruments($this->config['goid'], $currency); if (!$response->hasSucceed()) { return []; } $methods = []; $offlinePayment = false; foreach ((array) $response->json['enabledPaymentInstruments'] as $method) { if (in_array($method['paymentInstrument'], $this->config['ignoreInstruments'] ?? [])) { continue; } $methods[$method['paymentInstrument']] = [ 'name' => translate_shop($method['paymentInstrument'], 'gopay', true) ?: $method['label']['cs'], 'id' => $method['paymentInstrument'], 'image' => $method['image']['large'], ]; if (empty($method['enabledSwifts']) || !is_array($method['enabledSwifts'])) { continue; } foreach ($method['enabledSwifts'] as $swift) { if ($currency === 'PLN' && $swift['swift'] === 'DNBANOKK') { continue; // skip (GoPay did not accept this swift) } if (!$swift['isOnline']) { $offlinePayment = $method['paymentInstrument']; continue; // skip offline payments, show one method instead of 15 } $methods[$method['paymentInstrument'].'__'.$swift['swift']] = [ 'name' => $swift['label']['cs'], 'id' => $method['paymentInstrument'].'__'.$swift['swift'], 'image' => $swift['image']['large'], ]; } if (!$offlinePayment) { unset($methods[$method['paymentInstrument']]); } } if ($offlinePayment) { $method = $methods[$offlinePayment]; unset($methods[$offlinePayment]); $methods[$offlinePayment] = $method; } if (isset($methods['GOPAY'])) { $method = $methods['GOPAY']; unset($methods['GOPAY']); $methods['GOPAY'] = $method; } return $methods; } public function getEmbedScriptUrl() { return $this->getApiContext()->urlToEmbedJs(); } public function hasOnlinePayment() { return true; } public static function isEnabled($className) { $cfg = Config::get(); if (empty($cfg['Modules']['payments'][$className])) { return false; } return true; } /** * @throws PaymentException */ public function doReturnPayment(array $payment, float $amount): array { $amountCents = (int) floor($amount * -100); // musí být integer v centech $paymentId = $payment['payment_data']['session']; $refundResponse = $this->getApiContext()->refundPayment($paymentId, $amountCents); $this->kibanaLogger->notice('[GoPay] doReturnPayment response', [ 'order_id' => $payment['id_order'], 'payment_id' => $paymentId, 'amountCents' => $amountCents, 'refund_response_status' => $refundResponse->statusCode, 'refund_response' => $refundResponse->__toString(), ]); if (!$refundResponse->hasSucceed()) { $message = translate('returnFailed', 'orderPayment'); $errors = $refundResponse->json['errors'] ?? []; $error = reset($errors); if ($error['error_code'] == self::BAD_STATE_CODE && $this->isPaymentRefunded($paymentId)) { $result = [ 'id' => $paymentId, 'result' => 'FINISHED', ]; } if (!isset($result)) { if (!empty($error['message'])) { $message .= translate('returnFailedMessage', 'orderPayment').$error['message']; } addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $message, ['amount' => $amount, 'payment_id' => $paymentId, 'id_order' => $payment['id_order'], 'RESPONSE' => $refundResponse]); throw new PaymentException($message); } } else { $result = (array) $refundResponse->json; } switch ($result['result']) { case 'FAILED': $message = translate('returnFailed', 'orderPayment'); addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $message, ['amount' => $amount, 'payment_id' => $paymentId, 'id_order' => $payment['id_order'], 'RESPONSE' => $refundResponse]); throw new PaymentException($message); case 'ACCEPT': case 'FINISHED': return $result; } return $result; } /** * Check paid orders (and update orders.status_payed). */ public function checkPaidOrders() { if (getVal('test', $this->config)) { 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'] !== 'GoPay' || empty($paymentData['session'])) { continue; } // https://doc.gopay.com/cs/?lang=php#stav-platby $response = $context->getStatus($paymentData['session']); if (!$response->hasSucceed() || empty($response->json['state'])) { continue; } if ($response->json['state'] === '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 ($response->json['state'] === 'CANCELED' || $response->json['state'] === 'TIMEOUTED') { $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); } } // print_r(['id' => $paymentData['session'], 'state' => $response->json['state']]); } return 0; } public function isPaymentRefunded($paymentId): bool { $statusResponse = $this->getApiContext()->getStatus($paymentId); if (!$statusResponse->hasSucceed()) { return false; } $status = $statusResponse->json; return $status['state'] == 'REFUNDED'; } }