<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 21.09.2015
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\base\Controller;
use skeeks\cms\shop\models\ShopBill;
use skeeks\cms\shop\models\ShopOrder;
use skeeks\cms\shop\models\ShopPayment;
use yii\base\Exception;
use yii\helpers\Json;
use yii\web\NotFoundHttpException;

/**
 * @author Semenov Alexander <semenov@skeeks.com>
 */
class TinkoffController extends Controller
{
    /**
     * @inheritdoc
     */
    public $enableCsrfValidation = false;

    /**
     * Payment form
     *
     * @throws Exception
     */
    public function actionOrderForm()
    {
        if (!$key = \Yii::$app->request->get('key')) {
            throw new Exception('Order not found');
        }

        if (!$order = ShopOrder::find()->where(['key' => $key])->one()) {
            throw new Exception('Order not found');
        }

        return $this->render($this->action->id, [
            'model' => $order,
        ]);
    }

    /**
     * Payment form
     *
     * @throws Exception
     */
    public function actionBillForm()
    {
        /**
         * @var $bill ShopBill
         */

        if (!$code = \Yii::$app->request->get('code')) {
            throw new Exception('Bill not found');
        }

        if (!$bill = ShopBill::find()->where(['code' => $code])->one()) {
            throw new Exception('Bill not found');
        }

        return $this->render($this->action->id, [
            'model' => $bill,
        ]);

    }

    public function actionSuccess()
    {
        $orderId = \Yii::$app->request->get('OrderId');
        if (!$orderId) {
            throw new NotFoundHttpException('!!!');
        }

        if (!$shopOrder = ShopOrder::findOne($orderId)) {
            throw new NotFoundHttpException('!!!');
        }

        return $this->redirect($shopOrder->getPublicUrl(\Yii::$app->request->get()));
    }

    public function actionFail()
    {
        $orderId = \Yii::$app->request->get('OrderId');
        if (!$orderId) {
            throw new NotFoundHttpException('!!!');
        }

        if (!$shopOrder = ShopOrder::findOne($orderId)) {
            throw new NotFoundHttpException('!!!');
        }

        return $this->redirect($shopOrder->getPublicUrl(\Yii::$app->request->get()));
    }

    public function actionNotify()
    {
        \Yii::info("actionNotify", self::class);

        $json = file_get_contents('php://input');
        \Yii::info("JSON: ".$json, self::class);

        try {

            if (!$json) {
                throw new Exception('От банка не пришли данные json.');
            }

            $data = Json::decode($json);

            if (!isset($data['OrderId']) && !$data['OrderId']) {
                throw new Exception('Некорректны запрос от банка нет order id.');
            }

            /**
             * @var $shopBill ShopBill
             */
            if (!$shopBill = ShopBill::findOne($data['OrderId'])) {
                throw new Exception('Заказ не найден в базе.');
            }

            if ($shopBill->id != $data['OrderId']) {
                throw new Exception('Не совпадает номер заказа.');
            }

            $amount = $shopBill->money->amount * $shopBill->money->currency->subUnit;
            if ($amount != $data['Amount']) {
                throw new Exception('Не совпадает сумма заказа.');
            }

            if ($data['Status'] == "CONFIRMED") {
                \Yii::info("Успешный платеж", self::class);

                $transaction = \Yii::$app->db->beginTransaction();

                try {

                    $payment = new ShopPayment();
                    $payment->shop_buyer_id = $shopBill->shop_buyer_id;
                    $payment->shop_pay_system_id = $shopBill->shop_pay_system_id;
                    $payment->shop_order_id = $shopBill->shop_order_id;
                    $payment->amount = $shopBill->amount;
                    $payment->currency_code = $shopBill->currency_code;
                    $payment->comment = "Оплата по счету №{$shopBill->id} от ".\Yii::$app->formatter->asDate($shopBill->created_at);
                    $payment->external_data = $data;

                    if (!$payment->save()) {
                        throw new Exception("Не сохранился платеж: ".print_r($payment->errors, true));
                    }

                    $shopBill->paid_at = time();
                    $shopBill->shop_payment_id = $payment->id;

                    if (!$shopBill->save()) {
                        throw new Exception("Не обновился счет: ".print_r($shopBill->errors, true));
                    }

                    $shopBill->shopOrder->paid_at = time();
                    $shopBill->shopOrder->save();

                    $transaction->commit();
                } catch (\Exception $e) {
                    $transaction->rollBack();
                    \Yii::error($e->getMessage(), self::class);
                    return $e->getMessage();
                }

            }

        } catch (\Exception $e) {
            \Yii::error($e->getMessage(), self::class);
            return $e->getMessage();
        }

        $this->layout = false;
        return "OK";
    }
}


<?php
/**
 * @link https://cms.skeeks.com/
 * @copyright Copyright (c) 2010 SkeekS
 * @license https://cms.skeeks.com/license/
 * @author Semenov Alexander <semenov@skeeks.com>
 */

namespace skeeks\cms\shop\components;

use skeeks\cms\base\Component;
use skeeks\cms\components\Cms;
use skeeks\cms\models\CmsAgent;
use skeeks\cms\models\CmsContent;
use skeeks\cms\models\CmsUser;
use skeeks\cms\shop\models\ShopCart;
use skeeks\cms\shop\models\ShopOrder;
use skeeks\cms\shop\models\ShopOrderStatus;
use skeeks\cms\shop\models\ShopPersonType;
use skeeks\cms\shop\models\ShopTypePrice;
use yii\base\UserException;
use yii\helpers\ArrayHelper;
use yii\widgets\ActiveForm;

/**
 * @author Semenov Alexander <semenov@skeeks.com>
 *
 * @property ShopTypePrice    $baseTypePrice
 * @property ShopPersonType[] $shopPersonTypes
 * @property ShopTypePrice[]  $shopTypePrices
 * @property ShopTypePrice[]  $canBuyTypePrices
 *
 * @property ShopCart         $cart
 *
 * @property CmsContent       $shopContents
 * @property CmsContent       $storeContent
 * @property CmsContent       $stores
 *
 * @property array            $notifyEmails
 *
 * Class ShopComponent
 * @package skeeks\cms\shop\components
 */
class ShopComponent extends Component
{
    /**
     * @var string Email отдела продаж
     */
    public $email = "";

    /**
     * Начальный статус после создания заказа
     * @var string
     */
    public $start_order_status_id = "";
    /**
     * Конечный статус заказа
     * @var string
     */
    public $end_order_status_id = "";

    /**
     * Максимальное допустимое количество товаров
     * @var float
     */
    public $maxQuantity = 999999;
    /**
     * Минимально допустимое количество товаров
     * @var float
     */
    public $minQuantity = 0.01;
    /**
     * Оплата заказов онлайн системами, только после проверки менеджером
     * @var string
     */
    public $payAfterConfirmation = Cms::BOOL_N;
    /**
     * @var Контент который будет использоваться в качестве складов
     */
    public $storeCmsContentId;
    /**
     * @var Кого уведомить о новых товарах
     */
    public $notify_emails;
    /**
     * @var string
     */
    public $sessionFuserName = 'SKEEKS_CMS_SHOP';
    /**
     * @var ShopTypePrice
     */
    protected $_baseTypePrice;
    /**
     * @var array
     */
    protected $_shopTypePrices = [];
    /**
     * @var CartComponent
     */
    private $_cart = null;
    /**
     * @var ShopCart
     */
    private $_shopCart = null;
    /**
     * Можно задать название и описание компонента
     * @return array
     */
    static public function descriptorConfig()
    {
        return array_merge(parent::descriptorConfig(), [
            'name' => \Yii::t('skeeks/shop/app', 'Shop'),
        ]);
    }
    /**
     * @return ShopCart
     * @deprecated
     */
    public function getShopFuser()
    {
        return $this->cart;
    }
    public function renderConfigForm(ActiveForm $form)
    {
        echo $form->fieldSet(\Yii::t('skeeks/shop/app', 'Main'));

        //echo $form->field($this, 'email')->textInput()->hint(\Yii::t('skeeks/shop/app', 'Email of sales department'));

        echo $form->field($this, 'notify_emails')->textarea(['rows' => 3]);
        echo $form->field($this, 'start_order_status_id')->listBox(
            ArrayHelper::map(ShopOrderStatus::find()->all(), 'id', 'asText'),
            ['size' => 1]
        );
        echo $form->field($this, 'end_order_status_id')->listBox(
            ArrayHelper::map(ShopOrderStatus::find()->all(), 'id', 'asText'),
            ['size' => 1]
        );

        echo $form->fieldRadioListBoolean($this, 'payAfterConfirmation');
        echo $form->field($this, 'storeCmsContentId')->listBox(array_merge(['' => ' - '],
            CmsContent::getDataForSelect()), ['size' => 1]);

        echo $form->fieldSetEnd();
    }
    public function rules()
    {
        return ArrayHelper::merge(parent::rules(), [
            [['email'], 'string'],
            [['payAfterConfirmation'], 'string'],
            [['start_order_status_id'], 'integer'],
            [['end_order_status_id'], 'integer'],
            [['storeCmsContentId'], 'integer'],
            ['notify_emails', 'string'],
            ['start_order_status_id', 'required'],
            ['end_order_status_id', 'required'],
        ]);
    }
    public function attributeLabels()
    {
        return ArrayHelper::merge(parent::attributeLabels(), [
            'start_order_status_id'                => 'Начальный статус заказа',
            'end_order_status_id'                => 'Конечный статус заказа',
            'email'                => 'Email',
            'payAfterConfirmation' => \Yii::t('skeeks/shop/app',
                'Include payment orders only after the manager approval'),
            'storeCmsContentId'    => \Yii::t('skeeks/shop/app', 'Content storage'),
            'notify_emails'        => \Yii::t('skeeks/shop/app', 'Email notification address'),
        ]);
    }
    public function attributeHints()
    {
        return ArrayHelper::merge(parent::attributeHints(), [
            'start_order_status_id' => "Статус, который присваивается заказу сразу после его оформления",
            'end_order_status_id' => "Статус, который присваивается заказу после завершения работы с ним",
            'notify_emails' => \Yii::t('skeeks/shop/app',
                'Enter email addresses, separated by commas, they will come on new orders information'),
        ]);
    }
    /**
     *
     * Тип цены по умолчанию
     *
     * @return ShopTypePrice
     */
    public function getBaseTypePrice()
    {
        if (!$this->_baseTypePrice) {
            $this->_baseTypePrice = ShopTypePrice::find()->def()->one();
        }

        return $this->_baseTypePrice;
    }
    /**
     * @return ShopPersonType[]
     */
    public function getShopPersonTypes()
    {
        return ShopPersonType::find()->active()->all();
    }
    /**
     * Все типы цен магазина
     * @return ShopTypePrice[]
     */
    public function getShopTypePrices()
    {
        if (!$this->_shopTypePrices) {
            $this->_shopTypePrices = ShopTypePrice::find()->all();
        }

        return $this->_shopTypePrices;
    }

