396 lines
16 KiB
PHP
396 lines
16 KiB
PHP
<?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;
|
|
}
|
|
}
|