'payment', 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => 1, 'class' => $this->class, 'absolute' => true, ]); } protected function getSpreadedInstalments(): bool { return false; } public function processStep_1() { $proposalData = $this->getProposalData(); $redirectData = $this->getProposalRedirect($proposalData); $contractId = $redirectData['contractId']; $this->createPayment($contractId, $proposalData['price'], ['paymentClass' => $this->class]); redirection($redirectData['redirectionUrl']); } /** Return from gateway */ public function processStep_2() { $this->info(translate('payment_waiting_for_confirmation', 'payment')); } public function checkPaidOrders() { $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))') ->andWhere(Operator::inStringArray([$this->class], JsonOperator::value('op.payment_data', 'paymentClass'))) ->andWhere(JsonOperator::exists('op.payment_data', 'session')) ->execute(); foreach ($orderPayments as $orderPayment) { $paymentData = json_decode($orderPayment['payment_data'], true); if (empty($paymentData['session'])) { continue; } $this->setOrder($orderPayment['id_order']); $contractId = (int) $paymentData['session']; $response = $this->getPaymentStatus($contractId); if (!empty($response['errorCollection'])) { foreach ($response['errorCollection'] as $error) { addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, translate('returnFailedMessage', 'orderPayment'), $error); } } foreach ($response['businessCases'] as $businessCase) { $businessCaseStatus = (int) $businessCase['contractStatusId']; switch ($businessCaseStatus) { case 1: // Zakaz. opustil před odesláním case 2: // Čeká na posouzení case 3: // Posuzuje se case 4: // Odložen case 18: // Čeká na nahrání dokumentů case 19: // Čeká na platbu case 9: case 13: // Ke kontrole 9,13 $this->setStatus(self::STATUS_PENDING, $contractId); break; case 5: case 10: case 11: // Reklamace 5,10,11 case 7: // Zamítnuto / Neschválený návrh $this->setStatus(self::STATUS_STORNO, $contractId); break; case 12: // V pořádku doručeno do ESSOXu proplaceno / Zkontrolováno $newStatus = ($this->config['setPaymentCompletedManually'] ?? false) ? self::STATUS_PENDING : self::STATUS_FINISHED; $this->setStatus($newStatus, $contractId); break; default: $this->setStatus(self::STATUS_UNKNOWN, $contractId); } } } } protected function getPaymentStatus(int $contractId) { $response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/status?ContractId='.$contractId, [ 'Accept: application/json', 'Authorization: Bearer '.$this->getAccessToken(), ], '', false); return json_decode($response, true); } protected function getAccessToken() { $loginHash = md5(($this->config['clientKey'] ?? '').':'.($this->config['clientSecret'] ?? '').':'.($this->config['test'] ?? '')); $tokenCacheKey = 'payment_essox_access_token'.$loginHash; $token = getCache($tokenCacheKey); if (!$token) { $retrievedToken = $this->retrieveAccessToken(); $token = $retrievedToken['access_token']; setCache($tokenCacheKey, $token, $retrievedToken['expires_in'] - 60); } return $token; } protected function retrieveAccessToken(): array { $data = [ 'grant_type' => 'client_credentials', 'scope' => 'scopeFinit.consumerGoods.eshop', ]; $response = $this->requestEssoxCurl('/token', [ 'Content-Type: application/x-www-form-urlencoded', 'accept: application/json', 'Authorization: Basic '.base64_encode($this->config['clientKey'].':'.$this->config['clientSecret']), ], http_build_query($data)); $decodedData = json_decode($response, true); return ['access_token' => (string) $decodedData['access_token'], 'expires_in' => (int) $decodedData['expires_in']]; } public function getProposalData(): array { $phoneNumber = $this->order->invoice_phone; $checkPhonePrefixes = ['+420']; $phonePrefix = ''; foreach ($checkPhonePrefixes as $checkPrefix) { if (StringUtil::startsWith($phoneNumber, $checkPrefix)) { $phoneNumber = str_replace($checkPrefix, '', $phoneNumber); $phonePrefix = $checkPrefix; break; } } return [ 'firstName' => $this->order->invoice_name, 'surname' => $this->order->invoice_surname, 'mobilePhonePrefix' => $phonePrefix, 'mobilePhoneNumber' => $phoneNumber, 'email' => $this->order->invoice_email, 'price' => $this->order->getTotalPrice()->getPriceWithVat()->asFloat(), 'orderId' => $this->order->order_no, 'customerId' => $this->order->id_user, 'transactionId' => 1, 'shippingAddress' => [ 'street' => $this->order->delivery_street, 'houseNumber' => '', 'city' => $this->order->delivery_city, 'zip' => $this->order->delivery_zip, ], 'callbackUrl' => $this->getGenericPaymentUrl(2), 'spreadedInstalments' => $this->getSpreadedInstalments(), ]; } protected function getProposalRedirect($proposalData) { $response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/proposal', [ 'Content-Type: application/json', 'Authorization: Bearer '.$this->getAccessToken(), ], json_encode($proposalData)); return json_decode($response, true); } public function getEssoxCalcDetail(Decimal $price) { $price = roundPrice($price, -1, 'DB', 0)->asInteger(); if (!($this->config['productDetail'] ?? false) || $price < ($this->config['minPrice'] ?? 2000) || (!empty($this->config['maxPrice']) && $price > $this->config['maxPrice'])) { return null; } return path('kupshop_ordering_payment_legacypayment', [ 'step' => 5, 'class' => $this->class, 'price' => $price, ]); } public function processStep_5() { $price = roundPrice(getVal('price'), -1, 'DB', 0)->asInteger(); if ($price < ($this->config['minPrice'] ?? 2000) || (!empty($this->config['maxPrice']) && $price > $this->config['maxPrice'])) { return null; } redirection($this->getCalculatorUrl($price)); } protected function getCalculatorUrl($price) { $body = [ 'price' => $price, 'productId' => 0, ]; $response = $this->requestEssoxCurl('/consumergoods/v1/api/consumergoods/calculator', [ 'Content-Type: application/json', 'accept: application/json', 'Authorization: Bearer '.$this->getAccessToken(), ], json_encode($body)); $decodedData = json_decode($response, true); return $decodedData['redirectionUrl']; } protected function requestEssoxCurl(string $path, array $headers, string $encodedBody, $post = true) { $url = $this->getApiUrl().$path; $initTimeout = 30; $step = 1; $ch = curl_init(); if ($post) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedBody); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); while ($step <= 2) { curl_setopt($ch, CURLOPT_TIMEOUT, $initTimeout); $response = curl_exec($ch); $responseInfo = curl_getinfo($ch); $curl_errno = curl_errno($ch); curl_close($ch); if ($responseInfo['http_code'] === 0 && $step === 1) { $initTimeout = 10; $step++; continue; } if ($responseInfo['http_code'] != 200) { $debugInfo = ['url' => $url, 'responseInfo' => $responseInfo, 'curl_errno' => $curl_errno, 'RESPONSE' => $response]; $this->handleError($debugInfo); throw new PaymentException('Komunikace s platební bránou selhala. Zkuste prosím znovu později.'); } break; } return $response; } public function accept($totalPrice, $freeDelivery) { $price = $totalPrice->getPriceWithVat()->asFloat(); if ($price <= 0 && $this->order) { $price = $this->order->total_price; } // price has to be greater than 2000 Kč according to the documentation return parent::accept($totalPrice, $freeDelivery) && $price >= 2000; } public function hasOnlinePayment() { return true; } protected function getApiUrl(): string { return ($this->config['test'] ?? false) ? $this->apiTestUrl : $this->apiUrl; } public static function isEnabled($className) { $cfg = Config::get(); if (empty($cfg['Modules']['payments'][$className])) { return false; } return true; } public static function getSettingsConfiguration(): array { return [ 'fields' => [ 'clientKey' => [ 'title' => 'ID klienta (client_id)', 'type' => 'text', ], 'clientSecret' => [ 'title' => 'Secret (clientSecret)', 'type' => 'text', ], 'setPaymentCompletedManually' => [ 'title' => 'Manuální potvrzení platby', 'tooltip' => 'Pokud je vypnuto, platba objednávky se nastaví jako uhrazená automaticky po schválení úvěru. Pokud je zapnuto, přepnutí platby na uhrazenou je potřeba udělat manuálně.', 'type' => 'toggle', ], 'minPrice' => [ 'title' => 'Minimální částka', 'type' => 'number', 'placeholder' => 2000, 'tooltip' => 'Minimální částka u které tuto možnost zobrazit na detailu produktu. (min. 2000 Kč)', ], 'maxPrice' => [ 'title' => 'Maximální částka', 'type' => 'number', 'tooltip' => 'Maximální částka u které tuto možnost zobrazit na detailu produktu.', ], 'productDetail' => [ 'title' => 'Zobrazit na detailu produktu', 'type' => 'toggle', ], 'test' => [ 'title' => 'Testovací režim', 'type' => 'toggle', ], ], ]; } protected function handleError($debugInfo) { $message = $this->getName().': admin info - Komunikace s platební bránou selhala'; $sentry = false; $csobFault = false; switch ($debugInfo['responseInfo']['http_code'] ?? -1) { case 400: $reason = 'Nevalidní request. V dotazu chybí povinné pole nebo je v nevhodném / nevalidním formátu. Nevalidní uživatel. Neplatné pověření. Uživatel není oprávněný používat autorizační typ'; $sentry = true; break; case 401: unset($debugInfo['responseInfo']); $reason = 'Přístup odepřen (špatné přihlašovací údaje)'; $message .= ' - '.$reason; break; case 403: $reason = 'Klient není oprávněný provádět tento dotaz.'; $sentry = true; break; case 500: $reason = 'Chyba na straně CSOB serveru'; $csobFault = true; break; case 503: $reason = 'CSOB služba je dočasně nedostupná. Může být způsobena přetížením serveru nebo z důvodu údržby'; $csobFault = true; break; case 0: $reason = 'CSOB API timeout'; $csobFault = true; break; } $debugInfo = array_merge(['reason' => $reason ?? 'Neznámá chyba'], $debugInfo); if ($sentry) { getRaven()->captureMessage('Essox API error', [], ['extra' => $debugInfo]); } if ($csobFault) { $message .= ' - chyba na straně CSOB'; } addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $message, $debugInfo); } }