protected/modules/order/models/Order.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
/**
 * @property integer $id
 * @property integer $delivery_id
 * @property double $delivery_price
 * @property integer $payment_method_id
 * @property integer $paid
 * @property string $payment_time
 * @property string $payment_details
 * @property double $total_price
 * @property double $discount
 * @property double $coupon_discount
 * @property integer $separate_delivery
 * @property integer $status_id
 * @property string $date
 * @property integer $user_id
 * @property string $name
 * @property string $phone
 * @property string $email
 * @property string $comment
 * @property string $ip
 * @property string $url
 * @property string $note
 * @property string $modified
 * @property string $zipcode
 * @property string $country
 * @property string $city
 * @property string $street
 * @property string $house
 * @property string $apartment
 * @property integer $manager_id
 *
 * @property OrderProduct[] $products
 * @property Delivery $delivery
 * @property Payment $payment
 * @property User $user
 * @property OrderStatus $status
 *
 */
use yupe\widgets\YPurifier;

Yii::import('application.modules.order.OrderModule');
Yii::import('application.modules.order.events.OrderEvents');
Yii::import('application.modules.order.events.PayOrderEvent');
Yii::import('application.modules.order.events.OrderEvent');
Yii::import('application.modules.order.events.OrderChangeStatusEvent');

/**
 * Class Order
 */
class Order extends yupe\models\YModel
{
    /**
     *
     */
    const PAID_STATUS_NOT_PAID = 0;
    /**
     *
     */
    const PAID_STATUS_PAID = 1;

    /**
     *
     */
    const SCENARIO_USER = 'front';
    /**
     *
     */
    const SCENARIO_ADMIN = 'admin';

    /**
     * @var null
     */
    public $couponId = null;

    /**
     * @var OrderProduct[]
     */
    private $_orderProducts = [];

    /**
     * @var bool
     */
    private $hasProducts = false; // ставим в true, когда в сценарии front добавляем хотя бы один продукт

    /**
     * @var
     */
    protected $oldAttributes;

    /**
     * @var bool
     */
    private $productsChanged = false; // менялся ли список продуктов в заказе

