first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,399 @@
<?php
use Adyen\Webhook\Exception\AuthenticationException;
use Adyen\Webhook\Receiver\HmacSignature;
use Adyen\Webhook\Receiver\NotificationReceiver;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\BankAutoPaymentBundle\BankAutoPaymentBundle;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Exception\PaymentException;
use KupShop\OrderingBundle\Util\Order\PaymentOrderSubMethodTrait;
/**
* "adyen/php-api-library": "^15.2.0",
* "adyen/php-webhook-module": "^0.8.0".
*/
class Adyen extends Payment
{
use PaymentOrderSubMethodTrait;
public static $name = 'Adyen platební brána';
public static bool $canAutoReturn = true;
protected $templateCart = 'payment.Adyen.cart.tpl';
protected $templateOrderView = 'payment.Adyen.orderView.tpl';
public $class = 'Adyen';
protected $pay_method = Payment::METHOD_ONLINE;
protected $method;
public function createAdyenSession()
{
$session = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('session');
$adyenSession = $session->get('adyen_'.$this->orderId);
// Aby když mě to přesměruje na detail a nestihl přijit webhook, naukázal se mi znova platební widget
if ($adyenSession['paymentResult'] == \Adyen\Model\Checkout\SessionResultResponse::STATUS_COMPLETED) {
return false;
}
// Když chci změnit metodu, tak tohle přeskočit
if (!getVal('rpm')) {
// Pokud mám sessionId a není starší pěti minut, použiji ho.
if ($adyenSession['sessionId'] && $adyenSession['expired'] > time() - (5 * 60)) {
return true;
}
}
$service = new \Adyen\Service\Checkout\PaymentsApi($this->getApiClient());
$createCheckoutSessionRequest = new \Adyen\Model\Checkout\CreateCheckoutSessionRequest();
$createCheckoutSessionRequest->setCountryCode(Contexts::get(\KupShop\KupShopBundle\Context\CountryContext::class)->getActiveId())
->setMerchantAccount($this->config['merchantAccount'])
->setReturnUrl($this->getGenericPaymentUrl(5))
->setReference($this->order->order_no.'_'.time())
->setShopperEmail($this->order->getUserEmail())
->setShopperLocale($this->getLocale());
$amount = new \Adyen\Model\Checkout\Amount();
$amount->setValue($this->order->getRemainingPayment(true)->mul(DecimalConstants::hundred())->asInteger())
->setCurrency($this->order->getCurrency());
$createCheckoutSessionRequest->setAmount($amount);
$this->kibanaLogger->notice('[Adyen] Create session', [
'order' => $this->order->order_no,
'amount' => $amount->getValue(),
]);
if ($this->method && !getVal('rpm')) {
$adyenMethods = [$this->method];
} else {
$methods = $this->getAvailableMethods($amount);
$adyenMethods = array_values(array_map(fn ($val) => $val['type'], $methods));
}
$createCheckoutSessionRequest->setAllowedPaymentMethods($adyenMethods);
$items = [];
foreach ($this->order->fetchItems() as $item) {
$items[] = (new \Adyen\Model\Checkout\LineItem())
->setQuantity($item['pieces'])
->setTaxPercentage($item['vat'])
->setDescription($item['descr'])
->setAmountIncludingTax($item['total_price']['value_with_vat']->div(toDecimal($item['pieces']))->mul(DecimalConstants::hundred())->asInteger());
}
$createCheckoutSessionRequest->setLineItems($items);
$adyenSessionRequestResult = $service->sessions($createCheckoutSessionRequest);
$session->set('adyen_'.$this->orderId, [
'sessionId' => $adyenSessionRequestResult->getId(),
'sessionData' => $adyenSessionRequestResult->getSessionData(),
'expired' => time(),
'orderId' => $this->orderId,
]
);
return true;
}
/** Return from gateway */
public function processStep_5()
{
$sessionId = $this->request->get('sessionId');
$sessionResult = $this->request->get('sessionResult');
if ($sessionResult) {
$result = $this->getSessionStatus($sessionId, $sessionResult);
}
// Z informace redirectResult vubec nic nedostanu. Takze proste zakaznikovi zakazu znova zaplatit a bude se cekat az vysledek dorazi pres webhook.
if (($result ?? false) == \Adyen\Model\Checkout\SessionResultResponse::STATUS_COMPLETED || $this->request->get('redirectResult')) {
$adyenSession = $this->request->getSession()->get('adyen_'.$this->orderId) ?: [];
$this->request->getSession()->set('adyen_'.$this->orderId, $adyenSession + ['paymentResult' => \Adyen\Model\Checkout\SessionResultResponse::STATUS_COMPLETED]);
}
$this->success(translate('paymentSuccess', 'payment'));
}
public function getSessionStatus($sessionId, $sessionResult)
{
$paymentsApi = new \Adyen\Service\Checkout\PaymentsApi($this->getApiClient());
$sessionResultResponse = $paymentsApi->getResultOfPaymentSession($sessionId, ['queryParams' => ['sessionResult' => $sessionResult]]);
return $sessionResultResponse->getStatus();
}
public function startPayment()
{
$pathData = ['id' => $this->order->id];
if (!\User::getCurrentUser()) {
$pathData['cf'] = $this->order->getSecurityCode();
}
redirection(path('payment-redirect', $pathData));
}
public function processStep_1()
{
redirection($this->order->getDetailUrl());
}
/** Return from gateway */
public function processStep_10()
{
$request = json_decode($this->request->getContent() ?? '', true);
if (empty($request['notificationItems'])) {
$this->error('Not found !!');
}
// Setup NotificationReceiver with dependency injection or create an instance as follows
$notificationReceiver = new NotificationReceiver(new HmacSignature());
$_SERVER['PHP_AUTH_USER'] = $this->request->headers->get('php-auth-user');
$_SERVER['PHP_AUTH_PW'] = $this->request->headers->get('php-auth-pw');
if (!$notificationReceiver->isAuthenticated(
$request['notificationItems'][0]['NotificationRequestItem'],
$this->config['merchantAccount'],
$this->config['basic_name'],
$this->config['basic_pass']
)) {
throw new AuthenticationException('Incoming webhook wasn\'t authenticated!');
}
foreach ($request['notificationItems'] as $notificationItem) {
$notifItem = $notificationItem['NotificationRequestItem'];
if ($notificationReceiver->validateHmac($notifItem, $this->config['hmac']) && in_array($notifItem['eventCode'], ['AUTHORISATION', 'REFUND'])) {
$isRefund = ($notifItem['eventCode'] == 'REFUND');
$message = ($isRefund ? '[Adyen] Refund payment' : '[Adyen] Incoming payment');
$this->kibanaLogger->notice($message, [
'notificationItem' => $notifItem,
]);
$merchantReference = $notifItem['merchantReference'];
$ref = explode('_', $merchantReference);
$id_order = ($ref[0] ?? false);
if ($notifItem['success'] == 'true') {
if ($id_order) {
try {
$order = Order::createFromDbOrderNo($id_order);
$payment_data = [
'paymentClass' => self::class,
'session' => $notifItem['additionalData']['checkoutSessionId'] ?? null,
'pspReference' => $notifItem['pspReference'] ?? null,
];
$price = $notifItem['amount']['value'] / 100;
$note = "Platba modulu {$this->class}";
if ($isRefund) {
$price *= -1;
$note = "Vrácení platby: objednávka {$order->order_no}, ID platby: {$notifItem['originalReference']}";
$payment_data['originalReference'] = $notifItem['originalReference'];
}
$order?->insertPayment($price, $note, method: self::METHOD_ONLINE, payment_data: json_encode($payment_data));
if ($order && !empty($notifItem['paymentMethod'])) {
$this->setPaymentSubMethod($notifItem['paymentMethod'], $order);
}
} catch (InvalidArgumentException $exception) {
getRaven()->captureException($exception);
}
}
} else {
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION,
"Objednávka {$id_order}: {$message} error: {$notifItem['reason']}", $notifItem, [BankAutoPaymentBundle::LOG_TAG_ADYEN]);
}
}
}
$this->sendNotificationResponse(200, '[accepted]');
}
public function doReturnPayment(array $payment, float $amount)
{
if ($paymentPspReference = $payment['payment_data']['pspReference'] ?? null) {
$class = new \Adyen\Service\Checkout\ModificationsApi($this->getApiClient());
$paymentRefundRequest = new \Adyen\Model\Checkout\PaymentRefundRequest();
$paymentRefundRequest->setMerchantAccount($this->config['merchantAccount']);
$paymentRefundRequest->setReference($this->order->order_no.'_'.time());
$amountCents = (int) floor($amount * -100); // musí být integer v centech
$amount = new \Adyen\Model\Checkout\Amount();
$amount->setValue($amountCents)->setCurrency($this->order->getCurrency());
$paymentRefundRequest->setAmount($amount);
$paymentRefundResponse = $class->refundCapturedPayment($paymentPspReference, $paymentRefundRequest);
if ($paymentRefundResponse->getStatus() == 'received') {
// the refund request was successfully received by Adyen - no result yet,
// because you receive the outcome of the refund request asynchronously, in a REFUND webhook.
// Vrácení platby bylo odesláno na platební bránu.
throw new PaymentException(translate('returnSucceed', 'orderPayment'), 'Adyen');
}
}
// Vrácení platby se nezdařilo, vyřešte, prosím, na platební bráně.);
throw new PaymentException(translate('returnFailed', 'orderPayment'));
}
public function getApiClient(): Adyen\Client
{
if (!isset($this->apiClient)) {
$config = new \Adyen\Config();
$testEnv = (isDevelopment() || ($this->config['test'] ?? '0') == '1');
if (!$testEnv) {
$config->set('prefix', $this->config['api_urls_prefix']);
}
$client = new \Adyen\Client($config);
$client->setXApiKey($this->config['apikey']);
$client->setEnvironment($testEnv ? \Adyen\Environment::TEST : \Adyen\Environment::LIVE);
$client->setTimeout(5);
$this->apiClient = $client;
}
return $this->apiClient;
}
/**
* @throws \Adyen\AdyenException
*/
public function getAvailableMethods(Adyen\Model\Checkout\Amount|Decimal|null $amount = null)
{
$countryContext = Contexts::get(\KupShop\KupShopBundle\Context\CountryContext::class);
$currencyContext = Contexts::get(\KupShop\KupShopBundle\Context\CurrencyContext::class);
$currency = $currencyContext->getActiveId();
$country = $countryContext->getActiveId();
$cacheKey = "adyen-methods-{$currency}-{$country}-{$this->getLocale()}";
if (!$amount) {
if ($paymentMethods = getCache($cacheKey)) {
return $paymentMethods;
}
}
try {
$class = new \Adyen\Service\Checkout\PaymentsApi($this->getApiClient());
$paymentMethodsRequest = new \Adyen\Model\Checkout\PaymentMethodsRequest();
$paymentMethodsRequest->setMerchantAccount($this->config['merchantAccount']);
$paymentMethodsRequest->setCountryCode($countryContext->getActiveId());
$paymentMethodsRequest->setShopperLocale($this->getLocale());
if ($amount instanceof Decimal) {
$amount = (new \Adyen\Model\Checkout\Amount())->setValue($amount->mul(DecimalConstants::hundred())->asInteger())->setCurrency($currencyContext->getActiveId());
}
if ($amount) {
$paymentMethodsRequest->setAmount($amount);
}
$paymentMethodsResponse = $class->paymentMethods($paymentMethodsRequest);
$paymentMethods = [];
foreach ($paymentMethodsResponse->getPaymentMethods() as $method) {
if ($method->getType() == 'scheme') {
$images = array_map(function ($brand) {
return "https://checkoutshopper-live.adyen.com/checkoutshopper/images/logos/{$brand}.svg";
}, $method->getBrands());
} else {
$images = ["https://checkoutshopper-live.adyen.com/checkoutshopper/images/logos/{$method->getType()}.svg"];
}
$paymentMethods[$method->getType()] = [
'name' => $method->getName(),
'type' => $method->getType(),
'images' => $images,
];
}
} catch (Exception $e) {
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION,
'Chyba Adyen při načítání platebních metod: '.$e->getMessage(),
['key' => $cacheKey, 'config' => $this->config], [BankAutoPaymentBundle::LOG_TAG_ADYEN]);
return [];
}
if (!$amount) {
setCache($cacheKey, $paymentMethods);
}
return $paymentMethods;
}
public function hasOnlinePayment()
{
return true;
}
public static function getSettingsConfiguration(): array
{
return ['fields' => [
'apikey' => [
'title' => 'API klíč',
'type' => 'text',
],
'merchantAccount' => [
'title' => 'Merchant Account',
'type' => 'text',
],
'clientKey' => [
'title' => 'Client Key',
'type' => 'text',
],
'hmac' => [
'title' => 'HMAC key',
'type' => 'text',
],
'api_urls_prefix' => [
'title' => 'API URLs prefix',
'type' => 'text',
],
'basic_name' => [
'title' => 'Basic auth name',
'type' => 'text',
],
'basic_pass' => [
'title' => 'Basic auth pass',
'type' => 'text',
],
'test' => [
'title' => 'Testovací režim',
'type' => 'toggle',
],
]];
}
public function getLocale()
{
$languageContext = Contexts::get(\KupShop\KupShopBundle\Context\LanguageContext::class);
return $languageContext->getActive()->getLocale();
}
// private function checkWebhooks()
// {
// Maybe later alligator
//
// $merchantAccount = $this->config['merchantAccount'];
// $webhooksApi = new \Adyen\Service\Management\WebhooksMerchantLevelApi($this->getApiClient());
// $allWebhooks = $webhooksApi->listAllWebhooks($merchantAccount);
//
// $webhooks = $allWebhooks->getData();
//
// if ($webhooks) {
// return true;
// }
//
// $createMerchantWebhookRequest = new \Adyen\Model\Management\CreateMerchantWebhookRequest();
// $createMerchantWebhookRequest->setUrl(path('kupshop_ordering_payment_legacypayment', ['class' => 'Adyen', 'step' => 10], \Symfony\Component\Routing\Router::ABSOLUTE_URL))
// ->setDescription('API - wpjshop')
// ->setType('standard')
// ->setCommunicationFormat('json');
//
// $webhooksApi->setUpWebhook($merchantAccount, $createMerchantWebhookRequest);
// }
}

View File

