id ? (int) $this->id : null; } public function setIsNotification(bool $isNotification): Payment { $this->isNotification = $isNotification; return $this; } public function setDatabaseName($databaseName) { $this->databaseName = $databaseName; } public function hasCartTitle() { return (bool) $this->templateCart; } public function hasCartTemplate() { return (bool) $this->templateCart; } public function getGenericPaymentUrl(int $step = 1, array $params = []): string { return path('kupshop_ordering_payment_payment', array_merge([ 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => $step, 'class' => $this->class, ], $params), \Symfony\Component\Routing\Router::ABSOLUTE_URL); } /** @deprecated Use getGenericPaymentUrl() instead. */ public function getPaymentUrl() { return $this->getGenericPaymentUrl(); } /** Get row in cart with payment title. * @param Smarty $smarty * * @return string|null * * @internal param array $context */ public function getCartTitle($smarty) { if ($this->templateCart) { $params = [ 'object' => $this, ]; $_smarty_tpl_vars = $smarty->tpl_vars; $ret = $smarty->_subTemplateRender($this->templateCart, $smarty->cache_id, $smarty->compile_id, 0, null, $params, 0, false); $smarty->tpl_vars = $_smarty_tpl_vars; return $ret; } return null; } public function getInitTemplate($smarty) { if ($this->templateInit) { $params = [ 'object' => $this, ]; $_smarty_tpl_vars = $smarty->tpl_vars; $ret = $smarty->_subTemplateRender($this->templateInit, $smarty->cache_id, $smarty->compile_id, 0, null, $params, 0, false); $smarty->tpl_vars = $_smarty_tpl_vars; return $ret; } return null; } /** Get payment HTML when payment selected. */ public function getCartDescription() { if ($this->templateDescription) { $smarty = createSmarty(); $smarty->assign([ 'object' => $this, ]); return $smarty->fetch($this->templateDescription); } return null; } /** Get payment HTML on completed order. */ public function getOrderViewDescription($smarty = null) { $params = [ 'payment' => $this, 'order' => $this->order, ]; if ($smarty) { return $smarty->_subTemplateRender($this->templateOrderView, $smarty->cache_id, $smarty->compile_id, 0, null, $params, 0, false); } else { $smarty = createSmarty(false, true); $smarty->assign($params); return $smarty->fetch($this->templateOrderView); } } public static function isEnabled($className) { return true; } /** Get payment icon url and name. * @return array|null */ public function getIcon() { return null; } protected function getRestrictionUtils() { if (!isset($this->restrictionUtils)) { $this->restrictionUtils = ServiceContainer::getService(RestrictionUtils::class); } return $this->restrictionUtils->setType('payment'); } public function accept($totalPrice, $freeDelivery) { return $this->getRestrictionUtils()->checkHardRequirements($this); } public function check(CartBase $cart) { $this->checkRestrictions($cart->getPurchaseState()); ServiceContainer::getService('event_dispatcher')->dispatch( new \KupShop\OrderingBundle\Event\PaymentCheckEvent($this, $cart) ); } public function checkRestrictions(PurchaseState $purchaseState) { $customData = $this->getCustomData(); if ($restrictions = $customData['restrictions'] ?? false) { if (!isset($this->restrictionParams)) { $this->restrictionParams = $this->getRestrictionUtils() ->createParamsFromConfig($this->config ?? [], $customData['restrictions'] ?? []); } $this->getRestrictionUtils()->checkSoftRequirements( $purchaseState->getDeliveryRestrictionParams(), $this->restrictionParams ); if ($productsFilter = $restrictions['productsFilter'] ?? null) { $this->getRestrictionUtils()->checkProductsFilter($productsFilter, $purchaseState->getProductList()); } } } public function setException(Exception $exception) { $this->exception = $exception; } public function getName() { return empty($this->databaseName) ? static::$name : $this->databaseName; } public function __construct() { $this->loadConfig(); $this->kibanaLogger = ServiceContainer::getService('logger'); } public function __toString() { return static::class; } public function setOrder($order) { if (is_object($order)) { $this->orderId = $order->id; $this->order = $order; } else { $this->orderId = $order; $this->order = new Order(); if (!$this->order->createFromDB($this->orderId)) { $this->error(replacePlaceholders(translate('errorOrderNotFound', 'payment'), ['ID' => $this->orderId])); } } return $this->order; } public function getOrderNumber(): string { return $this->order->order_no; } public function loadConfig(?string $language = null) { $cfg = Config::get(); // pokud je poslana $language, tak naloaduju settingy pro ten danej jazyk // napr. v administraci getDefault vzdycky naloaduje default jazyk, ale v nekterych pripadech // chci settingy pro jiny jazyk (napr. kdyz vracim platbu) $dbcfg = $language ? Settings::getFromCache($language) : Settings::getDefault(); $dbConfig = $this->loadPaymentDbConfig($dbcfg); if (isset($dbcfg->payment_config['order_status_new']) && !isset($dbConfig['order_status_new'])) { $dbConfig['order_status_new'] = $dbcfg->payment_config['order_status_new']; } if (isset($dbcfg->payment_config['order_status_finished'])) { $dbConfig['order_status_finished'] = $dbcfg->payment_config['order_status_finished'] !== 'keep' ? $dbcfg->payment_config['order_status_finished'] : null; } $config = $cfg['Modules']['payments'][$this->class] ?? false; if (!is_array($config)) { $config = []; } // Do not load database configuration in development - use one from config_db.php if (isLocalDevelopment()) { $dbConfig = []; } $this->config = array_merge($config, $dbConfig); // neni pouzity isset kvuli tomu ze hodnota muze bejt null !! if (!array_key_exists('order_status_finished', $this->config)) { $this->config['order_status_finished'] = 1; $languageContext = Contexts::get(LanguageContext::class); if ($languageContext->translationActive()) { // hotfix, zahranicni platby menily stav objednavek $defaultDbcfg = Settings::getFromCache($languageContext->getDefaultId()); if (isset($defaultDbcfg->payment_config['order_status_finished'])) { $this->config['order_status_finished'] = $defaultDbcfg->payment_config['order_status_finished'] !== 'keep' ? $defaultDbcfg->payment_config['order_status_finished'] : null; } } } // load domain config if (array_key_exists('domain_config', $this->config)) { $domainContext = ServiceContainer::getService(DomainContext::class); $domain = $domainContext->getActiveId(); if (array_key_exists($domain, $this->config['domain_config'])) { $domain_config = $this->config['domain_config'][$domain]; $this->config = array_merge($this->config, $domain_config); } } } protected function loadPaymentDbConfig($dbcfg): array { return array_filter($dbcfg->payments[$this->class] ?? []); } public function getOrderId($session) { $session = sqlFormatInput($session); $orderId = returnSQLResult('SELECT id_order FROM '.getTableName('order_payments')." WHERE payment_data LIKE '%\"{$session}\"%'"); /* if (empty($orderId)) logError(__FILE__, __LINE__, "Payment: getOrderId: Cannot get order id of session: {$session}, POST:".print_r($_POST, true)); */ return $orderId; } public function getStatus($session = null) { $SQL = sqlQuery('SELECT id, status, payment_data FROM '.getTableName('order_payments')." WHERE id_order={$this->orderId} FOR UPDATE"); if (empty($session) && sqlNumRows($SQL) > 1) { logError(__FILE__, __LINE__, 'Payment: getStatus: empty session but multiple payments!'); } while (($payment = sqlFetchAssoc($SQL)) !== false) { if (!empty($payment['payment_data'])) { $payment_data = json_decode($payment['payment_data'], true); if (empty($session) || (isset($payment_data['session']) && $payment_data['session'] == $session)) { $this->status = $payment['status']; $this->paymentId = $payment['id']; return true; } } } return false; } public function createPayment($session, $price = null, $data = []) { $fields = [ 'id_order' => $this->orderId, 'price' => $price, 'note' => "Platba modulu {$this->class}", 'status' => self::STATUS_CREATED, 'payment_data' => json_encode(array_merge($data, ['session' => $session])), 'date' => date('Y-m-d H:i:s'), 'method' => $this->pay_method ?? self::METHOD_ONLINE, 'admin' => getAdminID(null), ]; $this->insertSQL('order_payments', $fields); $this->status = self::STATUS_CREATED; $this->paymentId = sqlInsertId(); $this->order->updatePayments(); return true; } public function setStatus($status, $session = null) { $change = false; // Použiju transakci, abych zajistil že zjištění stavu transakce a její následná změna jsou atomický a nevběhne tam mezitím jiný thread sqlGetConnection()->transactional(function () use ($status, $session, &$change) { if (!$this->getStatus($session)) { logError(__FILE__, __LINE__, 'Payment::setStatus: get status failed!'); $this->error('Payment::setStatus: get status failed!'); } if ($this->status != $status) { if ($this->status == self::STATUS_FINISHED) { return; } $change = true; sqlQuery('UPDATE '.getTableName('order_payments')." SET status={$status}, date=NOW() WHERE id={$this->paymentId}"); } }); // Tohle mám mimo transakci, protože to může trvat dlouho (odesílá se email o zaplacení). // Hlavní je, že se tdvakrát nezavolá změna stavu zaplacení platby if ($change) { $this->order->updatePayments(); $forceEmail = null; $this->changeOrderStatus($status, $forceEmail); } return true; } public function changeOrderStatus($paymentStatus, $forceEmail) { switch ($paymentStatus) { case self::STATUS_FINISHED: if ($this->order->isActive() && !$this->order->isPaid()) { break; } $status = $this->config['order_status_finished'] ?? null; if (is_null($status)) { $status = $this->order->status; } $this->order->changeStatus($status, translate('msgStatusFinished', 'payment'), false); if (($forceEmail !== false) && ($this->order->source != OrderInfo::ORDER_SOURCE_POS)) { // Send Payment email $this->order->sendPaymentReceipt($this->paymentId); } break; } $this->status = $paymentStatus; } private function getPayment() { return sqlQueryBuilder()->select('*') ->from('order_payments', 'op') ->where(\Query\Operator::equals(['op.id' => $this->paymentId]))->execute()->fetch(); } public function checkOrderIsActiveAndNotPaid() { if (!$this->order->isActive()) { $this->error(replacePlaceholders(translate('errorOrderCanceled', 'payment'), ['ID' => $this->order->order_no])); } if ($this->order->isPaid(true)) { $this->error(replacePlaceholders(translate('errorOrderAlreadyPaid', 'payment'), ['ID' => $this->order->order_no])); } } public function startPayment() { $this->step(1, translate('msgStartPayment', 'payment')); } public function processStep($index) { try { if ($index == 1) { $this->checkOrderIsActiveAndNotPaid(); } if ($index >= 0) { $reflectionMethod = new ReflectionMethod($this, 'processStep_'.intval($index)); return $reflectionMethod->invoke($this); } } catch (\KupShop\KupShopBundle\Exception\RedirectException $e) { throw $e; } catch (Exception $e) { addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'Payment error: '.$this->getName().': '.$e->getMessage()); return $this->error($e->getMessage()); } } public function step($index, $message, $data = []) { // logError(__FILE__, __LINE__, "Payment::Step: $index, $message, {$this->orderId}"); if ($this->isNotification) { $this->sendNotificationResponse(500, $message); } $redirect = [ 'URL' => 'launch.php', 's' => 'payment', 'IDo' => $this->orderId, 'class' => $this->class, 'step' => $index, 'cf' => $this->order ? $this->order->getSecurityCode() : '', 'message' => $message, ]; $redirect = array_merge($redirect, $data); redirection(createScriptURL($redirect)); } public function getStepUrl(int $step): string { return createScriptURL([ 's' => 'payment', 'IDo' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'step' => $step, 'class' => $this->class, 'absolute' => true, ]); } public function error($message) { if ($this->isNotification) { $this->sendNotificationResponse(500, $message); } if ($this->order) { addUserMessage($message, 'danger'); redirection($this->order->getUrl()); } if (getVal('step') != -1) { $this->step(-1, $message); } return false; } public function success($message) { if ($this->isNotification) { $this->sendNotificationResponse(200, 'OK'); } if ($this->order) { addUserMessage($message, 'success'); redirection($this->order->getDetailUrl(1)); } $this->step(-2, $message); } public function info($message) { if ($this->isNotification) { $this->sendNotificationResponse(200, $message); } addUserMessage($message, 'info'); redirection($this->order->getUrl()); } public function checkAuth() { $cf = getVal('cf'); if ($this->order->getSecurityCode() != $cf) { throw new NotFoundHttpException(translate('errorSecurityCode', 'payment')); } } public function storePaymentInfo() { return []; } public function loadPaymentInfo($data) { foreach ($data as $key => $value) { $this->$key = $value; } } public function requiresEET() { return false; } public function hasOnlinePayment() { return false; } public function hasPaymentDescription() { return $this->hasOnlinePayment(); } public function getPayMethod() { if (!$this->pay_method) { throw new RuntimeException('Missing pay_method in Payment class '.$this->getName()); } else { return $this->pay_method; } } /** * Implements ArrayAccess interface. */ public function offsetSet($offset, $value): void { $this->{$offset} = $value; } public function offsetExists($offset): bool { return isset($this->{$offset}); } public function offsetUnset($offset): void { unset($this->{$offset}); } public function offsetGet($offset): mixed { return isset($this->{$offset}) ? $this->{$offset} : null; } /** * @return false|array $payment (with decoded json payment_data as decoded_data) */ public function getPendingPayment() { $payments = $this->order->getPaymentsArray(); foreach ($payments as $payment) { $paymentData = json_decode($payment['payment_data']); if (($payment['status'] == self::STATUS_CREATED || $payment['status'] == self::STATUS_PENDING) and isset($paymentData->paymentClass) and $paymentData->paymentClass === static::class ) { $payment['decoded_data'] = $paymentData; return $payment; } } return false; } public function setRequest(Symfony\Component\HttpFoundation\Request $request): Payment { $this->request = $request; return $this; } public function orderCreatedPostProcess(Order $order) { if (($this->hasOnlinePayment() || (findModule(\Modules::BANK_AUTO_PAYMENTS) && $this->getPayMethod() === self::METHOD_TRANSFER)) && !empty($this->config['order_status_new']) && $order->status !== $this->config['order_status_new'] ) { $order->changeStatus($this->config['order_status_new'], null, false); } } public function deletePayment() { return sqlQueryBuilder()->delete('order_payments')->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute(); } public function processAdminWindowData($data) { return $data; } public function getCustomData() { if (isset($this->custom_data)) { return $this->custom_data; } if ($this->id !== null) { $this->custom_data = json_decode(sqlQueryBuilder()->select('data') ->from('delivery_type_payment') ->where(\Query\Operator::equals(['id' => $this->id])) ->execute() ->fetchColumn(), true); } return $this->custom_data; } /** * @param int|null $id */ public function setID($id = null) { $this->id = $id; } /** * @param null $custom_data */ public function setCustomData($custom_data): void { $this->custom_data = $custom_data; } private function isPaymentInstanceOfClass($payment) { $data = (is_array($payment['payment_data'])) ? $payment['payment_data'] : json_decode($payment['payment_data'], true); return ($data['paymentClass'] ?? '') === static::class; } protected function updateReturnPayment($payment, $returnId, $full) { if ($returnId) { $title = 'Vrácení částky: objednávka '.$this->order->order_no.', číslo platby: '.$payment['id']; $data = ['return_from_payment' => $payment['id'], 'paymentClass' => $this->class]; $this->updateSQL('order_payments', ['note' => $title, 'payment_data' => json_encode($data)], ['id' => $returnId]); } } public function getPaymentForReturn($amount, ?int $paymentId = null) { if ($paymentId) { $this->paymentId = $paymentId; $payment = $this->getPayment(); if ($this->isPaymentInstanceOfClass($payment)) { $payment['payment_data'] = json_decode($payment['payment_data'], true); return $payment; } } $paymentsArray = $this->order->getPaymentsArray(); $payment = false; foreach ($paymentsArray as $paytmp) { if ((int) $paytmp['status'] === Payment::STATUS_FINISHED && $paytmp['price'] >= ($amount * -1)) { $paytmp['payment_data'] = json_decode($paytmp['payment_data'], true); if (isset($paytmp['payment_data']['session']) && $this->isPaymentInstanceOfClass($paytmp)) { $payment = $paytmp; } } } if (!$payment) { throw new PaymentException('Platba nemohla být automaticky vrácena, protože neexistuje validní platba.'); } return $payment; } public function enabledReturnPayment(): bool { $dbcfg = \Settings::getDefault(); return (($dbcfg->payment_config['return_payment_auto_gate'] ?? null) !== 'N') && static::$canAutoReturn; } /** * @throws PaymentException */ public function doReturnPayment(array $payment, float $amount) { return false; } /** * @throws PaymentException */ public function returnPayment($amount, ?int $paymentId = null, ?int $returnId = null) { // TODO: zabránit znovuvrácení platby, když už se z ní vracelo - vracet pouze zbytek // TODO: zakázat editaci vrácené platby, pokud byla vrácena automatem if ($this->enabledReturnPayment()) { $payment = $this->getPaymentForReturn($amount, $paymentId); $this->updateReturnPayment($payment, $returnId, ($payment['price'] + $amount) == 0); if ($result = $this->doReturnPayment($payment, $amount)) { return $result; } } $currencyContext = Contexts::get(CurrencyContext::class); $currency = $currencyContext->getActive(); // Automatic return not implemented throw new PaymentException('Vrácení nelze pro tento způsob platby automatizovat. Vraťte zákazníkovi '.$amount * (-1).' '.$currency->getSymbol().' manuálně.'); } protected function sendNotificationResponse(int $httpResponseCode, $message) { http_response_code($httpResponseCode); echo $message; exit; } public function getPhoto(?string $photo, ?string $date_updated = null): ?array { $icon = $this->getPhotoPath($photo); if ($icon === null) { return null; } return getImage($this->id, basename($icon), dirname($icon), 8, $this::$name, strtotime($date_updated)); } public function getPhotoPath(?string $photo): ?string { return empty($photo) ? $this->defaultIcon : '../payment/'.$photo; } public function getClassName(): string { return $this->class; } /** * Pro platebni brany - vrati platebni metody. * * @return array|null */ public function getAvailableMethods() { return null; } public function getSelectedMethodId(): ?string { return $this->method ?? null; } public function getSelectedMethod(): ?array { if (empty($this->method)) { return null; } $methods = $this->getAvailableMethods(); return $methods[$this->method] ?? null; } }