    /**
     * @var null
     */
    private $_validCoupons = null;

    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return '{{store_order}}';
    }

    /**
     * @param null|string $className
     * @return $this
     */
    public static function model($className = __CLASS__)
    {
        return parent::model($className);
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        return [
            ['name, email, phone, zipcode, country, city, street, house, apartment', 'filter', 'filter' => 'trim'],
            ['name, email, phone, zipcode, country, city, street, house, apartment, comment', 'filter', 'filter' => [$obj = new YPurifier(), 'purify']],
            ['status_id, delivery_id', 'required'],
            ['name, email', 'required', 'on' => self::SCENARIO_USER],
            ['email', 'email'],
            [
                'delivery_id, separate_delivery, payment_method_id, paid, user_id, couponId, manager_id',
                'numerical',
                'integerOnly' => true,
            ],
            ['delivery_price, total_price, discount, coupon_discount', 'store\components\validators\NumberValidator'],
            ['name, phone, email, city, street', 'length', 'max' => 255],
            ['comment, note', 'length', 'max' => 1024],
            ['zipcode', 'length', 'max' => 30],
            ['house', 'length', 'max' => 50],
            ['country', 'length', 'max' => 150],
            ['apartment', 'length', 'max' => 10],
            ['url', 'unique'],
            [
                'user_id, paid, payment_time, payment_details, total_price, discount, coupon_discount, separate_delivery, status_id, date, ip, url, modified',
                'unsafe',
                'on' => self::SCENARIO_USER,
            ],
            [
                'id, delivery_id, delivery_price, payment_method_id, paid, payment_time, payment_details, total_price, discount, coupon_discount, separate_delivery, status_id, date, user_id, name, phone, email, comment, ip, url, note, modified, manager_id',
                'safe',
                'on' => 'search',
            ],
        ];
    }

    /**
     * @return array
     */
    public function relations()
    {
        return [
            'products' => [self::HAS_MANY, 'OrderProduct', 'order_id', 'order' => 'products.id ASC'],
            'delivery' => [self::BELONGS_TO, 'Delivery', 'delivery_id'],
            'payment' => [self::BELONGS_TO, 'Payment', 'payment_method_id'],
            'status' => [self::BELONGS_TO, 'OrderStatus', 'status_id'],
            'client' => [self::BELONGS_TO, 'Client', 'user_id'],
            'manager' => [self::BELONGS_TO, 'User', 'manager_id'],
            'couponsIds' => [self::HAS_MANY, 'OrderCoupon', 'order_id'],
            'coupons' => [self::HAS_MANY, 'Coupon', 'coupon_id', 'through' => 'couponsIds'],
        ];
    }

    /**
     * @return array
     */
    public function scopes()
    {
        return [
            'new' => [
                'condition' => 't.status_id = :status_id',
                'params' => [':status_id' => OrderStatus::STATUS_NEW],
            ],
        ];
    }

    /**
     * @return array
     */
    public function behaviors()
    {
        return [
            'CTimestampBehavior' => [
                'class' => 'zii.behaviors.CTimestampBehavior',
                'setUpdateOnCreate' => true,
                'createAttribute' => 'date',
                'updateAttribute' => 'modified',
            ],
        ];
    }

    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('OrderModule.order', '#'),
            'delivery_id' => Yii::t('OrderModule.order', 'Delivery method'),
            'delivery_price' => Yii::t('OrderModule.order', 'Delivery price'),
            'payment_method_id' => Yii::t('OrderModule.order', 'Payment method'),
            'paid' => Yii::t('OrderModule.order', 'Paid'),
            'payment_time' => Yii::t('OrderModule.order', 'Paid date'),
            'payment_details' => Yii::t('OrderModule.order', 'Payment details'),
            'total_price' => Yii::t('OrderModule.order', 'Total price'),
            'discount' => Yii::t('OrderModule.order', 'Discount (%)'),
            'coupon_discount' => Yii::t('OrderModule.order', 'Discount coupon'),
            'separate_delivery' => Yii::t('OrderModule.order', 'Separate delivery payment'),
            'status_id' => Yii::t('OrderModule.order', 'Status'),
            'date' => Yii::t('OrderModule.order', 'Created'),
            'user_id' => Yii::t('OrderModule.order', 'Client'),
            'name' => Yii::t('OrderModule.order', 'Client'),
            'phone' => Yii::t('OrderModule.order', 'Phone'),
            'email' => Yii::t('OrderModule.order', 'Email'),
            'comment' => Yii::t('OrderModule.order', 'Comment'),
            'ip' => Yii::t('OrderModule.order', 'IP'),
            'url' => Yii::t('OrderModule.order', 'Url'),
            'note' => Yii::t('OrderModule.order', 'Note'),
            'modified' => Yii::t('OrderModule.order', 'Update date'),
            'zipcode' => Yii::t('OrderModule.order', 'Zipcode'),
            'country' => Yii::t('OrderModule.order', 'Country'),
            'city' => Yii::t('OrderModule.order', 'City'),
            'street' => Yii::t('OrderModule.order', 'Street'),
            'house' => Yii::t('OrderModule.order', 'House'),
            'apartment' => Yii::t('OrderModule.order', 'Apartment'),
            'manager_id' => Yii::t('OrderModule.order', 'Manager'),
        ];
    }

    /**
     * @return CActiveDataProvider
     */
    public function search()
    {
        $criteria = new CDbCriteria;
        $criteria->with = ['delivery', 'payment', 'client', 'status'];

        $criteria->compare('t.id', $this->id);
        $criteria->compare('t.name', $this->name, true);
        $criteria->compare('delivery_id', $this->delivery_id);
        $criteria->compare('delivery_price', $this->delivery_price);
        $criteria->compare('payment_method_id', $this->payment_method_id);
        $criteria->compare('paid', $this->paid);
        $criteria->compare('payment_time', $this->payment_time);
        $criteria->compare('payment_details', $this->payment_details, true);
        $criteria->compare('total_price', $this->total_price);
        $criteria->compare('total_price', $this->total_price);
        $criteria->compare('discount', $this->discount);
        $criteria->compare('coupon_discount', $this->coupon_discount);
        $criteria->compare('separate_delivery', $this->separate_delivery);
        $criteria->compare('t.status_id', $this->status_id);
        $criteria->compare('date', $this->date, true);
        $criteria->compare('t.user_id', $this->user_id);
        $criteria->compare('phone', $this->phone, true);
        $criteria->compare('email', $this->email, true);
        $criteria->compare('comment', $this->comment, true);
        $criteria->compare('ip', $this->ip, true);
        $criteria->compare('url', $this->url, true);
        $criteria->compare('note', $this->note, true);
        $criteria->compare('modified', $this->modified, true);
        $criteria->compare('t.manager_id', $this->manager_id);

        if (null !== $this->couponId) {
            $criteria->with['couponsIds'] = ['together' => true];
            $criteria->addCondition('couponsIds.coupon_id = :id');
            $criteria->params = CMap::mergeArray($criteria->params, [':id' => $this->couponId]);
        }

        if (null !== $this->name) {
            $clientCriteria = new CDbCriteria();
            $clientCriteria->with['client'] = ['together' => true];
            $clientCriteria->addSearchCondition('client.last_name', $this->name, true, 'OR');
            $clientCriteria->addSearchCondition('client.first_name', $this->name, true, 'OR');
            $clientCriteria->addSearchCondition('client.middle_name', $this->name, true, 'OR');
            $clientCriteria->addSearchCondition('client.nick_name', $this->name, true, 'OR');
            $criteria->mergeWith($clientCriteria, 'OR');
        }

        return new CActiveDataProvider(
            __CLASS__, [
                'criteria' => $criteria,
                'sort' => ['defaultOrder' => $this->getTableAlias().'.id DESC'],
            ]
        );
    }

    /**
     * @return CActiveDataProvider
     */
    public function searchCoupons()
    {
        $criteria = new CDbCriteria;

        $criteria->compare('t.order_id', $this->id);

        $criteria->with = ['coupon'];

        return new CActiveDataProvider(
            OrderCoupon::_CLASS_(), [
                'criteria' => $criteria,
            ]
        );
    }

    /**
     *
     */
    public function afterFind()
    {
        $this->oldAttributes = $this->getAttributes();
        parent::afterFind();
    }

    /**
     * @return bool
     */
    public function beforeValidate()
    {
        if ($this->getScenario() === self::SCENARIO_USER) {

            if (!$this->hasProducts) {
                $this->addError('products', Yii::t('OrderModule.order', 'There are no selected products'));
            }
        }

        return parent::beforeValidate();
    }

    /**
     * @return float|int
     */
    public function getProductsCost()
    {
        $cost = 0;
        $products = $this->productsChanged ? $this->_orderProducts : $this->products;

        foreach ($products as $op) {
            $cost += $op->price * $op->quantity;
        }

        return $cost;
    }

    /**
     * @return bool
     */
    public function isCouponsAvailable()
    {
        return Yii::app()->hasModule('coupon');
    }

    /**
     * @return bool
     */
    public function hasCoupons()
    {
        return !empty($this->couponsIds);
    }

    /**
     * @return array
     */
    public function getCouponsCodes()
    {
        $codes = [];

        foreach ($this->coupons as $coupon) {
            $codes[] = $coupon->code;
        }

        return $codes;
    }

    /**
     * @return mixed
     */
    public function getCoupons()
    {
        return $this->coupons;
    }

    /**
     * @return int|mixed
     */
    public function getDeliveryCost()
    {
        $cost = $this->delivery_price;
        if ($this->isCouponsAvailable()) {
            $validCoupons = $this->getValidCoupons($this->getCouponsCodes());
            foreach ($validCoupons as $coupon) {
                if ($coupon->free_shipping) {
                    $cost = 0;
                }
            }
        }

        return $cost;
    }

    /**
     * Фильтрует переданные коды купонов и возвращает объекты купонов
     * @param $codes - массив кодов купонов
     * @return Coupon[] - массив объектов-купонов
     */
    public function getValidCoupons($codes)
    {
        if ($this->_validCoupons !== null) {
            return $this->_validCoupons;
        }

        $productsTotalPrice = $this->getProductsCost();

        $validCoupons = [];

        /* @var $coupon Coupon */
        /* проверим купоны на валидность */
        foreach ($codes as $code) {

            $coupon = Coupon::model()->getCouponByCode($code);

            if (null !== $coupon && $coupon->getIsAvailable($productsTotalPrice)) {
                $validCoupons[] = $coupon;
            }
        }

        return $validCoupons;
    }

    /**
     * Получает скидку для переданных купонов
     * @param $coupons Coupon[]
     * @return float - скидка
     */
    public function getCouponDiscount(array $coupons)
    {
        $productsTotalPrice = $this->getProductsCost();
        $delta = 0.00;
        if ($this->isCouponsAvailable()) {
            foreach ($coupons as $coupon) {
                switch ($coupon->type) {
                    case CouponType::TYPE_SUM:
                        $delta += $coupon->value;
                        break;
                    case CouponType::TYPE_PERCENT:
                        $delta += ($coupon->value / 100) * $productsTotalPrice;
                        break;
                }
            }
        }

        return $delta;
    }

    /**
     * @param array $attributes
     * @param array $products
     * @param int $status
     * @param int $client
     *
     * @return bool
     */
    public function store(array $attributes, array $products, $client = null, $status = OrderStatus::STATUS_NEW)
    {
        $isNew = $this->getIsNewRecord();
        $transaction = Yii::app()->getDb()->beginTransaction();

        try {

            $this->status_id = (int)$status;
            $this->user_id = $client;
            $this->setAttributes($attributes);
            $this->setProducts($products);

            if (!$this->save()) {
                return false;
            }

            Yii::app()->eventManager->fire(
                $isNew ? OrderEvents::CREATED : OrderEvents::UPDATED,
                new OrderEvent($this)
            );

            $transaction->commit();

            return true;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }

    /**
     * @param array $coupons
     * @return bool
     */
    public function applyCoupons(array $coupons)
    {
        if (!$this->isCouponsAvailable()) {
            return true;
        }

        $coupons = $this->getValidCoupons($coupons);

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

        try {

            foreach ($coupons as $coupon) {

                $model = new OrderCoupon();

                $model->setAttributes(
                    [
                        'order_id' => $this->id,
                        'coupon_id' => $coupon->id,
                        'create_time' => new CDbExpression('NOW()'),
                    ]
                );

                $model->save();

                $coupon->decreaseQuantity();
            }

            $this->coupon_discount = $this->getCouponDiscount($coupons);

            $this->delivery_price = $this->getDeliveryCost();

            $this->update(['coupon_discount', 'delivery_price']);

            $transaction->commit();

            return true;
        } catch (Exception $e) {
            $transaction->rollback();

            return false;
        }
    }

    /**
     * @return bool
     * @TODO вынести всю логику в saveData
     */
    public function beforeSave()
    {
        $this->total_price = $this->getProductsCost();

        if ($this->getIsNewRecord()) {
            $this->url = md5(uniqid(time(), true));
            $this->ip = Yii::app()->getRequest()->userHostAddress;
            if ($this->getScenario() === self::SCENARIO_USER) {
                $this->delivery_price = $this->delivery ? $this->delivery->getCost($this->total_price) : 0;
                $this->separate_delivery = $this->delivery ? $this->delivery->separate_payment : null;
            }
        }

        $this->delivery_price = $this->getDeliveryCost();

        // Контроль остатков товара на складе
        // @TODO реализовать перерасчет остатков товаров при изменении списка заказанных товаров администратором
        if ($this->getIsNewRecord() && Yii::app()->getModule('store')->controlStockBalances) {
            foreach ($this->_orderProducts as $orderProduct) {
                $product = $orderProduct->product;
                if ($orderProduct->quantity > $product->getAvailableQuantity()) {
                    $this->addError(
                        'products',
                        Yii::t(
                            "OrderModule.order",
                            'Not enough product «{product_name}» in stock, maximum - {n} items', [
                                '{product_name}' => $product->getName(),
                                '{n}' => $product->getAvailableQuantity(),
                            ]
                        )
                    );
                    return false;
                }

                // Изменение значения количества товара
                $newQuantity = $product->quantity - $orderProduct->quantity;
                $product->saveAttributes(['quantity' => $newQuantity]);
            }
        }

        return parent::beforeSave();
    }

    /**
     *
     */
    public function afterDelete()
    {
        foreach ($this->products as $product) {
            $product->delete();
        }
        parent::afterDelete();
    }

    /**
     * @return array
     */
    public function getPaidStatusList()
    {
        return [
            self::PAID_STATUS_PAID => Yii::t("OrderModule.order", 'Paid'),
            self::PAID_STATUS_NOT_PAID => Yii::t("OrderModule.order", 'Not paid'),
        ];
    }

    /**
     * @return string
     */
    public function getPaidStatus()
    {
        $data = $this->getPaidStatusList();

        return isset($data[$this->paid]) ? $data[$this->paid] : Yii::t("OrderModule.order", '*unknown*');
    }

    /**
     *
     * Формат массива:
     * <pre>
     * array(
     *    '45' => array( //реальный id или сгенерированный новый, у новых внутри массива нет id
     *        'id' => '10', //если нет id, то новый
     *        'variant_ids' => array('10', '20, '30'), // массив с id вариантов
     *        'quantity' = > '5',
     *        'price' => '1000',
     *        'product_id' => '123',
     *    )
     * )
     * </pre>
     * @param $orderProducts Array
     */
    public function setProducts($orderProducts)
    {
        $this->productsChanged = true;
        $orderProductsObjectsArray = [];
        if (is_array($orderProducts)) {
            foreach ($orderProducts as $key => $op) {
                $product = null;
                if (isset($op['product_id'])) {
                    $product = Product::model()->findByPk($op['product_id']);
                }
                $variantIds = isset($op['variant_ids']) ? $op['variant_ids'] : [];
                if ($product) {
                    $this->hasProducts = true;
                }

                /* @var $orderProduct OrderProduct */
                $orderProduct = null;
                if (isset($op['id'])) {
                    $orderProduct = OrderProduct::model()->findByPk($op['id']);
                }

                if (!$orderProduct) {
                    $orderProduct = new OrderProduct();
                    $orderProduct->product_id = $product->id;
                    $orderProduct->product_name = $product->name;
                    $orderProduct->sku = $product->sku;
                }

                if ($this->getScenario() == self::SCENARIO_USER) {
                    $orderProduct->price = $product->getPrice($variantIds);
                } else {
                    $orderProduct->price = $op['price'];
                }

                $orderProduct->variantIds = $variantIds;
                $orderProduct->quantity = $op['quantity'];

                $orderProductsObjectsArray[] = $orderProduct;
            }
            $this->_orderProducts = $orderProductsObjectsArray;
        }
    }

    /**
     * Массив объектов OrderProduct
     * @param $products
     */
    private function updateOrderProducts($products)
    {
        if (!$this->productsChanged) {
            return;
        }

        $validOrderProductIds = [];

        foreach ($products as $var) {
            /* @var $var OrderProduct */
            if ($var->getIsNewRecord()) {
                $var->order_id = $this->id;
            }

            if ($var->save()) {
                $validOrderProductIds[] = $var->id;
            }
        }

        $criteria = new CDbCriteria();
        $criteria->addCondition('order_id = :order_id');
        $criteria->params = [':order_id' => $this->id];
        $criteria->addNotInCondition('id', $validOrderProductIds);
        OrderProduct::model()->deleteAll($criteria);
    }

    /**
     *
     */
    public function afterSave()
    {
        $this->updateOrderProducts($this->_orderProducts);
        parent::afterSave();
    }

    /**
     * @return float
     */
    public function getTotalPrice()
    {
        return (float)$this->total_price - (float)$this->discount - (float)$this->coupon_discount;
    }

    /**
     * @return float
     */
    public function getDeliveryPrice()
    {
        return (float)$this->delivery_price;
    }

    /**
     * @return float
     */
    public function getTotalPriceWithDelivery()
    {
        $price = $this->getTotalPrice();

        if (!$this->separate_delivery) {
            $price += $this->getDeliveryPrice();
        }

        return $price;
    }

    /**
     * @return bool
     */
    public function isPaid()
    {
        return (int)$this->paid === static::PAID_STATUS_PAID;
    }

    /**
     * @return mixed
     */
    public function isPaymentMethodSelected()
    {
        return $this->payment_method_id;
    }


    /**
     * @param Payment $payment
     * @param int $paid
     * @return bool
     */
    public function pay(Payment $payment, $paid = self::PAID_STATUS_PAID)
    {
        if ($this->isPaid()) {
            return true;
        }

        $this->setAttributes([
            'paid' => (int)$paid,
            'payment_method_id' => $payment->id,
            'payment_time' => new CDbExpression('NOW()'),
        ]);

        $result = $this->save();

        if ($result) {
            Yii::app()->eventManager->fire(OrderEvents::SUCCESS_PAID, new PayOrderEvent($this, $payment));
        } else {
            Yii::app()->eventManager->fire(OrderEvents::FAILURE_PAID, new PayOrderEvent($this, $payment));
        }

        return $result;
    }

    /**
     * @param Payment $payment
     * @return bool
     */
    public function unpay(Payment $payment)
    {
        $this->setAttributes([
            'paid' => self::PAID_STATUS_NOT_PAID,
            'payment_method_id' => null,
        ]);

        $result = $this->save();

        Yii::app()->eventManager->fire(OrderEvents::FAILURE_PAID, new PayOrderEvent($this, $payment));

        return $result;
    }




    /**
     * @param $url
     * @return static
     */
    public function findByUrl($url)
    {
        return $this->findByAttributes(['url' => $url]);
    }

    /**
     * @return bool
     * @TODO реализовать перерасчет остатков товаров при отмене заказа
     */
    public function isStatusChanged()
    {
        if ($this->oldAttributes['status_id'] != $this->status_id) {

            Yii::app()->eventManager->fire(OrderEvents::STATUS_CHANGED, new OrderChangeStatusEvent($this));

            return true;
        }

        return false;
    }

    /**
     * @param $number
     * @return static
     */
    public function findByNumber($number)
    {
        return $this->findByPk($number);
    }

    /**
     * @param string $separator
     * @return string
     */
    public function getAddress($separator = ', ')
    {
        return implode($separator, [
            $this->zipcode,
            $this->country,
            $this->city,
            $this->street,
            $this->house,
            $this->apartment,
        ]);
    }

    /**
     * @return CActiveDataProvider
     */
    public function getProducts()
    {
        return new CActiveDataProvider(
            'OrderProduct', [
                'criteria' => [
                    'condition' => 'order_id = :id',
                    'params' => [
                        ':id' => $this->id,
                    ],
                ],
            ]
        );
    }

    /**
     * @return string
     */
    public function getStatusTitle()
    {
        return isset($this->status) ? $this->status->name : Yii::t('OrderModule.order', '*unknown*');
    }

    /**
     * @param IWebUser $user
     * @return bool
     */
    public function checkManager(IWebUser $user)
    {
        if (!$this->manager_id) {
            return true;
        }

        if (((int)$this->manager_id === (int)$user->getId()) || $user->isSuperUser()) {
            return true;
        }

        return false;
    }
}