@@ -0,0 +1,431 @@
<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\Exception\PaymentException;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Psr\Log\LoggerInterface;
/**
* Requires composer package ondrakoupil/csob-eapi-paygate.
*
* V configu je nutno mít cestu privátnímu klíč merchanta (privateKeyPath), který je vygenerován CSOBckem. merchantId je taky od CSOBcka
*/
class CSOB extends Payment
{
public static $name = 'ČSOB platební brána';
protected ?string $defaultIcon = '../../common/static/payments/csob.svg';
public $class = 'CSOB';
protected $pay_method = Payment::METHOD_ONLINE;
public static bool $canAutoReturn = true;
protected ?\OndraKoupil\Csob\Client $client = null;
protected LoggerInterface $logger;
protected OrderItemInfo $orderItemInfo;
protected string $publicKeyIntegration = 'bundles/KupShop/KupShopBundle/Resources/payments/mips_iplatebnibrana.csob.cz.pub';
protected string $publicKeyProd = 'bundles/KupShop/KupShopBundle/Resources/payments/mips_platebnibrana.csob.cz.pub';
public function __construct()
{
parent::__construct();
$this->logger = ServiceContainer::getService('logger');
$this->orderItemInfo = ServiceContainer::getService(OrderItemInfo::class);
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'test' => [
'title' => 'Testovací režim',
'type' => 'toggle',
],
],
];
}
public function processStep_1()
{
$this->checkActivePaymentAlreadyExists();
$payment = $this->initPayment();
$url = $this->getClient()->getPaymentProcessUrl($payment);
redirection($url);
}
public function initPayment()
{
$payment = $this->getInitPayment();
try {
$response = $this->getClient()->paymentInit($payment);
} catch (Exception $e) {
$errMessage = translate('payment_exception_communication', 'payment');
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_COMMUNICATION,
'Chyba v komunikaci s CSOB platební bránou',
['message' => $e->getMessage()]);
$this->logger->error('CSOB communication error', ['exception' => $e->getMessage()]);
throw new PaymentException($errMessage);
}
$payId = $payment->getPayId();
$this->createPayment($payId, $this->order->getRemainingPayment(), ['paymentClass' => $this->class]);
return $payment;
}
protected function checkActivePaymentAlreadyExists()
{
$activePaymentExists = sqlQueryBuilder()->select("op.id, JSON_UNQUOTE(JSON_EXTRACT(op.payment_data, '$.session')) sessionId")->from('order_payments', 'op')
->innerJoin('op', 'orders', 'o', 'o.id = op.id_order')
->where(\Query\Operator::equals(['o.order_no' => $this->order->order_no]))
->andWhere(\Query\Operator::inIntArray([
static::STATUS_CREATED,
static::STATUS_PENDING,
], 'op.status'))
->andWhere(JsonOperator::contains('op.payment_data', 'paymentClass', $this->class))
->orderBy('date', 'DESC')
->execute()->fetchAssociative();
$payId = $activePaymentExists['sessionId'] ?? false;
if (empty($activePaymentExists) || !$payId) {
return;
}
$client = $this->getClient();
$paymentStatus = $client->paymentStatus($payId);
$this->paymentChangeStatus($paymentStatus, $payId);
if (in_array($paymentStatus, [1, 2], true)) {
// if payment already exists and is active, dont create new payment and redirect user to the existing one
$url = $client->getPaymentProcessUrl($payId);
redirection($url);
}
}
// Vrácení z platební brány
public function processStep_2()
{
$client = $this->getClient();
$response = $client->receiveReturningCustomer();
$payId = $response['payId'];
$this->paymentChangeStatus($response['paymentStatus'], $payId);
switch ($this->status) {
case self::STATUS_FINISHED:
$this->info(translate('paymentSuccess', 'payment'));
break;
case self::STATUS_PENDING:
$this->info(translate('payment_unexpected_status', 'payment'));
break;
case self::STATUS_STORNO:
$this->info(translate('payment_rejected_status', 'payment'));
break;
case self::STATUS_UNKNOWN:
$this->info(translate('payment_unexpected_status', 'payment'));
}
}
/**
* @return void
*
* https://github.com/csob/platebnibrana/wiki/Pr%C5%AFb%C4%9Bh-platby#user-content-%C5%BDivotn%C3%AD-cyklus-transakce-
*/
protected function paymentChangeStatus($status, $payId)
{
switch ($status) {
case 1:
case 2:
$this->setStatus(Payment::STATUS_PENDING, $payId);
break;
case 4:
case 7:
case 8:
$this->setStatus(Payment::STATUS_FINISHED, $payId);
break;
case 3:
case 5:
case 6:
$this->setStatus(Payment::STATUS_STORNO, $payId);
break;
case 9:
case 10:
// Vrácení platby;
break;
default:
$this->setStatus(Payment::STATUS_UNKNOWN, $payId);
}
}
protected function getInitPayment(): OndraKoupil\Csob\Payment
{
$payment = new \OndraKoupil\Csob\Payment($this->order->order_no);
$payment->orderNo = $this->getPaymentReference();
$payment->currency = $this->order->currency;
$payment->customerId = $this->order->id_user;
$payment->language = $this->order->id_language;
// ///// CUSTOMER
$customer = new \OndraKoupil\Csob\Metadata\Customer();
$customer->name = $this->order->invoice_name.' '.$this->order->invoice_surname;
$customer->email = $this->order->invoice_email;
$customer->mobilePhone = $this->order->invoice_phone;
$customerLogin = new \OndraKoupil\Csob\Metadata\Login();
$customerLogin->auth = $this->order->id_user ? 'account' : 'guest';
$userAccountData = $this->getUserAccountData($this->order->id_user, $this->order->invoice_email);
$customerAccount = new \OndraKoupil\Csob\Metadata\Account();
if ($userAccountData['createdAt'] ?? false) {
$customerAccount->setCreatedAt($userAccountData['createdAt']);
}
if ($userAccountData['changedAt'] ?? false) {
$customerAccount->setChangedAt($userAccountData['changedAt']);
}
$customerAccount->paymentsDay = $userAccountData['paymentsDay'] ?? 0;
$customerAccount->paymentsYear = $userAccountData['paymentsYear'] ?? 0;
$customer->setLogin($customerLogin);
$customer->setAccount($customerAccount);
$payment->setCustomer($customer);
// /////
$order = new \OndraKoupil\Csob\Metadata\Order();
$order->type = 'purchase';
$order->availability = 'now';
$order->deliveryMode = 3;
$order->nameMatch = ($this->order->invoice_name.$this->order->invoice_surname) == ($this->order->delivery_name.$this->order->delivery_surname);
if ($this->order->delivery_street && $this->order->delivery_city && $this->order->delivery_zip && $this->order->delivery_country) {
$order->setShipping(new \OndraKoupil\Csob\Metadata\Address($this->order->delivery_street,
$this->order->delivery_city,
$this->order->delivery_zip,
$this->order->delivery_country));
}
if ($this->order->invoice_street && $this->order->invoice_city && $this->order->invoice_zip && $this->order->invoice_country) {
$order->setBilling(new \OndraKoupil\Csob\Metadata\Address($this->order->invoice_street,
$this->order->invoice_city,
$this->order->invoice_zip,
$this->order->invoice_country));
}
$payment->setOrder($order);
$itemsPrice = DecimalConstants::zero();
$deliveryPrice = DecimalConstants::zero();
foreach ($this->order->fetchItems() as $item) {
$price = $item['total_price']['value_with_vat'];
if ($this->orderItemInfo->getItemType($item) == OrderItemInfo::TYPE_DELIVERY) {
$deliveryPrice = $deliveryPrice->add($price);
} else {
$itemsPrice = $itemsPrice->add($price);
}
}
if (!$itemsPrice->isZero()) {
$payment->addCartItem(translate('purchaseAtShop', 'payment'), 1,
$this->getAmountHundreds($itemsPrice));
}
if (!$deliveryPrice->isZero()) {
$payment->addCartItem(translate('shipping', 'payment'), 1,
$this->getAmountHundreds($deliveryPrice));
}
return $payment;
}
public function getPaymentReference()
{
$orderNo = $this->order->order_no;
if (isLocalDevelopment()) {
return '0'.substr($orderNo, -9);
}
return substr($orderNo, -10);
}
protected function getUserAccountData($idUser, string $email): array
{
$userStats = [];
if ($idUser) {
$user = sqlQueryBuilder()->select('*')->from('users')->where(\Query\Operator::equals(['id' => $this->order->id_user]))
->execute()->fetchAssociative();
$dateTimeReg = new DateTime($user['date_reg']);
$dateTimeUpdated = new DateTime($user['date_updated']);
if ($dateTimeReg > (new DateTime('1990-01-01'))) {
$userStats['createdAt'] = $dateTimeReg;
}
if ($dateTimeUpdated > (new DateTime('1990-01-01'))) {
$userStats['changedAt'] = $dateTimeUpdated;
}
}
$prevOrdersSql = sqlQueryBuilder()->select('COUNT(*)')
->from('order_payments', 'op')
->innerJoin('op', 'orders', 'o', 'o.id = op.id_order')
->andWhere(\Query\Operator::equals($idUser ? ['id_user' => $idUser] : ['invoice_email' => $email]))
->groupBy($idUser ? 'id_user' : 'invoice_email');
$userStats['paymentsDay'] = $prevOrdersSql->andWhere('op.date >= now() - INTERVAL 1 DAY')
->execute()->fetchOne();
$userStats['paymentsYear'] = $prevOrdersSql->andWhere('op.date >= now() - INTERVAL 1 YEAR')
->execute()->fetchOne();
return $userStats;
}
public function getClient($skipCache = false): OndraKoupil\Csob\Client
{
if (!$skipCache && $this->client) {
return $this->client;
}
$pathFinder = \KupShop\KupShopBundle\Util\System\PathFinder::getService();
if ($this->config['test'] ?? false) {
$publicKeyPath = $pathFinder->enginePath($this->publicKeyIntegration);
$gatewayUrl = \OndraKoupil\Csob\GatewayUrl::TEST_1_9;
} else {
$publicKeyPath = $pathFinder->enginePath($this->publicKeyProd);
$gatewayUrl = \OndraKoupil\Csob\GatewayUrl::PRODUCTION_1_9;
}
$domainContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\DomainContext::class);
$config = new \OndraKoupil\Csob\Config(
$this->config['merchantId'],
$this->config['privateKeyPath'],
$publicKeyPath,
$this->config['merchantName'] ?? $domainContext->getActiveWithScheme(),
// Adresa, kam se mají zákazníci vracet poté, co zaplatí
$this->getGenericPaymentUrl(2),
// URL adresa API - výchozí je adresa testovacího (integračního) prostředí,
// až budete připraveni přepnout se na ostré rozhraní, sem zadáte
// adresu ostrého API. Nezapomeňte také na ostrý veřejný klíč banky.
$gatewayUrl
);
return $this->client = new \OndraKoupil\Csob\Client($config);
}
public function doReturnPayment(array $payment, float $amount)
{
if ($payment['status'] != Payment::STATUS_FINISHED) {
throw new PaymentException(translate('returnFailed', 'orderPayment'));
}
$payId = $payment['payment_data']['session'] ?? null;
if (!$payId) {
throw new PaymentException('Payment does not have assigned checkout_id');
}
$client = $this->getClient();
$paymentStatus = $client->paymentStatus($payId);
switch ($paymentStatus) {
case 4:
case 7:
if ($payment['price'] + $amount > PHP_FLOAT_EPSILON) {
$message = translate('returnFailedOnlyFullAmountWhenNotCharged', 'orderPayment');
throw new PaymentException($message);
}
$response = $client->paymentReverse($payId);
break;
case 8:
$amountDecimals = ($payment['price'] + $amount > PHP_FLOAT_EPSILON) ? $this->getAmountHundreds(toDecimal($amount)) : null;
$response = $client->paymentRefund($payId, false, $amountDecimals);
break;
}
if (($response['resultCode'] ?? 1) != 0) {
$message = translate('returnFailed', 'orderPayment');
addActivityLog(\KupShop\AdminBundle\Util\ActivityLog::SEVERITY_ERROR,
\KupShop\AdminBundle\Util\ActivityLog::TYPE_COMMUNICATION,
$message,
['amount' => $amount, 'payment_id' => $payment['payment_data']['session'], 'RESPONSE' => $response]);
throw new PaymentException($message);
}
return $response;
}
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(\Query\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']);
$client = $this->getClient(true);
$payId = $paymentData['session'];
$paymentStatus = $client->paymentStatus($payId);
$this->paymentChangeStatus($paymentStatus, $payId);
}
}
protected function getAmountHundreds(Decimal $amount)
{
return $amount->mul(DecimalConstants::hundred())->abs()->asFloat();
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,10 @@
<?php
class DefaultPayment extends Payment
{
public static $name = 'Platba';
public $class = 'DefaultPayment';
protected $pay_method = Payment::METHOD_UNKNOWN;
}

View File

@@ -0,0 +1,36 @@
<?php
use KupShop\OrderingBundle\Exception\PaymentException;
class Dobirka extends Payment
{
public static $name = 'Platba na dobírku';
protected ?string $defaultIcon = '../../common/static/payments/dobirka.svg';
public $class = 'Dobirka';
public $method;
protected $pay_method = Payment::METHOD_COD;
public function requiresEET()
{
return false;
}
/**
* @param $cart \Cart
*/
public function check(CartBase $cart)
{
if ($cart->hasVirtualProducts()) {
$this->exception = new PaymentException(translate_shop('errorDobirkaVirtualProducts', 'payment'), translate_shop('errorDobirkaVirtualProducts_short', 'payment'));
}
if (!empty($this->exception) && $cart->max_step != 0) {
throw $this->exception;
}
return parent::check($cart);
}
}

View File

@@ -0,0 +1,138 @@
<?php
use KupShop\KupShopBundle\Config;
class Essox extends Payment
{
public static $name = 'Essox';
protected $templateOrderView = 'payment.Essox.orderView.tpl';
// protected $templateCart = 'payment.Essox.cart.tpl';
public $class = 'Essox';
protected $pay_method = Payment::METHOD_INSTALLMENTS;
// Config
public $url = 'https://e-smlouvy.essox.cz';
public $userName;
public $encryptKey;
public function __construct()
{
parent::__construct();
$this->readConfig();
}
public function readConfig()
{
foreach ($this->config as $key => $value) {
$this->$key = $value;
}
}
public function getEssoxOrderUrl()
{
return $this->getRequestUrl('NewContract', $this->order->total_price->asInteger(), ['OrderId' => $this->order->order_no]);
}
protected function base64_url_encode($input)
{
return strtr(base64_encode($input), '+/', '-_');
}
protected function getRequestUrl($method, $price, $customData)
{
$Timestamp = date('YmdHis', time()); // aktualni cas
$UserName = $this->userName; // vase prihlasovaci jmeno
$Password = $this->encryptKey; // vase heslo
$HashKey = $UserName.'#'.$Password.'#'.$price.'#'.$Timestamp;
$HashKey = sha1($HashKey);
$extendedParameters = join('', array_map(function ($key, $value) {
return "<{$key}>{$value}</{$key}>";
}, array_keys($customData), $customData));
$xml = "<FinitServiceRequest>
<Version>1.0</Version>
<ServiceName>{$method}</ServiceName>
<BaseParameters>
<UserName>{$UserName}</UserName>
<Price>{$price}</Price>
<Timestamp>{$Timestamp}</Timestamp>
<HashKey>{$HashKey}</HashKey>
</BaseParameters>
<ExtendedParameters>
{$extendedParameters}
</ExtendedParameters>
</FinitServiceRequest>";
return $this->url.'?ESXCode=5&ESXAuth='.$this->base64_url_encode(trim($xml));
}
public function accept($totalPrice, $freeDelivery)
{
$totalPrice = $totalPrice->getPriceWithVat()->asFloat();
if ($totalPrice <= 0 && $this->order) {
$totalPrice = $this->order->total_price;
}
return parent::accept($totalPrice, $freeDelivery) && $totalPrice >= 2000;
}
public function getEssoxCalcUrl(Decimal $price)
{
$price = roundPrice($price, -1, 'DB', 0)->asInteger();
if ($price < 2000) {
return null;
}
return path('kupshop_ordering_payment_legacypayment', [
'step' => 5,
'class' => $this->class,
'price' => $price,
]);
}
public function processStep_1()
{
redirection($this->getEssoxOrderUrl());
}
public function processStep_5()
{
$price = roundPrice(getVal('price'), -1, 'DB', 0)->asInteger();
if ($price < 2000) {
return null;
}
redirection($this->getRequestUrl('Calculation', $price, []));
}
public function startPayment()
{
// Zakázat automatický redirect na bránu, už se z ní nikdy nevrátí
return false;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
if (!class_exists('Essox2Splatky')) {
require_once 'class.Essox2Splatky.php';
}
class Essox2Rozdeleni extends Essox2Splatky
{
public static $name = 'Essox - rozdělení platby';
public $class = 'Essox2Rozdeleni';
protected int $splitParts = 4;
protected $templateOrderView = 'payment.Essox2Rozdeleni.orderView.tpl';
protected function getSpreadedInstalments(): bool
{
return true;
}
public function processStep_5()
{
return '';
}
public function getEssoxCalcDetail(Decimal $price): ?array
{
$priceVal = roundPrice($price, -1, 'DB', 0)->asFloat();
if (!($this->config['productDetail'] ?? false) || $priceVal < ($this->config['minPrice'] ?? 2000) || $priceVal > ($this->config['maxPrice'] ?? 30000)) {
return null;
}
return $this->splitPrice($price);
}
protected function splitPrice(Decimal $price): array
{
$part = $price->div(toDecimal($this->splitParts))->floor();
return [
'parts' => $this->splitParts,
'price' => $part->asInteger(),
];
}
public function accept($totalPrice, $freeDelivery)
{
$price = $totalPrice->getPriceWithVat()->asFloat();
if ($price <= 0 && $this->order) {
$price = $this->order->total_price;
}
// price has to be lower than 30000 Kč according to the documentation
return parent::accept($totalPrice, $freeDelivery) && $price <= 30000;
}
public static function getSettingsConfiguration(): array
{
$essoxConfig = parent::getSettingsConfiguration();
$essoxConfig['fields']['maxPrice']['tooltip'] = 'Maximální částka u které tuto možnost zobrazit na detailu produktu. (max. 30000 Kč)';
$essoxConfig['fields']['maxPrice']['placeholder'] = '30000';
return $essoxConfig;
}
}

View File

@@ -0,0 +1,424 @@
<?php
declare(strict_types=1);
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Exception\PaymentException;
use Query\Operator;
// Dokumentace ESSOX API: https://drive.google.com/drive/folders/1j7OFhsrUl1F3ZQt3Lo7d8yOyvLW7ioN5?usp=sharing
class Essox2Splatky extends Payment
{
public static $name = 'Essox - splátky';
public $class = 'Essox2Splatky';
protected string $apiUrl = 'https://apiv32.essox.cz';
protected string $apiTestUrl = 'https://testapiv32.essox.cz';
protected $templateOrderView = 'payment.Essox2Splatky.orderView.tpl';
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl()
{
return createScriptURL([
's' => '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);
}
}

View File

@@ -0,0 +1,14 @@
<?php
if (!class_exists('GiroCheckoutGiropay')) {
require_once 'class.GiroCheckoutGiropay.php';
}
class GiroCheckoutCrCard extends GiroCheckoutGiropay
{
public static $name = 'GiroCheckout Credit Card';
public $class = 'GiroCheckoutCrCard';
protected $methodCode = 'creditCardTransaction';
}

View File

@@ -0,0 +1,14 @@
<?php
if (!class_exists('GiroCheckoutGiropay')) {
require_once 'class.GiroCheckoutGiropay.php';
}
class GiroCheckoutEps extends GiroCheckoutGiropay
{
public static $name = 'GiroCheckout EPS';
public $class = 'GiroCheckoutEps';
protected $methodCode = 'epsTransaction';
}

View File

@@ -0,0 +1,283 @@
<?php
use girosolution\GiroCheckout_SDK\GiroCheckout_SDK_Notify;
use girosolution\GiroCheckout_SDK\GiroCheckout_SDK_Request;
use KupShop\KupShopBundle\Config;
/**
* composer require girosolution/girocheckout-sdk.
*
* https://github.com/girosolution/girocheckout_sdk
* http://api.girocheckout.de/en:girocheckout:giropay:start
*/
class GiroCheckoutGiropay extends Payment
{
public static $name = 'GiroCheckout Giropay';
public $template = 'payment.OmnipayNestpay.tpl';
// protected $templateCart = 'payment.GiroCheckout.cart.tpl';
public $class = 'GiroCheckoutGiropay';
protected $methodCode = 'giropayTransaction';
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
// public function storePaymentInfo()
// {
// $data = parent::storePaymentInfo();
// $data['method'] = explode('-', getVal('payment_id'))[1];
//
// return $data;
// }
public function getPaymentUrl()
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 1,
'class' => $this->class,
]);
}
/* Payment steps */
public function processStep_1()
{
$returnUrl = createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 5,
'class' => $this->class,
'absolute' => true,
]);
$webhookUrl = createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 10,
'class' => $this->class,
'absolute' => true,
]);
$method = $this->determinePaymentMethod();
$methodSpecificConfig = $this->getMethodSpecificConfig($method);
$amount = roundPrice($this->order->getRemainingPayment())->asFloat();
$amountInCents = roundPrice($this->order->getRemainingPayment())->mul(DecimalConstants::hundred())->asInteger();
$paymentIDSuffix = uniqid('', true);
$this->createPayment(
null,
$amount,
['paymentClass' => static::class, 'method' => $method, 'IDSuffix' => $paymentIDSuffix]
);
// save session (use our identifier that we send as merchantTxId)
$session = $this->paymentId.'-'.$paymentIDSuffix;
$this->updateCustomPaymentData('session', $session);
// create request of selected type
$request = new GiroCheckout_SDK_Request($method);
$request->setSecret($methodSpecificConfig['projectPassphrase']); // String
$request->addParam('merchantId', $methodSpecificConfig['merchantID']) // Integer merchant ID of a giropay project
->addParam('projectId', $methodSpecificConfig['projectID']) // integer project ID of a giropay project
->addParam('merchantTxId', $session) // String(255) unique transaction id of the merchant
->addParam('amount', $amountInCents) // Integer if a decimal currency is used, the amount has to be in the smallest unit of value, eg. Cent, Penny
->addParam('currency', 'EUR') // String(3)
->addParam('purpose', 'Bestellung '.$this->orderId) // String(27)
// bic - This parameter must not be used anymore. All giropay transactions now use an external bank selection form!
->addParam('urlRedirect', $returnUrl) // URL, where the buyer has to be sent after payment
->addParam('urlNotify', $webhookUrl); // URL, where the notification has to be sent after payment
$request = $this->addRequestParams($request);
// the hash field is auto generated by the SDK
$request->submit();
if ($request->requestHasSucceeded()) {
// save gcReference (giropay request param "reference")
$this->updateCustomPaymentData(
'gcReference',
$request->getResponseParam('reference')
);
$request->redirectCustomerToPaymentProvider();
} else {
// if the transaction did not succeed, update your local system, get the responsecode and notify the customer
throw new Exception($request->getResponseMessage($request->getResponseParam('rc'), 'DE'));
}
}
protected function addRequestParams(GiroCheckout_SDK_Request $request): GiroCheckout_SDK_Request
{
return $request;
}
/**
* @param string $paymentIdentifier kupshopPaymentID-uniqueSuffix
*/
private function getPaymentByUniqueIdentifier(string $paymentIdentifier, bool $ignoreStatus = false): ?array
{
foreach ($this->order->getPaymentsArray() as $payment) {
$paymentData = json_decode($payment['payment_data']);
if (($payment['status'] == static::STATUS_CREATED || $payment['status'] == static::STATUS_PENDING || $ignoreStatus)
&& isset($paymentData->paymentClass)
&& $paymentData->paymentClass === static::class
&& $payment['id'] == explode('-', $paymentIdentifier)[0]
&& $paymentData->IDSuffix == (explode('-', $paymentIdentifier)[1] ?? null)
) {
$payment['decoded_data'] = $paymentData;
return $payment;
}
}
return null;
}
protected function updateCustomPaymentData($keyName, $value)
{
$paymentRow = sqlQueryBuilder()->select('*')->from('order_payments')
->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute()->fetch();
$data = json_decode($paymentRow['payment_data'] ?? '{}');
$data->$keyName = $value;
sqlQueryBuilder()->update('order_payments')
->directValues(['payment_data' => json_encode($data)])
->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute();
}
public function processStep_5()
{
$method = $this->determinePaymentMethod();
$methodSpecificConfig = $this->getMethodSpecificConfig($method);
$notify = new GiroCheckout_SDK_Notify($method);
$notify->setSecret($methodSpecificConfig['projectPassphrase']);
$notify->parseNotification($_GET);
$payment = $this->getPaymentByUniqueIdentifier($notify->getResponseParam('gcMerchantTxId'), true);
if (is_null($payment) || $payment['status'] == static::STATUS_STORNO) {
$this->step(-3, 'storno');
} elseif ($payment['status'] == static::STATUS_FINISHED) {
$this->success(translate('paymentSuccess', 'payment'));
}
$this->paymentId = $payment['id'];
$this->saveTransactionData($notify, $payment);
if ($notify->paymentSuccessful()) {
// change payment status to finished
if (!$this->setStatus(Payment::STATUS_FINISHED, $payment['decoded_data']->session)) {
throw new Exception('GiroCheckout::setStatus failed!');
}
$this->success(translate('paymentSuccess', 'payment'));
} else {
if (!$this->setStatus(Payment::STATUS_STORNO, $payment['decoded_data']->session)) {
throw new Exception('GiroCheckout::setStatus failed!');
}
$this->step(-3, 'storno');
}
}
/**
* GiroPurchase webhook handler - check for approved payment.
*/
public function processStep_10()
{
$this->setIsNotification(true);
$method = $this->determinePaymentMethod();
$methodSpecificConfig = $this->getMethodSpecificConfig($method);
$notify = new GiroCheckout_SDK_Notify($method);
$notify->setSecret($methodSpecificConfig['projectPassphrase']);
$notify->parseNotification($_GET);
$payment = $this->getPaymentByUniqueIdentifier($notify->getResponseParam('gcMerchantTxId'));
if (is_null($payment)) {
// send #400: The merchant did not process the notification and does not wish to be notified again.
$notify->sendBadRequestStatus();
exit;
}
$this->paymentId = $payment['id'];
$this->saveTransactionData($notify, $payment);
if ($notify->paymentSuccessful()) {
// change payment status to finished
if (!$this->setStatus(Payment::STATUS_FINISHED, $payment['decoded_data']->session)) {
throw new Exception('GiroCheckout::setStatus failed!');
}
// send #200: The notification was processed correctly.
$notify->sendOkStatus();
exit;
} else {
if (!$this->setStatus(Payment::STATUS_STORNO, $payment['decoded_data']->session)) {
throw new Exception('GiroCheckout::setStatus failed!');
}
// send #200: The notification was processed correctly.
$notify->sendOkStatus();
exit;
}
}
// public function getAvailableMethods(): array
// {
// $availableMethods = [];
// foreach ($this->config['methods'] as $requestType => $methodConfig) {
// $availableMethods[$requestType] = ['name' => $methodConfig['name'] ?? $requestType];
// }
//
// return $availableMethods;
// }
/**
* determine payment method (request type) with fallback to first available method.
*/
private function determinePaymentMethod(): string
{
// $method = $this->order->getDataAll()['payment_data']['method'] ?? null;
// $methods = $this->getAvailableMethods();
// reset($methods);
//
// return isset($methods[$method]) ? $method : key($methods);
return $this->methodCode;
}
private function getMethodSpecificConfig(?string $method = null): array
{
// return $this->config['methods'][$method ?? $this->determinePaymentMethod()];
return $this->config;
}
private function saveTransactionData(GiroCheckout_SDK_Notify $notify, array $payment): void
{
// save transaction reference ID and result code
$json = $payment['decoded_data'];
$json->resultCode = $notify->getResponseParam('gcResultPayment'); // http://api.girocheckout.de/en:girocheckout:resultcodes
$this->updateSQL('order_payments', ['payment_data' => json_encode($json)], ['id' => $payment['id']]);
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,40 @@
<?php
use girosolution\GiroCheckout_SDK\GiroCheckout_SDK_Request;
if (!class_exists('GiroCheckoutGiropay')) {
require_once 'class.GiroCheckoutGiropay.php';
}
class GiroCheckoutPaydirek extends GiroCheckoutGiropay
{
public static $name = 'GiroCheckout Paydirekt';
public $class = 'GiroCheckoutPaydirek';
protected $methodCode = 'paydirektTransaction';
public function accept($totalPrice, $freeDelivery)
{
$totalPrice = $totalPrice->getPriceWithVat()->asFloat();
if ($totalPrice <= 0 && $this->order) {
$totalPrice = $this->order->total_price;
}
// max 50000 EUR (http://api.girocheckout.de/girocheckout:paydirekt:start#initialisierung_einer_paydirekt_zahlung)
return parent::accept($totalPrice, $freeDelivery) && $totalPrice < 50000;
}
protected function addRequestParams(GiroCheckout_SDK_Request $request): GiroCheckout_SDK_Request
{
$request->addParam('shippingAddresseFirstName', $this->order->delivery_name) // Vorname des Addressaten, Pflicht bei Warenkorbtypen PHYSICAL, DIGITAL und MIXED, optional bei ANONYMOUS_DONATION und AUTHORITIES_PAYMENT.
->addParam('shippingAddresseLastName', $this->order->delivery_surname) // Nachname des Addressaten, Pflicht bei Warenkorbtypen PHYSICAL, DIGITAL und MIXED, optional bei ANONYMOUS_DONATION und AUTHORITIES_PAYMENT.
->addParam('shippingZipCode', $this->order->delivery_zip) // PLZ des Addressaten. Dies ist Pflicht bei Warenkörben der Typen PHYSICAL und MIXED, optional bei DIGITAL, ANONYMOUS_DONATION und AUTHORITIES_PAYMENT.
->addParam('shippingCity', $this->order->delivery_city) // Ort des Addressaten. Dies ist Pflicht bei Warenkörben der Typen PHYSICAL und MIXED, optional bei DIGITAL, ANONYMOUS_DONATION und AUTHORITIES_PAYMENT.
->addParam('shippingCountry', $this->order->delivery_country) // Ländercode (ISO 3166-1). Dies ist Pflicht bei Warenkörben der Typen PHYSICAL und MIXED, optional bei DIGITAL, ANONYMOUS_DONATION und AUTHORITIES_PAYMENT.
->addParam('shippingEmail', $this->order->invoice_email) // Email-Adresse des Käufers. Dies ist Pflicht bei digitalen Warenkörben (DIGITAL), bei allen anderen optional.
->addParam('orderId', $this->orderId); // Bestellnummer, zulässige Zeichen: A-Z a-z 0-9 + ? / - : ( ) . , ' (KEINE Blanks), maximale Länge: 20
return $request;
}
}

View File

@@ -0,0 +1,596 @@
<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\EANValidator;
use KupShop\OrderingBundle\Exception\PaymentException;
use KupShop\OrderingBundle\Util\Order\PaymentOrderSubMethodTrait;
/**
* !!!! POZOR !!!!
* Pokud se nasazuje na betu testovací GoPay, je potřeba mít v configu test => 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';
}
}

View File

@@ -0,0 +1,154 @@
<?php
use KupShop\KupShopBundle\Config;
class HelloBank extends Payment
{
public static $name = 'HelloBank';
public $template = 'payment.HelloBank.tpl';
protected $templateOrderView = 'payment.HelloBank.orderView.tpl';
public $class = 'HelloBank';
protected $pay_method = Payment::METHOD_INSTALLMENTS;
protected $url = 'https://www.cetelem.cz/cetelem2_webshop.php/zadost-o-pujcku/on-line-zadost-o-pujcku';
public function getCalcUrl(Decimal $price)
{
$price = roundPrice($price, -1, 'DB', 0)->asInteger();
if ($price < 2000) {
return null;
}
return path('hellobank_calc', ['price' => $price]);
}
public function getPaymentUrl(int $step = 1): string
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => $step,
'class' => $this->class,
]);
}
public function getGatewayUrl()
{
return $this->url;
}
public function getData()
{
return [
'kodProdejce' => $this->config['kodProdejce'],
'cenaZbozi' => roundPrice($this->order->getRemainingPaymentInCZK(), -1, 'DB', 0)->asInteger(),
'calc' => '1',
'url_back_ok' => createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 5,
'class' => $this->class,
'absolute' => true,
]),
'url_back_ko' => createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 6,
'class' => $this->class,
'absolute' => true,
]),
'obj' => $this->orderId,
'numklient' => $this->order->id_user,
'doprava' => '1', // $this->order->getDeliveryType()->isInPerson() ? '0' : '1',
];
}
public function accept($totalPrice, $freeDelivery)
{
$totalPrice = $totalPrice->getPriceWithVat()->asFloat();
if ($totalPrice <= 0 && $this->order) {
$totalPrice = $this->order->total_price;
}
return parent::accept($totalPrice, $freeDelivery) && $totalPrice >= 2000;
}
public function processStep_1()
{
}
public function processStep_5()
{
// success - authorized
$this->processResult(true);
$this->success('Žádost o úvěr byla schválena');
}
public function processStep_6()
{
// not authorized yet
$this->processResult(false);
$this->error('Žádost o úvěr zatím nebyla autorizována');
}
protected function processResult(bool $ok = false)
{
$remainingPayment = roundPrice($this->order->getRemainingPayment())->asFloat();
if ($remainingPayment > 0.00) {
$session = getVal('numwrk').'_'.uniqid();
$this->createPayment(
$session,
$remainingPayment,
['paymentClass' => self::class]
);
// save return params
$returnParams = ['stav', 'numaut', 'vdr', 'numwrk', 'jmeno', 'prijmeni', 'splatka', 'numklient', 'obj'];
$paymentRow = sqlQueryBuilder()->select('*')->from('order_payments')
->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute()->fetch();
$data = json_decode($paymentRow['payment_data'] ?? '{}');
$data->returnParams = [];
foreach ($returnParams as $param) {
$data->returnParams[$param] = getVal($param);
}
sqlQueryBuilder()->update('order_payments')->directValues(['payment_data' => json_encode($data)])
->where(\Query\Operator::equals(['id' => $this->paymentId]))->execute();
$paymentStatus = $ok ? Payment::STATUS_FINISHED : Payment::STATUS_PENDING;
// change payment status
if (!$this->setStatus($paymentStatus, $session)) {
logError(__FILE__, __LINE__, 'PayPal::updatePaymentStatus: setStatus failed!');
throw new \Exception('Set status failed');
}
}
}
public function startPayment()
{
return false;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,21 @@
<?php
class HeurekaPayment extends Payment
{
public static $name = 'Platba přes Heureku';
public $class = 'HeurekaPayment';
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
public static function isEnabled($className)
{
if (findModule(Modules::HEUREKA_CART) || findModule(Modules::DROPSHIP)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,18 @@
<?php
class Hotovost extends Payment
{
public static $name = 'Platba v hotovosti';
protected ?string $defaultIcon = '../../common/static/payments/prodejna_hotove.svg';
public $class = 'Hotovost';
public $method;
protected $pay_method = Payment::METHOD_CASH;
public function requiresEET()
{
return true;
}
}

View File

@@ -0,0 +1,227 @@
<?php
use Diglin\Intrum\CreditDecision\Request;
use Diglin\Intrum\CreditDecision\Response;
use Diglin\Intrum\CreditDecision\TransportV0017;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\Compat\SymfonyBridge;
class Intrum extends Payment
{
public static $name = 'Intrum';
public $template = 'payment.Intrum.tpl';
public $class = self::class;
protected $pay_method = Payment::METHOD_INVOICE;
public static $REQUEST_ENQUIRY_CREDIT_ASSESSMENT = 0;
public static $REQUEST_ENQUIRY_BUSINESS_TRANSACTION = 1;
public static $REQUEST_SEND_PAYMENT_TRANSACTION_RECEIPT = 3;
public static $REQUEST_SEND_PAYMENT_TRANSACTION_CANCEL = 6;
public $states = [
1 => 'There are serious negative indicators',
2 => 'All payment methods',
3 => 'Manual post-processing (currently not yet in use)',
4 => 'Postal address is incorrect',
5 => 'Enquiry exceeds the credit limit (the credit limit is specified in the cooperation agreement)',
6 => 'Customer specifications not met (currently not yet in use)',
7 => 'Enquiry exceeds the net credit limit (enquiry amount plus open items exceeds credit limit)',
8 => 'Person queried is not of creditworthy age',
9 => 'Delivery address does not match invoice address (for payment guarantee only)',
10 => 'Household cannot be identified at this address',
11 => 'Country is not supported',
12 => 'Party queried is not a natural person',
13 => 'System is in maintenance mode',
14 => 'Address with high fraud risk',
15 => 'Allowance is too low',
16 => 'Application data incomplete',
17 => 'Send contract documents for external credit check',
18 => 'External credit check in progress',
19 => 'Customer is on client blacklist',
20 => 'Customer is on client whitelist',
21 => 'Customer is on Intrum blacklist',
22 => 'Address is a P.O. box',
23 => 'Address not in residential area',
24 => 'Ordering person not legitimated',
25 => 'IP Address temporarily blacklisted',
50 => 'Blacklist WSNP (NL only)',
51 => 'Blacklist Bankruptcy (NL only)',
52 => 'Blacklist Fraud (NL only)',
];
/**
* @throws Exception
*/
public function check(CartBase $cart)
{
// skip if not last step
$step = end($cart->steps);
if (!($step ?? false) || !($step['selected'] ?? false)) {
return;
}
if (empty($cart->invoice['birthdate'])
|| DateTime::createFromFormat('d-m-Y', $cart->invoice['birthdate']) === false
) {
addUserMessage(replacePlaceholders(translate('checkDataOrChangeDelivery', 'payment'), ['URL' => '/'.$cart->steps['user']['url']]), 'danger');
redirection($cart->steps['delivery']['url']);
}
$hash = $this->createHash(array_merge($cart->invoice, [$cart->totalPricePay->printValue()]));
$oldHash = $cart->getData($this->class.'_hash');
$counter = $cart->getData($this->class.'_counter');
try {
if (isset($oldHash) && $oldHash === $hash) {
$requestID = $cart->getData($this->class.'_enquiryCreditRequest');
$requestResponse = $this->selectSQL('intrum_requests', ['id' => $requestID])->fetch()['response'];
if ($requestResponse) {
$this->checkRawResponse($requestResponse, $requestID);
} else {
$cart->setData($this->class.'_hash', null);
$cart->setData($this->class.'_enquiryCreditRequest', null);
$cart->setData($this->class.'_counter', null);
$cart->setData($this->class.'_counter', 1);
$this->enquiryCreditAssessment($cart, $hash);
}
return;
}
if (!isset($counter) || $counter <= 2) {
$cart->setData($this->class.'_counter', ($counter ?? 0) + 1);
$this->enquiryCreditAssessment($cart, $hash);
} else {
addUserMessage(mb_ucfirst(translate('changePaymentMethod', 'payment')), 'danger');
redirection($cart->steps['delivery']['url']);
}
} catch (RuntimeException $e) {
addUserMessage(replacePlaceholders(translate('checkDataOrChangeDelivery', 'payment'), ['URL' => '/'.$cart->steps['user']['url']]), 'danger');
redirection($cart->steps['delivery']['url']);
}
}
/**
* @return bool TRUE if response status is 2 (All payment methods)
*
* @throws Exception
*/
private function checkRawResponse(string $rawResponse, int $requestID): bool
{
$response = new Response();
$response->setRawResponse($rawResponse);
$response->processResponse();
if ($response->getCustomerRequestStatus() !== 2) {
throw new RuntimeException($this->states[$response->getCustomerRequestStatus()] ?? 'Unknown Intrum error in request '.$requestID.'.');
}
return true;
}
/**
* @throws Exception
*/
private function enquiryCreditAssessment(CartBase $cart, string $hash)
{
$streetArray = explode(' ', $cart->invoice['street']);
$houseNumber = count($streetArray) > 1 ? array_pop($streetArray) : ' ';
$firstLine = join(' ', $streetArray);
$birthdate = DateTime::createFromFormat('d-m-Y', $cart->invoice['birthdate'])->format('Y-m-d');
$data = [
'customer_reference' => 'uid_'.uniqid(),
'person' => [
'first_name' => $cart->invoice['name'],
'last_name' => $cart->invoice['surname'],
'gender' => $cart->invoice['gender'] === 'female' ? 2 : 1,
'date_of_birth' => $birthdate, // YYYY-MM-DD
'current_address' => [
'first_line' => $firstLine,
'house_number' => $houseNumber,
'post_code' => $cart->invoice['zip'],
'country_code' => $cart->invoice['country'],
'town' => $cart->invoice['city'],
],
'communication_numbers' => [
'mobile' => $cart->invoice['phone'],
'email' => $cart->invoice['email'],
],
'extra_info' => [
[
'name' => 'ORDERCLOSED',
'value' => 'NO',
],
[
'name' => 'ORDERAMOUNT',
'value' => $cart->totalPricePay->printValue(2),
],
[
'name' => 'ORDERCURRENCY',
'value' => $cart->invoice['currency'],
],
[
'name' => 'IP',
// 'value' => $_SERVER['REMOTE_ADDR'],
'value' => explode(',', SymfonyBridge::getCurrentRequest()->getClientIp())[0],
],
],
],
];
$uid = uniqid(null, true);
$this->insertSQL('intrum_requests', ['uid' => $uid, 'type' => static::$REQUEST_ENQUIRY_CREDIT_ASSESSMENT]);
$requestID = sqlInsertId();
$dom = new \DOMDocument('1.0', 'UTF-8');
/* @var $request Request */
$request = $dom->appendChild(new Request());
$request->setVersion('1.00');
$request->setClientId($this->config['clientId']);
$request->setUserID($this->config['userID']);
$request->setPassword($this->config['password']);
$request->setEmail($this->config['email']);
$request->setRequestId($uid);
$request->createRequest($data);
$requestXML = $dom->saveXML();
$this->updateSQL('intrum_requests', ['request' => $requestXML], ['id' => $requestID]);
$transport = new TransportV0017();
$transport->setMode($this->config['mode'] ?? 'test');
$responseRequest = $transport->sendRequest($requestXML);
$successful = false;
try {
$successful = $this->checkRawResponse($responseRequest, $requestID);
} finally {
$this->updateSQL('intrum_requests', [
'response' => $responseRequest,
'successful' => $successful,
], ['id' => $requestID]);
$cart->setData($this->class.'_enquiryCreditRequest', $requestID);
$cart->setData($this->class.'_hash', $hash);
}
}
private function createHash(array $data)
{
array_multisort($data);
return md5(json_encode($data));
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,62 @@
<?php
class OmnipayNestpay extends \KupShop\OrderingBundle\OmniPay
{
public static $name = 'Nestpay platební brána';
public $template = 'payment.OmnipayNestpay.tpl';
// protected $templateCart = 'payment.OmnipayNestpay.cart.tpl';
public $class = 'OmnipayNestpay';
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
public static function getOmnipayName(): string
{
return 'Nestpay';
}
public function configureGateway(Omnipay\Common\GatewayInterface $gateway, Order $order): Omnipay\Common\GatewayInterface
{
$gateway->setBank($this->config['bank']);
$gateway->setClientId($this->config['clientId']);
$gateway->setStoreKey($this->config['storeKey']);
return $gateway;
}
public function getGatewayOptions(Order $order): array
{
$returnUrl = $this->getGenericPaymentUrl(5);
$options = [
'amount' => $this->order->convertPriceToEUR(roundPrice($this->order->getRemainingPayment())->asFloat()),
'currency' => $this->config['currency'],
'orderid' => $this->order->order_no,
'returnUrl' => $returnUrl,
'cancelUrl' => $returnUrl,
'shopUrl' => $returnUrl,
'lang' => $this->config['lang'],
];
if (!empty($this->order->invoice_name)) {
$options['billToName'] = $this->order->invoice_name.' '.$this->order->invoice_surname;
}
if (!empty($this->order->invoice_firm)) {
$options['billToCompany'] = $this->order->invoice_firm;
}
return $options;
}
protected function createPaymentFromResponse(Omnipay\Common\Message\ResponseInterface $response)
{
$this->createPayment(
$response->getTransactionReference(),
$this->order->convertPriceFromEUR($response->getData()['amount']),
[]
);
}
}

View File

@@ -0,0 +1,168 @@
<?php
use KupShop\KupShopBundle\Config;
/**
* Requires composer package "mattiabasone/pagonline: ^1.0".
*/
class PagOnline extends Payment
{
public static $name = 'PagOnline Imprese platební brána';
public $template = 'payment.OmnipayNestpay.tpl';
public $class = 'PagOnline';
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl($step = 1)
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => $step,
'class' => $this->class,
'paymentId' => $this->paymentId,
'absolute' => true,
]);
}
private function getPayment(bool $new = false)
{
$kupshopPayment = $this->getPendingPayment();
if ($kupshopPayment && $new) {
$status = Payment::STATUS_UNKNOWN;
sqlQuery('UPDATE '.getTableName('order_payments')." SET status={$status}, date=NOW() WHERE id={$kupshopPayment['id']}");
$kupshopPayment = false;
}
if ($kupshopPayment && !$new) {
return $kupshopPayment;
} else {
$this->createPayment(
null,
$amount = roundPrice($this->order->getRemainingPayment())->asFloat(),
['paymentClass' => self::class]
);
$paymentRow = $this->selectSQL('order_payments', ['id' => $this->paymentId])->fetch();
$json = json_decode($paymentRow['payment_data'], true);
$json['session'] = $this->paymentId;
$json['shopID'] = $this->order->id.'_'.$paymentRow['id'].'_'.uniqid();
$paymentRow['payment_data'] = json_encode($json);
$this->updateSQL('order_payments', $paymentRow, ['id' => $this->paymentId]);
return $this->getPendingPayment();
}
}
public function savePaymentData(array $data, array $payment)
{
$paymentRow = $this->selectSQL('order_payments', ['id' => $payment['id']])->fetch();
$json = json_decode($paymentRow['payment_data'], true);
$json = array_merge($json, $data);
$paymentRow['payment_data'] = json_encode($json);
$this->updateSQL('order_payments', $paymentRow, ['id' => $payment['id']]);
}
/* Payment steps */
public function processStep_1()
{
$merchangConfig = $this->getMerchantConfig();
$amount = roundPrice($this->order->getRemainingPayment())->asFloat();
if ($amount <= (float) 0) {
$this->step(-3, 'storno');
}
$payment = $this->getPayment(true);
// Initialize the payment page
// See https://github.com/mattiabasone/PagOnline
$init = new \PagOnline\Init\IgfsCgInit();
$init->serverURL = $merchangConfig['url'];
$init->tid = $merchangConfig['tid'];
$init->kSig = $merchangConfig['kSig'];
$init->shopID = $payment['decoded_data']->shopID;
$init->shopUserRef = $this->order->invoice_email;
$init->shopUserName = $this->order->invoice_surname.','.$this->order->invoice_name;
$init->trType = 'AUTH';
$init->currencyCode = 'EUR';
$init->amount = (int) ($amount * 100); // Amount without comma (500 = 5,00)
$init->langID = 'IT';
$init->notifyURL = $this->getPaymentUrl(2);
$init->errorURL = $this->getPaymentUrl(-3);
// $init->addInfo1 = 'myFirstAddintionalInfo';
if (!$init->execute()) {
// Something went wrong, save error
$this->savePaymentData([
'error' => $init->errorDesc,
'rc' => $init->rc,
], $payment);
$this->step(-3, 'storno');
} else {
// save PagOnline PaymentID
$this->savePaymentData(['session' => $init->paymentID], $payment);
// Redirect user to payment gateway
redirection($init->redirectURL);
}
}
public function processStep_2()
{
$merchangConfig = $this->getMerchantConfig();
$payment = $this->getPayment();
$verify = new \PagOnline\Init\IgfsCgVerify();
$verify->setRequestTimeout(15);
$verify->serverURL = $merchangConfig['url'];
$verify->tid = $merchangConfig['tid']; // per servizio MyBank usare UNI_MYBK
$verify->kSig = $merchangConfig['kSig'];
$verify->shopID = $payment['decoded_data']->shopID;
$verify->paymentID = $payment['decoded_data']->session;
if (!$verify->execute()) {
// save error
$this->savePaymentData([
'error' => $verify->errorDesc,
'rc' => $verify->rc,
], $payment);
$this->step(-3, 'storno');
return;
}
if (!$this->setStatus(Payment::STATUS_FINISHED, $verify->paymentID)) {
throw new Exception('PagOnline::updatePaymentStatus: setStatus failed!');
}
$this->success(translate('paymentSuccess', 'payment'));
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
protected function getMerchantConfig(): array
{
$defaultTestConfig = [
'url' => 'https://testeps.netswgroup.it/UNI_CG_SERVICES/services',
'tid' => 'UNI_ECOM',
'kSig' => 'UNI_TESTKEY',
];
return array_merge($defaultTestConfig, $this->config);
}
}

