Completed
Push — master ( ef526f...bbad74 )
by Scott
02:42
created

Discount::getProductsOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php namespace Bedard\Shop\Models;
2
3
use Carbon\Carbon;
4
use Flash;
5
use Lang;
6
use Model;
7
use October\Rain\Database\ModelException;
8
use Queue;
9
10
/**
11
 * Discount Model.
12
 */
13
class Discount extends Model
14
{
15
    use \Bedard\Shop\Traits\Subqueryable,
16
        \Bedard\Shop\Traits\Timeable,
17
        \October\Rain\Database\Traits\Purgeable,
18
        \October\Rain\Database\Traits\Validation;
19
20
    /**
21
     * @var string The database table used by the model.
22
     */
23
    public $table = 'bedard_shop_discounts';
24
25
    /**
26
     * @var array Default attributes
27
     */
28
    public $attributes = [
29
        'amount' => 0,
30
        'amount_exact' => 0,
31
        'amount_percentage' => 0,
32
        'is_percentage' => true,
33
    ];
34
35
    /**
36
     * @var array Attribute casting
37
     */
38
    public $casts = [
39
        'amount' => 'float',
40
        'is_percentage' => 'boolean',
41
    ];
42
43
    /**
44
     * @var array Guarded fields
45
     */
46
    protected $guarded = ['*'];
47
48
    /**
49
     * @var array Fillable fields
50
     */
51
    protected $fillable = [
52
        'amount',
53
        'amount_exact',
54
        'amount_percentage',
55
        'end_at',
56
        'is_percentage',
57
        'name',
58
        'start_at',
59
    ];
60
61
    /**
62
     * @var array Purgeable vields
63
     */
64
    protected $purgeable = [
65
        'amount_exact',
66
        'amount_percentage',
67
    ];
68
69
    /**
70
     * @var array Relations
71
     */
72
    public $belongsToMany = [
73
        'categories' => [
74
            'Bedard\Shop\Models\Category',
75
            'table' => 'bedard_shop_category_discount',
76
        ],
77
        'products' => [
78
            'Bedard\Shop\Models\Product',
79
            'table' => 'bedard_shop_discount_product',
80
        ],
81
    ];
82
83
    public $hasMany = [
84
        'prices' => [
85
            'Bedard\Shop\Models\Price',
86
            'delete' => true,
87
        ],
88
    ];
89
90
    /**
91
     * @var  array Validation rules
92
     */
93
    public $rules = [
94
        'end_at' => 'date',
95
        'name' => 'required',
96
        'start_at' => 'date',
97
        'amount_exact' => 'numeric|min:0',
98
        'amount_percentage' => 'numeric|min:0|max:100',
99
    ];
100
101
    /**
102
     * After save.
103
     *
104
     * @return void
105
     */
106
    public function afterSave()
107
    {
108
        $this->savePrices();
109
    }
110
111
    /**
112
     * After validate.
113
     *
114
     * @return void
115
     */
116
    public function afterValidate()
117
    {
118
        $this->validateDates();
119
    }
120
121
    /**
122
     * Before save.
123
     *
124
     * @return void
125
     */
126
    public function beforeSave()
127
    {
128
        $this->setAmount();
129
    }
130
131
    /**
132
     * Calculate the discounted price.
133
     *
134
     * @param  float $basePrice
135
     * @return float
136
     */
137
    public function calculatePrice($basePrice)
138
    {
139
        $value = $this->is_percentage
140
            ? $basePrice - ($basePrice * ($this->amount / 100))
141
            : $basePrice - $this->amount;
142
143
        if ($value < 0) {
144
            $value = 0;
145
        }
146
147
        return round($value, 2);
148
    }
149
150
    /**
151
     * Filter form fields.
152
     *
153
     * @param  object   $fields
154
     * @return void
155
     */
156
    public function filterFields($fields)
157
    {
158
        $fields->amount_exact->hidden = $this->is_percentage;
159
        $fields->amount_percentage->hidden = ! $this->is_percentage;
160
    }
161
162
    /**
163
     * Fetch all of the products within the scope of this discount.
164
     *
165
     * @return array
166
     */
167
    public function getAllProductIds()
168
    {
169
        $productIds = $this->products()->lists('id');
170
        $categories = $this->categories()
171
            ->with(['products' => function ($product) {
172
                return $product->select('id');
173
            }])
174
            ->get(['id']);
175
176
        foreach ($categories as $category) {
177
            foreach ($category->products as $product) {
178
                $productIds[] = $product->id;
179
            }
180
        }
181
182
        return array_unique($productIds);
183
    }
184
185
    /**
186
     * Get the exact amount.
187
     *
188
     * @return float
189
     */
190
    public function getAmountExactAttribute()
191
    {
192
        return ! $this->is_percentage ? $this->amount : 0;
193
    }
194
195
    /**
196
     * Get the percentage amount.
197
     *
198
     * @return float
199
     */
200
    public function getAmountPercentageAttribute()
201
    {
202
        return $this->is_percentage ? $this->amount : 0;
203
    }
204
205
    /**
206
     * Get categories options.
207
     *
208
     * @return array
209
     */
210
    public function getCategoriesOptions()
211
    {
212
        return Category::lists('name', 'id');
213
    }
214
215
    /**
216
     * Get products options.
217
     *
218
     * @return array
219
     */
220
    public function getProductsOptions()
221
    {
222
        return Product::lists('name', 'id');
223
    }
224
225
    /**
226
     * Save the prices created by this discount.
227
     *
228
     * @return void
229
     */
230
    public function savePrices()
231
    {
232
        $id = $this->id;
233
        Queue::push(function ($job) use ($id) {
234
            $discount = Discount::findOrFail($id);
235
            $productIds = $discount->getAllProductIds();
236
            $products = Product::whereIn('id', $productIds)->select('id', 'base_price')->get();
237
            $discount->prices()->delete();
238
239
            foreach ($productIds as $productId) {
240
                $product = $products->find($productId);
241
                if ($product) {
242
                    Price::create(Discount::buildPriceArray($discount, $product));
243
                }
244
            }
245
246
            $job->delete();
247
        });
248
    }
249
250
    /**
251
     * Sync the prices of all non-expired discounts.
252
     *
253
     * @return void
254
     */
255
    public static function syncAllPrices()
256
    {
257
        $data = [];
258
        $discounts = self::isNotExpired()->with('categories.products', 'products')->get();
259
        foreach ($discounts as $discount) {
260
            $products = $discount->products;
261
            foreach ($discount->categories as $category) {
262
                $products = $products->merge($category->products);
263
            }
264
265
            foreach ($products as $product) {
266
                $data[] = self::buildPriceArray($discount, $product);
267
            }
268
        }
269
270
        Price::whereNotNull('discount_id')->delete();
271
        Price::insert($data);
272
    }
273
274
    /**
275
     * Sync the prices of a single product.
276
     *
277
     * @param  Product $product
278
     * @param  array   $categoryIds
279
     * @return void
280
     */
281
    public static function syncProductPrice(Product $product, array $categoryIds)
282
    {
283
        $discounts = self::isNotExpired()
284
            ->whereHas('products', function ($query) use ($product) {
285
                return $query->where('id', $product->id);
286
            })
287
            ->orWhereHas('categories', function ($query) use ($categoryIds) {
288
                return $query->whereIn('id', $categoryIds);
289
            })
290
            ->get();
291
292
        $data = [];
293
        foreach ($discounts as $discount) {
294
            $data[] = self::buildPriceArray($discount, $product);
295
        }
296
297
        Price::whereProductId($product->id)->whereNotNull('discount_id')->delete();
298
        Price::insert($data);
299
    }
300
301
    /**
302
     * Build the array needed to insert price models.
303
     *
304
     * @param  \Bedard\Shop\Models\Discount $discount
305
     * @param  \Bedard\Shop\Models\Product  $product
306
     * @return array
307
     */
308
    public static function buildPriceArray(Discount $discount, Product $product)
309
    {
310
        return [
311
            'discount_id' => $discount->id,
312
            'end_at' => $discount->end_at,
313
            'price' => $discount->calculatePrice($product->base_price),
314
            'product_id' => $product->id,
315
            'start_at' => $discount->start_at,
316
        ];
317
    }
318
319
    /**
320
     * This exists to makes statuses sortable by assigning them a value.
321
     *
322
     * Expired  0
323
     * Running  1
324
     * Upcoming 2
325
     *
326
     * @param  \October\Rain\Database\Builder   $query
327
     * @return \October\Rain\Database\Builder
328
     */
329
    public function scopeSelectStatus($query)
330
    {
331
        $grammar = $query->getQuery()->getGrammar();
332
        $start_at = $grammar->wrap($this->table.'.start_at');
333
        $end_at = $grammar->wrap($this->table.'.end_at');
334
        $now = Carbon::now();
335
336
        $subquery = 'CASE '.
337
            "WHEN ({$end_at} IS NOT NULL AND {$end_at} < '{$now}') THEN 0 ".
338
            "WHEN ({$start_at} IS NOT NULL AND {$start_at} > '{$now}') THEN 2 ".
339
            'ELSE 1 '.
340
        'END';
341
342
        return $query->selectSubquery($subquery, 'status');
343
    }
344
345
    /**
346
     * Set the discount amount.
347
     *
348
     * @return  void
349
     */
350
    public function setAmount()
351
    {
352
        $exact = $this->getOriginalPurgeValue('amount_exact');
353
        $percentage = $this->getOriginalPurgeValue('amount_percentage');
354
355
        $this->amount = $this->is_percentage
356
            ? $percentage
357
            : $exact;
358
    }
359
360
    /**
361
     * Ensure the start and end dates are valid.
362
     *
363
     * @return void
364
     */
365
    public function validateDates()
366
    {
367
        // Start date must be after the end date
368
        if ($this->start_at !== null &&
369
            $this->end_at !== null &&
370
            $this->start_at >= $this->end_at) {
371
            Flash::error(Lang::get('bedard.shop::lang.discounts.form.start_at_invalid'));
372
            throw new ModelException($this);
373
        }
374
    }
375
}
376