    /**
     * Типы цен по которым можно купить товар на сайте пользователю
     *
     * @param null|CmsUser $user
     * @return array
     */
    public function getCanBuyTypePrices($user = null)
    {
        $result = [];

        if (!$user) {
            $user = \Yii::$app->user->identity;
        }

        foreach ($this->shopTypePrices as $typePrice) {
            if (\Yii::$app->authManager->checkAccess($user ? $user->id : null, $typePrice->buyPermissionName)
                || $typePrice->isDefault
            ) {
                $result[$typePrice->id] = $typePrice;
            }
        }

        return $result;
    }
    /**
     * @return array|null|ShopCart
     */
    public function getCart()
    {
        if (\Yii::$app instanceof \yii\console\Application) {
            return null;
        }

        if ($this->_shopCart instanceof ShopCart) {
            return $this->_shopCart;
        }

        if (isset(\Yii::$app->user) && \Yii::$app->user && \Yii::$app->user->isGuest) {
            //Если пользователь гость
            //Проверка сессии
            if (\Yii::$app->getSession()->offsetExists($this->sessionFuserName)) {
                $fuserId = \Yii::$app->getSession()->get($this->sessionFuserName);
                $shopCart = ShopCart::find()->where(['id' => $fuserId])->one();
                //Поиск юзера
                if ($shopCart) {
                    $this->_shopCart = $shopCart;
                }
            }

            if (!$this->_shopCart) {
                $shopCart = new ShopCart();
                //$shopCart->save();
                //\Yii::$app->getSession()->set($this->sessionFuserName, $shopCart->id);
                $this->_shopCart = $shopCart;
            }
        } else {
            //Если пользователь авторизован
            $this->_shopCart = ShopCart::find()->where(['cms_user_id' => \Yii::$app->user->identity->id])->one();
            //Если у авторизовнного пользоывателя уже есть пользователь корзины
            if ($this->_shopCart) {
                //Проверка сессии, а было ли чего то в корзине
                if (\Yii::$app->getSession()->offsetExists($this->sessionFuserName)) {
                    $fuserId = \Yii::$app->getSession()->get($this->sessionFuserName);
                    $shopCart = ShopCart::find()->where(['id' => $fuserId])->one();

                    /**
                     * @var $shopCart ShopCart
                     */
                    if ($shopCart) {
                        $this->_shopCart->shopOrder->addShopOrderItems($shopCart->shopOrder->shopOrderItems);
                        $shopCart->delete();
                    }

                    //Эти данные в сессии больше не нужны
                    \Yii::$app->getSession()->remove($this->sessionFuserName);
                }
            } else {
                //Проверка сессии, а было ли чего то в корзине
                if (\Yii::$app->getSession()->offsetExists($this->sessionFuserName)) {
                    $fuserId = \Yii::$app->getSession()->get($this->sessionFuserName);
                    $shopCart = ShopCart::find()->where(['id' => $fuserId])->one();
                    //Поиск юзера
                    /**
                     * @var $shopCart ShopCart
                     */
                    if ($shopCart) {
                        $shopCart->cms_user_id = \Yii::$app->user->identity->id;
                        $shopCart->save();
                    }

                    $this->_shopCart = $shopCart;
                    \Yii::$app->getSession()->remove($this->sessionFuserName);
                } else {
                    $shopCart = new ShopCart([
                        'cms_user_id' => \Yii::$app->user->identity->id,
                    ]);

                    $shopCart->save();
                    $this->_shopCart = $shopCart;
                }
            }
        }

        /**
         * Если у корзины нет заказа, нужно его создать
         */
        /*if (!$this->_shopCart->shop_order_id) {
            $shopOrder = new ShopOrder();
            $shopOrder->cms_site_id = \Yii::$app->cms->site->id;
            if (!$shopOrder->save()) {
                throw new UserException("Заказ-черновик не создан: ".print_r($shopOrder->errors, true));
            }
            $this->_shopCart->shop_order_id = $shopOrder->id;
            $this->_shopCart->save(false);
        }*/

        return $this->_shopCart;
    }

    /**
     * @param ShopCart $shopCart
     * @return $this
     */
    public function setCart(ShopCart $shopCart)
    {
        $this->_shopCart = $shopCart;
        return $this;
    }

    /**
     * @return $this
     */
    public function getShopContents()
    {
        $query = \skeeks\cms\models\CmsContent::find()->orderBy("priority ASC")->andWhere([
            'id' => \yii\helpers\ArrayHelper::map(\skeeks\cms\shop\models\ShopContent::find()->all(), 'content_id',
                'content_id'),
        ]);

        $query->multiple = true;
        return $query->all();
    }

    /**
     * TODO: is @deprecated remove it!
     * @return array
     */
    public function getArrayForSelectElement()
    {

        if (!$data = CmsContent::getDataForSelect()) {
            return [];
        }

        $ids = ArrayHelper::map($this->shopContents, 'id', 'id');

        $result = [];
        foreach ($data as $typeKey => $type) {
            if ($type) {
                $contents = [];
                foreach ($type as $key => $value) {
                    if (in_array($key, $ids)) {
                        $contents[$key] = $value;
                    }
                }

                if ($contents) {
                    $result[$typeKey] = $contents;
                }
            }
        }

        return $result;
    }

    /**
     * @return CmsContent
     */
    public function getStoreContent()
    {
        if (!$contentId = (int)$this->storeCmsContentId) {
            return null;
        }

        return CmsContent::findOne($contentId);
    }

    /**
     * @return array
     */
    public function getStores()
    {
        if ($this->storeContent) {
            return $this->storeContent->getCmsContentElements()->all();
        }

        return [];
    }