View File

@@ -0,0 +1,600 @@
<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\OrderList\OrderList;
use PayPal\Core\PayPalConstants;
use Query\Operator;
use Symfony\Component\Routing\Router;
/**
* Dependencies: `composer require paypal/rest-api-sdk-php=^1.14`
* Example config: $cfg['Modules']['payments']['PayPal'] = [
* 'clientID' => 'client id',
* 'secret' => 'client secret',
* webProfileID - create temporary (3 hours): symfony kupshop:paypal-create-web-profile
* or permanent profile: symfony kupshop:paypal-create-web-profile --permanent
* optional image path as an argument: symfony kupshop:paypal-create-web-profile --permanent templates/images/logo.png
* 'webProfileID' => 'web profile id',
* 'mode' => 'sandbox' OR 'live',
* 'enableLog' => false,
* ].
*/
class PayPal extends Payment
{
public static $name = 'PayPal platební brána';
public $template = 'payment.PayPal.tpl';
public $class = 'PayPal';
public $tp_id_payment;
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
private $apiContext;
protected $allowedCurrencies = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD',
'HUF', 'ILS', 'JPY', 'MYR', 'MXN', 'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP',
'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD', ];
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'clientID' => [
'title' => 'ID klienta (clientID)',
'type' => 'text',
],
'secret' => [
'title' => 'Secret',
'type' => 'text',
],
'mode' => [
'title' => 'Režim',
'type' => 'select',
'options' => ['live' => 'Produkční režim', 'sandbox' => 'Testovaci režim'],
],
'tracking' => [
'title' => 'Tracking info',
'type' => 'toggle',
'tooltip' => 'Odesílat číslo balíku z e-shopu k platbě do PayPalu.',
],
'webhook' => [
'title' => 'Webhook URL',
'text' => path('kupshop_ordering_payment_legacypayment', ['class' => 'PayPal', 'step' => 10], Router::ABSOLUTE_URL),
],
],
];
}
/* Payment steps */
public function processStep_1()
{
$apiContext = $this->getApiContext();
// always create new paypal payment in step 1
// $kupshopPayment = $this->getPendingPayment();
$kupshopPayment = false;
if ($kupshopPayment) {
// use already created payment
$payment = $this->getPayPalPayment($apiContext, $kupshopPayment['decoded_data']->session);
} else {
// create new payment
$dbcfg = Settings::getDefault();
if ($this->order->getRemainingPayment() <= 0) {
$this->success(translate('paymentSuccess', 'payment'));
}
$payerInfoAddress = new \PayPal\Api\Address();
$payerInfoAddress->setState($this->order->invoice_state ?? '')
->setCountryCode($this->order->invoice_country)
->setCity($this->order->invoice_city)
->setLine1($this->order->invoice_street)
->setLine2($this->order->invoice_custom_address ?? '')
->setPostalCode($this->order->invoice_zip);
$payerInfo = new \PayPal\Api\PayerInfo();
$payerInfo->setEmail($this->order->getUserEmail())
->setBillingAddress($payerInfoAddress)
->setFirstName($this->order->invoice_name)
->setLastName($this->order->invoice_surname);
$payer = new \PayPal\Api\Payer();
$payer->setPaymentMethod('paypal')
->setPayerInfo($payerInfo);
if ($useOriginalCurrency = in_array($this->order->currency, $this->allowedCurrencies)) {
$totalPrice = $this->order->getRemainingPayment();
} else {
$totalPrice = $this->convertPriceToEUR(
(float) $this->order->getRemainingPayment()
);
}
// shipping address
$shippingAddress = new \PayPal\Api\ShippingAddress();
$shippingAddress->setState($this->order->delivery_state ?? '')
->setRecipientName(implode(' ', [$this->order->delivery_name, $this->order->delivery_surname]))
->setCountryCode($this->order->delivery_country)
->setCity($this->order->delivery_city)
->setLine1($this->order->delivery_street)
->setLine2($this->order->delivery_custom_address ?? '')
->setPostalCode($this->order->delivery_zip);
// Set item list
$item1 = new \PayPal\Api\Item();
$item1->setName('Payment for order '.$this->getOrderNumber().' on '.$dbcfg->shop_firm_name)
->setCurrency($useOriginalCurrency ? $this->order->currency : 'EUR')
->setQuantity(1)
->setPrice($totalPrice);
$itemList = new \PayPal\Api\ItemList();
$itemList->setItems([$item1])
->setShippingAddress($shippingAddress);
// to set payment details see https://developer.paypal.com/docs/api/quickstart/create-process-order/
$amount = new \PayPal\Api\Amount();
$amount->setCurrency($useOriginalCurrency ? $this->order->currency : 'EUR')
->setTotal($totalPrice);
$transaction = new \PayPal\Api\Transaction();
$transaction->setAmount($amount)
->setDescription('Payment for order '.$this->getOrderNumber().' on '.$dbcfg->shop_firm_name)
->setInvoiceNumber($this->getOrderNumber())
->setItemList($itemList);
$redirectUrls = new \PayPal\Api\RedirectUrls();
$redirectUrls->setReturnUrl(
createScriptURL(
[
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 2,
'class' => $this->class,
'absolute' => true,
]
)
)
->setCancelUrl(
createScriptURL(
[
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 4,
'class' => $this->class,
'absolute' => true,
]
)
);
$payment = new \PayPal\Api\Payment();
$payment->setIntent('sale')
->setPayer($payer)
->setRedirectUrls($redirectUrls)
->setTransactions([$transaction]);
if (isset($this->config['webProfileID'])) {
$payment->setExperienceProfileId($this->config['webProfileID']);
}
// nechci umoznit nastavit pri platbe dorucovaci adresu, protoze oni si ji pak nastavi a ocekavaji, ze jim
// to prijde na tu adresu co nastavili v PayPalu. Takze bysme museli tu adresu tahat zpatky do e-shopu...
// a jednodusi je jim tu zmenu neumoznit
// bohuzel to musim udelat takhle osklive, protoze ta knihovna tu optionu neumoznuje nastavit nejakym setterem
$payment->application_context = [
// If available, uses the merchant-provided shipping address, which the customer cannot change on the PayPal pages.
// If the merchant does not provide an address, the customer can enter the address on PayPal pages.
'shipping_preference' => 'SET_PROVIDED_ADDRESS',
];
try {
$payment->create($apiContext);
} catch (PayPal\Exception\PayPalConnectionException $e) {
$data = json_decode($e->getData(), true);
if (($data['name'] ?? false) === 'VALIDATION_ERROR') {
logError(__FILE__, __LINE__, 'PayPal exception: '.print_r($e->getData(), true));
$this->error(translate('PayPalConnectionException', 'paypal'));
} else {
throw $e;
}
}
// save payment
$transaction = $payment->getTransactions()[0];
if ($useOriginalCurrency) {
$paymentAmount = $transaction->getAmount()->getTotal();
} else {
$paymentAmount = $this->order->convertPriceFromEUR($transaction->getAmount()->getTotal());
}
$this->createPayment(
$payment->getId(),
$paymentAmount,
['paymentClass' => self::class]
);
}
// redirect user to PayPal for the payment approval
$approvalUrl = $payment->getApprovalLink();
redirection($approvalUrl);
}
/**
* Success return from paypal - execute payment.
*/
public function processStep_2()
{
logError(__FILE__, __LINE__, 'PayPal response: '.print_r([$_GET, $_POST, $_SERVER], true));
$apiContext = $this->getApiContext();
$payPalPaymentID = getVal('paymentId', null, false);
$this->processPayment($this->getPayPalPayment($apiContext, $payPalPaymentID), true);
}
/**
* Waiting for approval - current payment status is created.
*/
public function processStep_3()
{
$apiContext = $this->getApiContext();
$payPalPaymentID = getVal('paymentId', null, false);
if (!$payPalPaymentID) {
$kupshopPayment = $this->getPendingPayment();
if ($kupshopPayment) {
// use already created payment
$payPalPaymentID = $kupshopPayment['decoded_data']->session;
} else {
$this->step(-3, 'storno');
}
}
$this->processPayment($this->getPayPalPayment($apiContext, $payPalPaymentID));
}
/**
* Storno.
*/
public function processStep_4()
{
$apiContext = $this->getApiContext();
$payPalPaymentID = getVal('paymentId', null, false);
if (!$payPalPaymentID) {
$kupshopPayment = $this->getPendingPayment();
if ($kupshopPayment) {
// use already created payment
$payPalPaymentID = $kupshopPayment['decoded_data']->session;
} else {
$this->step(-3, 'storno');
}
}
$payment = $this->getPayPalPayment($apiContext, $payPalPaymentID);
$payment->setState('failed');
$this->processPayment($payment);
}
/**
* PayPal webhook handler - check for approved payment.
*/
public function processStep_10()
{
$logger = ServiceContainer::getService('logger');
$logger->notice('PayPal: webhook', [
'data' => $this->request->getContent(),
]);
$this->isNotification = true;
$data = json_decode($this->request->getContent(), true);
// only process PAYMENT.SALE.COMPLETED and payment must already exist
if ($data['event_type'] !== 'PAYMENT.SALE.COMPLETED'
|| empty($data['resource']['parent_payment'])
) {
return;
}
$apiContext = $this->getApiContext();
$payPalPaymentID = $data['resource']['parent_payment'];
$payment = $this->getPayPalPayment($apiContext, $payPalPaymentID, false);
if (!$payment) {
return;
}
if (!($orderId = $this->getOrderId($payPalPaymentID))) {
$this->sendNotificationResponse(400, 'Order not found!');
}
// set order - getStatus method needs it
$this->setOrder($orderId);
// only proceed if payment already exists
if (!$this->getStatus($payPalPaymentID)) {
return;
}
$this->processPayment(
$payment,
false,
false
);
$this->sendNotificationResponse(200, 'OK');
}
/**
* Process payment procedure
* (used in step 2 - after return from PayPal, step 3 - waiting for approval, step 4 - storno and step 10 - webhook).
*
* @throws Exception
*/
protected function processPayment(
PayPal\Api\Payment $payment,
bool $enableRedirectToStep3 = false,
bool $enableRedirects = true,
) {
$this->tp_id_payment = $payment->getId();
$paymentState = $payment->getState();
if ($paymentState === 'created') {
$apiContext = $this->getApiContext();
// Execute payment with payer id
$execution = new \PayPal\Api\PaymentExecution();
$execution->setPayerId($payment->getPayer()->getPayerInfo()->getPayerId());
try {
// Execute payment
$payment->execute($execution, $apiContext);
} catch (Exception $ex) {
logError(__FILE__, __LINE__, 'PayPal exception: '.$ex->getMessage());
if ($ex instanceof \PayPal\Exception\PayPalConnectionException) {
logError(__FILE__, __LINE__, 'PayPal exception: '.print_r($ex->getData(), true));
}
$this->step(-3, 'storno');
}
}
// determine kupshop unified payment state from paypal payment state
switch ($paymentState) {
case 'created':
$this->status = $unifiedState = Payment::STATUS_CREATED;
break;
case 'approved':
$this->status = $unifiedState = Payment::STATUS_FINISHED;
break;
case 'failed':
$this->status = $unifiedState = Payment::STATUS_STORNO;
break;
default:
logError(__FILE__, __LINE__, 'PayPal\Api\Payment invalid state "'.$paymentState.'"');
if ($enableRedirects) {
$this->step(-3, 'storno');
}
return;
}
// change payment status
if (!$this->setStatus($unifiedState, $this->tp_id_payment)) {
logError(__FILE__, __LINE__, 'PayPal::updatePaymentStatus: setStatus failed!');
throw new \Exception('Set status failed');
}
if ($enableRedirects) {
switch ($unifiedState) {
case Payment::STATUS_FINISHED:
if ($sale = $payment->getTransactions()[0]->getRelatedResources()[0]->getSale()) {
if ($transactionID = $sale->getId()) {
$payment_data = $this->selectSQL('order_payments', ['id' => $this->paymentId], ['payment_data'])->fetchColumn();
$payment_data = json_decode($payment_data, true);
$payment_data['transactionID'] = $transactionID;
$this->updateSQL('order_payments', ['payment_data' => json_encode($payment_data)], ['id' => $this->paymentId]);
}
}
$this->success(translate('paymentSuccess', 'payment'));
break;
case Payment::STATUS_STORNO:
$this->step(-3, 'storno');
break;
case Payment::STATUS_CREATED:
if ($enableRedirectToStep3) {
$this->step(3, 'wait', ['paymentId' => $payment->getId()]);
}
break;
}
}
}
/**
* @return false|\PayPal\Api\Payment
*/
protected function getPayPalPayment(
PayPal\Rest\ApiContext $apiContext,
$payPalPaymentID,
bool $enableRedirects = true,
) {
try {
$payment = \PayPal\Api\Payment::get($payPalPaymentID, $apiContext);
} catch (Exception $ex) {
logError(__FILE__, __LINE__, 'PayPal\Api\Payment::get exception');
if ($enableRedirects) {
$this->step(-3, 'storno');
}
return false;
}
return $payment;
}
/**
* @return \PayPal\Rest\ApiContext
*/
public function getApiContext()
{
if (!isset($this->apiContext)) {
$apiContext = new PayPal\Rest\ApiContext(
new \PayPal\Auth\OAuthTokenCredential($this->config['clientID'], $this->config['secret'])
);
$apiConfig = [
'mode' => isset($this->config['mode']) ? $this->config['mode'] : 'sandbox',
'cache.enabled' => true,
// 'http.CURLOPT_CONNECTTIMEOUT' => 30
// 'http.headers.PayPal-Partner-Attribution-Id' => '123123123'
// 'log.AdapterFactory' => '\PayPal\Log\DefaultLogFactory' // Factory class implementing \PayPal\Log\PayPalLogFactory
];
if (isset($this->config['enableLog']) && $this->config['enableLog']) {
$apiConfig['log.LogEnabled'] = true;
$apiConfig['log.FileName'] = './PayPal.log';
$apiConfig['log.LogLevel'] = ($apiConfig['mode'] == 'sandbox') ? 'DEBUG' : 'INFO'; // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
}
$apiContext->setConfig($apiConfig);
$this->apiContext = $apiContext;
}
return $this->apiContext;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
public function sendOrderTrackings(): void
{
if (($this->config['tracking'] ?? 0) != 1) {
return;
}
$orderList = ServiceContainer::getService(OrderList::class);
$orderList->andSpec(function (Query\QueryBuilder $qb) {
$qb->join('o', 'delivery_type', 'dt', 'dt.id = o.id_delivery')
->join('dt', 'delivery_type_payment', 'dtp_paypal_filter', 'dtp_paypal_filter.id = dt.id_payment');
return Operator::andX(
// musi byt vyplnene tracking cislo
'o.package_id IS NOT NULL',
// filtruju objednavky, u kterych nebyl tracking jeste odeslany
'JSON_VALUE(o.note_admin, "$.paypalTrackingSent") IS NULL',
// filtruju pouze objednavky s PayPal platbou
Operator::equals(['dtp_paypal_filter.class' => $this->class]),
// beru v potaz objednavky za max. posledni tyden
'o.date_handle IS NOT NULL AND o.date_handle >= DATE(NOW() - INTERVAL 7 DAY)',
// objednavka musi byt vyrizena
Operator::inIntArray(getStatuses('handled'), 'o.status'),
// objednavka musi byt zaplacena
Operator::equals(['o.status_payed' => 1]),
// objednavka nesmi byt osobni odber
Operator::not(\Query\Order::byInPersonDelivery())
);
});
foreach ($orderList->getOrders() as $order) {
$this->sendOrderTrackingToPayPal($order);
}
}
public function sendOrderTrackingToPayPal(Order $order): bool
{
$apiContext = $this->getApiContext();
$token = $apiContext->getCredential()->getAccessToken($apiContext->getConfig());
$url = $this->config['mode'] === 'sandbox' ? PayPalConstants::REST_SANDBOX_ENDPOINT : PayPalConstants::REST_LIVE_ENDPOINT;
[$carrier, $carrierOtherName] = $this->getOrderCarrier($order);
$orderData = [
'status' => 'SHIPPED',
'carrier' => $carrier,
'carrier_other_name' => $carrierOtherName,
'last_update_time' => $order->date_handle->format('c'),
'tracking_number' => $order->package_id,
];
$trackers = [];
$payments = array_filter($order->getPaymentsArray(), fn ($x) => $x['status'] == Payment::STATUS_FINISHED);
foreach ($payments as $payment) {
$paymentData = json_decode($payment['payment_data'] ?: '', true) ?: [];
if (empty($paymentData['transactionID'])) {
throw new RuntimeException('PayPal payment is missing "transactionID"! Order: '.$order->order_no);
}
$trackers[] = array_merge($orderData, ['transaction_id' => $paymentData['transactionID']]);
}
if (empty($trackers)) {
return false;
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url.'v1/shipping/trackers-batch');
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer '.$token,
]);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode(['trackers' => $trackers]));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($curl);
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$result = json_decode($response ?: '', true) ?: [];
if (!in_array($responseCode, [200, 201])) {
addActivityLog(
ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_COMMUNICATION,
'[PayPal] Nepodařila se odeslat informace o číslu balíku! Objednávka: '.$order->order_no,
['errors' => $result['errors'] ?? []]);
return false;
}
$order->setData('paypalTrackingSent', true);
$order->logHistory('[PayPal] Byla odeslána informace o číslu balíku z e-shopu do PayPalu.');
return true;
}
protected function getOrderCarrier(Order $order): array
{
$delivery = null;
if ($deliveryType = $order->getDeliveryType()) {
$delivery = $deliveryType->getDelivery();
}
if ($delivery instanceof PPL) {
return ['PPL', null];
} elseif ($delivery instanceof DHL) {
return ['DHL', null];
} elseif ($delivery instanceof BalikDoRuky) {
return ['CESKA_CZ', null];
} elseif ($delivery instanceof GLS && $order->delivery_country === 'CZ') {
return ['GLS_CZ', null];
}
return ['OTHER', $delivery->name];
}
}

