Completed
Push — master ( 69de0f...497890 )
by Scott
05:04
created

Discount::syncAllPrices()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 11
nc 5
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
     * Save the prices created by this discount.
207
     *
208
     * @return void
209
     */
210
    public function savePrices()
211
    {
212
        $id = $this->id;
213
        Queue::push(function ($job) use ($id) {
214
            $discount = Discount::findOrFail($id);
215
            $productIds = $discount->getAllProductIds();
216
            $products = Product::whereIn('id', $productIds)->select('id', 'base_price')->get();
217
            $discount->prices()->delete();
218
219
            foreach ($productIds as $productId) {
220
                $product = $products->find($productId);
221
                if ($product) {
222
                    Price::create(Discount::buildPriceArray($discount, $product));
223
                }
224
            }
225
226
            $job->delete();
227
        });
228
    }
229
230
    /**
231
     * Sync the prices of all non-expired discounts.
232
     *
233
     * @return void
234
     */
235
    public static function syncAllPrices()
236
    {
237
        $data = [];
238
        $discounts = self::isNotExpired()->with('categories.products', 'products')->get();
239
        foreach ($discounts as $discount) {
240
            $products = $discount->products;
241
            foreach ($discount->categories as $category) {
242
                $products = $products->merge($category->products);
243
            }
244
245
            foreach ($products as $product) {
246
                $data[] = self::buildPriceArray($discount, $product);
247
            }
248
        }
249
250
        Price::whereNotNull('discount_id')->delete();
251
        Price::insert($data);
252
    }
253
254
    /**
255
     * Sync the prices of a single product.
256
     *
257
     * @param  Product $product
258
     * @param  array   $categoryIds
259
     * @return void
260
     */
261
    public static function syncProductPrice(Product $product, array $categoryIds)
262
    {
263
        $discounts = self::isNotExpired()
264
            ->whereHas('products', function ($query) use ($product) {
265
                return $query->where('id', $product->id);
266
            })
267
            ->orWhereHas('categories', function ($query) use ($categoryIds) {
268
                return $query->whereIn('id', $categoryIds);
269
            })
270
            ->get();
271
272
        $data = [];
273
        foreach ($discounts as $discount) {
274
            $data[] = self::buildPriceArray($discount, $product);
275
        }
276
277
        Price::whereProductId($product->id)->whereNotNull('discount_id')->delete();
278
        Price::insert($data);
279
    }
280
281
    /**
282
     * Build the array needed to insert price models.
283
     *
284
     * @param  \Bedard\Shop\Models\Discount $discount
285
     * @param  \Bedard\Shop\Models\Product  $product
286
     * @return array
287
     */
288
    public static function buildPriceArray(Discount $discount, Product $product) {
289
        return [
290
            'discount_id' => $discount->id,
291
            'end_at' => $discount->end_at,
292
            'price' => $discount->calculatePrice($product->base_price),
293
            'product_id' => $product->id,
294
            'start_at' => $discount->start_at,
295
        ];
296
    }
297
298
    /**
299
     * This exists to makes statuses sortable by assigning them a value.
300
     *
301
     * Expired  0
302
     * Running  1
303
     * Upcoming 2
304
     *
305
     * @param  \October\Rain\Database\Builder   $query
306
     * @return \October\Rain\Database\Builder
307
     */
308
    public function scopeSelectStatus($query)
309
    {
310
        $grammar = $query->getQuery()->getGrammar();
311
        $start_at = $grammar->wrap($this->table.'.start_at');
312
        $end_at = $grammar->wrap($this->table.'.end_at');
313
        $now = Carbon::now();
314
315
        $subquery = 'CASE '.
316
            "WHEN ({$end_at} IS NOT NULL AND {$end_at} < '{$now}') THEN 0 ".
317
            "WHEN ({$start_at} IS NOT NULL AND {$start_at} > '{$now}') THEN 2 ".
318
            'ELSE 1 '.
319
        'END';
320
321
        return $query->selectSubquery($subquery, 'status');
322
    }
323
324
    /**
325
     * Set the discount amount.
326
     *
327
     * @return  void
328
     */
329
    public function setAmount()
330
    {
331
        $exact = $this->getOriginalPurgeValue('amount_exact');
332
        $percentage = $this->getOriginalPurgeValue('amount_percentage');
333
334
        $this->amount = $this->is_percentage
335
            ? $percentage
336
            : $exact;
337
    }
338
339
    /**
340
     * Ensure the start and end dates are valid.
341
     *
342
     * @return void
343
     */
344
    public function validateDates()
345
    {
346
        // Start date must be after the end date
347
        if ($this->start_at !== null &&
348
            $this->end_at !== null &&
349
            $this->start_at >= $this->end_at) {
350
            Flash::error(Lang::get('bedard.shop::lang.discounts.form.start_at_invalid'));
351
            throw new ModelException($this);
352
        }
353
    }
354
}
355