    /**
     * @return array
     */
    public function getNotifyEmails()
    {
        $emailsAll = [];
        if ($this->notify_emails) {
            $emails = explode(",", $this->notify_emails);

            foreach ($emails as $email) {
                $emailsAll[] = trim($email);
            }
        }

        return $emailsAll;
    }
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 14.09.2015
 */

namespace skeeks\cms\shop\models;

use skeeks\cms\components\Cms;
use skeeks\cms\measure\models\Measure;
use skeeks\cms\models\CmsContentElement;
use skeeks\modules\cms\money\models\Currency;
use Yii;
use yii\db\AfterSaveEvent;
use yii\helpers\ArrayHelper;

/**
 * This is the model class for table "{{%shop_product}}".
 *
 * @property integer                     $id
 * @property integer                     $created_by
 * @property integer                     $updated_by
 * @property integer                     $created_at
 * @property integer                     $updated_at
 * @property double                      $quantity
 * @property string                      $quantity_trace
 * @property double                      $weight
 * @property string                      $price_type
 * @property string                      $measure_ratio
 * @property integer                     $recur_scheme_length
 * @property string                      $recur_scheme_type
 * @property integer                     $trial_price_id
 * @property string                      $without_order
 * @property string                      $select_best_price
 * @property integer                     $vat_id
 * @property string                      $vat_included
 * @property string                      $tmp_id
 * @property string                      $can_buy_zero
 * @property string                      $negative_amount_trace
 * @property string                      $barcode_multi
 * @property string                      $purchasing_price
 * @property string                      $purchasing_currency
 * @property double                      $quantity_reserved
 * @property integer                     $measure_id
 * @property double                      $width
 * @property double                      $length
 * @property double                      $height
 * @property string                      $subscribe
 * @property string                      $product_type
 *
 * @property Measure                     $measure
 * @property ShopCmsContentElement       $cmsContentElement
 * @property ShopTypePrice               $trialPrice
 * @property ShopVat                     $vat
 * @property Currency                    $purchasingCurrency
 * @property ShopProductPrice[]          $shopProductPrices
 * @property ShopViewedProduct[]         $shopViewedProducts
 *
 * @property string                      $baseProductPriceValue
 * @property string                      $baseProductPriceCurrency
 *
 * @property ShopProductPrice            $baseProductPrice
 * @property ShopProductPrice            $minProductPrice
 * @property ShopProductPrice[]          $viewProductPrices
 * @property ShopProductQuantityChange[] $shopProductQuantityChanges
 * @property ShopQuantityNoticeEmail[]   $shopQuantityNoticeEmails
 *
 * @property ShopCmsContentElement       $tradeOffers
 * @property ShopOrderItem[] $shopOrderItems
 * @property ShopOrder[] $shopOrders
 */
class ShopProduct extends \skeeks\cms\models\Core
{
    const TYPE_SIMPLE = 'simple';
    const TYPE_OFFERS = 'offers';
    const TYPE_OFFER = 'offer';
    static public $instances = [];
    private $_baseProductPriceValue = null;
    private $_baseProductPriceCurrency = null;
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%shop_product}}';
    }
    /**
     * @return array
     */
    static public function possibleProductTypes()
    {
        return [
            static::TYPE_SIMPLE => \Yii::t('skeeks/shop/app', 'Plain'),
            static::TYPE_OFFERS => \Yii::t('skeeks/shop/app', 'With quotations'),
            static::TYPE_OFFER  => \Yii::t('skeeks/shop/app', 'Товар-предложение'),
        ];
    }
    /**
     * @param CmsContentElement $cmsContentElement
     * @return static
     */
    static public function getInstanceByContentElement(CmsContentElement $cmsContentElement)
    {
        if ($self = ArrayHelper::getValue(static::$instances, $cmsContentElement->id)) {
            return $self;
        }

        /**
         * @version SkeekS CMS > 2.6.0
         */
        if ($cmsContentElement instanceof \skeeks\cms\shop\models\ShopCmsContentElement) {
            static::$instances[$cmsContentElement->id] = $cmsContentElement->shopProduct;
            return $cmsContentElement->shopProduct;
        }

        if (!$self = static::find()->where(['id' => $cmsContentElement->id])->one()) {
            $self = new static([
                'id' => $cmsContentElement->id,
            ]);

            $self->save();
            $self->refresh();
        }

        static::$instances[$cmsContentElement->id] = $self;

        return $self;
    }
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        $this->on(self::EVENT_BEFORE_INSERT, [$this, "_beforeSaveEvent"]);
        $this->on(self::EVENT_BEFORE_UPDATE, [$this, "_beforeSaveEvent"]);

        $this->on(self::EVENT_AFTER_INSERT, [$this, "_afterSaveEvent"]);
        $this->on(self::EVENT_AFTER_UPDATE, [$this, "_afterSaveEvent"]);

        $this->on(self::EVENT_AFTER_INSERT, [$this, "_updateParentAfterInsert"]);

        $this->on(self::EVENT_AFTER_INSERT, [$this, "_logQuantityInsert"]);
        $this->on(self::EVENT_BEFORE_UPDATE, [$this, "_logQuantityUpdate"]);
    }
    public function _logQuantityInsert($event)
    {
        $log = new ShopProductQuantityChange();

        $log->shop_product_id = $this->id;
        $log->quantity = $this->quantity;
        $log->quantity_reserved = $this->quantity_reserved;
        $log->measure_id = $this->measure_id;
        $log->measure_ratio = $this->measure_ratio;

        $log->save();
    }

    public function _logQuantityUpdate($event)
    {
        if (
            ($this->isAttributeChanged('quantity', false)
                || $this->isAttributeChanged('quantity_reserved', false)
                || $this->isAttributeChanged('measure_id', false)
                || $this->isAttributeChanged('measure_ratio', false))
            ||
            !$this->shopProductQuantityChanges
        ) {
            $log = new ShopProductQuantityChange();

            $log->shop_product_id = $this->id;
            $log->quantity = $this->quantity;
            $log->quantity_reserved = $this->quantity_reserved;
            $log->measure_id = $this->measure_id;
            $log->measure_ratio = $this->measure_ratio;

            $log->save();
        }
    }
    /**
     * Перед сохранением модели, всегда следим за типом товара
     * @param $event
     */
    public function _beforeSaveEvent($event)
    {
        //Проверка измененного типа
        if ($this->isAttributeChanged('product_type')) {
            //Выставили что у него есть предложения
            if ($this->product_type == self::TYPE_OFFERS) {
                if (!$this->getTradeOffers()->all()) {
                    $this->product_type = self::TYPE_SIMPLE;
                }
            } elseif ($this->product_type == self::TYPE_SIMPLE) //Если указали что товар простой, значит у него не должно быть предложений
            {
                if ($this->getTradeOffers()->all()) {
                    $this->product_type = self::TYPE_OFFERS;
                }
            } elseif ($this->product_type == self::TYPE_OFFER) //Если указали что товар простой, значит у него не должно быть предложений
            {
                if (!$this->cmsContentElement->parent_content_element_id) {
                    $this->product_type = self::TYPE_SIMPLE;
                }

            }
        }
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getTradeOffers()
    {
        $childContentId = null;
        if ($this->cmsContentElement && $this->cmsContentElement->shopContent) {
            $childContentId = $this->cmsContentElement->shopContent->children_content_id;
        }

        return $this
            ->hasMany(ShopCmsContentElement::class, ['parent_content_element_id' => 'id'])
            ->andWhere(["content_id" => $childContentId])
            //->joinWith('cmsContentElement')
            //->joinWith('cmsContentElement.cmsContent')
            //->andWhere(["content.id" => $this->cmsContentElement->cmsContent->parent_content_id])
            ->orderBy(['priority' => SORT_ASC]);
    }
    /**
     * После сохранения следим за ценами создаем если нет
     * @param $event
     */
    public function _afterSaveEvent(AfterSaveEvent $event)
    {
        //Prices update
        if ($this->_baseProductPriceCurrency !== null || $this->_baseProductPriceValue !== null) {
            /**
             * @var $baseProductPrice ShopProductPrice
             */
            $baseProductPrice = $this->getBaseProductPrice()->one();

            if (!$baseProductPrice) {
                $baseProductPrice = new ShopProductPrice([
                    'product_id' => $this->id,
                ]);

                $baseProductPrice->type_price_id = \Yii::$app->shop->baseTypePrice->id;

                if ($this->_baseProductPriceValue) {
                    $baseProductPrice->price = $this->_baseProductPriceValue;
                }

                if ($this->_baseProductPriceCurrency) {
                    $baseProductPrice->currency_code = $this->_baseProductPriceCurrency;
                }

                $baseProductPrice->save();
            } else {
                $isChanged = false;
                //Установка и сохранение только если что то изменилось
                if ($this->_baseProductPriceValue !== null && $this->_baseProductPriceValue != $baseProductPrice->price) {
                    $baseProductPrice->price = $this->_baseProductPriceValue;
                    $isChanged = true;
                }

                if ($this->_baseProductPriceCurrency && $this->_baseProductPriceCurrency != $baseProductPrice->currency_code) {
                    $baseProductPrice->currency_code = $this->_baseProductPriceCurrency;
                    $isChanged = true;
                }

                if ($isChanged) {
                    $baseProductPrice->save();
                }
            }

        } else {
            if (!$this->baseProductPrice) {
                $baseProductPrice = new ShopProductPrice([
                    'product_id' => $this->id,
                ]);

                $baseProductPrice->type_price_id = \Yii::$app->shop->baseTypePrice->id;
                $baseProductPrice->save();
            }
        }

        if (in_array('product_type', (array)$event->changedAttributes)) {
            if ($this->product_type == self::TYPE_OFFER) {
                if ($this->cmsContentElement->parentContentElement->shopProduct) {
                    $sp = $this->cmsContentElement->parentContentElement->shopProduct;
                    $sp->product_type = self::TYPE_OFFERS;
                    $sp->save();
                }
            }
        }

    }
    /**
     *
     * Базовая цена по умолчанию
     *
     * @return \yii\db\ActiveQuery
     */
    public function getBaseProductPrice()
    {
        return $this->hasOne(ShopProductPrice::class, [
            'product_id' => 'id',
        ])->andWhere(['type_price_id' => \Yii::$app->shop->baseTypePrice->id]);
    }
    /**
     * Если втавленный элемент является дочерним для другого то родительскому нужно изменить тип
     * @param $event
     */
    public function _updateParentAfterInsert($event)
    {
        //Если есть родительский элемент
        if ($this->cmsContentElement->parent_content_element_id) {
            $parentProduct = $this->cmsContentElement->parentContentElement->shopProduct;
            $parentProduct->setAttribute('product_type', self::TYPE_OFFERS);
            $parentProduct->save();
        }
    }
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [
                [
                    'created_by',
                    'updated_by',
                    'created_at',
                    'updated_at',
                    'recur_scheme_length',
                    'trial_price_id',
                    'vat_id',
                    'measure_id',
                ],
                'integer',
            ],
            [
                [
                    'quantity',
                    'weight',
                    'purchasing_price',
                    'quantity_reserved',
                    'width',
                    'length',
                    'height',
                    'measure_ratio',
                ],
                'number',
            ],
            [
                [
                    'quantity_trace',
                    'price_type',
                    'recur_scheme_type',
                    'without_order',
                    'select_best_price',
                    'vat_included',
                    'can_buy_zero',
                    'negative_amount_trace',
                    'barcode_multi',
                    'subscribe',
                ],
                'string',
                'max' => 1,
            ],
            [['tmp_id'], 'string', 'max' => 40],
            [['purchasing_currency'], 'string', 'max' => 3],
            [['quantity_trace', 'can_buy_zero', 'negative_amount_trace'], 'default', 'value' => Cms::BOOL_N],
            [['weight', 'width', 'length', 'height', 'purchasing_price'], 'default', 'value' => 0],
            [['subscribe'], 'default', 'value' => Cms::BOOL_Y],
            [['measure_ratio'], 'default', 'value' => 1],
            [['measure_ratio'], 'number', 'min' => 0.0001, 'max' => 9999999],
            [['purchasing_currency'], 'default', 'value' => Yii::$app->money->currencyCode],

            [['baseProductPriceValue'], 'number'],
            [['baseProductPriceValue'], 'default', 'value' => 0.00 ],
            [['baseProductPriceValue'], function() {
                if ($this->baseProductPriceValue == "0") {
                    $this->baseProductPriceValue = 0.00;
                }
            }],

            [['baseProductPriceCurrency'], 'string', 'max' => 3],

            [['vat_included'], 'default', 'value' => Cms::BOOL_Y],
            [
                ['measure_id'],
                'default',
                'value' => function () {
                    return (int)Measure::find()->def()->one()->id;
                },
            ],

            [['product_type'], 'string', 'max' => 10],
            [['product_type'], 'default', 'value' => static::TYPE_SIMPLE],
            /*[
                'product_type', function ($attribute) {
                    if ($this->{$attribute} == self::TYPE_OFFER) {
                        return false;
                    }

                },

            ],*/

            [['quantity'], 'default', 'value' => 1],
            [['quantity_reserved'], 'default', 'value' => 0],
        ];
    }
    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id'                    => \Yii::t('skeeks/shop/app', 'ID'),
            'created_by'            => \Yii::t('skeeks/shop/app', 'Created By'),
            'updated_by'            => \Yii::t('skeeks/shop/app', 'Updated By'),
            'created_at'            => \Yii::t('skeeks/shop/app', 'Created At'),
            'updated_at'            => \Yii::t('skeeks/shop/app', 'Updated At'),
            'quantity'              => \Yii::t('skeeks/shop/app', 'Available quantity'),
            'quantity_trace'        => \Yii::t('skeeks/shop/app', 'Include quantitative account'),
            'weight'                => \Yii::t('skeeks/shop/app', 'Weight (gramm)'),
            'price_type'            => \Yii::t('skeeks/shop/app', 'Price Type'),
            'recur_scheme_length'   => \Yii::t('skeeks/shop/app', 'Recur Scheme Length'),
            'recur_scheme_type'     => \Yii::t('skeeks/shop/app', 'Recur Scheme Type'),
            'trial_price_id'        => \Yii::t('skeeks/shop/app', 'Trial Price ID'),
            'without_order'         => \Yii::t('skeeks/shop/app', 'Without Order'),
            'select_best_price'     => \Yii::t('skeeks/shop/app', 'Select Best Price'),
            'vat_id'                => \Yii::t('skeeks/shop/app', 'VAT rate'),
            'vat_included'          => \Yii::t('skeeks/shop/app', 'VAT included in the price'),
            'tmp_id'                => \Yii::t('skeeks/shop/app', 'Tmp ID'),
            'can_buy_zero'          => \Yii::t('skeeks/shop/app', 'Allow purchase if product is absent'),
            'negative_amount_trace' => \Yii::t('skeeks/shop/app', 'Allow negative quantity'),
            'barcode_multi'         => \Yii::t('skeeks/shop/app', 'Barcode Multi'),
            'purchasing_price'      => \Yii::t('skeeks/shop/app', 'Purchase price'),
            'purchasing_currency'   => \Yii::t('skeeks/shop/app', 'Currency purchase price'),
            'quantity_reserved'     => \Yii::t('skeeks/shop/app', 'Reserved quantity'),
            'measure_id'            => \Yii::t('skeeks/shop/app', 'Unit of measurement'),
            'measure_ratio'         => \Yii::t('skeeks/shop/app', 'The coefficient unit'),
            'width'                 => \Yii::t('skeeks/shop/app', 'Width (mm)'),
            'length'                => \Yii::t('skeeks/shop/app', 'Length (mm)'),
            'height'                => \Yii::t('skeeks/shop/app', 'Height (mm)'),
            'subscribe'             => \Yii::t('skeeks/shop/app', 'Allow subscription without explanation'),
            'product_type'          => \Yii::t('skeeks/shop/app', 'Product type'),
        ];
    }
    /**
     *
     * Отметить просмотр текущего товара согласно текущим данным
     *
     * @return bool
     */
    public function createNewView()
    {
        if ($this->isNewRecord) {
            return false;
        }

        $shopViewdProduct = new ShopViewedProduct();
        $shopViewdProduct->name = $this->cmsContentElement->name;
        $shopViewdProduct->shop_product_id = $this->id;
        $shopViewdProduct->site_id = \Yii::$app->cms->site->id;
        $shopViewdProduct->shop_fuser_id = \Yii::$app->shop->cart->id;

        return $shopViewdProduct->save();
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getMeasure()
    {
        return $this->hasOne(Measure::class, ['id' => 'measure_id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getCmsContentElement()
    {
        return $this->hasOne(ShopCmsContentElement::class, ['id' => 'id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getTrialPrice()
    {
        return $this->hasOne(ShopTypePrice::class, ['id' => 'trial_price_id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getVat()
    {
        return $this->hasOne(ShopVat::class, ['id' => 'vat_id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getPurchasingCurrency()
    {
        return $this->hasOne(Currency::class, ['code' => 'purchasing_currency']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopProductPrices()
    {
        return $this->hasMany(ShopProductPrice::class, ['product_id' => 'id'])->from(['prices' => ShopProductPrice::tableName()]);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopViewedProducts()
    {
        return $this->hasMany(ShopViewedProduct::class, ['shop_product_id' => 'id']);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopProductQuantityChanges()
    {
        return $this->hasMany(ShopProductQuantityChange::class,
            ['shop_product_id' => 'id'])->orderBy(['created_at' => SORT_DESC]);
    }
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopQuantityNoticeEmails()
    {
        return $this->hasMany(ShopQuantityNoticeEmail::class, ['shop_product_id' => 'id']);
    }

    /**
     * Цены доступные к просмотру
     *
     * @param null $shopFuser
     * @return $this
     */
    public function getViewProductPrices($shopFuser = null)
    {
        if ($shopFuser === null) {
            $shopFuser = \Yii::$app->shop->cart;
        }

        return $this->hasMany(ShopProductPrice::class, [
            'product_id' => 'id',
        ])->andWhere([
            'type_price_id' => ArrayHelper::map($shopFuser->viewTypePrices, 'id', 'id'),
        ])->orderBy(['price' => SORT_ASC]);
    }

    /**
     *
     * Лучшая цена по которой может купить этот товар пользователь, среди всех доступных
     *
     * @param null $shopFuser
     * @return $this
     */
    public function getMinProductPrice($shopFuser = null)
    {
        if ($shopFuser === null) {
            $shopFuser = \Yii::$app->shop->cart;
        }

        return $this->hasOne(ShopProductPrice::class, [
            'product_id' => 'id',
        ])
            ->select([
                'shop_product_price.*',
                'realPrice' => '( (SELECT course FROM money_currency WHERE money_currency.code = shop_product_price.currency_code) * shop_product_price.price )',
            ])
            ->leftJoin('money_currency', 'money_currency.code = shop_product_price.currency_code')
            ->orWhere([
                'and',
                ['>', 'price', 0],
                ['type_price_id' => ArrayHelper::map($shopFuser->buyTypePrices, 'id', 'id')],
            ])
            ->orWhere(
                ['type_price_id' => \Yii::$app->shop->baseTypePrice->id]
            )
            ->orderBy(['realPrice' => SORT_ASC]);
    }

    /**
     * Значение базовой цены
     *
     * @return string
     */
    public function getBaseProductPriceValue()
    {
        if ($this->_baseProductPriceValue !== null) {
            return $this->_baseProductPriceValue;
        } else {
            $this->_baseProductPriceValue = $this->baseProductPrice ? $this->baseProductPrice->price : null;
        }

        return $this->_baseProductPriceValue;
    }

    /**
     * @param $value
     * @return $this
     */
    public function setBaseProductPriceValue($value)
    {
        $this->_baseProductPriceValue = $value;
        return $this;
    }
    /**
     * Валюта базовой цены
     *
     * @return string
     */
    public function getBaseProductPriceCurrency()
    {
        if ($this->_baseProductPriceCurrency) {
            return $this->_baseProductPriceCurrency;
        } else {
            $this->_baseProductPriceCurrency = $this->baseProductPrice ? $this->baseProductPrice->currency_code : \Yii::$app->money->currencyCode;
        }

        return $this->_baseProductPriceCurrency;
    }

    /**
     * @param $value
     * @return $this
     */
    public function setBaseProductPriceCurrency($value)
    {
        $this->_baseProductPriceCurrency = $value;
        return $this;
    }

    /**
     * Товар с предложениями?
     * @return bool
     */
    public function isTradeOffers()
    {
        return (bool)($this->product_type == \skeeks\cms\shop\models\ShopProduct::TYPE_OFFERS);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopOrderItems()
    {
        return $this->hasMany(ShopOrderItem::className(), ['shop_product_id' => 'id']);
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getShopOrders()
    {
        return $this->hasMany(ShopOrder::className(), ['id' => 'shop_order_id'])->via('shopOrderItems');
    }

}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 21.09.2015
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\shop\models\ShopBill;
use skeeks\cms\shop\models\ShopOrder;
use skeeks\cms\shop\models\ShopPayment;
use skeeks\cms\shop\paySystems\YandexKassaPaySystem;
use YandexCheckout\Client;
use yii\base\Exception;
use yii\helpers\ArrayHelper;
use yii\web\Controller;

/**
 * Class YandexKassaController
 * @package skeeks\cms\shop\controllers
 */
class YandexKassaController extends Controller
{
    public function beforeAction($action)
    {
        if (in_array($action->id, ['check-order', 'payment-aviso'])) {
            $this->enableCsrfValidation = false;
            \Yii::$app->response->setStatusCode(200);
            \Yii::$app->response->format = \yii\web\Response::FORMAT_RAW;
            \Yii::$app->response->headers->set('Content-Type', 'application/xml; charset=utf-8');
            $this->layout = false;

            \Yii::info("{$action->id} URL: ".\Yii::$app->request->absoluteUrl, YandexKassaPaySystem::class);
        }

        return parent::beforeAction($action);
    }

    public function actionCheckOrder()
    {
        $request = YandexKassaPaySystem::getRequest();
        \Yii::info("{$action->id} REQUEST2: ".print_r($request, true), YandexKassaPaySystem::class);

        try {
            $shopOrder = $this->getBill($request);
            /**
             * @var $yandexKassa YandexKassaPaySystem
             */
            $yandexKassa = $shopOrder->shopPaySystem->handler;
            if (!$yandexKassa->checkRequest($request)) {
                $response = $yandexKassa->buildResponse("checkOrder", $request['invoiceId'], 1);
                return $response;
            }

            /*if (\Yii::$app->request->post('Status') == "CONFIRMED")
            {
                \Yii::info("Успешный платеж", YandexKassaPaySystem::class);
                $shopOrder->processNotePayment();
            }*/

        } catch (\Exception $e) {
            \Yii::error($e->getMessage(), YandexKassaPaySystem::class);
            $response = $yandexKassa->buildResponse("checkOrder", $request['invoiceId'], 500, $e->getMessage());
            return $response;
        }

        if ($request['orderSumAmount'] < 1) {
            \Yii::error("The amount should be more than 1 rubles.", YandexKassaPaySystem::class);
            $response = $yandexKassa->buildResponse("checkOrder", $request['invoiceId'], 100,
                "The amount should be more than 100 rubles.");
        } else {
            $response = $yandexKassa->buildResponse("checkOrder", $request['invoiceId'], 0);
        }

        \Yii::info("Response actionCheckOrder: ".$response, YandexKassaPaySystem::class);
        return $response;
    }
    /**
     * @param $request
     * @return ShopOrder
     * @throws Exception
     * @deprecated
     */
    public function getOrder($request)
    {
        if (!$orderNumber = ArrayHelper::getValue($request, 'orderNumber')) {
            throw new Exception('Некорректный запрос от банка. Не указан orderNumber.');
        }

        /**
         * @var $shopOrder ShopOrder
         */
        if (!$shopOrder = ShopOrder::findOne($orderNumber)) {
            throw new Exception('Заказ не найден в базе.');
        }

        if ($shopOrder->id != $orderNumber) {
            throw new Exception('Не совпадает номер заказа.');
        }

        if ((float)$shopOrder->money->getValue() != (float)ArrayHelper::getValue($request, 'orderSumAmount')) {
            throw new Exception('Не совпадает сумма заказа.');
        }

        return $shopOrder;
    }
    /**
     * @param $request
     * @return ShopBill
     * @throws Exception
     */
    public function getBill($request)
    {
        if (!$orderNumber = ArrayHelper::getValue($request, 'orderNumber')) {
            throw new Exception('Некорректный запрос от банка. Не указан orderNumber.');
        }

        /**
         * @var $shopOrder ShopBill
         */
        if (!$shopOrder = ShopBill::findOne($orderNumber)) {
            throw new Exception('Заказ не найден в базе.');
        }

        if ($shopOrder->id != $orderNumber) {
            throw new Exception('Не совпадает номер заказа.');
        }

        if ((float)$shopOrder->money->amount != (float)ArrayHelper::getValue($request, 'orderSumAmount')) {
            throw new Exception('Не совпадает сумма заказа.');
        }

        return $shopOrder;
    }
    /**
     * @return array|string
     */
    public function actionPaymentAviso()
    {
        $request = YandexKassaPaySystem::getRequest();
        \Yii::info("{$this->action->id} REQUEST2: ".print_r($request, true), YandexKassaPaySystem::class);

        try {
            $bill = $this->getBill($request);
            /**
             * @var $yandexKassa YandexKassaPaySystem
             */
            $yandexKassa = $bill->shopPaySystem->handler;
            if (!$yandexKassa->checkRequest($request)) {
                $response = $yandexKassa->buildResponse("paymentAviso", $request['invoiceId'], 1);
                return $response;
            }

        } catch (\Exception $e) {
            \Yii::error($e->getMessage(), YandexKassaPaySystem::class);
            $response = $yandexKassa->buildResponse("paymentAviso", $request['invoiceId'], 500, $e->getMessage());
            return $response;
        }

        \Yii::info("Успешный платеж", YandexKassaPaySystem::class);

        //$shopOrder->processNotePayment();

        $transaction = \Yii::$app->db->beginTransaction();

        try {

            $payment = new ShopPayment();
            $payment->shop_buyer_id = $bill->shop_buyer_id;
            $payment->shop_pay_system_id = $bill->shop_pay_system_id;
            $payment->shop_order_id = $bill->shop_order_id;
            $payment->amount = $bill->amount;
            $payment->currency_code = $bill->currency_code;
            $payment->comment = "Оплата по счету №{$bill->id} от ".\Yii::$app->formatter->asDate($bill->created_at);
            $payment->external_data = $response;

            if (!$payment->save()) {
                throw new Exception("Не сохранился платеж: ".print_r($payment->errors, true));
            }

            $bill->paid_at = time();
            $bill->shop_payment_id = $payment->id;

            if (!$bill->save()) {
                throw new Exception("Не обновился счет: ".print_r($payment->errors, true));
            }

            $bill->shopOrder->paid_at = time();
            $bill->shopOrder->save();

            $transaction->commit();

            return $this->redirect($bill->shopOrder->url);

        } catch (\Exception $e) {
            $transaction->rollBack();
            \Yii::error($e->getMessage(), YandexKassaPaySystem::class);
            throw $e;
        }

        $response = $yandexKassa->buildResponse("paymentAviso", $request['invoiceId'], 0);

        \Yii::info("Response actionCheckOrder: ".$response, YandexKassaPaySystem::class);

        return $response;
    }
    /**
     * Payment form
     *
     * @throws Exception
     */
    public function actionBillForm()
    {
        if (!$code = \Yii::$app->request->get('code')) {
            throw new Exception('Order not found');
        }

        if (!$bill = ShopBill::find()->where(['code' => $code])->one()) {
            throw new Exception('Order not found');
        }

        /*return $this->render($this->action->id, [
            'model' => $bill,
        ]);*/

        /* @var $yandexKassa \skeeks\cms\shop\paySystems\YandexKassaPaySystem */
        /* @var $model \skeeks\cms\shop\models\ShopBill */

        $model = $bill;
        $yandexKassa = $model->shopPaySystem->paySystemHandler;
        $money = $model->money->convertToCurrency("RUB");
        $returnUrl = $model->getUrl(true);

        $client = new Client();
        $client->setAuth($yandexKassa->shop_id, $yandexKassa->shop_password);
        $payment = $client->createPayment(
            array(
                'amount' => array(
                    'value' => $money->amount,
                    'currency' => 'RUB',
                ),
                'confirmation' => array(
                    'type' => 'redirect',
                    'return_url' => $returnUrl,
                ),
                'description' => 'Заказ №' . $model->shop_order_id,
            ),
            uniqid('', true)
        );

        \Yii::info(print_r($payment, true), self::class);

        if (!$payment->id) {
            throw new Exception('Yandex kassa payment id not found');
        }

        $model->external_data = [
            'id' => $payment->id
        ];
        $model->save();

        /*$paymentId = $payment->id;
        $idempotenceKey = uniqid('', true);
        $response = $client->capturePayment(
          array(
              'amount' => array(
                  'value' => '2.00',
                  'currency' => 'RUB',
              ),
          ),
          $paymentId,
          $idempotenceKey
        );*/

        return $this->redirect($payment->confirmation->getConfirmationUrl());
    }

    public $enableCsrfValidation = false;

    public function actionPaymentListener()
    {
        \Yii::info(__METHOD__, self::class);
        /*\Yii::info(print_r($_SERVER, true), self::class);
        \Yii::info(print_r(\Yii::$app->request->post(), true), self::class);
        \Yii::info(print_r($_GET, true), self::class);
        \Yii::info(print_r($_POST, true), self::class);
        \Yii::info(print_r($_REQUEST, true), self::class);*/

        //todo: fix it
        /**
         * @var $shopBill ShopBill
         */
        $shopBill = ShopBill::find()->orderBy(['id' => SORT_DESC])->limit(1)->one();

        \Yii::info("Счет: " . print_r($shopBill->toArray(), true), self::class);

        /* @var $yandexKassa \skeeks\cms\shop\paySystems\YandexKassaPaySystem */
        /* @var $model \skeeks\cms\shop\models\ShopBill */

        $model = $shopBill;
        $yandexKassa = $model->shopPaySystem->paySystemHandler;

        if ($shopBill->paid_at) {
            \Yii::info("Счет: " . $shopBill->id . " уже оплаечен", self::class);
            return "Ok";
        }

        //\Yii::info(print_r($model->shopPaySystem, true), self::class);
        //\Yii::info(print_r($yandexKassa, true), self::class);

        if ($paymentId = ArrayHelper::getValue($shopBill->external_data, 'id')) {

            \Yii::info("Запрос информации о платеже: " . print_r([
                'shop_id' => $yandexKassa->shop_id,
                'shop_password' => $yandexKassa->shop_password,
                ], true), self::class);

            $client = new Client();
            $client->setAuth($yandexKassa->shop_id, $yandexKassa->shop_password);
            $payment = $client->getPaymentInfo($paymentId);

            \Yii::info("Информация о платеже в yandex kassa: " . print_r($payment, true), self::class);

            if ($payment->status == "waiting_for_capture") {

                $money = $model->money->convertToCurrency("RUB");

                $idempotenceKey = uniqid('', true);
                $response = $client->capturePayment(
                    array(
                        'amount' => array(
                          'value' => $money->amount,
                          'currency' => 'RUB',
                        ),
                    ),
                    $paymentId,
                    $idempotenceKey
                );

                \Yii::info("Подтверждение оплаты: " . print_r($response, true), self::class);
            }

            $payment = $client->getPaymentInfo($paymentId);

            if ($payment->status == "succeeded") {

                $transaction = \Yii::$app->db->beginTransaction();

                try {

                    $payment = new ShopPayment();
                    $payment->shop_buyer_id = $shopBill->shop_buyer_id;
                    $payment->shop_pay_system_id = $shopBill->shop_pay_system_id;
                    $payment->shop_order_id = $shopBill->shop_order_id;
                    $payment->amount = $shopBill->amount;
                    $payment->currency_code = $shopBill->currency_code;
                    $payment->comment = "Оплата по счету №{$shopBill->id} от ".\Yii::$app->formatter->asDate($shopBill->created_at);
                    $payment->external_data = $response;

                    if (!$payment->save()) {
                        throw new Exception("Не сохранился платеж: ".print_r($payment->errors, true));
                    }

                    $shopBill->paid_at = time();
                    $shopBill->shop_payment_id = $payment->id;

                    if (!$shopBill->save()) {
                        throw new Exception("Не обновился счет: ".print_r($payment->errors, true));
                    }

                    $shopBill->shopOrder->paid_at = time();
                    $shopBill->shopOrder->save();

                    $transaction->commit();

                    return $this->redirect($shopBill->shopOrder->url);

                } catch (\Exception $e) {
                    $transaction->rollBack();
                    \Yii::error($e->getMessage(), YandexKassaPaySystem::class);
                    throw $e;
                }

            }
        }
        /*$client = new Client();
        $client->setAuth($yandexKassa->shop_id, $yandexKassa->shop_password);
        $client->getPaymentInfo()*/

        return "Ok";
    }
}


<?php
/**
 * This file is part of the O2System Framework package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @author         Steeve Andrian Salim
 * @copyright      Copyright (c) Steeve Andrian Salim
 */

// ------------------------------------------------------------------------

namespace O2System\Curl;

// ------------------------------------------------------------------------

use O2System\Spl\Patterns\Structural\Provider\AbstractProvider;
use O2System\Spl\Patterns\Structural\Provider\ValidationInterface;

/**
 * Class MultiRequest
 *
 * @package O2System\Curl
 */
class MultiRequest extends AbstractProvider implements ValidationInterface
{
    /**
     * MultiRequest::$curlHandle
     *
     * Multi request curl handle.
     *
     * @var resource
     */
    protected $curlHandle;

    // ------------------------------------------------------------------------

    /**
     * MultiRequest::__construct
     */
    public function __construct()
    {
        $this->curlHandle = curl_multi_init();
    }

    // ------------------------------------------------------------------------

    /**
     * MultiRequest::getResponse
     *
     * Get response from multiple curl request.
     *
     * @return array
     */
    public function get()
    {
        $responses = [];
        $handle = curl_multi_init();

        foreach ($this as $request) {
            if ($request instanceof Request) {
                curl_multi_add_handle($handle, $curlHandles[] = $request->getHandle());
            }
        }

        if ( ! empty($curlHandles)) {
            // execute the handles
            $running = null;
            do {
                curl_multi_exec($handle, $running);
            } while ($running > 0);

            // get content and remove handles
            foreach ($curlHandles as $curlHandle) {
                $responses[] = curl_multi_getcontent($curlHandle);
                curl_multi_remove_handle($handle, $curlHandle);
            }

        }

        // all done
        curl_multi_close($handle);

        return $responses;
    }

    // ------------------------------------------------------------------------

    /**
     * MultiRequest::validate
     *
     * Checks if the object is a valid instance.
     *
     * @param object $object The object to be validated.
     *
     * @return bool Returns TRUE on valid or FALSE on failure.
     */
    public function validate($object)
    {
        if ($object instanceof Request) {
            return true;
        }

        return false;
    }
}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 21.09.2015
 */

namespace skeeks\cms\shop\controllers;

use skeeks\cms\base\Controller;
use skeeks\cms\components\Cms;
use skeeks\cms\helpers\RequestResponse;
use skeeks\cms\models\CmsUser;
use skeeks\cms\models\forms\SignupForm;
use skeeks\cms\shop\models\ShopBasket;
use skeeks\cms\shop\models\ShopBuyer;
use skeeks\cms\shop\models\ShopDiscountCoupon;
use skeeks\cms\shop\models\ShopFuser;
use skeeks\cms\shop\models\ShopOrder;
use skeeks\cms\shop\models\ShopOrder2discountCoupon;
use skeeks\cms\shop\models\ShopOrderItem;
use skeeks\cms\shop\models\ShopPersonType;
use skeeks\cms\shop\models\ShopPersonTypeProperty;
use skeeks\cms\shop\models\ShopProduct;
use yii\base\Exception;
use yii\filters\VerbFilter;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;

/**
 * Class CartController
 * @package skeeks\cms\shop\controllers
 */
class CartController extends Controller
{
    public $defaultAction = 'cart';

    /**
     * @return array
     */
    public function behaviors()
    {
        return ArrayHelper::merge(parent::behaviors(), [

            'verbs' => [
                'class'   => VerbFilter::class,
                'actions' => [
                    'add-product'               => ['post'],
                    'remove-basket'             => ['post'],
                    'clear'                     => ['post'],
                    'update-basket'             => ['post'],
                    'shop-person-type-validate' => ['post'],
                    'shop-person-type-submit'   => ['post'],
                    'remove-discount-coupon'    => ['post'],
                    'add-discount-coupon'       => ['post'],
                ],
            ],
        ]);
    }

    /**
     * @return string
     */
    public function actionCart()
    {
        $this->view->title = \Yii::t('skeeks/shop/app', 'Basket').' | '.\Yii::t('skeeks/shop/app', 'Shop');
        return $this->render($this->action->id);
    }

    /**
     * @return string
     */
    public function actionCheckout()
    {
        $this->view->title = \Yii::t('skeeks/shop/app', 'Checkout').' | '.\Yii::t('skeeks/shop/app', 'Shop');
        return $this->render($this->action->id);
    }

    /**
     * Adding a product to the cart.
     *
     * @return array|\yii\web\Response
     */
    public function actionAddProduct()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {

            $product_id = \Yii::$app->request->post('product_id');
            $quantity = \Yii::$app->request->post('quantity');

            /**
             * @var ShopProduct $product
             */
            $product = ShopProduct::find()->where(['id' => $product_id])->one();

            if (!$product) {
                $rr->message = \Yii::t('skeeks/shop/app', 'This product is not found, it may be removed.');
                return (array)$rr;
            }

            if ($product->measure_ratio > 1) {
                if ($quantity % $product->measure_ratio != 0) {
                    $quantity = $product->measure_ratio;
                }
            }

            if (\Yii::$app->shop->cart->isNewRecord) {
                \Yii::$app->shop->cart->save();
                \Yii::$app->getSession()->set(\Yii::$app->shop->sessionFuserName, \Yii::$app->shop->cart->id);
            }

            $shopBasket = ShopOrderItem::find()->where([
                'shop_order_id'   => \Yii::$app->shop->cart->shopOrder->id,
                'shop_product_id' => $product_id,
            ])->one();

            if (!$shopBasket) {
                $shopBasket = new ShopOrderItem([
                    'shop_order_id'   => \Yii::$app->shop->cart->shopOrder->id,
                    'shop_product_id' => $product->id,
                    'quantity'        => 0,
                ]);
            }

            $shopBasket->quantity = $shopBasket->quantity + $quantity;

            if (!$shopBasket->recalculate()->save()) {
                $rr->success = false;
                $rr->message = \Yii::t('skeeks/shop/app', 'Failed to add item to cart');
            } else {
                $shopBasket->recalculate()->save();

                $rr->success = true;
                $rr->message = \Yii::t('skeeks/shop/app', 'Item added to cart');
            }

            \Yii::$app->shop->cart->shopOrder->link('cmsSite', \Yii::$app->cms->site);
            $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

    /**
     * Removing the basket position
     *
     * @return array|\yii\web\Response
     * @throws \Exception
     */
    public function actionRemoveBasket()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {
            $basket_id = \Yii::$app->request->post('basket_id');

            $shopBasket = ShopOrderItem::find()->where(['id' => $basket_id])->one();
            if ($shopBasket) {
                if ($shopBasket->delete()) {
                    $rr->success = true;
                    $rr->message = \Yii::t('skeeks/shop/app', 'Position successfully removed');
                }
            }

            \Yii::$app->shop->cart->shopOrder->link('cmsSite', \Yii::$app->cms->site);
            $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

    /**
     * Cleaning the entire basket
     *
     * @return array|\yii\web\Response
     * @throws \Exception
     */
    public function actionClear()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {
            foreach (\Yii::$app->shop->cart->shopOrder->shopOrderItems as $basket) {
                $basket->delete();
            }

            \Yii::$app->shop->cart->shopOrder->link('cmsSite', \Yii::$app->cms->site);
            $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
            $rr->success = true;
            $rr->message = "";

            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

    /**
     * Updating the positions of the basket, such as changing the number of
     *
     * @return array|\yii\web\Response
     * @throws \Exception
     */
    public function actionUpdateBasket()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {
            $basket_id = (int)\Yii::$app->request->post('basket_id');
            $quantity = (float)\Yii::$app->request->post('quantity');

            /**
             * @var $shopBasket ShopBasket
             */
            $shopBasket = ShopOrderItem::find()->where(['id' => $basket_id])->one();
            if ($shopBasket) {
                if ($quantity > 0) {
                    $product = $shopBasket->product;

                    if ($product->measure_ratio > 1) {
                        if ($quantity % $product->measure_ratio != 0) {
                            $quantity = $product->measure_ratio;
                        }
                    }

                    $shopBasket->quantity = $quantity;
                    if ($shopBasket->recalculate()->save()) {
                        $rr->success = true;
                        $rr->message = \Yii::t('skeeks/shop/app', 'Postion successfully updated');
                    }

                } else {
                    if ($shopBasket->delete()) {
                        $rr->success = true;
                        $rr->message = \Yii::t('skeeks/shop/app', 'Position successfully removed');
                    }
                }

            }

            \Yii::$app->shop->cart->shopOrder->link('cmsSite', \Yii::$app->cms->site);
            $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

    /**
     * @return array|\yii\web\Response
     */
    public function actionRemoveDiscountCoupon()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {
            $couponId = \Yii::$app->request->post('coupon_id');

            try {
                if (!$couponId) {
                    throw new Exception(\Yii::t('skeeks/shop/app', 'Not set coupon code'));
                }

                ShopOrder2discountCoupon::deleteAll(['discount_coupon_id' => $couponId, 'order_id' => \Yii::$app->shop->cart->shopOrder->id]);

                \Yii::$app->shop->cart->recalculate()->save();

                $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
                $rr->success = true;
                $rr->message = \Yii::t('skeeks/shop/app', 'Your coupon was successfully deleted');

            } catch (\Exception $e) {
                $rr->message = $e->getMessage();
                return (array)$rr;
            }

            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

    /**
     * Adding a product to the cart.
     *
     * @return array|\yii\web\Response
     */
    public function actionAddDiscountCoupon()
    {
        $rr = new RequestResponse();

        if ($rr->isRequestAjaxPost()) {
            $couponCode = \Yii::$app->request->post('coupon_code');

            try {
                if (!$couponCode) {
                    throw new Exception(\Yii::t('skeeks/shop/app', 'Not set coupon code'));
                }

                $applyShopDiscountCoupon = ShopDiscountCoupon::find()
                    ->where(['coupon' => $couponCode])
                    ->andWhere(['is_active' => 1])
                    ->andWhere(['>','active_to', time()])
                    ->one();

                if (!$applyShopDiscountCoupon) {
                    throw new Exception(\Yii::t('skeeks/shop/app', 'Coupon does not exist or is not active'));
                }

                /*$discount_coupons = [];
                if (\Yii::$app->shop->cart->shopOrder->shopDiscountCoupons) {
                    $discount_coupons = ArrayHelper::map(\Yii::$app->shop->cart->shopOrder->shopDiscountCoupons, 'id', 'id');
                }
                $discount_coupons[] = $applyShopDiscountCoupon->id;
                array_unique($discount_coupons);*/

                $map = new ShopOrder2discountCoupon();
                $map->order_id = \Yii::$app->shop->cart->shopOrder->id;
                $map->discount_coupon_id = $applyShopDiscountCoupon->id;

                $map->save();
                //\Yii::$app->shop->cart->shopOrder->shopDiscountCoupons = $discount_coupons;
                //\Yii::$app->shop->cart->shopOrder->save();
                \Yii::$app->shop->cart->shopOrder->recalculate()->save();

                $rr->data = \Yii::$app->shop->cart->shopOrder->jsonSerialize();
                $rr->success = true;
                $rr->message = \Yii::t('skeeks/shop/app', 'Coupon successfully installed');

            } catch (\Exception $e) {
                $rr->message = $e->getMessage();
                return (array)$rr;
            }

            return (array)$rr;
        } else {
            return $this->goBack();
        }
    }

}


<?php
/**
 * @author Semenov Alexander <semenov@skeeks.com>
 * @link http://skeeks.com/
 * @copyright 2010 SkeekS (СкикС)
 * @date 22.09.2015
 */

namespace skeeks\cms\shop\widgets;

use skeeks\cms\helpers\UrlHelper;
use skeeks\cms\shop\assets\ShopAsset;
use yii\base\Widget;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;

/**
 * Class ShopGlobalWidget
 * @package skeeks\cms\shop\widgets
 */
class ShopGlobalWidget extends Widget
{
    static public $isRegisteredAssets = false;

    public $clientOptions = [];

    public function init()
    {
        parent::init();
        $this->clientOptions = ArrayHelper::merge($this->baseClientOptions(), $this->clientOptions);
    }

    /**
     * @return array
     */
    public function baseClientOptions()
    {
        return [
            'backend-add-product'            => UrlHelper::construct('shop/cart/add-product')->toString(),
            'backend-remove-basket'          => UrlHelper::construct('shop/cart/remove-basket')->toString(),
            'backend-update-basket'          => UrlHelper::construct('shop/cart/update-basket')->toString(),
            'backend-clear-cart'             => UrlHelper::construct('shop/cart/clear')->toString(),
            'backend-remove-discount-coupon' => UrlHelper::construct('shop/cart/remove-discount-coupon')->toString(),
            'backend-add-discount-coupon'    => UrlHelper::construct('shop/cart/add-discount-coupon')->toString(),
        ];
    }

    /**
     * Подготовка данных для шаблона
     * @return $this
     */
    public function run()
    {
        if (static::$isRegisteredAssets === false) {
            ShopAsset::register($this->getView());
            $options = (array)$this->clientOptions;
            $options['cartData'] = \Yii::$app->shop->cart->shopOrder->jsonSerialize();

            $options = Json::encode($options);

            $this->getView()->registerJs(<<<JS
    (function(sx, $, _)
    {
        sx.Shop = new sx.classes.shop.App($options);
    })(sx, sx.$, sx._);
JS
            );
            static::$isRegisteredAssets = true;
        }

        return parent::run();
    }

}


<?php
/**
 * This file is part of the O2System Framework package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @author         Steeve Andrian Salim
 * @copyright      Copyright (c) Steeve Andrian Salim
 */

// ------------------------------------------------------------------------

namespace O2System\Curl;

// ------------------------------------------------------------------------

use O2System\Kernel\Http\Message\Uri;
use O2System\Psr\Http\Message\UriInterface;
use O2System\Spl\Exceptions\Logic\BadFunctionCall\BadPhpExtensionCallException;

/**
 * Class Request
 *
 * @package O2System\Curl
 */
class Request
{
    /**
     * Request::$curlAutoClose
     *
     * Flag for automatic close the connection when it has finished processing
     * and not be pooled for reuse.
     *
     * @var bool
     */
    public $curlAutoClose = true;
    /**
     * Request::$uri
     *
     * Request uri instance.
     *
     * @var Uri
     */
    protected $uri;

    /**
     * Request::$curlOptions
     *
     * Request Curl handle options.
     *
     * @var array
     */
    protected $curlOptions;
    /**
     * Request::$curlHeaders
     *
     * Request Curl handle headers.
     *
     * @var array
     */
    protected $curlHeaders = [];

    // ------------------------------------------------------------------------

    /**
     * Request::__construct
     *
     * @throws \O2System\Spl\Exceptions\Logic\BadFunctionCall\BadPhpExtensionCallException
     */
    public function __construct()
    {
        if ( ! function_exists('curl_init')) {
            throw new BadPhpExtensionCallException('E_CURL_NOT_LOADED');
        }

        // default, TRUE to return the transfer as a string of the return value of curl_exec() instead of outputting it out directly.
        $this->curlOptions[ CURLOPT_RETURNTRANSFER ] = true;

        // default, TRUE to output verbose information.
        $this->curlOptions[ CURLOPT_VERBOSE ] = true;

        // default, TRUE to include the header in the output.
        $this->curlOptions[ CURLOPT_HEADER ] = true;

        // default, lets CURL decide which version to use.
        $this->curlOptions[ CURLOPT_HTTP_VERSION ] = CURL_HTTP_VERSION_NONE;

        // default, http user agent using o2system curl.
        $this->curlOptions[ CURLOPT_USERAGENT ] = 'Curl/1.0 (O2System)';

        // default, TRUE to automatically set the Referer: field in requests where it follows a Location: redirect.
        $this->curlOptions[ CURLOPT_AUTOREFERER ] = true;

        // default, FALSE to stop cURL from verifying the peer's certificate.
        $this->curlOptions[ CURLOPT_SSL_VERIFYPEER ] = false;

        // default, 0 to not check the names.
        $this->curlOptions[ CURLOPT_SSL_VERIFYHOST ] = 0;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setOptions
     *
     * Sets curl options.
     *
     * @see http://php.net/manual/en/function.curl-setopt.php
     *
     * @param array $options
     *
     * @return static
     */
    public function setOptions(array $options)
    {
        foreach ($options as $option => $value) {
            $this->setOption($option, $value);
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setOption
     *
     * Sets custom curl option.
     *
     * @see http://php.net/manual/en/function.curl-setopt.php
     *
     * @param int   $option The curl option number.
     * @param mixed $value  The value of curl option.
     *
     * @return static
     */
    public function setOption($option, $value)
    {
        $this->curlOptions[ $option ] = $value;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setUri
     *
     * @param \O2System\Psr\Http\Message\UriInterface $uri
     *
     * @return static
     */
    public function setUri(UriInterface $uri)
    {
        $this->uri = $uri;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setHttpVersion
     *
     * Sets which http version to use (default, lets CURL decide which version to use).
     *
     * @param int $httpVersion Supported encodings are "CURL_HTTP_VERSION_NONE", "CURL_HTTP_VERSION_1_0",
     *                         "CURL_HTTP_VERSION_1_1", and "CURL_HTTP_VERSION_2".
     *
     * @return static
     */
    public function setHttpVersion($httpVersion)
    {
        if (in_array($httpVersion, [
                CURL_HTTP_VERSION_NONE,
                CURL_HTTP_VERSION_1_0,
                CURL_HTTP_VERSION_1_1,
                CURL_HTTP_VERSION_2,
            ]
        )) {
            $this->curlOptions[ CURLOPT_HTTP_VERSION ] = $httpVersion;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setUserAgent
     *
     * Sets the contents of the "User-Agent: " header to be used in a HTTP request.
     *
     * @param string $userAgent
     *
     * @return static
     */
    public function setUserAgent($userAgent)
    {
        $this->curlOptions[ CURLOPT_USERAGENT ] = trim($userAgent);

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setReferer
     *
     * Sets the contents of the "Referer: " header to be used in a HTTP request.
     *
     * @param string $referer
     *
     * @return static
     */
    public function setReferer($referer)
    {
        $this->curlOptions[ CURLOPT_REFERER ] = trim($referer);

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setEncoding
     *
     * Sets the contents of the "Accept-Encoding: " header. This enables decoding of the response.
     * Supported encodings are "identity", "deflate", and "gzip".
     * If an empty string, "", is set, a header containing all supported encoding types is sent.
     *
     * @param string $encoding Supported encodings are "identity", "deflate", and "gzip".
     *
     * @return static
     */
    public function setEncoding($encoding)
    {
        if (in_array($encoding, ['identity', 'deflate', 'gzip'])) {
            $this->curlOptions[ CURLOPT_ENCODING ] = $encoding;
        } else {
            $this->curlOptions[ CURLOPT_ENCODING ] = '';
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setTimeout
     *
     * Sets the maximum number of seconds to allow cURL functions to execute.
     *
     * @param int  $timeout        The number of seconds.
     * @param bool $isMilliseconds The number units is uses milliseconds format.
     *
     * @return static
     */
    public function setTimeout($timeout, $isMilliseconds = false)
    {
        if ($isMilliseconds) {
            $this->curlOptions[ CURLOPT_TIMEOUT_MS ] = (int)$timeout;
        } else {
            $this->curlOptions[ CURLOPT_TIMEOUT ] = (int)$timeout;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setConnectionTimeout
     *
     * Sets the number of seconds to wait while trying to connect. Use 0 to wait indefinitely.
     *
     * @param int  $timeout        The number of seconds.
     * @param bool $isMilliseconds The number units is uses milliseconds format.
     *
     * @return static
     */
    public function setConnectionTimeout($timeout, $isMilliseconds = false)
    {
        if ($isMilliseconds) {
            $this->curlOptions[ CURLOPT_CONNECTTIMEOUT_MS ] = (int)$timeout;
        } else {
            $this->curlOptions[ CURLOPT_CONNECTTIMEOUT ] = (int)$timeout;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setMaximumRedirects
     *
     * Sets the maximum amount of HTTP redirections to follow. Use this option alongside CURLOPT_FOLLOWLOCATION.
     *
     * @param string $maximum The numbers of maximum redirections.
     *
     * @return static
     */
    public function setMaximumRedirects($maximum)
    {
        $this->curlOptions[ CURLOPT_MAXREDIRS ] = (int)$maximum;
        $this->curlOptions[ CURLOPT_FOLLOWLOCATION ] = true;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setSslCaInfo
     *
     * Sets the name of a file holding one or more certificates to verify the peer with.
     * It's also set the curl options:
     * 1. CURLOPT_SSL_VERIFYPEER value into TRUE.
     * 2. CURLOPT_SSL_VERIFYHOST value into 2 to check the existence of a common name and also verify that it matches
     * the hostname provided.
     * 3. CURLOPT_SSL_VERIFYSTATUS value into TRUE to verify the certificate status.
     *
     * @param string $caInfoFilePath Path to ssl certificate file.
     *
     * @return static
     */
    public function setSslCaInfo($caInfoFilePath)
    {
        if (is_file($caInfoFilePath)) {
            $this->setSslVerify(2, true);
            $this->curlOptions[ CURLOPT_CAINFO ] = pathinfo($caInfoFilePath, PATHINFO_BASENAME);
            $this->curlOptions[ CURLOPT_CAPATH ] = dirname($caInfoFilePath);
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setSslVerify
     *
     * Sets the SSL Verify Peer into TRUE.
     *
     * @param int  $verifyHost   0. To not check the names. In production environments the value of this option should
     *                           be kept at 2 (default value).
     *                           1. To check the existence of a common name in the SSL peer certificate.
     *                           2. To check the existence of a common name and also verify that it matches the
     *                           hostname provided.
     * @param bool $verifyStatus TRUE to verify the certificate's status.
     *
     * @return static
     */
    public function setSslVerify($verifyHost, $verifyStatus = false)
    {
        $this->curlOptions[ CURLOPT_SSL_VERIFYPEER ] = true;

        $verifyHost = in_array($verifyHost, range(0, 3)) ? $verifyHost : 0;
        $this->curlOptions[ CURLOPT_SSL_VERIFYHOST ] = (int)$verifyHost;
        $this->curlOptions[ CURLOPT_SSL_VERIFYSTATUS ] = (bool)$verifyStatus;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setAuthentication
     *
     * Sets the HTTP authentication method(s) to use.
     *
     * @param string     $username The HTTP authentication username.
     * @param string     $password The HTTP authentication password.
     * @param int|string $method   The HTTP authentication method. The options are:
     *                             1. CURLAUTH_BASIC
     *                             2. CURLAUTH_DIGEST
     *                             3. CURLAUTH_GSSNEGOTIATE
     *                             4. CURLAUTH_NTLM
     *                             5. CURLAUTH_ANY (default)
     *                             6. CURLAUTH_ANYSAFE
     *
     * @return static
     */
    public function setAuthentication($username = '', $password = '', $method = CURLAUTH_ANY)
    {
        if (defined('CURLOPT_USERNAME')) {
            $this->curlOptions[ CURLOPT_USERNAME ] = $username;
        }

        $this->curlOptions[ CURLOPT_USERPWD ] = "$username:$password";
        $this->curlOptions[ CURLOPT_HTTPAUTH ] = CURLAUTH_ANY;

        if (in_array($method, [
            CURLAUTH_BASIC,
            CURLAUTH_DIGEST,
            CURLAUTH_GSSNEGOTIATE,
            CURLAUTH_NTLM,
            CURLAUTH_ANY,
            CURLAUTH_ANYSAFE,
        ])) {
            $this->curlOptions[ CURLOPT_HTTPAUTH ] = $method;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setProxy
     *
     * Set the HTTP proxy to tunnel requests through.
     *
     * @param string     $address  The HTTP proxy address.
     * @param int|string $port     The HTTP proxy port.
     * @param int|string $type     The HTTP proxy type, available options:
     *                             1. CURLPROXY_HTTP
     *                             2. CURLPROXY_HTTP_1_0
     *                             3. CURLPROXY_SOCKS4
     *                             4. CURLPROXY_SOCKS5
     *                             5. CURLPROXY_SOCKS4A
     *                             6. CURLPROXY_SOCKS5_HOSTNAME
     *
     * @return static
     */
    public function setProxy($address, $port = 1080, $type = CURLPROXY_HTTP)
    {
        $this->curlOptions[ CURLOPT_PROXY ] = $address;
        $this->curlOptions[ CURLOPT_PROXYPORT ] = $port;
        $this->curlOptions[ CURLOPT_PROXYTYPE ] = $type;
        $this->curlOptions[ CURLOPT_HTTPPROXYTUNNEL ] = true;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setProxyAuthentication
     *
     * Sets the username and password to use for the connection to the proxy.
     *
     * @param string     $username The HTTP Proxy authentication username.
     * @param string     $password The HTTP Proxy authentication password.
     * @param int|string $method   The HTTP Proxy authentication method. The options are:
     *                             1. CURLAUTH_BASIC
     *                             2. CURLAUTH_NTLM
     *
     * @return static
     */
    public function setProxyAuthentication($username, $password, $method = CURLAUTH_BASIC)
    {
        if (array_key_exists(CURLOPT_HTTPPROXYTUNNEL, $this->curlOptions)) {
            $this->curlOptions[ CURLOPT_PROXYUSERPWD ] = "$username:$password";
        }

        $this->curlOptions[ CURLOPT_PROXYAUTH ] = CURLAUTH_BASIC;

        if (in_array($method, [
            CURLAUTH_BASIC,
            CURLAUTH_NTLM,
        ])) {
            $this->curlOptions[ CURLOPT_PROXYAUTH ] = $method;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setHeaders
     *
     * Sets curl request with headers.
     *
     * @param array $headers
     *
     * @return static
     */
    public function setHeaders(array $headers)
    {
        foreach ($headers as $name => $value) {
            $this->addHeader($name, $value);
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::addHeader
     *
     * Add curl request header.
     *
     * @param string          $name  Case-insensitive header field name.
     * @param string|string[] $value Header value(s).
     *
     * @return static
     */
    public function addHeader($name, $value)
    {
        $this->curlHeaders[ $name ] = $value;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setCookie
     *
     * Sets the cookie contents to be used in the HTTP request.
     *
     * @param string $cookieFile The contents of the "Cookie: " header to be used in the HTTP request.
     *                           Note that multiple cookies are separated with a semicolon followed by a space
     *                           (e.g., "fruit=apple; colour=red")
     *
     * @return static
     */
    public function setCookie($cookie)
    {
        $this->curlOptions[ CURLOPT_COOKIE ] = $cookie;

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::setCookieFile
     *
     * Sets the cookie file to be used in the HTTP request.
     *
     * @param string      $cookieFile The name of the file containing the cookie data.
     * @param string|null $cookieJar  The name of a file to save all internal cookies to when the handle is closed,
     *                                e.g. after a call to curl_close.
     *
     * @return static
     */
    public function setCookieFile($cookieFile, $cookieJar = null)
    {
        if (is_file($cookieFile)) {
            $cookieJar = empty($cookieJar) ? $cookieFile : $cookieJar;
            $this->curlOptions[ CURLOPT_COOKIEFILE ] = $cookieFile;
            $this->curlOptions[ CURLOPT_COOKIEJAR ] = $cookieJar;
        }

        return $this;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::get
     *
     * Get response use HTTP GET request method.
     *
     * @param array $query Additional HTTP GET query.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function get(array $query = [])
    {
        $this->uri = $this->uri->withQuery($query);

        $this->curlOptions[ CURLOPT_HTTPGET ] = true;

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::getResponse
     *
     * Get curl response.
     *
     * @return Response|bool
     */
    public function getResponse()
    {
        if (array_key_exists(CURLOPT_URL, $this->curlOptions)) {
            $handle = curl_init();
        } else {
            $handle = curl_init($this->uri->__toString());
        }

        $headers = [];
        if (count($this->curlHeaders)) {
            foreach ($this->curlHeaders as $key => $value) {
                $headers[] = trim($key) . ': ' . trim($value);
            }

            $this->curlOptions[ CURLOPT_HTTPHEADER ] = $headers;
        }

        if (curl_setopt_array($handle, $this->curlOptions)) {
            $response = (new Response($handle))
                ->setInfo(curl_getinfo($handle))
                ->setContent(curl_exec($handle));

            if ($this->curlAutoClose) {
                curl_close($handle);
            }

            return $response;
        }

        return false;
    }

    // ------------------------------------------------------------------------

    /**
     * Request::post
     *
     * Get response use HTTP POST request method.
     *
     * @param array $fields Additional HTTP POST fields.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function post(array $fields = [])
    {
        $this->curlOptions[ CURLOPT_POST ] = true;
        $this->curlOptions[ CURLOPT_POSTFIELDS ] = http_build_query($fields, null, '&', PHP_QUERY_RFC3986);

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::delete
     *
     * Get response use custom HTTP DELETE request method.
     *
     * @param array $fields Additional HTTP POST fields.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function delete(array $fields = [])
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'DELETE';

        if (count($fields)) {
            $this->curlOptions[ CURLOPT_POST ] = true;
            $this->curlOptions[ CURLOPT_POSTFIELDS ] = http_build_query($fields, null, '&', PHP_QUERY_RFC3986);
        }

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::head
     *
     * Get response use custom HTTP HEAD request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function head()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'HEAD';
        $this->curlOptions[ CURLOPT_HTTPGET ] = true;
        $this->curlOptions[ CURLOPT_HEADER ] = true;
        $this->curlOptions[ CURLOPT_NOBODY ] = true;

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::trace
     *
     * Get response use custom HTTP TRACE request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function trace()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'TRACE';

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::trace
     *
     * Get response use custom HTTP OPTIONS request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function options()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'OPTIONS';

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::patch
     *
     * Get response use custom HTTP PATCH request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function patch()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'PATCH';

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::connect
     *
     * Get response use custom HTTP CONNECT request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function connect()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'CONNECT';

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::download
     *
     * Get response use custom HTTP DOWNLOAD request method.
     *
     * @return bool|\O2System\Curl\Response
     */
    public function download()
    {
        $this->curlOptions[ CURLOPT_CUSTOMREQUEST ] = 'DOWNLOAD';
        $this->curlOptions[ CURLOPT_BINARYTRANSFER ] = true;
        $this->curlOptions[ CURLOPT_RETURNTRANSFER ] = false;

        return $this->getResponse();
    }

    // ------------------------------------------------------------------------

    /**
     * Request::getHandle
     *
     * Gets curl handle resource.
     *
     * @return resource
     */
    public function getHandle()
    {
        if ($this->curlAutoClose) {
            $this->curlOptions[ CURLOPT_FORBID_REUSE ] = true;
            $this->curlOptions[ CURLOPT_FRESH_CONNECT ] = true;
        }

        $this->curlOptions[ CURLOPT_URL ] = $this->uri->__toString();

        $handle = curl_init($this->uri->__toString());

        curl_setopt_array($handle, $this->curlOptions);

        return $handle;
    }
}