View File

@@ -0,0 +1,30 @@
<?php
if (!class_exists('PayPal')) {
require_once 'class.PayPal.php';
}
/**
* Dependencies: `composer require paypal/paypal-checkout-sdk`
* enable PayPalCheckoutBundle (payments submodule Modules::SUB_PAYPAL_CHECKOUT)
* $cfg['Modules']['payments']['PayPalCheckout'] = [
* 'delivery_type_id' => 3,
* 'clientID' => 'clientID',
* 'secret' => 'secret',
* 'mode' => 'sandbox',
* ];.
*/
class PayPalCheckout extends PayPal
{
public static $name = 'PayPal Checkout platební brána';
public $template = 'payment.PayPal.tpl';
public $class = 'PayPalCheckout';
public $tp_id_payment;
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
}

View File

@@ -0,0 +1,392 @@
<?php
declare(strict_types=1);
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use Symfony\Component\Routing\Router;
require_once 'class.PayPal.php';
/**
* Dependencies: `composer require paypal/rest-api-sdk-php=^1.14`
* Example config: $cfg['Modules']['payments']['PayPal'] = [
* 'clientID' => 'client id',
* 'secret' => 'client secret',
* webProfileID - create temporary (3 hours): symfony kupshop:paypal-create-web-profile
* or permanent profile: symfony kupshop:paypal-create-web-profile --permanent
* optional image path as an argument: symfony kupshop:paypal-create-web-profile --permanent templates/images/logo.png
* 'webProfileID' => 'web profile id',
* 'mode' => 'sandbox' OR 'live',
* 'enableLog' => false,
* ].
*/
class PayPalPlus extends PayPal
{
public static $name = 'PayPal Plus platební brána';
protected $templateCart = 'payment.PayPalPlus.cart.tpl';
protected $templateOrderView = 'payment.PayPalPlus.orderView.tpl';
protected $templateInit = 'payment.PayPalPlus.init.tpl';
public $class = 'PayPalPlus';
/** Vytáhne existující, nebo vytvoří novou platbu */
public function getPayment()
{
$cart = ServiceContainer::getService(\KupShop\OrderingBundle\Cart::class);
// Preferuj ID z objednavky
if (isset($this->order) && ($this->order->getData('paypalplus')['id'] ?? false)) {
$payment = $this->getPayPalPayment($this->getApiContext(), $this->order->getData('paypalplus')['id']);
} elseif ($cart->getData('paypalplus') && $cart->getData('paypalplus')['id']) {
$payment = $this->getPayPalPayment($this->getApiContext(), $cart->getData('paypalplus')['id']);
} else {
$payment = $this->createPayPalPayment();
$cart->setData('paypalplus', ['id' => $payment->getId(), 'token' => $payment->getToken()]);
}
return $payment;
}
public function getPaymentUrl()
{
if (empty($this->order)) {
throw new \KupShop\OrderingBundle\Exception\PaymentException(translate('sameDevicePayment', 'payment'));
}
return path('kupshop_content_orders_order',
['id' => $this->order->id, 'cf' => $this->order->getSecurityCode(), 'status' => 1, 'immediate_pay' => 1]);
}
/** Pošle patch request pokud není platba aktualizovana - nema security code */
public function makeSurePaymentIsUpdated()
{
$payment = $this->getPayment();
if (empty($payment->getTransactions()[0]->getCustom())) {
return $this->updatePayment();
}
return $payment;
}
/** Vytvoří payment s dummy daty - protože se musí tvořit už v košíku */
protected function createPayPalPayment($useOriginalCurrency = true, $totalPrice = 0): PayPal\Api\Payment
{
$apiContext = $this->getApiContext();
$dbcfg = Settings::getDefault();
if ($this->order && $remainingPayment = roundPrice($this->order->getRemainingPayment())->asFloat() <= 0) {
$this->success(translate('paymentSuccess', 'payment'));
}
$payer = new \PayPal\Api\Payer();
$payer->setPaymentMethod('paypal');
$amount = new \PayPal\Api\Amount();
$amount->setCurrency('EUR')->setTotal('2.00');
$item1 = new \PayPal\Api\Item();
$item1->setName('Payment for order '.($this->order ? $this->order->order_no : '').' on '.$dbcfg->shop_firm_name)
->setCurrency('EUR')
->setQuantity(1)
->setPrice($totalPrice);
$itemList = new \PayPal\Api\ItemList();
$itemList->setItems([$item1]);
$transaction = new \PayPal\Api\Transaction();
$transaction->setAmount($amount)
->setDescription('Payment for order')
->setInvoiceNumber('123456')
->setItemList($itemList);
$redirectUrls = new \PayPal\Api\RedirectUrls();
$redirectUrls->setReturnUrl(
path('kupshop_ordering_payment_legacypayment',
array_merge([
'step' => 2,
'class' => $this->class,
], []),
Router::ABSOLUTE_URL)
)->setCancelUrl(
path('kupshop_ordering_payment_legacypayment',
array_merge([
'step' => 4,
'class' => $this->class,
], []),
Router::ABSOLUTE_URL)
);
$payment = new \PayPal\Api\Payment();
$payment->setIntent('sale')
->setPayer($payer)
->setTransactions([$transaction])
->setRedirectUrls($redirectUrls);
try {
$payment->create($apiContext);
} catch (PayPal\Exception\PayPalConnectionException $e) {
$data = $e->getData();
if ($data) {
$data = json_decode($data, true);
}
if (($data['name'] ?? false) === 'VALIDATION_ERROR') {
$this->error(translate('PayPalConnectionException', 'paypal'));
ServiceContainer::getService('logger')->error('PayPal exception: '.print_r($e->getData(), true),
['file' => __FILE__, 'line' => __LINE__]);
} else {
throw $e;
}
}
return $payment;
}
/** Funkce pro patch paymentu reálnými daty */
public function updatePayment(): PayPal\Api\Payment
{
$payment = $this->getPayment();
if ($useOriginalCurrency = in_array($this->order->currency, $this->allowedCurrencies)) {
$totalPrice = roundPrice($this->order->getRemainingPayment())->asFloat();
} else {
$totalPrice = $this->order->convertPriceToEUR(
roundPrice($this->order->getRemainingPayment())->asFloat()
);
}
$patchAmount = new \PayPal\Api\Patch();
$patchAddress = new \PayPal\Api\Patch();
$patchPayer = new \PayPal\Api\Patch();
$patchItems = new \PayPal\Api\Patch();
$patchCustom = new \PayPal\Api\Patch();
$patchInvoiceNo = new \PayPal\Api\Patch();
$amount = new \PayPal\Api\Amount();
$amount->setCurrency($useOriginalCurrency ? ($this->order ? $this->order->currency : '') : 'EUR')
->setTotal($totalPrice);
$patchAmount->setOp('replace')
->setPath('/transactions/0/amount')
->setValue($amount);
$payerDeliveryAddress = new \PayPal\Api\ShippingAddress();
$payerDeliveryAddress->setState($this->order->delivery_state ?? '')
->setCountryCode($this->order->delivery_country ?? '')
->setCity($this->order->delivery_city ?? '')
->setLine1($this->order->delivery_street ?? '')
->setLine2($this->order->delivery_custom_address ?? '')
->setPostalCode($this->order->delivery_zip ?? '')
->setRecipientName($this->order->delivery_name.' '.$this->order->delivery_surname);
$patchAddress->setOp('add')
->setPath('/transactions/0/item_list/shipping_address')
->setValue($payerDeliveryAddress);
$payerInfoAddress = new \PayPal\Api\Address();
$payerInfoAddress->setState($this->order->invoice_state ?? '')
->setCountryCode($this->order->invoice_country ?? '')
->setCity($this->order->invoice_city ?? '')
->setLine1($this->order->invoice_street ?? '')
->setLine2($this->order->invoice_custom_address ?? '')
->setPostalCode($this->order->invoice_zip ?? '');
$payerInfo = new \PayPal\Api\PayerInfo();
$payerInfo->setEmail($this->order ? $this->order->getUserEmail() : '')
->setBillingAddress($payerInfoAddress)
->setFirstName($this->order->invoice_name ?? '')
->setLastName($this->order->invoice_surname ?? '');
$patchPayer->setOp('add')
->setPath('/payer/payer_info')
->setValue($payerInfo);
$items = [];
$itemList = new \PayPal\Api\ItemList();
foreach ($this->order->fetchItems() as $orderItem) {
$item = new \PayPal\Api\Item();
$item->setName($orderItem['descr'])
->setCurrency($useOriginalCurrency ? $this->order->currency ?? '' : 'EUR')
->setQuantity($orderItem['pieces'])
->setPrice($orderItem['piece_price']['value_with_vat']->asFloat());
$items[] = $item;
}
$itemList->setItems($items);
$patchItems->setOp('replace')
->setPath('/transactions/0/item_list')
->setValue($itemList);
$patchCustom->setOp('add')
->setPath('/transactions/0/custom')
->setValue($this->order->getSecurityCode());
$patchInvoiceNo->setOp('add')
->setPath('/transactions/0/invoice_number')
->setValue($this->order->order_no);
$patchRequest = new \PayPal\Api\PatchRequest();
$patchRequest->setPatches([
$patchAddress,
$patchAmount,
$patchPayer,
$patchItems,
$patchCustom,
$patchInvoiceNo,
]);
try {
$payment->update($patchRequest, $this->getApiContext());
} catch (Exception $ex) {
ServiceContainer::getService('logger')->error('PayPal exception: '.$ex->getMessage(), ['file' => __FILE__, 'line' => __LINE__]);
if ($ex instanceof \PayPal\Exception\PayPalConnectionException) {
ServiceContainer::getService('logger')->error('PayPal exception: '.print_r($ex->getData(), true),
['file' => __FILE__, 'line' => __LINE__]);
}
$this->error('storno');
}
return $payment;
}
public function processStep_1()
{
// $this->error('');
}
public function processStep_2()
{
ServiceContainer::getService('logger')->error('PayPal response: '.print_r([$_GET, $_POST, $_SERVER], true),
['file' => __FILE__, 'line' => __LINE__]);
$payPalPaymentID = getVal('paymentId', null, false);
$apiContext = $this->getApiContext();
$payment = $this->getPayPalPayment($apiContext, $payPalPaymentID);
$error = false;
if ($payment) {
$this->orderId = $this->selectSQL('orders',
['order_no' => $payment->getTransactions()[0]->getInvoiceNumber()],
['id'])->fetchOne();
$this->order = new Order();
$this->order->createFromDB($this->orderId);
if ($this->order->getSecurityCode() == $payment->getTransactions()[0]->getCustom()) {
// Při platbě paypal umožňuje změnit adresu doručení....
// $address = $payment->getTransactions()[0]->getItemList()->getShippingAddress()->toArray();
// $address = $payment->toArray();
// if (isset($address['payer']['payer_info']['first_name']) && isset($address['payer']['payer_info']['last_name'])) {
// // $this->order->updateSQL('orders', [
// // 'delivery_name' => $address['payer']['payer_info']['first_name'],
// // 'delivery_surname' => $address['payer']['payer_info']['last_name'],
// // 'delivery_street' => $address['payer']['payer_info']['shipping_address']['line1'],
// // 'delivery_city' => $address['payer']['payer_info']['shipping_address']['city'],
// // 'delivery_zip' => $address['payer']['payer_info']['shipping_address']['postal_code'],
// // 'delivery_country' => $address['payer']['payer_info']['shipping_address']['country_code'],
// // 'delivery_phone' => $address['payer']['payer_info']['phone'] ?? '',
// // 'delivery_state' => $address['payer']['payer_info']['shipping_address']['state'],
// // ], ['id' => $this->orderId]);
//
// $name = explode(' ', $address['recipient_name']);
// $firstname = $name[0] ?? '';
// unset($name[0]);
// $lastname = implode(' ', $name);
// $this->order->updateSQL('orders', [
// 'delivery_name' => $firstname,
// 'delivery_surname' => $lastname ?? '',
// 'delivery_street' => $address['line1'],
// 'delivery_city' => $address['city'],
// 'delivery_zip' => $address['postal_code'],
// 'delivery_country' => $address['country_code'],
// 'delivery_phone' => $address['payer']['payer_info']['phone'] ?? '',
// 'delivery_state' => $address['payer']['payer_info']['shipping_address']['state'],
// ], ['id' => $this->orderId]);
// }
$this->createPayment(
$payment->getId(),
$payment->getTransactions()[0]->getAmount()->getTotal(),
['paymentClass' => self::class]
);
$this->processPayment($payment);
$paymentInstructions = $payment->getPaymentInstruction();
if ($paymentInstructions) {
$paypalData = $this->order->getData('paypalplus');
$paypalData['payment_instruction'] = $paymentInstructions->toArray();
$this->order->setData('paypalplus', $paypalData);
}
$this->step(3, 'wait', ['paymentId' => $payment->getId()]);
} else {
$error = true;
}
} else {
$error = true;
}
if ($error) {
$this->error(translate('payment_status_check_error', 'payment'));
}
}
public function processStep_3()
{
$apiContext = $this->getApiContext();
$payPalPaymentID = getVal('paymentId', null, false);
if (!$payPalPaymentID) {
$kupshopPayment = $this->getPendingPayment();
if ($kupshopPayment) {
// use already created payment
$payPalPaymentID = $kupshopPayment['decoded_data']->session;
} else {
$this->error('storno');
}
}
$this->processPayment($this->getPayPalPayment($apiContext, $payPalPaymentID));
}
public function processStep_4()
{
$token = getVal('token', null, false);
if (!$token) {
$this->error('storno');
}
$payPalId = sqlQueryBuilder()
->select("id, json_extract(note_admin, '$.paypalplus.id') as paypalID")
->from('orders')
->where("json_extract(note_admin, '$.paypalplus.token') = :token")
->setParameter('token', $token)
->execute()
->fetchAssociative();
if (!$payPalId) {
$this->error('storno');
}
$this->orderId = $payPalId['id'];
$this->order = new \Order();
$this->order->createFromDB($this->orderId);
$this->error(translate('payment_storno', 'payment'));
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'clientID' => [
'title' => 'clientID',
'type' => 'text',
],
'secret' => [
'title' => 'secret',
'type' => 'text',
],
'mode' => [
'title' => 'Režim',
'type' => 'select',
'options' => ['live' => 'Produkční režim', 'sandbox' => 'Testovaci režim'],
],
],
];
}
}

