Files
kupshop/class/payments/class.CSOB.php
2025-08-02 16:30:27 +02:00

432 lines
16 KiB
PHP

<?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;
}
}