modules/XPay/XPaymentsCloud/src/EventListener/PaymentProcessor.php line 91

Open in your IDE?
  1. <?php
  2. /**
  3.  * Copyright (c) 2011-present Qualiteam software Ltd. All rights reserved.
  4.  * See https://www.x-cart.com/license-agreement.html for license details.
  5.  */
  6. namespace XPay\XPaymentsCloud\EventListener;
  7. use Doctrine\ORM\EntityManagerInterface;
  8. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  9. use XCart\Event\Payment\PaymentActionEvent;
  10. use XCart\Payment\RequestProcessor;
  11. use XCart\Payment\URLGenerator\BackendURLGenerator;
  12. use XLite\Core\Config;
  13. use XLite\Core\ConfigCell;
  14. use XLite\Core\Session;
  15. use XLite\Core\Translation;
  16. use XLite\Model\Address;
  17. use XLite\Model\AddressField;
  18. use XLite\Model\Cart;
  19. use XLite\Model\Currency;
  20. use XLite\Model\Order;
  21. use XLite\Model\Payment\BackendTransaction;
  22. use XLite\Model\Payment\Method;
  23. use XLite\Model\Payment\Transaction;
  24. use XLite\Model\Profile;
  25. use XLite\Model\Repo\AddressField as AddressFieldRepo;
  26. use XPay\XPaymentsCloud\Core\Database;
  27. use XPay\XPaymentsCloud\Main as XPaymentsHelper;
  28. use XPay\XPaymentsCloud\Model\Payment\XpaymentsFraudCheckData as FraudCheckData;
  29. use XPay\XPaymentsCloud\Model\Subscription\Subscription;
  30. use XPay\XPaymentsCloud\View\FormField\Select\CardNumberDisplayFormat as Format;
  31. use XPaymentsCloud\ApiException;
  32. use XPaymentsCloud\Client;
  33. use XPaymentsCloud\Model\Payment as XpPayment;
  34. final class PaymentProcessor
  35. {
  36.     private Session $XCSession;
  37.     private ConfigCell $config;
  38.     private AddressFieldRepo $addressFieldRepo;
  39.     public function __construct(
  40.         private BackendURLGenerator $backendURLGenerator,
  41.         private Translation $translation,
  42.         private SessionInterface $session,
  43.         private EntityManagerInterface $entityManager,
  44.     ) {
  45.         $this->XCSession        Session::getInstance();
  46.         $this->config           Config::getInstance();
  47.         $this->addressFieldRepo Database::getRepo(AddressField::class);
  48.     }
  49.     public function onPaymentGenerateWidgetDataAction(PaymentActionEvent $event): void
  50.     {
  51.         /** @var Method $method */
  52.         $method $event->getMethod();
  53.         $transaction $event->getTransaction();
  54.         /** @var Cart $cart */
  55.         $cart $transaction->getOrder();
  56.         $profile $cart->getProfile();
  57.         $event->setOutputData([
  58.             'widgetConfig' => json_encode([
  59.                 'account'      => $method->getSetting('account'),
  60.                 'widgetKey'    => $method->getSetting('widget_key'),
  61.                 'showSaveCard' => !$this->isHideSaveCardCheckbox($cart),
  62.                 'customerId'   => $profile $profile->getXpaymentsCustomerId() : '',
  63.                 'order'        => [
  64.                     'total'        => $this->getXpaymentsCartTotal($cart),
  65.                     'currency'     => $transaction->getCurrency()->getCode(),
  66.                     'tokenizeCard' => XPaymentsHelper::isDelayedPaymentEnabled(),
  67.                 ],
  68.                 'company'      => [
  69.                     'name'        => $this->config->Company->company_name,
  70.                     'countryCode' => $this->config->Company->location_country,
  71.                 ],
  72.                 'language'     => $this->XCSession->getLanguage()->getCode(),
  73.             ], JSON_THROW_ON_ERROR),
  74.         ]);
  75.     }
  76.     public function onPaymentInitAction(PaymentActionEvent $event): void
  77.     {
  78.         /** @var Client $client */
  79.         $client XPaymentsHelper::getClient();
  80.         $data     $event->getData();
  81.         $metaData $event->getMetaData();
  82.         $transaction $event->getTransaction();
  83.         $order       $transaction->getOrder();
  84.         $profile     $order->getProfile();
  85.         $currency $transaction->getCurrency();
  86.         $orderHasSubscriptions $order->hasXpaymentsSubscriptions();
  87.         $outputData = [];
  88.         try {
  89.             if ($this->isInitialPaymentTokenizeOnly($order)) {
  90.                 $response $client->doTokenizeCard(
  91.                     $data['token'],
  92.                     $this->translation->translate('Card Setup'),
  93.                     $profile $profile->getXpaymentsCustomerId() : '',
  94.                     $this->prepareCardSetupCart(
  95.                         $profile,
  96.                         $profile->getBillingAddress(),
  97.                         $currency
  98.                     ),
  99.                     $metaData['success_url'] ?? $this->backendURLGenerator->generateReturnURL($transaction''true),
  100.                     $this->backendURLGenerator->generateCallbackURL($transaction''true),
  101.                 );
  102.             } else {
  103.                 $response $client->doPay(
  104.                     $data['token'],
  105.                     $transaction->getPublicId(),
  106.                     $profile $profile->getXpaymentsCustomerId() : '',
  107.                     $this->prepareCart($transaction),
  108.                     $metaData['success_url'] ?? $this->backendURLGenerator->generateReturnURL($transaction''true),
  109.                     $this->backendURLGenerator->generateCallbackURL($transaction''true),
  110.                     $orderHasSubscriptions true null
  111.                 );
  112.             }
  113.             /** @var XpPayment $payment */
  114.             $payment $response->getPayment();
  115.             $note    $response->message ?: $payment->message;
  116.             if (!is_null($response->redirectUrl)) {
  117.                 // Should redirect to continue payment
  118.                 $transaction->setXpaymentsId($payment->xpid);
  119.                 $url $response->redirectUrl;
  120.                 if (!\XLite\Core\Converter::isURL($url)) {
  121.                     throw new ApiException('Invalid 3-D Secure URL');
  122.                 }
  123.                 $status RequestProcessor::OUTPUT_CODE_SEPARATE;
  124.                 $event->setOutputMetaData([
  125.                     'url'    => $url,
  126.                     'method' => 'get',
  127.                     'xpaymentsBuyWithWallet' => $data['xpaymentsBuyWithWallet'] ?? '',
  128.                     'xpaymentsWalletId' => $data['xpaymentsWalletId'] ?? '',
  129.                 ]);
  130.             } else {
  131.                 $status $this->processPaymentFinish($transaction$payment$eventfalse);
  132.                 if ($status !== RequestProcessor::OUTPUT_CODE_FAILED) {
  133.                     $this->processSubscriptions($transaction$payment);
  134.                 }
  135.             }
  136.         } catch (ApiException $exception) {
  137.             $status RequestProcessor::OUTPUT_CODE_FAILED;
  138.             XPaymentsHelper::log('Error: ' $exception->getMessage());
  139.             $event->addOutputTransactionData('xpaymentsMessage'$exception->getMessage(), 'Message');
  140.             $note $exception->getPublicMessage() ?: 'Failed to process the payment!';
  141.         }
  142.         $event->setOutputData($outputData);
  143.         $event->setOutputNote(substr($note0255));
  144.         $event->setOutputCode($status);
  145.     }
  146.     public function onPaymentPayAction(PaymentActionEvent $event): void
  147.     {
  148.         $transaction $event->getTransaction();
  149.         if ($transaction->isXpayments()) {
  150.             try {
  151.                 /** @var Client $client */
  152.                 $client XPaymentsHelper::getClient();
  153.                 $response $client->doContinue(
  154.                     $transaction->getXpaymentsId()
  155.                 );
  156.                 $payment $response->getPayment();
  157.                 $note $response->message ?: $payment->message;
  158.                 $result $this->processPaymentFinish($transaction$payment$event);
  159.                 if ($result !== RequestProcessor::OUTPUT_CODE_FAILED) {
  160.                     $this->processSubscriptions($transaction$payment);
  161.                 }
  162.             } catch (\XPaymentsCloud\ApiException $exception) {
  163.                 $result RequestProcessor::OUTPUT_CODE_FAILED;
  164.                 $note $exception->getMessage();
  165.                 XPaymentsHelper::log('Error: ' $note);
  166.                 $event->addOutputTransactionData('xpaymentsMessage'$note'Message');
  167.             }
  168.             $event->setOutputNote($note);
  169.             $event->setOutputCode($result);
  170.         } else {
  171.             // Invalid non-XP transaction
  172.             TopMessage::addError('Transaction was lost!');
  173.         }
  174.     }
  175.     private function isHideSaveCardCheckbox(Cart $cart)
  176.     {
  177.         $anonymous = (!$cart->getProfile() || $cart->getProfile()->getAnonymous())
  178.             && (
  179.                 !$this->session->get('order_create_profile')
  180.                 || !$this->session->get('createProfilePassword')
  181.             );
  182.         $cartHasSubscriptions $cart->hasXpaymentsSubscriptions();
  183.         return $anonymous || $cartHasSubscriptions || XPaymentsHelper::isDelayedPaymentEnabled();
  184.     }
  185.     private function getXpaymentsCartTotal(Cart $cart)
  186.     {
  187.         return XPaymentsHelper::isDelayedPaymentEnabled()
  188.             ? $this->getCardSetupAmount()
  189.             : $cart->getTotal();
  190.     }
  191.     private function getCardSetupAmount()
  192.     {
  193.         $result null;
  194.         try {
  195.             $response XPaymentsHelper::getClient()->doGetTokenizationSettings();
  196.             $result   $response->tokenizeCardAmount;
  197.         } catch (ApiException $e) {
  198.             XPaymentsHelper::log($e->getMessage());
  199.         }
  200.         return $result;
  201.     }
  202.     private function isInitialPaymentTokenizeOnly(Order $order): bool
  203.     {
  204.         $result XPaymentsHelper::isDelayedPaymentEnabled();
  205.         if (!$result) {
  206.             $result $order->hasXpaymentsSubscriptionsAndTotalIsZero() || ($order->hasOnlyTrialSubscriptionItems() && $order->getOpenTotal() <= Order::ORDER_ZERO);
  207.         }
  208.         return $result;
  209.     }
  210.     private function prepareCardSetupCart(Profile $profileAddress $addressCurrency $currency)
  211.     {
  212.         return [
  213.             'login'          => $this->getLoginForCart($profile),
  214.             'currency'       => $currency->getCode(),
  215.             'billingAddress' => $this->prepareAddress($address$profile->getLogin()),
  216.             'merchantEmail'  => $this->getMerchantEmail(),
  217.         ];
  218.     }
  219.     private function getLoginForCart(Profile $profile): string
  220.     {
  221.         return $profile->getLogin() . ' (User ID #' $profile->getProfileId() . ')';
  222.     }
  223.     protected function prepareAddress(Address $addressstring $email): array
  224.     {
  225.         $result = [];
  226.         $addressFields = [
  227.             'firstname' => 'N/A',
  228.             'lastname'  => '',
  229.             'address'   => 'N/A',
  230.             'city'      => 'N/A',
  231.             'state'     => 'N/A',
  232.             'country'   => 'XX'// WA fix for MySQL 5.7 with strict mode
  233.             'zipcode'   => 'N/A',
  234.             'phone'     => '',
  235.             'fax'       => '',
  236.             'company'   => '',
  237.         ];
  238.         foreach ($addressFields as $field => $defValue) {
  239.             $method = ($field === 'address') ? 'street' $field;
  240.             if (
  241.                 $address
  242.                 && ($this->addressFieldRepo->findOneBy(['serviceName' => $method]) || method_exists($address'get' $method))
  243.                 && $address->$method
  244.             ) {
  245.                 $result[$field] = $address->$method;
  246.                 if (is_object($result[$field])) {
  247.                     $result[$field] = $result[$field]->getCode();
  248.                 }
  249.             }
  250.             if (empty($result[$field])) {
  251.                 $result[$field] = $defValue;
  252.             }
  253.         }
  254.         $result['email'] = $email;
  255.         return $result;
  256.     }
  257.     private function getMerchantEmail(): string
  258.     {
  259.         $email $this->config->Company->orders_department;
  260.         // Try modern serialized emails or fallback to plain string
  261.         $emails = @unserialize($email);
  262.         return (is_array($emails) && !empty($emails))
  263.             ? reset($emails)
  264.             : $email;
  265.     }
  266.     private function prepareCart(Transaction $transaction): array
  267.     {
  268.         $order    $transaction->getOrder();
  269.         $profile  $order->getProfile();
  270.         $currency $transaction->getCurrency();
  271.         if ($order->getOrderNumber()) {
  272.             $description 'Order #' $order->getOrderNumber();
  273.         } else {
  274.             $description 'Payment transaction: ' $transaction->getPublicId();
  275.         }
  276.         $result = [
  277.             'login'         => $this->getLoginForCart($profile),
  278.             'items'         => [],
  279.             'currency'      => $currency->getCode(),
  280.             'shippingCost'  => 0.00,
  281.             'taxCost'       => 0.00,
  282.             'discount'      => 0.00,
  283.             'totalCost'     => 0.00,
  284.             'description'   => $description,
  285.             'merchantEmail' => $this->getMerchantEmail(),
  286.         ];
  287.         $billing  $profile->getBillingAddress();
  288.         $shipping $profile->getShippingAddress();
  289.         $email    $profile->getLogin();
  290.         if ($billing && $shipping) {
  291.             $result['billingAddress']  = $this->prepareAddress($billing$email);
  292.             $result['shippingAddress'] = $this->prepareAddress($shipping$email);
  293.         } elseif ($billing) {
  294.             $result['billingAddress'] = $result['shippingAddress'] = $this->prepareAddress($billing$email);
  295.         } else {
  296.             $result['billingAddress'] = $result['shippingAddress'] = $this->prepareAddress($shipping$email);
  297.         }
  298.         // Set items
  299.         if ($order->getItems()) {
  300.             foreach ($order->getItems() as $item) {
  301.                 $itemElement = [
  302.                     'sku'      => (string) ($item->getSku() ?: $item->getName()),
  303.                     'name'     => (string) ($item->getName() ?: $item->getSku()),
  304.                     'price'    => $this->roundCurrency($item->getPrice()),
  305.                     'quantity' => $item->getAmount(),
  306.                 ];
  307.                 if (!$itemElement['sku']) {
  308.                     $itemElement['sku'] = 'N/A';
  309.                 }
  310.                 if (!$itemElement['name']) {
  311.                     $itemElement['name'] = 'N/A';
  312.                 }
  313.                 $result['items'][] = $itemElement;
  314.             }
  315.         }
  316.         // Set costs
  317.         $result['shippingCost'] = $this->roundCurrency(
  318.             $order->getSurchargesSubtotal(\XLite\Model\Base\Surcharge::TYPE_SHIPPINGfalse)
  319.         );
  320.         $result['taxCost']      = $this->roundCurrency(
  321.             $order->getSurchargesSubtotal(\XLite\Model\Base\Surcharge::TYPE_TAXfalse)
  322.         );
  323.         $result['totalCost']    = $this->roundCurrency($order->getTotal());
  324.         $result['discount']     = $this->roundCurrency(
  325.             abs($order->getSurchargesSubtotal(\XLite\Model\Base\Surcharge::TYPE_DISCOUNTfalse))
  326.         );
  327.         return $result;
  328.     }
  329.     private function roundCurrency($data): string
  330.     {
  331.         return sprintf('%01.2f'round($data2));
  332.     }
  333.     protected function processPaymentFinish(Transaction $transactionXpPayment $paymentPaymentActionEvent $event$isRebill false)
  334.     {
  335.         $this->setTransactionDataCells($transaction$payment$event);
  336.         $this->setFraudCheckData($transaction$payment);
  337.         if ($payment->initialTransactionId) {
  338.             $transaction->setPublicId($payment->initialTransactionId ' (' $transaction->getPublicId() . ')');
  339.         }
  340.         if ($payment->customerId) {
  341.             $transaction->getOrigProfile()->setXpaymentsCustomerId($payment->customerId);
  342.         }
  343.         $status $payment->status;
  344.         $order  $transaction->getOrder();
  345.         if (
  346.             !$isRebill
  347.             && $this->isInitialPaymentTokenizeOnly($order)
  348.         ) {
  349.             if (
  350.                 !empty($payment->card->savedCardId)
  351.                 || !empty($payment->parentXpid)
  352.             ) {
  353.                 if ($order->hasOnlyTrialSubscriptionItems()) {
  354.                     $result RequestProcessor::OUTPUT_CODE_COMPLETED;
  355.                 } else {
  356.                     if (empty($payment->card->savedCardId)) {
  357.                         // TODO: rework delayed payment so it will match used cards by xpid, not by cardId
  358.                         // (to avoid unnecessary api calls)
  359.                         $savedCardId $this->getSavedCardIdByXpid($payment->parentXpid$payment->customerId);
  360.                     } else {
  361.                         $savedCardId $payment->card->savedCardId;
  362.                     }
  363.                     $order->setDelayedPaymentSavedCardId($savedCardId);
  364.                     $transaction->setValue(0.00);
  365.                     $transaction->setType(\XLite\Model\Payment\BackendTransaction::TRAN_TYPE_AUTH);
  366.                     $result RequestProcessor::OUTPUT_CODE_PENDING;
  367.                 }
  368.             } else {
  369.                 $result RequestProcessor::OUTPUT_CODE_FAILED;
  370.             }
  371.         } else {
  372.             if (
  373.                 $status === XpPayment::AUTH
  374.                 || $status === XpPayment::CHARGED
  375.             ) {
  376.                 $result RequestProcessor::OUTPUT_CODE_COMPLETED;
  377.                 $this->setTransactionTypeByStatus($transaction$status);
  378.                 $this->registerBackendTransaction($transaction$payment);
  379.             } elseif (
  380.                 $status === XpPayment::DECLINED
  381.             ) {
  382.                 $result RequestProcessor::OUTPUT_CODE_FAILED;
  383.             } else {
  384.                 $result RequestProcessor::OUTPUT_CODE_PENDING;
  385.             }
  386.         }
  387.         return $result;
  388.     }
  389.     protected function setTransactionDataCells(Transaction $transactionXpPayment $paymentPaymentActionEvent $event)
  390.     {
  391.         $transaction->setXpaymentsId($payment->xpid);
  392.         $event->addOutputTransactionData('xpaymentsMessage'$payment->lastTransaction->message'Message');
  393.         if (is_object($payment->details)) {
  394.             // Set payment details i.e. something that returned from the gateway
  395.             $details get_object_vars($payment->details);
  396.             foreach ($details as $title => $value) {
  397.                 if (!empty($value) && !preg_match('/(\[Kount\]|\[NoFraud\]|\[Signifyd\])/i'$title)) {
  398.                     $name $this->getTransactionDataCellName($title'xpaymentsDetails.');
  399.                     //$transaction->setDataCell($name, $value, $title);
  400.                     $event->addOutputTransactionData($name$value$title);
  401.                 }
  402.             }
  403.         }
  404.         if (is_object($payment->verification)) {
  405.             // Set verification (AVS and CVV)
  406.             if (!empty($payment->verification->avsRaw)) {
  407.                 $event->addOutputTransactionData('xpaymentsAvsResult'$payment->verification->avsRaw'AVS Check Result');
  408.             }
  409.             if (!empty($payment->verification->cvvRaw)) {
  410.                 $event->addOutputTransactionData('xpaymentsCvvResult'$payment->verification->cvvRaw'CVV Check Result');
  411.             }
  412.         }
  413.         if (
  414.             is_object($payment->card)
  415.             && !empty($payment->card->last4)
  416.         ) {
  417.             // Set masked card details
  418.             if (
  419.                 empty($payment->card->first6)
  420.                 || Format::FORMAT_MASKED === $this->config->XPay->XPaymentsCloud->card_number_display_format
  421.             ) {
  422.                 $first6 '******';
  423.             } else {
  424.                 $first6 $payment->card->first6;
  425.             }
  426.             $event->addOutputTransactionData(
  427.                 'xpaymentsCardNumber',
  428.                 sprintf('%s******%s'$first6$payment->card->last4),
  429.                 'Card number',
  430.                 'C'
  431.             );
  432.             if (
  433.                 !empty($payment->card->expireMonth)
  434.                 && !empty($payment->card->expireYear)
  435.             ) {
  436.                 if (Format::FORMAT_MASKED === $this->config->XPay->XPaymentsCloud->card_number_display_format) {
  437.                     $payment->card->expireMonth '**';
  438.                     $payment->card->expireYear '****';
  439.                 }
  440.                 $event->addOutputTransactionData(
  441.                     'xpaymentsCardExpirationDate',
  442.                     sprintf('%s/%s'$payment->card->expireMonth$payment->card->expireYear),
  443.                     'Expiration date',
  444.                     'C'
  445.                 );
  446.             }
  447.             if (!empty($payment->card->type)) {
  448.                 $event->addOutputTransactionData(
  449.                     'xpaymentsCardType',
  450.                     $payment->card->type,
  451.                     'Card type',
  452.                     'C'
  453.                 );
  454.             }
  455.             if (!empty($payment->card->cardholderName)) {
  456.                 $event->addOutputTransactionData(
  457.                     'xpaymentsCardholder',
  458.                     $payment->card->cardholderName,
  459.                     'Cardholder name',
  460.                     'C'
  461.                 );
  462.             }
  463.         }
  464.     }
  465.     protected function setFraudCheckData(Transaction $transactionXpPayment $payment): void
  466.     {
  467.         if (!$payment->fraudCheck) {
  468.             return;
  469.         }
  470.         if ($oldFraudCheckData $transaction->getXpaymentsFraudCheckData()) {
  471.             foreach ($oldFraudCheckData as $fraudCheckData) {
  472.                 $this->entityManager->remove($fraudCheckData);
  473.             }
  474.         }
  475.         // Maximum fraud result within several services (if there are more than one)
  476.         $maxFraudResult FraudCheckData::RESULT_UNKNOWN;
  477.         // Code of the service which got "most fraud" result
  478.         $maxFraudResultCode '';
  479.         // Flag to check if any errors which prevented fraud check occurred
  480.         $errorsFound false;
  481.         foreach ($payment->fraudCheck as $service) {
  482.             // Ignore "noname" services. This must be filled in on the X-Payments Cloud side
  483.             if (!$service['code'] || !$service['service']) {
  484.                 continue;
  485.             }
  486.             if (!$maxFraudResultCode) {
  487.                 // Use first the code, so that something is specified
  488.                 $maxFraudResultCode $service['code'];
  489.             }
  490.             $fraudCheckData = new FraudCheckData();
  491.             $fraudCheckData->setTransaction($transaction);
  492.             $transaction->addXpaymentsFraudCheckData($fraudCheckData);
  493.             $fraudCheckData->setCode($service['code']);
  494.             $fraudCheckData->setService($service['service']);
  495.             $module $service['module'] ?? '';
  496.             $fraudCheckData->setModule($module);
  497.             if (!empty($service['result'])) {
  498.                 $fraudCheckData->setResult($service['result']);
  499.                 if ((int) $service['result'] > $maxFraudResult) {
  500.                     $maxFraudResult = (int) $service['result'];
  501.                     $maxFraudResultCode $service['code'];
  502.                 }
  503.             }
  504.             if (!empty($service['status'])) {
  505.                 $fraudCheckData->setStatus($service['status']);
  506.             }
  507.             if (!empty($service['score'])) {
  508.                 $fraudCheckData->setScore($service['score']);
  509.             }
  510.             if (!empty($service['transactionId'])) {
  511.                 $fraudCheckData->setServiceTransactionId($service['transactionId']);
  512.             }
  513.             if (!empty($service['url'])) {
  514.                 $fraudCheckData->setUrl($service['url']);
  515.             }
  516.             if (!empty($service['message'])) {
  517.                 $fraudCheckData->setMessage($service['message']);
  518.                 if ($service['result'] === FraudCheckData::RESULT_UNKNOWN) {
  519.                     // Unknown result with message should be shown as error
  520.                     $errorsFound true;
  521.                 }
  522.             }
  523.             if (!empty($service['errors'])) {
  524.                 $errors implode("\n"$service['errors']);
  525.                 $fraudCheckData->setErrors($errors);
  526.                 $errorsFound true;
  527.             }
  528.             if (!empty($service['rules'])) {
  529.                 $rules implode("\n"$service['rules']);
  530.                 $fraudCheckData->setRules($rules);
  531.             }
  532.             if (!empty($service['warnings'])) {
  533.                 $warnings implode("\n"$service['warnings']);
  534.                 $fraudCheckData->setWarnings($warnings);
  535.             }
  536.         }
  537.         // Convert maximum fraud result to the order's fraud status
  538.         $status Order::FRAUD_STATUS_UNKNOWN;
  539.         switch ($maxFraudResult) {
  540.             case FraudCheckData::RESULT_UNKNOWN:
  541.                 if ($errorsFound) {
  542.                     $status Order::FRAUD_STATUS_ERROR;
  543.                 } else {
  544.                     $status Order::FRAUD_STATUS_UNKNOWN;
  545.                 }
  546.                 break;
  547.             case FraudCheckData::RESULT_ACCEPTED:
  548.                 $status Order::FRAUD_STATUS_CLEAN;
  549.                 break;
  550.             case FraudCheckData::RESULT_MANUAL:
  551.             case FraudCheckData::RESULT_PENDING:
  552.                 $status Order::FRAUD_STATUS_REVIEW;
  553.                 break;
  554.             case FraudCheckData::RESULT_FAIL:
  555.                 $status Order::FRAUD_STATUS_FRAUD;
  556.                 break;
  557.         }
  558.         $transaction->getOrder()
  559.             ->setXpaymentsFraudStatus($status)
  560.             ->setXpaymentsFraudType($maxFraudResultCode)
  561.             ->setXpaymentsFraudCheckTransactionId($transaction->getTransactionId());
  562.     }
  563.     /**
  564.      * Request savedCardId for xpid
  565.      * We do not use getPaymentInfo here because it's a heavy call
  566.      * Instead, we request cards saved for particular customer and filter them
  567.      */
  568.     protected function getSavedCardIdByXpid(string $xpidstring $customerId): string
  569.     {
  570.         $client XPaymentsHelper::getClient();
  571.         try {
  572.             $response $client->doGetCustomerCards(
  573.                 $customerId
  574.             );
  575.             if (is_array($response->cards)) {
  576.                 foreach ($response->cards as $card) {
  577.                     if ($card['xpid'] === $xpid) {
  578.                         return $card['cardId'];
  579.                     }
  580.                 }
  581.             }
  582.         } catch (ApiException $e) {
  583.             XPaymentsHelper::log($e->getMessage());
  584.         }
  585.         return '';
  586.     }
  587.     private function getTransactionDataCellName(string $titlestring $prefix ''): string
  588.     {
  589.         return $prefix \Includes\Utils\Converter::convertToLowerCamelCase(
  590.                 preg_replace('/[^a-z0-9_-]+/i''_'$title)
  591.             );
  592.     }
  593.     private function setTransactionTypeByStatus(Transaction $transaction$responseStatus)
  594.     {
  595.         // Initial transaction type is not known currently before payment, try to guess it from X-P transaction status
  596.         if ($responseStatus === XpPayment::AUTH) {
  597.             $transaction->setType(\XLite\Model\Payment\BackendTransaction::TRAN_TYPE_AUTH);
  598.         } elseif ($responseStatus === XpPayment::CHARGED) {
  599.             $transaction->setType(\XLite\Model\Payment\BackendTransaction::TRAN_TYPE_SALE);
  600.         }
  601.     }
  602.     protected function registerBackendTransaction(Transaction $transactionXpPayment $payment)
  603.     {
  604.         $type null;
  605.         $value null;
  606.         switch ($payment->status) {
  607.             case XpPayment::INITIALIZED:
  608.                 $type BackendTransaction::TRAN_TYPE_SALE;
  609.                 break;
  610.             case XpPayment::AUTH:
  611.                 $type BackendTransaction::TRAN_TYPE_AUTH;
  612.                 break;
  613.             case XpPayment::DECLINED:
  614.                 if ($payment->authorized->amount === && $payment->charged->amount === 0) {
  615.                     $type BackendTransaction::TRAN_TYPE_DECLINE;
  616.                 } else {
  617.                     $type BackendTransaction::TRAN_TYPE_VOID;
  618.                 }
  619.                 break;
  620.             case XpPayment::CHARGED:
  621.                 if ($payment->amount === $payment->charged->amount) {
  622.                     $type BackendTransaction::TRAN_TYPE_CAPTURE;
  623.                     $value $this->getActualAmount('captured'$transaction$payment->amount);
  624.                 } else {
  625.                     $type BackendTransaction::TRAN_TYPE_CAPTURE_PART;
  626.                     $value $this->getActualAmount('captured'$transaction$payment->amount);
  627.                 }
  628.                 break;
  629.             case XpPayment::REFUNDED:
  630.                 $type BackendTransaction::TRAN_TYPE_REFUND;
  631.                 $value $this->getActualAmount('refunded'$transaction$payment->amount);
  632.                 break;
  633.             case XpPayment::PART_REFUNDED:
  634.                 $type BackendTransaction::TRAN_TYPE_REFUND_PART;
  635.                 $value $this->getActualAmount('refunded'$transaction$payment->amount);
  636.                 break;
  637.             default:
  638.         }
  639.         if ($type) {
  640.             $backendTransaction $transaction->createBackendTransaction($type);
  641.             if ($payment->status !== XpPayment::INITIALIZED) {
  642.                 $backendTransaction->setStatus(BackendTransaction::STATUS_SUCCESS);
  643.             }
  644.             if ($value >= 0.01) {
  645.                 $backendTransaction->setValue($value);
  646.             }
  647.             $backendTransaction->setDataCell('xpaymentsMessage'$payment->lastTransaction->message'Message');
  648.             $backendTransaction->registerTransactionInOrderHistory('callback');
  649.         }
  650.     }
  651.     private function getActualAmount($actionTransaction $transaction$baseAmount)
  652.     {
  653.         $amount 0;
  654.         $btTypes = [];
  655.         if ($action === 'refunded') {
  656.             $btTypes = [
  657.                 BackendTransaction::TRAN_TYPE_REFUND,
  658.                 BackendTransaction::TRAN_TYPE_REFUND_PART,
  659.                 BackendTransaction::TRAN_TYPE_REFUND_MULTI,
  660.             ];
  661.         } elseif ($action === 'captured') {
  662.             $btTypes = [
  663.                 BackendTransaction::TRAN_TYPE_CAPTURE,
  664.                 BackendTransaction::TRAN_TYPE_CAPTURE_PART,
  665.                 BackendTransaction::TRAN_TYPE_CAPTURE_MULTI,
  666.             ];
  667.         }
  668.         foreach ($transaction->getBackendTransactions() as $bt) {
  669.             if ($bt->isCompleted() && in_array($bt->getType(), $btTypes)) {
  670.                 $amount += $bt->getValue();
  671.             }
  672.         }
  673.         $amount $baseAmount $amount;
  674.         return max(0$amount);
  675.     }
  676.     private function processSubscriptions(Transaction $transactionXpPayment $payment)
  677.     {
  678.         $order $transaction->getOrder();
  679.         if (
  680.             $order->hasXpaymentsSubscriptions()
  681.             && (
  682.                 !empty($payment->card->saved)
  683.                 || !empty($payment->parentXpid)
  684.             )
  685.         ) {
  686.             /** @var Client $client */
  687.             $client XPaymentsHelper::getClient();
  688.             try {
  689.                 $response $client->doCreateSubscriptions(
  690.                     $this->prepareSubscriptionPlans(),
  691.                     $payment->customerId,
  692.                     $payment->parentXpid ?: $payment->xpid
  693.                 );
  694.                 if ($response->getSubscriptions()) {
  695.                     $this->createSubscriptions($order$response->getSubscriptions());
  696.                 }
  697.             } catch (ApiException $e) {
  698.                 XPaymentsHelper::log($e->getMessage());
  699.             }
  700.         }
  701.     }
  702.     protected function prepareSubscriptionPlans(Transaction $transaction): array
  703.     {
  704.         $result = [];
  705.         $cart $transaction->getOrder();
  706.         if ($cart->getItems()) {
  707.             foreach ($cart->getItems() as $item) {
  708.                 if ($item->isXpaymentsSubscription()) {
  709.                     $uniqOrderItemId $item->getXpaymentsUniqueId();
  710.                     if (!$uniqOrderItemId) {
  711.                         $uniqOrderItemId \XLite\Core\Converter::generateRandomToken();
  712.                         $item->setXpaymentsUniqueId($uniqOrderItemId);
  713.                     }
  714.                     $plan $item->getProduct()->getXpaymentsSubscriptionPlan();
  715.                     $subscriptionPlan = [
  716.                         'subscriptionSchedule' => [
  717.                             'type'    => $plan->getType(),
  718.                             'number'  => $plan->getNumber(),
  719.                             'period'  => $plan->getPeriod(),
  720.                             'reverse' => $plan->getReverse(),
  721.                             'periods' => $plan->getPeriods(),
  722.                         ],
  723.                         'callbackUrl'          => Subscription::getCallbackUrl(),
  724.                         'recurringAmount'      => $item->getAmount() * $item->getXpaymentsDisplayFeePrice(),
  725.                         'description'          => $item->getDescription(),
  726.                         'uniqueOrderItemId'    => $item->getXpaymentsUniqueId(),
  727.                     ];
  728.                     if ($item->hasTrialPeriod()) {
  729.                         $subscriptionPlan['trialDuration'] = $plan->getTrialDuration();
  730.                         $subscriptionPlan['trialDurationUnit'] = $plan->getTrialDurationUnit();
  731.                     }
  732.                     $result[] = $subscriptionPlan;
  733.                 }
  734.             }
  735.         }
  736.         return $result;
  737.     }
  738.     protected function createSubscriptions(Order $order, array $xpaymentsSubscriptions)
  739.     {
  740.         $shippingAddress $order->getProfile()->getShippingAddress();
  741.         $shippingId $order->getShippingId();
  742.         foreach ($xpaymentsSubscriptions as $xpaymentsSubscription) {
  743.             /** @var Subscription $subscription */
  744.             $subscription Database::getRepo('XPay\XPaymentsCloud\Model\Subscription\Subscription')
  745.                 ->findOneBy(['xpaymentsSubscriptionPublicId' => $xpaymentsSubscription->getPublicId()]);
  746.             if (!$subscription) {
  747.                 $item null;
  748.                 if ($xpaymentsSubscription->getUniqueOrderItemId()) {
  749.                     /** @var \XLite\Model\OrderItem $item */
  750.                     $item Database::getRepo('XLite\Model\OrderItem')
  751.                         ->findOneBy([
  752.                             'xpaymentsUniqueId' => $xpaymentsSubscription->getUniqueOrderItemId(),
  753.                         ]);
  754.                 }
  755.                 if ($item) {
  756.                     $subscription = new Subscription();
  757.                     $item->setXpaymentsSubscription($subscription);
  758.                     $subscription
  759.                         ->setInitialOrderItem($item)
  760.                         ->setShippingId($shippingId)
  761.                         ->setShippingAddress($shippingAddress)
  762.                         ->setCalculateShipping($item->getProduct()->getXpaymentsSubscriptionPlan()->getCalculateShipping())
  763.                         ->setDataFromApi($xpaymentsSubscription);
  764.                     $this->entityManager->persist($subscription);
  765.                 }
  766.             }
  767.         }
  768.     }
  769. }