View File

@@ -0,0 +1,354 @@
<?php
use KupShop\KupShopBundle\Config;
class PayU extends Payment
{
public static $name = 'PayU platební brána';
public $template = 'payment.PayU.tpl';
protected $templateCart = 'payment.PayU.cart.tpl';
public $class = 'PayU';
public $url = 'https://secure.payu.com/paygw/';
// Session
public $ts;
public $session_id;
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
public function setOrder($order)
{
if (empty($order)) {
$this->readParams();
$order = $this->getOrderId($this->session_id);
}
if (empty($order)) {
$payu_response = $this->makeStatusRequest();
$trans = $this->readResponse($payu_response);
$result = $this->readStatus($trans);
logError(__FILE__, __LINE__, 'Not Found: result:'.print_r($result, true));
if ($result['code'] == 2 || $result['code'] == 1) {
// If payment canceled or not paid yet, confirm
echo 'OK';
exit;
}
if ($result['code'] == 99) {
logError(__FILE__, __LINE__, 'Missed payment!! Creating ...');
$order = intval($trans->order_id);
}
logError(__FILE__, __LINE__, 'Not Found: payu_response:'.$payu_response);
}
return parent::setOrder($order);
}
public function storePaymentInfo()
{
$data = parent::storePaymentInfo();
$data['method'] = explode('-', getVal('payment_id'))[1];
return $data;
}
public function checkAuth()
{
$this->readParams();
if ($this->session_id && !getVal('cf')) {
$parts = explode('_', $this->session_id);
$_GET['cf'] = getVal(0, $parts);
}
}
public function readParams()
{
$this->session_id = getVal('session_id');
$this->ts = time();
}
public function readResponse($payu_response)
{
$xml = simplexml_load_string($payu_response);
if (!$xml) {
logError(__FILE__, __LINE__, "Chyba parsovani xml: {$payu_response}");
}
if (strval($xml->status) != 'OK') {
logError(__FILE__, __LINE__, 'Transakce vratila chybu');
}
return $xml->trans;
}
public function readStatus($trans)
{
// incorrect POS ID number specified in response
if ($trans->pos_id != $this->config['pos']) {
return ['code' => false, 'message' => 'incorrect POS number'];
}
// calculating signature for comparison with sig sent by PayU
$sig = md5($trans->pos_id.$trans->session_id.$trans->order_id.$trans->status.$trans->amount.$trans->desc.$trans->ts.$this->config['key2']);
// incorrect signature in response in comparison to locally calculated one
if ($trans->sig != $sig) {
return ['code' => false, 'message' => 'incorrect signature'];
}
// different messages depending on transaction status. For status description, see documentation
switch ($trans->status) {
case 1:
return ['code' => $trans->status, 'message' => 'new'];
case 2:
return ['code' => $trans->status, 'message' => 'cancelled'];
case 3:
return ['code' => $trans->status, 'message' => 'rejected'];
case 4:
return ['code' => $trans->status, 'message' => 'started'];
case 5:
return ['code' => $trans->status, 'message' => 'awaiting receipt'];
case 6:
return ['code' => $trans->status, 'message' => 'no authorization'];
case 7:
return ['code' => $trans->status, 'message' => 'payment rejected'];
case 99:
return ['code' => $trans->status, 'message' => 'payment received - ended'];
case 888:
return ['code' => $trans->status, 'message' => 'incorrect status'];
default:
return ['code' => false, 'message' => 'no status'];
}
}
public function checkReceivedSignature()
{
// some parameters are missing
if (!isset($_POST['pos_id']) || !isset($_POST['session_id']) || !isset($_POST['ts']) || !isset($_POST['sig'])) {
logError(__FILE__, __LINE__, 'ERROR: EMPTY PARAMETERS');
}
// received POS ID is different than expected
if ($_POST['pos_id'] != $this->config['pos']) {
logError(__FILE__, __LINE__, 'ERROR: INCORRECT POS ID');
}
// verification of received signature
$sig = md5($_POST['pos_id'].$_POST['session_id'].$_POST['ts'].$this->config['key2']);
// incorrect signature
if ($_POST['sig'] != $sig) {
logError(__FILE__, __LINE__, 'ERROR: INCORRECT SIGNATURE');
}
return true;
}
public function makeStatusRequest()
{
// signature that will be sent to PayU with request
$sig = md5($this->config['pos'].$this->session_id.$this->ts.$this->config['key1']);
// preparing parameters string to be sent to PayU
$parameters = 'pos_id='.$this->config['pos'].'&session_id='.$this->session_id.'&ts='.$this->ts.'&sig='.$sig;
// sending request via CURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url.'UTF/Payment/get');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$payu_response = curl_exec($ch);
curl_close($ch);
return $payu_response;
}
public function step($index, $message, $data = [])
{
if ($index == 1) {
if (!empty($this->method)) {
$data['method'] = $this->method;
} else {
$data['method'] = getVal('payu_method');
}
}
parent::step($index, $message, $data);
}
public function processStep_1()
{
$this->ts = strval(time());
$this->session_id = "{$this->order->getSecurityCode()}_{$this->ts}";
$this->method = getVal('method');
return true;
}
public function processStep_5()
{
$this->error('Platba byla zrušena.');
}
public function processStep_6()
{
$this->updatePaymentStatus();
if ($this->status == Payment::STATUS_FINISHED) {
$this->success('Platba proběhla v pořádku.');
} else {
$this->step(2, 'Platba byla zaznamenána, čeká se na zaplacení. O přijetí platby Vám zašleme email.');
}
}
public function processStep_7()
{
$this->checkReceivedSignature();
if ($this->updatePaymentStatus()) {
logError(__FILE__, __LINE__, 'PayU step 7 - OK');
echo 'OK';
} else {
logError(__FILE__, __LINE__, 'PayU step 7 - KO');
echo 'KO';
}
exit;
}
public function ensurePaymentExists($trans)
{
if (!$this->getStatus($this->session_id)) {
$this->createPayment($this->session_id, intval($trans->amount) / 100, ['id_payu' => $trans->id]);
}
}
public function updatePaymentStatus()
{
$payu_response = $this->makeStatusRequest();
logError(__FILE__, __LINE__, 'updatePaymentStatus: payu_response:'.$payu_response);
$trans = $this->readResponse($payu_response);
$result = $this->readStatus($trans);
logError(__FILE__, __LINE__, 'updatePaymentStatus: response:'.print_r($result, true));
if ($result['code']) {
if ($result['code'] == '2') {
// transaction canceled
return true;
}
$this->ensurePaymentExists($trans);
// change of transaction status in system of the shop
if ($result['code'] == '99') {
// payment sucessful so we send back OK
if (!$this->setStatus(Payment::STATUS_FINISHED, $this->session_id)) {
logError(__FILE__, __LINE__, 'Payment::updatePaymentStatus: setStatus failed!');
}
return true;
} elseif ($result['code'] == '4') {
// transaction pending
if (!$this->setStatus(Payment::STATUS_PENDING, $this->session_id)) {
logError(__FILE__, __LINE__, 'Payment::updatePaymentStatus: setStatus failed!');
}
return true;
} elseif ($result['code'] == '1') {
// transaction started
if (!$this->setStatus(Payment::STATUS_CREATED, $this->session_id)) {
logError(__FILE__, __LINE__, 'Payment::updatePaymentStatus: setStatus failed!');
}
return true;
}
logError(__FILE__, __LINE__, 'updatePaymentStatus: not handled');
return false;
} else {
logError(__FILE__, __LINE__, 'updatePaymentStatus: error: code='.$result['code'].' message='.$result['message']."\n{$payu_response}");
}
return false;
}
public function getName()
{
$methods = $this->getAvailableMethods();
return parent::getName().' - '.getVal($this->method, $methods, ['name' => 'Neznámý typ PayU platby'])['name'];
}
public function getAvailableMethods()
{
if (!($methods = getCache('payu-methods'))) {
$methods = $this->fetchAvailableMethods();
setCache('payu-methods', $methods);
}
return $methods;
}
public function fetchAvailableMethods()
{
$key = substr($this->config['key1'], 0, 2);
$xml = new SimpleXMLElement($this->url."UTF/xml/{$this->config['pos']}/{$key}/paytype.xml", 0, true);
$payTypes = $xml->xpath('//paytype');
$methods = [];
foreach ($payTypes as $payment) {
$payment = (array) $payment;
$payment['enable'] = $payment['enable'] == 'true';
$methods[$payment['type']] = $payment;
}
return $methods;
}
public function requiresEET()
{
return true;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,18 @@
<?php
class PlatebniKarta extends Payment
{
public static $name = 'Platba kartou';
protected ?string $defaultIcon = '../../common/static/payments/prodejna_kartou.svg';
public $class = 'PlatebniKarta';
public $method;
protected $pay_method = Payment::METHOD_CARD;
public function requiresEET()
{
return true;
}
}

View File

@@ -0,0 +1,45 @@
<?php
use KupShop\KupShopBundle\Context\PosContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\POSBundle\Event\PosPaymentEvent;
use KupShop\POSBundle\Util\PosPaymentUtil;
use Query\Operator;
class PosCustom extends Payment
{
public static $name = '[Pokladna] Nastavitelná platba';
protected ?string $defaultIcon = '../../common/static/payments/prodejna_hotove.svg';
public $class = 'PosCustom';
public $method;
protected $pay_method = Payment::METHOD_UNKNOWN;
public function createPayment($session, $price = null, $data = [])
{
$pos = Contexts::get(PosContext::class)->getActive();
$posPaymentUtil = ServiceContainer::getService(PosPaymentUtil::class);
$posPaymentUtil->changeOrderDelivery($pos->getId(), $this->order, 'CUSTOM');
$idPayment = sqlQueryBuilder()
->select('id_payment')
->from('delivery_type', 'dt')
->andWhere(Operator::equals(['dt.id' => $pos->getCustomDeliveryType()]))
->execute()
->fetchOne();
$eventDispatcher = ServiceContainer::getService('event_dispatcher');
$eventDispatcher->dispatch(new PosPaymentEvent($pos, $price, $idPayment, $this->order), PosPaymentEvent::POS_CUSTOM_PAYMENT);
return true;
}
public static function isEnabled($className)
{
return findModule(Modules::NEW_POS);
}
}

View File

@@ -0,0 +1,31 @@
<?php
use KupShop\POSBundle\Util\PosOrderUtil;
class PosInvoice extends Payment
{
public static $name = '[Pokladna] Platba fakturou';
protected ?string $defaultIcon = '../../common/static/payments/prodejna_hotove.svg';
public $class = 'PosInvoice';
public $method;
protected $pay_method = Payment::METHOD_INVOICE;
public function createPayment($session, $price = null, $data = [])
{
$this->order->changeStatus(
PosOrderUtil::getHandledOrderStatus(),
'Vytvořeno v pokladně '.date(\Settings::getDateFormat().' '.\Settings::getTimeFormat(), time()),
false,
);
return true;
}
public static function isEnabled($className)
{
return findModule(Modules::NEW_POS);
}
}

View File

@@ -0,0 +1,43 @@
<?php
use KupShop\BankAutoPaymentBundle\PaymentSources\FioBankApi;
class Prevodem extends Payment
{
public static $name = 'Platba převodem';
protected ?string $defaultIcon = '../../common/static/payments/prevodem.svg';
protected $templateOrderView = 'payment.Prevod.orderView.tpl';
public $class = 'Převod';
public $method;
protected $pay_method = Payment::METHOD_TRANSFER;
public function hasPaymentDescription()
{
// Potřebujeme umožnit editaci v adminu i když to je vyplé v nastavení
return (Settings::getDefault()['payment_config']['show_payment_instruction'] ?? 0) == 1
|| (getVal('FORCE_QR') == 1 && !empty(getAdminUser()));
}
public function requiresEET()
{
return false;
}
protected function loadPaymentDbConfig($dbcfg): array
{
$fioSetting = null;
if (findModule(\Modules::BANK_AUTO_PAYMENTS, 'fio_bank') && isset($dbcfg->payments[FioBankApi::getName()])) {
$fioSetting = array_filter($dbcfg->payments[FioBankApi::getName()],
function ($value) {
return $value !== null && $value !== false && $value !== '';
});
}
return is_array($fioSetting) ? $fioSetting : [];
}
}

View File

@@ -0,0 +1,175 @@
<?php
use Firebase\JWT\JWT;
use KupShop\KupShopBundle\Config;
/**
* Dependencies: `composer require firebase/php-jwt=^5.4.0`
* Class Quatro.
*/
class Quatro extends Payment
{
public static $name = 'Quatro';
public $class = 'Quatro';
protected $pay_method = Payment::METHOD_INSTALLMENTS;
protected $templateOrderView = 'payment.Quatro.orderView.tpl';
public function getCalcUrl(Decimal $price): ?string
{
$price = roundPrice($price, -1, 'DB', 0)->asInteger();
if (empty($this->config['seller']) || $price > 10000 || $price < 100) {
return null;
}
return "https://quatro.vub.sk/kalkulacka/{$this->config['seller']}?cenaTovaru={$price}";
}
public function getGatewayUrl(): ?string
{
if (empty($this->config['seller'])) {
return null;
}
return "https://quatroapi.vub.sk/stores/{$this->config['seller']}/create-application";
}
public function processStep_1()
{
}
public function processStep_2()
{
// hack protože natvrdo lepěj ? ke callbacku
$cn = str_replace('?cn=', '', getVal('h'));
$id = getVal('id');
$state = getVal('state');
$sign = getVal('hmacSign');
if (hash_hmac('sha1', "cn={$cn}&id={$id}&state={$state}", base64_decode($this->config['key'])) != strtolower($sign) && !isDevelopment()) {
throw new \KupShop\OrderingBundle\Exception\PaymentException('Chyba ověření podpisu');
}
$remainingPayment = roundPrice($this->order->getRemainingPayment())->asFloat();
if ($remainingPayment > 0.00) {
if (!$this->getPendingPayment()) {
$this->createPayment(
$id,
$remainingPayment,
['paymentClass' => self::class]
);
}
if (getVal('state') == 'signed') {
$paymentStatus = Payment::STATUS_FINISHED;
} elseif (getVal('state') == 'canceled') {
$paymentStatus = Payment::STATUS_STORNO;
} else {
$paymentStatus = Payment::STATUS_PENDING;
}
// change payment status
if (!$this->setStatus($paymentStatus, $id)) {
logError(__FILE__, __LINE__, 'Payment::updatePaymentStatus: setStatus failed!');
throw new \Exception('Set status failed');
}
}
}
protected function getSubject()
{
$subject = '';
foreach ($this->order->fetchItems() as $item) {
if (!$item['id_product']) {
continue;
}
/** @var Product $product */
$product = $item['product'];
$subject .= "{$product->fetchSections()[0]->getName()} - {$product->fetchProducer()['name']} - {$product->title},";
}
$subject = substr($subject, 0, -1);
if (strlen($subject) > 250) {
$subject = substr($subject, 0, 247).'...';
}
return $subject;
}
public function getPayload()
{
$payload = [
'application' => [
'orderNumber' => $this->order->order_no,
'applicant' => [
'firstName' => $this->order->invoice_name,
'lastName' => $this->order->invoice_surname,
'email' => $this->order->invoice_email,
'mobile' => $this->order->invoice_phone,
'permanentAddress' => [
'addressLine' => $this->order->invoice_street,
'city' => $this->order->invoice_city,
'zipCode' => $this->order->invoice_zip,
'country' => $this->order->invoice_country,
],
],
'subject' => $this->getSubject(),
'totalAmount' => $this->order->total_price->asFloat(),
'goodsAction' => null,
'callback' => $this->getGenericPaymentUrl(2, ['h' => '']),
],
'iat' => time(),
];
$jwt = JWT::encode($payload, base64_decode($this->config['key']), 'HS256');
return $jwt;
}
// https://www.kupshop.local/platby/Quatro/1/49698/?cf=2171114784fcbceb29f9b6bdc6f07e48&h=?cn=1000018425&id=0514186c-3eb6-4150-ac31-048eb330507d&state=canceled&hmacSign=95F512D8D7F14A02376A78CE94382A3F2301DA5E
public function accept($totalPrice, $freeDelivery)
{
$totalPrice = $totalPrice->getPriceWithVat()->asFloat();
if ($totalPrice <= 0 && $this->order) {
$totalPrice = $this->order->total_price;
}
return parent::accept($totalPrice, $freeDelivery) && $totalPrice >= 100 && $totalPrice <= 10000;
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'key' => [
'title' => 'Bezpečnostní klíč',
'type' => 'text',
],
'seller' => [
'title' => 'Kód prodejny',
'type' => 'text',
],
],
];
}
public function startPayment()
{
return false;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,292 @@
<?php
use KupShop\KupShopBundle\Config;
use Ticketpark\SaferpayJson\Request\Container;
use Ticketpark\SaferpayJson\Request\Exception\HttpRequestException;
use Ticketpark\SaferpayJson\Request\Exception\SaferpayErrorException;
use Ticketpark\SaferpayJson\Request\PaymentPage\AssertRequest;
use Ticketpark\SaferpayJson\Request\PaymentPage\InitializeRequest;
use Ticketpark\SaferpayJson\Request\RequestConfig;
use Ticketpark\SaferpayJson\Request\Transaction\CaptureRequest;
/**
* Requires composer package "ticketpark/saferpay-json-api: ~4.4".
*/
class Saferpay extends Payment
{
public static $name = 'Saferpay platební brána';
public $template = 'payment.Saferpay.tpl';
public $class = 'Saferpay';
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl($step = 1)
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => $step,
'class' => $this->class,
'paymentId' => $this->paymentId,
'absolute' => true,
]);
}
private function getPayment($id = null)
{
if (isset($id)) {
$payment = $this->selectSQL('order_payments', ['id' => $id])
->fetch();
if (!$payment) {
$this->step(-3, 'storno');
}
$payment['decoded_data'] = json_decode($payment['payment_data']);
return $payment;
}
$kupshopPayment = $this->getPendingPayment();
if (!$kupshopPayment) {
$kupshopPayment = $this->initializeSaferpayPayment();
}
$this->paymentId = $kupshopPayment['id'];
return $kupshopPayment;
}
/**
* @return array $kupshopPayment
*/
private function initializeSaferpayPayment(): array
{
$amount = roundPrice($this->order->getRemainingPayment())->asFloat();
$this->createPayment(
null,
$amount,
['paymentClass' => self::class]
);
// -----------------------------
// Step 1:
// Initialize the required payment page data
// See https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize
$requestConfig = new RequestConfig(
$this->config['apiKey'],
$this->config['apiSecret'],
$this->config['customerID'],
getVal('test', $this->config, false)
);
$containerAmount = new Container\Amount(
$amount * 100, // amount in cents
$this->order->currency
);
$containerPayment = new Container\Payment($containerAmount);
$containerPayment
->setOrderId($this->order->order_no)
->setDescription('Order No. '.$this->order->order_no);
$returnUrls = new Container\ReturnUrls(
$this->getPaymentUrl(2),
$this->getPaymentUrl(3),
$this->getPaymentUrl(3)
);
// -----------------------------
// Step 2:
// Create the request with required data
$initializeRequest = new InitializeRequest(
$requestConfig,
$this->config['terminalID'],
$containerPayment,
$returnUrls
);
$containerAddress = (new Container\Address())
->setFirstName($this->order->invoice_name)
->setLastName($this->order->invoice_surname)
->setStreet($this->order->invoice_street)
->setZip($this->order->invoice_zip)
->setCity($this->order->invoice_city)
->setCountryCode($this->order->invoice_country);
$containerPayer = (new Container\Payer())
->setLanguageCode('de')
->setBillingAddress($containerAddress);
$initializeRequest->setPayer($containerPayer);
// Note: More data can be provided to InitializeRequest with setters,
// for example: $initializeRequest->setPayer()
// See Saferpay documentation for available options.
// -----------------------------
// Step 3:
// Execute and check for successful response
try {
$response = $initializeRequest->execute();
} catch (SaferpayErrorException $e) {
logError(__FILE__, __LINE__, 'Saferpay::initializeSaferpayPayment: ErrorResponse! ErrorName: '.$e->getErrorResponse()->getErrorName().', errorDetail: '.json_encode($e->getErrorResponse()->getErrorDetail()));
if (!$this->setStatus(Payment::STATUS_STORNO, $this->paymentId)) {
throw new Exception('Saferpay::updatePaymentStatus: setStatus failed!');
}
$this->step(-3, 'storno');
} catch (HttpRequestException $e) {
$this->deletePayment();
$this->step(-1, 'Server error. Try again please.');
}
// Save the response token, you will need it later to verify the payment
$token = $response->getToken();
// Redirect to the payment page
$redirectURL = $response->getRedirectUrl();
$paymentRow = $this->selectSQL('order_payments', ['id' => $this->paymentId])->fetch();
$json = json_decode($paymentRow['payment_data']);
$json->session = $this->paymentId;
$json->token = $token;
$json->redirectURL = $redirectURL;
$paymentRow['payment_data'] = json_encode($json);
$logger = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('logger');
$logger->notice('Saferpay:initializeSaferpayPayment(): '.$paymentRow['id_order'], ['response' => $response, 'paymentRow' => $paymentRow]);
$this->updateSQL('order_payments', $paymentRow, ['id' => $this->paymentId]);
$paymentRow['decoded_data'] = $json;
return $paymentRow;
}
/* Payment steps */
public function processStep_1()
{
$amount = roundPrice($this->order->getRemainingPayment())->asFloat();
if ($amount <= (float) 0) {
$this->step(-3, 'storno');
}
$payment = $this->getPayment();
if (!isset($payment['decoded_data']->redirectURL)) {
$this->updateSQL('order_payments', ['status' => Payment::STATUS_STORNO], ['id' => $payment['id']]);
$payment = $this->getPayment();
}
redirection($payment['decoded_data']->redirectURL);
}
public function processStep_2()
{
$payment = $this->getPayment(getVal('paymentId', null, false));
// Return success if payment already finished
if ($payment['status'] === strval(Payment::STATUS_FINISHED)) {
$this->success(translate('paymentSuccess', 'payment'));
}
$token = $payment['decoded_data']->token;
// -----------------------------
// Step 1:
// Prepare the assert request
// See http://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Assert
$requestConfig = new RequestConfig(
$this->config['apiKey'],
$this->config['apiSecret'],
$this->config['customerID'],
getVal('test', $this->config, false)
);
// -----------------------------
// Step 2:
// Create the request with required data
$assertRequest = new AssertRequest(
$requestConfig,
$token,
);
// -----------------------------
// Step 3:
// Execute and check for successful response
try {
$response = $assertRequest->execute();
} catch (SaferpayErrorException $e) {
$logger = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('logger');
$logger->notice('Saferpay:processStep_2() ErrorResponse: '.$payment['id_order'], ['response' => $e->getErrorResponse(), 'payment' => $payment]);
if (!$this->setStatus(Payment::STATUS_STORNO, $payment['id'])) {
throw new Exception('Saferpay::updatePaymentStatus: setStatus failed! (ErrorResponse: '.$e->getErrorResponse()->getErrorMessage().')');
}
$this->step(-3, 'storno');
}
// A transaction id you received with a successful assert request (see ../PaymentPage/example-assert.php)
$transactionId = $response->getTransaction()->getId();
// -----------------------------
// Step 1:
// Prepare the capture request
// https://saferpay.github.io/jsonapi/#Payment_v1_Transaction_Capture
$transactionReference = (new Container\TransactionReference())
->setTransactionId($transactionId);
// -----------------------------
// Step 2:
// Create the request with required data
$captureRequest = new CaptureRequest(
$requestConfig,
$transactionReference
);
// -----------------------------
// Step 3:
// Execute and check for successful response
try {
$captureResponse = $captureRequest->execute();
} catch (SaferpayErrorException $e) {
/** @var $sentryLogger \KupShop\KupShopBundle\Util\Logging\SentryLogger */
$sentryLogger = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService(\KupShop\KupShopBundle\Util\Logging\SentryLogger::class);
$sentryLogger->captureMessage(
$e->getErrorResponse()->getErrorMessage(),
[],
['transaction_id' => $transactionId],
true
);
}
// save transactionID
$paymentRow = $this->selectSQL('order_payments', ['id' => $payment['id']])->fetch();
$json = json_decode($paymentRow['payment_data']);
$json->transactionID = $transactionId;
$paymentRow['payment_data'] = json_encode($json);
$this->updateSQL('order_payments', $paymentRow, ['id' => $payment['id']]);
$logger = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('logger');
$logger->notice('Saferpay:processStep_2() finished: '.$paymentRow['id_order'], ['response' => $response, 'captureResponse' => $captureResponse, 'paymentRow' => $paymentRow]);
// change payment status to finished
if (!$this->setStatus(Payment::STATUS_FINISHED, $payment['id'])) {
throw new Exception('Saferpay::updatePaymentStatus: setStatus failed!');
}
$this->success(translate('paymentSuccess', 'payment'));
}
public function processStep_3()
{
$payment = $this->getPayment(getVal('paymentId', null, false));
if (!$this->setStatus(Payment::STATUS_STORNO, $payment['id'])) {
throw new Exception('Saferpay::updatePaymentStatus: setStatus failed!');
}
$this->step(-3, 'storno');
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,449 @@
<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
class ThePay extends Payment
{
public static $name = 'ThePay platební brána';
public $template = 'payment.ThePay.tpl';
protected $templateCart = 'payment.ThePay.cart.tpl';
public $class = 'ThePay';
public $tp_id_payment;
/**
* @var TpDataApiPayment
*/
public $tp_payment;
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
/** Get payment icon url and name.
* @return array|null
*/
public function getIcon()
{
$method = getVal($this->method, $this->getAvailableMethods());
if ($method) {
return [
'url' => "https://www.thepay.cz/gate/images/logos/public/209x127/{$method['id']}.png",
'name' => $method['name'],
];
}
return null;
}
public function storePaymentInfo()
{
$data = parent::storePaymentInfo();
$data['method'] = explode('-', getVal('payment_id'))[1];
return $data;
}
/* Payment creation */
public function getPayment()
{
$payment = new TpPayment($this->getMerchantConfig());
$payment->setValue(roundPrice($this->order->getRemainingPayment())->asFloat());
if (findModule(\Modules::CURRENCIES)) {
$payment->setCurrency($this->order->currency ?: 'CZK');
}
$payment->setCustomerEmail($this->order->invoice_email);
$payment->setDescription('Platba objednávky '.$this->order->order_no);
$payment->setReturnUrl(createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 5,
'class' => $this->class,
'absolute' => true,
]));
$payment->setMerchantData($this->order->order_no);
if (empty($this->method)) {
$paymentData = $this->order->getData('payment_data');
if (!$paymentData) {
$paymentData = ['methodSelectionAllowed' => 1];
}
$this->loadPaymentInfo($paymentData);
}
$payment->setMethodId($this->method);
return $payment;
}
public function getPaymentUrl()
{
try {
return $this->getMerchantConfig()->gateUrl.'?'.$this->buildQuery();
} catch (Exception $e) {
return '/';
}
}
/* Payment status change */
public function getPaymentStatus()
{
$payment = TpDataApiHelper::getPayment($this->getMerchantConfig(), $this->tp_id_payment)->getPayment();
if ($this->orderId != $payment->getMerchantData() && $this->order->order_no != $payment->getMerchantData()) {
$this->error('Neplatné číslo objednávky: '.$this->orderId.' != '.$payment->getMerchantData());
}
$payment->setId($this->tp_id_payment);
$payment->setMerchantData($this->orderId);
$this->tp_payment = $payment;
return true;
}
public function checkReceivedSignature()
{
try {
$payment = new TpReturnedPayment($this->getMerchantConfig());
$payment->verifySignature();
} catch (TpMissingParameterException $e) {
return $this->error('Chybějící položky v odpověďi platební brány');
} catch (TpInvalidSignatureException $e) {
logError(__FILE__, __LINE__, 'ThePay: Neplatná odpověď platební brány! '.print_r([$payment, $this->getMerchantConfig()], true));
return $this->error('Neplatná odpověď platební brány');
}
$this->tp_id_payment = $payment->getPaymentId();
return $payment;
}
public function updatePaymentStatus()
{
// Ensure payment exists
if (!$this->getStatus($this->tp_id_payment)) {
$this->createPayment($this->tp_id_payment, $this->tp_payment->getValue(), []);
}
switch ($this->tp_payment->getState()) {
case TpReturnedPayment::STATUS_OK: // platba je zaplacena, můžeme si ji uložit jako zaplacenou a dále zpracovat (vyřídit objednávku atp.).
if (!$this->setStatus(Payment::STATUS_FINISHED, $this->tp_id_payment)) {
logError(__FILE__, __LINE__, 'ThePay::updatePaymentStatus: setStatus failed!');
}
return true;
case TpReturnedPayment::STATUS_CANCELED: // zákazník platbu stornoval
case TpReturnedPayment::STATUS_ERROR: // při zpracování platby došlo k chybě
if (!$this->setStatus(Payment::STATUS_STORNO, $this->tp_id_payment)) {
logError(__FILE__, __LINE__, 'ThePay::updatePaymentStatus: setStatus failed!');
}
break;
case TpReturnedPayment::STATUS_UNDERPAID: // zákazník zaplatil pouze část platby
// Modify paid value
$this->updateSQL('order_payments', ['price' => $this->tp_payment->getReceivedValue()], ['id' => $this->paymentId]);
if (!$this->setStatus(Payment::STATUS_FINISHED, $this->tp_id_payment)) {
logError(__FILE__, __LINE__, 'ThePay::updatePaymentStatus: setStatus failed!');
}
return true;
case TpReturnedPayment::STATUS_CARD_DEPOSIT: // částka byla zablokována na účtu zákazníka - pouze pro platbu kartou
case TpReturnedPayment::STATUS_WAITING: // platba proběhla úspěšně, ale čeká na potvrzení od poskytovatele platební metody. S vyřízením objednávky je nutno počkat na potvrzovací request se statusem TpReturnedPayment:: STATUS_OK, pokud nepřijde, platba nebyla dokončena.
if (!$this->setStatus(Payment::STATUS_PENDING, $this->tp_id_payment)) {
logError(__FILE__, __LINE__, 'ThePay::updatePaymentStatus: setStatus failed!');
}
break;
}
return false;
}
/* Payment steps */
public function processStep_1()
{
redirection($this->getPaymentUrl());
}
public function processStep_5()
{
$this->checkReceivedSignature();
try {
$this->getPaymentStatus();
$this->updatePaymentStatus();
} catch (\KupShop\KupShopBundle\Exception\RedirectException $e) {
throw $e;
} catch (Exception $e) {
return $this->error($e->getMessage());
}
switch ($this->status) {
case Payment::STATUS_FINISHED:
$this->success(translate('paymentSuccess', 'payment'));
break;
case Payment::STATUS_STORNO:
$this->step(-3, 'storno');
break;
default:
$this->success('Platba byla zaznamenána a čeká se na její potvrzení. O přijetí platby Vám zašleme email.');
}
}
/* Payment description */
public function getName()
{
if (is_null($this->method)) {
return parent::getName();
}
$methods = $this->getAvailableMethods();
return parent::getName().' - '.getVal($this->method, $methods, ['name' => 'Neznámý typ ThePay platby'])['name'];
}
public function getAvailableMethods()
{
$domainContextID = ServiceContainer::getService(\KupShop\KupShopBundle\Context\DomainContext::class)->getActiveId();
if (!($methods = getCache('thepay-methods-'.$domainContextID))) {
$methods = $this->fetchAvailableMethods();
setCache('thepay-methods-'.$domainContextID, $methods);
}
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
if ($currencyContext->getActiveId() == 'EUR') {
if (isset($methods[21])) {
$method = $methods[21];
$methods = [];
$methods[21] = $method;
} elseif (isset($methods[31])) {
$method = $methods[31];
$methods = [];
$methods[31] = $method;
} else {
$methods = [];
}
}
return $methods;
}
/* Payment methods */
public function getMerchantConfig()
{
static $config = null;
if (!$config) {
$config = new TpMerchantConfig();
if (getVal('test', $this->config)) {
$defaults = [
'gateUrl' => 'https://www.thepay.cz/demo-gate/',
'webServicesWsdl' => 'https://www.thepay.cz/demo-gate/api/api-demo.wsdl',
'dataWebServicesWsdl' => 'https://www.thepay.cz/demo-gate/api/data-demo.wsdl',
];
unset($this->config['merchantId']);
unset($this->config['accountId']);
unset($this->config['password']);
unset($this->config['dataApiPassword']);
} else {
$defaults = [
'gateUrl' => 'https://www.thepay.cz/gate/',
'webServicesWsdl' => 'https://www.thepay.cz/gate/api/gate-api.wsdl',
'dataWebServicesWsdl' => 'https://www.thepay.cz/gate/api/data.wsdl',
];
}
$this->config = array_merge($defaults, $this->config);
foreach ($this->config as $key => $value) {
$config->$key = $value;
}
}
return $config;
}
public function fetchAvailableMethods()
{
try {
/** @var TpDataApiGetPaymentMethodsResponse $response */
$response = TpDataApiHelper::getPaymentMethods($this->getMerchantConfig());
} catch (TpSoapException $e) {
return [];
}
$overwriteMethodNames = $this->config['overwriteMethodNames'] ?? [
21 => translate('creditCardMethodTitle', 'payment'),
31 => translate('creditCardMethodTitle', 'payment'),
];
$methods = [];
foreach ($response->getMethods() as $method) {
$methods[$method->getId()] = [
'name' => $overwriteMethodNames[$method->getId()] ?? $method->getName(),
'id' => $method->getId(),
];
}
return $methods;
}
/* Utils */
public function buildQuery($args = [])
{
$payment = $this->getPayment();
if (isset($this->methodSelectionAllowed)) {
$args['methodSelectionAllowed'] = $this->methodSelectionAllowed;
}
$out = array_merge(
$payment->getArgs(), // Arguments of the payment
$args, // Optional helper arguments
['signature' => $payment->getSignature()] // Signature
);
$str = [];
foreach ($out as $key => $val) {
$str[] = rawurlencode($key).'='.rawurlencode($val);
}
return implode('&', $str);
}
public function requiresEET()
{
return true;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
/**
* Check paid orders (and update orders.status_payed).
*/
public function checkPaidOrders()
{
if (getVal('test', $this->config)) {
return false;
}
$searchParams = new TpDataApiGetPaymentsSearchParams();
// select only paid payments
// 1 waiting for payment, payment wasnt completed
// 2 payment was successfully completed
// 3 payment wasnt completed, payment was canceled by customer
// 4 some error occurred
// 6 payment is partly paid
// 7 payment is waiting for confirmation from payment system
// 8 payment was cancelled and the money has been returned to customer
// 9 amount is blocked on customers account (in case of payments by card)
$searchParams->setState([2]);
// filter payments by accountId
$searchParams->setAccountId($this->getAccountIds());
// finishedOnFrom datetime - date when payment was paid
$dateTimeFrom = new DateTime();
$dateTimeFrom->modify('-1 day');
$searchParams->setFinishedOnFrom($dateTimeFrom);
// set ordering (default order is DESC, use setOrderHow('ASC') to ascending)
$ordering = new TpDataApiOrdering();
$ordering->setOrderBy('finishedOn');
$ordering->setOrderHow('ASC');
// set pagination
$pagination = new TpDataApiPaginationRequest();
// default 50
$pagination->setItemsOnPage(200);
$pagination->setPage(0);
$totalPages = 1;
do {
$response = TpDataApiHelper::getPayments($this->getMerchantConfig(), $searchParams, $pagination, $ordering);
foreach ($response->getPayments() as $payment) {
$order = returnSQLResult('SELECT id FROM orders WHERE order_no=:order_no', [
'order_no' => $payment->getMerchantData(),
]);
if ($order) {
$this->setOrder($order);
$this->tp_id_payment = $payment->getId();
$this->tp_payment = $payment;
try {
$this->updatePaymentStatus();
} catch (\KupShop\KupShopBundle\Exception\RedirectException $e) {
throw $e;
} catch (Exception $e) {
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, 'ThePay error: '.$e->getMessage());
}
}
}
// use pagesCount received on first request only, otherwise it could (theoretically) loop forever
if ($response->getPagination()->getPage() <= 0) {
$totalPages = $response->getPagination()->getTotalPages();
}
// default = 0
$pagination->setPage($response->getPagination()->getPage() + 1);
} while ($response->getPagination()->getPage() + 1 < $totalPages);
return 0;
}
private function getAccountIds(): array
{
$ids = [$this->config['accountId']];
// load all account ids from local config
foreach ($this->config['domain_config'] ?? [] as $domain => $config) {
$ids[] = $config['accountId'];
}
// load all account ids from db config
$languageContext = Contexts::get(LanguageContext::class);
foreach ($languageContext->getSupported() as $language) {
$settings = Settings::getFromCache($language->getId());
$dbConfig = array_filter($settings->payments[$this->class] ?? []);
if ($dbConfig['accountId'] ?? false) {
$ids[] = $dbConfig['accountId'];
}
}
return array_filter(array_unique($ids));
}
}

View File

@@ -0,0 +1,395 @@
<?php
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\EANValidator;
use KupShop\OrderingBundle\Exception\PaymentException;
use KupShop\OrderingBundle\Util\Order\PaymentOrderSubMethodTrait;
use ThePay\ApiClient\Exception\ApiException;
use ThePay\ApiClient\Exception\MissingExtensionException;
use ThePay\ApiClient\Filter\PaymentMethodFilter;
use ThePay\ApiClient\Model\CreatePaymentCustomer;
use ThePay\ApiClient\Model\CreatePaymentItem;
use ThePay\ApiClient\ValueObject\LanguageCode;
/**
* composer require "thepay/api-client":"1.2.4".
*/
class ThePay20 extends Payment
{
use PaymentOrderSubMethodTrait;
public static $name = 'ThePay 2.0 platební brána';
public static bool $canAutoReturn = true;
protected $templateCart = 'payment.ThePay20.cart.tpl';
public $class = 'ThePay20';
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
protected $apiContext;
protected $gatewayUrl;
/** 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 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;
}
}

View File

@@ -0,0 +1,509 @@
<?php
declare(strict_types=1);
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\Exception\PaymentException;
use Query\Operator;
/**
* Implementace pouze pro "Immediate Capture"
* Klient musí mít v twisto portalu povoleno "Okamžité odbavení" (Immediate Capture).
*/
class TwistoPay extends Payment
{
public static $name = 'Twisto Pay';
public $class = 'TwistoPay';
protected ?string $defaultIcon = '../../common/static/payments/twisto_pay.svg';
public static bool $canAutoReturn = true;
protected string $apiUrl = 'https://api.twisto.cz/psp/smi';
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl()
{
return createScriptURL([
's' => 'payment',
'IDo' => $this->order->id,
'cf' => $this->order->getSecurityCode(),
'step' => 1,
'class' => $this->class,
'absolute' => true,
]);
}
public function processStep_1()
{
if ($paymentExists = $this->checkPaymentAlreadyExists()) {
if (empty($paymentExists['sessionId'])) {
$this->info(translate('payment_already_exists', 'payment'));
}
$redirectExists = $this->requestCheckoutData($paymentExists['sessionId']);
if (empty($redirectExists['uri'])) {
$this->info(translate('payment_already_exists', 'payment'));
}
redirection($redirectExists['uri']);
}
$checkoutData = $this->getCheckoutData();
$redirectData = $this->getCheckoutRedirect($checkoutData);
if (empty($redirectData['id']) || empty($redirectData['uri'])) {
$this->createApiException(['message' => 'ID or URI not received', 'id' => $redirectData['id'], 'uri' => $redirectData['uri']]);
}
$this->createPayment($redirectData['id'], $redirectData['order']['amount'], ['paymentClass' => $this->class]);
redirection($redirectData['uri']);
}
protected function checkPaymentAlreadyExists()
{
return sqlQueryBuilder()->select("op.id, JSON_UNQUOTE(JSON_EXTRACT(op.payment_data, '$.session')) sessionId")->from('order_payments', 'op')
->innerJoin('op', 'orders', 'o', 'o.id = op.id_order')
->where(Operator::equals(['o.order_no' => $this->order->order_no]))
->andWhere(Operator::orX(
JsonOperator::contains('op.payment_data', 'paymentClass', 'TwistoPay'),
JsonOperator::contains('op.payment_data', 'paymentClass', 'TwistoPayIn3')
))->execute()->fetchAssociative();
}
public function processStep_2()
{
$checkoutId = getVal('checkout_id');
$status = getVal('status');
switch ($status) {
case 'captured':
case 'authorized':
$this->setStatus(Payment::STATUS_FINISHED, $checkoutId);
$this->info(translate('paymentSuccess', 'payment'));
break;
case 'rejected':
$this->setStatus(Payment::STATUS_STORNO, $checkoutId);
$this->info(translate('payment_rejected_status', 'payment'));
// no break
case 'error':
$this->setStatus(Payment::STATUS_STORNO, $checkoutId);
$this->info(translate('payment_unexpected_status', 'payment'));
}
}
protected function getCheckoutRedirect($checkoutData)
{
$response = $this->requestCurl('/checkouts', json_encode($checkoutData));
return json_decode($response, true);
}
protected function getCheckoutData()
{
$items = [];
foreach ($this->order->fetchItems() as $item) {
$items[] = $this->getCheckoutItem($item);
}
$this->setItemsRoundingDiscount($items);
$requestBody = [
'type' => $this->getCheckoutType(),
'config' => [
'redirect_uri' => $this->getGenericPaymentUrl(2),
],
'shopper' => $this->removeEmptyValues([
'first_name' => $this->order->invoice_name,
'last_name' => $this->order->invoice_surname,
'phone' => $this->order->invoice_phone,
'billing_address' => $this->removeEmptyValues([
'line1' => $this->order->invoice_street,
'line2' => $this->order->invoice_custom_address,
'city' => $this->order->invoice_city,
'state' => $this->order->invoice_state,
'postal_code' => $this->order->invoice_zip,
'country' => $this->order->invoice_country,
]),
'statistics' => $this->getCheckoutUserStatistics($this->order->id_user, $this->order->invoice_email),
// "personal_id" => $this->order->id_user ?? '',
'email' => $this->order->invoice_email,
]),
'order' => $this->removeEmptyValues([
'reference' => $this->order->order_no,
'amount' => $this->getOrderPrice(),
'currency' => $this->order->getTotalPrice()->getCurrency()->getId(),
'items' => $items,
]),
];
/** @var LoggerInterface $logger */
$logger = ServiceContainer::getService('logger');
$logger->notice('Twisto - objednavka', [
'Request' => $requestBody,
]);
return $requestBody;
}
protected function removeEmptyValues(array $array): array
{
foreach ($array as $key => $value) {
if ((is_string($value) && !$value) || is_null($value)) {
unset($array[$key]);
}
}
return $array;
}
protected function getCheckoutUserStatistics($idUser, string $email): array
{
$userStats = [];
if ($idUser) {
$user = sqlQueryBuilder()->select('*')->from('users')->where(Operator::equals(['id' => $this->order->id_user]))
->execute()->fetchAssociative();
if ($user['date_reg'] && $user['date_reg'] !== '0000-00-00 00:00:00') {
$userStats['account_created'] = (new DateTime($user['date_reg']))->format('Y-m-d\TH:i:s\Z');
}
$userStats['currency'] = $user['currency'];
}
$prevOrdersStats = sqlQueryBuilder()->select('AVG(o.total_price) avg, SUM(o.total_price) sum, MAX(o.total_price) max, COUNT(*) count')->from('orders',
'o')
->where(Operator::equals($idUser ? ['id_user' => $idUser] : ['invoice_email' => $email]))
->andWhere('status_storno = 0')
->andWhere(Operator::inIntArray(getStatuses('handled'), 'o.status'))
->groupBy($idUser ? 'id_user' : 'invoice_email')
->execute()->fetchAssociative() ?: [];
$stats = ($prevOrdersStats['count'] ?? false) ? [
'sales_total_count' => $prevOrdersStats['count'] ?? 0,
'sales_total_amount' => $prevOrdersStats['sum'] ?? 0,
'sales_avg_amount' => $prevOrdersStats['avg'] ?? 0,
'sales_max_amount' => $prevOrdersStats['max'] ?? 0,
'has_previous_purchases' => true,
] : ['has_previous_purchases' => false];
// skipped fields
// "refunds_total_amount" => "string",
// "previous_chargeback" => true,
// "last_login" => "2019-08-24T14:15:22Z",
return array_merge($userStats, $stats);
}
protected function getCheckoutItem(array $item): array
{
if (($item['note']['item_type'] ?? '') == 'delivery') {
$type = 'shipping';
} elseif (($item['note']['item_type'] ?? '') == 'discount') {
$type = 'discount';
} elseif (!empty($item['note']['bonus_points'])) {
$type = 'store_credit';
} else {
$type = 'sku';
}
return [
'name' => $item['descr'],
'amount' => $item['piece_price']['value_with_vat']->asFloat(),
'quantity' => (int) $item['pieces'],
'type' => $type,
];
}
protected function setItemsRoundingDiscount(&$items)
{
$itemsPrice = array_sum(array_map(function ($item) {
return $item['amount'] * $item['quantity'];
}, $items));
$difference = $this->getOrderPrice() - $itemsPrice;
if (abs($difference) > PHP_FLOAT_EPSILON && abs($difference) < 10) {
$items[] = [
'name' => 'Rounding discount',
'amount' => round($difference, 3),
'quantity' => 1,
'type' => ($difference < 0) ? 'discount' : 'sku',
];
}
}
protected function getOrderPrice(): float
{
return $this->order->getTotalPrice()->getPriceWithVat()->asFloat();
}
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' => self::METHOD_ONLINE,
];
$this->insertSQL('order_payments', $fields);
$this->status = self::STATUS_CREATED;
$this->paymentId = (int) sqlInsertId();
$this->order->updatePayments();
return true;
}
protected function requestCheckoutData($checkoutId)
{
$response = $this->requestCurl('/checkouts/'.$checkoutId, '', false);
return json_decode($response, true);
}
protected function requestRefund($checkoutId, $amount, $reference)
{
$body = $this->removeEmptyValues([
'checkout_id' => $checkoutId,
'amount' => abs($amount),
'reference' => $reference,
]);
return $this->requestCurl('/refunds', json_encode($body));
}
protected function requestCheckoutCancel($checkoutId)
{
return $this->requestCurl("/checkouts/{$checkoutId}/cancel", '');
}
protected function requestCurl(string $path, string $encodedBody, $post = true)
{
$url = $this->getApiUrl().$path;
$ch = curl_init();
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedBody);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer '.$this->getAccessToken(),
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
$responseInfo = curl_getinfo($ch);
$curl_errno = curl_errno($ch);
curl_close($ch);
if ($responseInfo['http_code'] >= 300 || $responseInfo['http_code'] === 0) {
$debugInfo = ['url' => $url, 'responseInfo' => $responseInfo, 'curl_errno' => $curl_errno, 'RESPONSE' => $response];
$this->createApiException($debugInfo);
}
return $response;
}
protected function createApiException($debugInfo)
{
$errMessage = 'Komunikace s platební bránou Twisto selhala. Zkuste prosím znovu později.';
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $errMessage, $debugInfo);
getRaven()->captureMessage('Twisto API error', [], ['extra' => $debugInfo]);
throw new PaymentException($errMessage);
}
protected function createWebhookError($httpCode, $debugInfo, $responseText = '')
{
$debugInfo = is_array($debugInfo) ? $debugInfo : [$debugInfo];
$errMessage = 'Twisto webhook se nepodařilo zpracovat.';
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_COMMUNICATION, $errMessage, $debugInfo);
getRaven()->captureMessage('Twisto webhook error', [], ['extra' => $debugInfo]);
$this->sendNotificationResponse($httpCode, $responseText);
}
/**
* Webhook handler.
*/
public function processStep_10()
{
if (!$this->request->isMethod('POST')) {
$this->sendNotificationResponse(405, '');
}
try {
$requestBody = $this->request->toArray();
} catch (Exception $e) {
$this->createWebhookError(422, $this->request->getContent(), 'Unreadable JSON');
}
$checkoutId = getVal('checkout_id', $requestBody);
$state = getVal('state', $requestBody);
if (!$checkoutId) {
$this->createWebhookError(400, array_merge($requestBody, ['err' => 'missing checkout id']), '');
}
$idOrder = $this->getOrderId($checkoutId);
if (!$idOrder) {
$this->sendNotificationResponse(400, 'Order not found');
}
$this->setOrder($idOrder);
$this->getStatus($checkoutId);
$this->updateStatus($state, $checkoutId);
$this->sendNotificationResponse(200, 'OK');
}
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,
], 'op.status'))
->andWhere('op.date > (DATE_SUB(CURDATE(), INTERVAL 1 MONTH))')
->andWhere(Operator::inStringArray(['TwistoPay', 'TwistoPayIn3'],
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']);
$checkoutId = $paymentData['session'];
$response = $this->requestCheckoutData($checkoutId);
$this->updateStatus($response['state'], $checkoutId);
}
}
protected function updateStatus($state, $checkoutId)
{
switch ($state) {
case 'created':
$this->setStatus(Payment::STATUS_PENDING, $checkoutId);
break;
case 'approved':
case 'completed':
$this->setStatus(Payment::STATUS_FINISHED, $checkoutId);
break;
case 'cancelled':
case 'error':
$this->setStatus(Payment::STATUS_STORNO, $checkoutId);
}
}
public function doReturnPayment(array $payment, float $amount)
{
if ($payment['status'] != Payment::STATUS_FINISHED) {
throw new PaymentException(translate('returnFailed', 'orderPayment'));
}
$checkoutId = $payment['payment_data']['session'] ?? null;
if (!$checkoutId) {
throw new PaymentException('Payment does not have assigned checkout_id');
}
$checkoutData = $this->requestCheckoutData($checkoutId);
switch ($checkoutData['state']) {
case 'completed':
$reference = $checkoutData['order']['reference'].'_'.$payment['id'].'_'.rand();
$response = $this->requestRefund($checkoutId, $amount, $reference);
break;
case 'created':
case 'approved':
if ($checkoutData['order']['amount'] + $amount > PHP_FLOAT_EPSILON) {
$message = translate('returnFailedOnlyFullAmount', 'orderPayment');
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_COMMUNICATION,
$message,
['amount' => $amount, 'payment_id' => $checkoutData, 'RESPONSE' => $checkoutData['state']]);
throw new PaymentException($message);
}
$response = $this->requestCheckoutCancel($checkoutId);
break;
default:
$message = translate('returnFailedInvalidState', 'orderPayment').": {$checkoutData['state']}";
addActivityLog(ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_COMMUNICATION,
$message,
['amount' => $amount, 'payment_id' => $checkoutData, 'RESPONSE' => $checkoutData['state']]);
throw new PaymentException($message);
}
return ['response' => $response];
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'token' => [
'title' => 'Token',
'type' => 'text',
],
'webhook' => [
'title' => 'Webhook url',
'text' => path('kupshop_ordering_payment_legacypayment', ['class' => 'TwistoPay', 'step' => 10], \Symfony\Component\Routing\Router::ABSOLUTE_URL),
],
],
];
}
protected function getCheckoutType(): string
{
return 'standard';
}
public function hasOnlinePayment()
{
return true;
}
protected function getApiUrl(): string
{
return $this->apiUrl;
}
protected function getAccessToken(): string
{
return $this->config['token'] ?? '';
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
if (!class_exists('TwistoPay')) {
require_once 'class.TwistoPay.php';
}
class TwistoPayIn3 extends TwistoPay
{
public static $name = 'Twisto Pay in three';
public $class = 'TwistoPayIn3';
protected ?string $defaultIcon = '../../common/static/payments/twisto_payin3.svg';
protected function getCheckoutType(): string
{
return 'pay-in-three';
}
public function accept($totalPrice, $freeDelivery)
{
$price = $totalPrice->getPriceWithVat()->asFloat();
if ($price <= 0 && $this->order) {
$price = $this->order->total_price;
}
return parent::accept($totalPrice, $freeDelivery) && $price >= 1500;
}
}

View File

@@ -0,0 +1,251 @@
<?php
use KupShop\KupShopBundle\Config;
/**
* Requires composer packages alcohol/iso3166, alcohol/iso4217 and adamstipak/webpay-php:~1.1.3 (https://github.com/wpj-cz/gp-webpay-php-sdk).
*/
class WebPay extends Payment
{
public static $name = 'WebPay platební brána';
// public $template = 'payment.WebPay.tpl';
public $class = 'WebPay';
public $tp_id_payment;
/** @var \AdamStipak\Webpay\Api */
public $apiContext;
public $method;
protected $pay_method = Payment::METHOD_ONLINE;
public function getPaymentUrl()
{
return $this->getGenericPaymentUrl(1);
}
public function getGatewayUrl()
{
return $this->config['webpayUrl'];
}
public function getData(): array
{
$request = $this->createPaymentRequest();
return $this->getApiContext()->createPaymentParam($request);
}
private function getPayment(bool $alwaysNew = true)
{
$kupshopPayment = $this->getPendingPayment();
if (!$alwaysNew && $kupshopPayment) {
return $kupshopPayment;
} else {
$this->createPayment(
null,
$this->order->getRemainingPayment(),
['paymentClass' => self::class]
);
$paymentRow = $this->selectSQL('order_payments', ['id' => $this->paymentId])->fetch();
$json = json_decode($paymentRow['payment_data'], true);
$json['session'] = $this->paymentId;
$paymentRow['payment_data'] = json_encode($json);
$this->updateSQL('order_payments', $paymentRow, ['id' => $this->paymentId]);
$paymentRow['decoded_data'] = $json;
return $paymentRow;
}
}
private function createPaymentRequest(): AdamStipak\Webpay\PaymentRequest
{
$iso4217 = new Alcohol\ISO4217();
if (class_exists('Alcohol\ISO3166\ISO3166')) {
$iso3166 = new Alcohol\ISO3166\ISO3166();
}
$amount = $this->order->getRemainingPayment();
if ($amount <= (float) 0) {
$this->step(-3, 'storno');
}
$addInfo = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><additionalInfoRequest xmlns="http://gpe.cz/gpwebpay/additionalInfo/request" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>');
$addInfo['version'] = '4.0';
$cardHolderInfo = $addInfo->addChild('cardholderInfo');
$cardHolderDetails = $cardHolderInfo->addChild('cardholderDetails');
$cardHolderDetails->name = mb_substr($this->order->invoice_name.' '.$this->order->invoice_surname, 0, 255);
$cardHolderDetails->email = mb_substr($this->order->invoice_email, 0, 255);
if (!empty($this->order->invoice_street) && isset($iso3166)
&& !empty($this->order->invoice_name)
&& !empty($this->order->invoice_city)
&& !empty($this->order->invoice_zip)
&& !empty($this->order->invoice_country)
&& !empty($this->order->invoice_email)
) {
$billingDetails = $cardHolderInfo->addChild('billingDetails');
$billingDetails->name = mb_substr($this->order->invoice_name.' '.$this->order->invoice_surname, 0, 255);
$billingDetails->address1 = mb_substr($this->order->invoice_street, 0, 50);
if (!empty($this->order->invoice_custom_address)) {
$billingDetails->address2 = mb_substr($this->order->invoice_custom_address, 0, 50);
}
$billingDetails->city = mb_substr($this->order->invoice_city, 0, 50);
$billingDetails->postalCode = mb_substr($this->order->invoice_zip, 0, 16);
$billingDetails->country = (int) $iso3166->getByAlpha2($this->order->invoice_country)['numeric'];
// phone has weird validation rules: cvc-pattern-valid: Value '+420777852147' is not facet-valid with respect to pattern '\d{1,15}' for type 'phoneValue'.
if (!empty($this->order->invoice_phone)) {
$billingDetails->phone = mb_substr(preg_replace('/([^0-9]+)/', '', $this->order->invoice_phone), 0, 20);
}
$billingDetails->email = mb_substr($this->order->invoice_email, 0, 255);
}
if (!empty($this->order->delivery_street) && isset($iso3166)
&& !empty($this->order->delivery_name)
&& !empty($this->order->delivery_city)
&& !empty($this->order->delivery_zip)
&& !empty($this->order->delivery_country)
) {
$shippingDetails = $cardHolderInfo->addChild('shippingDetails');
$shippingDetails->name = mb_substr($this->order->delivery_name.' '.$this->order->delivery_surname, 0, 255);
$shippingDetails->address1 = mb_substr($this->order->delivery_street, 0, 50);
if (!empty($this->order->delivery_custom_address)) {
$shippingDetails->address2 = mb_substr($this->order->delivery_custom_address, 0, 50);
}
$shippingDetails->city = mb_substr($this->order->delivery_city, 0, 50);
$shippingDetails->postalCode = mb_substr($this->order->delivery_zip, 0, 16);
$shippingDetails->country = (int) $iso3166->getByAlpha2($this->order->delivery_country)['numeric'];
// phone has weird validation rules: cvc-pattern-valid: Value '+420777852147' is not facet-valid with respect to pattern '\d{1,15}' for type 'phoneValue'.
if (!empty($this->order->delivery_phone)) {
$shippingDetails->phone = mb_substr(preg_replace('/([^0-9]+)/', '', $this->order->delivery_phone), 0, 20);
}
}
$paymentInfo = $addInfo->addChild('paymentInfo');
$paymentInfo->transactionType = '01';
$shoppingCartInfo = $addInfo->addChild('shoppingCartInfo');
$shoppingCartItems = $shoppingCartInfo->addChild('shoppingCartItems');
foreach ($this->order->fetchItems() as $item) {
if ($item['piece_price']['value_without_vat']->asInteger() < 0) {
continue; // skip items with negative price (must be unsignedLong - 12 digits max)
}
$shoppingCartItem = $shoppingCartItems->addChild('shoppingCartItem');
if (!empty($item['id_product'])) {
$itemCodeValue = $item['id_product'].(!empty($item['id_variation']) ? ('_'.$item['id_variation']) : '');
if (mb_strlen($itemCodeValue) <= 20) {
$shoppingCartItem->itemCode = $itemCodeValue;
}
}
$shoppingCartItem->itemDescription = mb_substr($item['descr'], 0, 50);
$shoppingCartItem->itemQuantity = $item['pieces'];
$shoppingCartItem->itemUnitPrice = $item['piece_price']['value_without_vat']->asInteger();
}
$request = new \AdamStipak\Webpay\PaymentRequest(
$this->getPayment()['id'],
$amount,
(int) $iso4217->getByAlpha3($this->order->currency)['numeric'],
1,
$this->getGenericPaymentUrl(5),
$this->order->id
);
$request->setParam('REFERENCENUMBER', $this->order->order_no);
$request->setParam('ADDINFO', str_replace(PHP_EOL, '', $addInfo->asXML()));
return $request;
}
/* Payment steps */
public function processStep_1()
{
$this->template = 'payment.WebPay.tpl';
}
public function processStep_5()
{
$response = new \AdamStipak\Webpay\PaymentResponse(
$_REQUEST['OPERATION'],
$_REQUEST['ORDERNUMBER'],
$_REQUEST['MERORDERNUM'],
$_REQUEST['PRCODE'],
$_REQUEST['SRCODE'],
$_REQUEST['RESULTTEXT'],
$_REQUEST['DIGEST'],
$_REQUEST['DIGEST1']
);
try {
$this->getApiContext()->verifyPaymentResponse($response);
} catch (\AdamStipak\Webpay\PaymentResponseException $e) {
// change payment status to finished
if (!$this->setStatus(Payment::STATUS_STORNO, $_REQUEST['ORDERNUMBER'])) {
logError(__FILE__, __LINE__, 'WebPay::updatePaymentStatus: setStatus failed!');
exit;
}
if ($_REQUEST['PRCODE'] != 50) { // nejedna se o "Drzitel karty zrusil platbu"
// PaymentResponseException has $prCode, $srCode for properties for logging GP Webpay response error codes.
logError(__FILE__, __LINE__, 'WebPay error: '.$_REQUEST['PRCODE'].' - '.$_REQUEST['RESULTTEXT'].', exception:'.$e);
}
$this->error(translate('payment_storno', 'payment'));
} catch (Exception $e) {
// Digest is not correct.
logError(__FILE__, __LINE__, 'WebPay exception: '.$e);
$this->error(translate('payment_storno', 'payment'));
}
if ($_REQUEST['PRCODE'] == 0) {
$this->status = Payment::STATUS_FINISHED;
// change payment status to finished
if (!$this->setStatus(Payment::STATUS_FINISHED, $_REQUEST['ORDERNUMBER'])) {
logError(__FILE__, __LINE__, 'WebPay::updatePaymentStatus: setStatus failed!');
exit;
}
$this->success(translate('paymentSuccess', 'payment'));
}
}
/* Payment methods */
public function getApiContext(): AdamStipak\Webpay\Api
{
if (!isset($this->apiContext)) {
$signer = new \AdamStipak\Webpay\Signer(
$this->config['privateKeyFilepath'],
$this->config['privateKeyPassword'],
$this->config['GPpublicKeyFilepath']
);
$this->apiContext = new \AdamStipak\Webpay\Api(
$this->config['merchantNumber'],
$this->config['webpayUrl'],
$signer
);
}
return $this->apiContext;
}
public function hasOnlinePayment()
{
return true;
}
public static function isEnabled($className)
{
$cfg = Config::get();
if (empty($cfg['Modules']['payments'][$className])) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/** Dependencies: `composer require dodo-it/omnipay-wspay:dev-master */
class WsPay extends \KupShop\OrderingBundle\OmniPay
{
public static $name = 'WsPay platební brána';
public $class = 'WsPay';
public $method;
protected $pay_method = Payment::METHOD_COD;
public static function getOmnipayName(): string
{
return 'WsPay';
}
public static function getSettingsConfiguration(): array
{
return [
'fields' => [
'shopID' => [
'title' => 'Shop ID',
'type' => 'text',
],
'secretKey' => [
'title' => 'Secret key',
'type' => 'text',
],
],
];
}
/**
* @param Omnipay\WsPay\Gateway $gateway
*/
public function configureGateway(Omnipay\Common\GatewayInterface $gateway, Order $order): Omnipay\Common\GatewayInterface
{
$gateway->setTestMode(false);
if (isDevelopment()) {
$gateway->setTestMode(true);
}
$returnUrl = $this->getGenericPaymentUrl(5);
$totalAmount = str_replace('.', ',', $order->total_price->printFloatValue(2));
$gateway
->setShopId($this->config['shopID'])
->setSecretKey($this->config['secretKey'])
->setShoppingCartId($order->order_no)
->setSignature($this->config['shopID'], $this->config['secretKey'], $order->order_no, $totalAmount)
->setTotalAmount($totalAmount)
->setCurrency($order->getCurrency())
->setReturnUrl($returnUrl)
->setReturnErrorURL($returnUrl)
->setCancelURL($returnUrl)
->setLang($order->getLanguage())
->setCustomerEmail($this->order->invoice_email)
->setCustomerFirstName($this->order->invoice_name)
->setCustomerLastName($this->order->invoice_surname)
->setCustomerCountry($this->order->invoice_country)
->setCustomerCity($this->order->invoice_city)
->setCustomerAddress($this->order->invoice_street)
->setCustomerZIP($this->order->invoice_zip)
->setCustomerPhone($this->order->invoice_phone);
return $gateway;
}
public function hasOnlinePayment()
{
return true;
}
}