• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

conedevelopment / bazar / 20248285521

15 Dec 2025 09:37PM UTC coverage: 61.163% (-2.3%) from 63.48%
20248285521

Pull #235

github

web-flow
Merge c10e94de1 into 090ff8496
Pull Request #235: Coupons & Discounts

58 of 144 new or added lines in 18 files covered. (40.28%)

14 existing lines in 3 files now uncovered.

989 of 1617 relevant lines covered (61.16%)

14.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.63
/src/Traits/AsOrder.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Bazar\Traits;
6

7
use Cone\Bazar\Bazar;
8
use Cone\Bazar\Enums\Currency;
9
use Cone\Bazar\Interfaces\Inventoryable;
10
use Cone\Bazar\Interfaces\LineItem;
11
use Cone\Bazar\Interfaces\Taxable;
12
use Cone\Bazar\Models\AppliedCoupon;
13
use Cone\Bazar\Models\Coupon;
14
use Cone\Bazar\Models\Item;
15
use Cone\Bazar\Models\Shipping;
16
use Cone\Bazar\Support\Facades\Shipping as ShippingManager;
17
use Cone\Root\Interfaces\Models\User;
18
use Illuminate\Database\Eloquent\Casts\Attribute;
19
use Illuminate\Database\Eloquent\Relations\BelongsTo;
20
use Illuminate\Database\Eloquent\Relations\MorphMany;
21
use Illuminate\Database\Eloquent\Relations\MorphOne;
22
use Illuminate\Database\Eloquent\Relations\MorphToMany;
23
use Illuminate\Database\Eloquent\SoftDeletes;
24
use Illuminate\Support\Arr;
25
use Illuminate\Support\Collection;
26
use Illuminate\Support\Facades\App;
27

28
trait AsOrder
29
{
30
    /**
31
     * Boot the trait.
32
     */
33
    public static function bootAsOrder(): void
34
    {
35
        static::deleting(static function (self $model): void {
60✔
36
            if (! in_array(SoftDeletes::class, class_uses_recursive($model)) || $model->forceDeleting) {
×
37
                $model->items->each->delete();
×
38
                $model->shipping->delete();
×
39
            }
40
        });
60✔
41
    }
42

43
    /**
44
     * Get the user for the model.
45
     */
46
    public function user(): BelongsTo
47
    {
48
        return $this->belongsTo(get_class(App::make(User::class)));
17✔
49
    }
50

51
    /**
52
     * Get the items for the model.
53
     */
54
    public function items(): MorphMany
55
    {
56
        return $this->morphMany(Item::getProxiedClass(), 'checkoutable');
35✔
57
    }
58

59
    /**
60
     * Get the shipping for the model.
61
     */
62
    public function shipping(): MorphOne
63
    {
64
        return $this->morphOne(Shipping::getProxiedClass(), 'shippable')->withDefault([
24✔
65
            'driver' => ShippingManager::getDefaultDriver(),
24✔
66
        ]);
24✔
67
    }
68

69
    /**
70
     * Get the coupons for the model.
71
     */
72
    public function coupons(): MorphToMany
73
    {
NEW
74
        return $this->morphToMany(Coupon::getProxiedClass(), 'couponable', 'bazar_couponables')
×
NEW
75
            ->using(AppliedCoupon::getProxiedClass())
×
NEW
76
            ->withPivot('value')
×
NEW
77
            ->withTimestamps();
×
78
    }
79

80
    /**
81
     * Get the items.
82
     */
83
    public function getItems(): Collection
84
    {
85
        return $this->items;
18✔
86
    }
87

88
    /**
89
     * Get the line items.
90
     */
91
    public function getLineItems(): Collection
92
    {
93
        return $this->getItems()->filter->isLineItem();
5✔
94
    }
95

96
    /**
97
     * Get the fees.
98
     */
99
    public function getFees(): Collection
100
    {
101
        return $this->getItems()->filter->isFee();
×
102
    }
103

104
    /**
105
     * Get the taxables.
106
     */
107
    public function getTaxables(): Collection
108
    {
109
        return $this->getItems()->when($this->needsShipping(), function (Collection $items): Collection {
16✔
110
            return $items->merge([$this->shipping]);
16✔
111
        });
16✔
112
    }
113

114
    /**
115
     * Get the total attribute.
116
     *
117
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<float, never>
118
     */
119
    protected function total(): Attribute
120
    {
121
        return new Attribute(
6✔
122
            get: function (): float {
6✔
123
                return $this->getTotal();
6✔
124
            }
6✔
125
        );
6✔
126
    }
127

128
    /**
129
     * Get the formatted total attribute.
130
     *
131
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
132
     */
133
    protected function formattedTotal(): Attribute
134
    {
135
        return new Attribute(
3✔
136
            get: function (): string {
3✔
137
                return $this->getFormattedTotal();
3✔
138
            }
3✔
139
        );
3✔
140
    }
141

142
    /**
143
     * Get the subtotal attribute.
144
     *
145
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<float, never>
146
     */
147
    protected function subtotal(): Attribute
148
    {
149
        return new Attribute(
5✔
150
            get: function (): float {
5✔
151
                return $this->getSubtotal();
5✔
152
            }
5✔
153
        );
5✔
154
    }
155

156
    /**
157
     * Get the formatted subtotal attribute.
158
     *
159
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
160
     */
161
    protected function formattedSubtotal(): Attribute
162
    {
163
        return new Attribute(
3✔
164
            get: function (): string {
3✔
165
                return $this->getFormattedSubtotal();
3✔
166
            }
3✔
167
        );
3✔
168
    }
169

170
    /**
171
     * Get the tax attribute.
172
     *
173
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<float, never>
174
     */
175
    protected function tax(): Attribute
176
    {
177
        return new Attribute(
4✔
178
            get: function (): float {
4✔
179
                return $this->getTax();
4✔
180
            }
4✔
181
        );
4✔
182
    }
183

184
    /**
185
     * Get the formatted tax attribute.
186
     *
187
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
188
     */
189
    protected function formattedTax(): Attribute
190
    {
191
        return new Attribute(
3✔
192
            get: function (): string {
3✔
193
                return $this->getFormattedTax();
3✔
194
            }
3✔
195
        );
3✔
196
    }
197

198
    /**
199
     * Determine if the model needs shipping.
200
     */
201
    public function needsShipping(): bool
202
    {
203
        return $this->items->some(static function (Item $item): bool {
21✔
204
            return ! $item->isFee()
21✔
205
                && $item->buyable instanceof Inventoryable
21✔
206
                && $item->buyable->isPhysical();
21✔
207
        });
21✔
208
    }
209

210
    /**
211
     * Get the currency.
212
     */
213
    public function getCurrency(): Currency
214
    {
215
        return $this->currency ?: Bazar::getCurrency();
17✔
216
    }
217

218
    /**
219
     * Get the checkoutable model's total.
220
     */
221
    public function getTotal(): float
222
    {
223
        $value = $this->items->sum(static function (Item $item): float {
9✔
224
            return $item->getTotal();
9✔
225
        });
9✔
226

227
        $value += $this->needsShipping() ? $this->shipping->getTotal() : 0;
9✔
228

229
        return round($value < 0 ? 0 : $value, 2);
9✔
230
    }
231

232
    /**
233
     * Get the formatted total.
234
     */
235
    public function getFormattedTotal(): string
236
    {
237
        return $this->checkoutable?->getCurrency()?->format($this->getTotal()) ?: '';
3✔
238
    }
239

240
    /**
241
     * Get the checkoutable model's subtotal.
242
     */
243
    public function getSubtotal(): float
244
    {
245
        $value = $this->getLineItems()->sum(static function (LineItem $item): float {
5✔
246
            return $item->getSubtotal();
5✔
247
        });
5✔
248

249
        return round($value < 0 ? 0 : $value, 2);
5✔
250
    }
251

252
    /**
253
     * Get the formatted subtotal.
254
     */
255
    public function getFormattedSubtotal(): string
256
    {
257
        return $this->checkoutable?->getCurrency()?->format($this->getSubtotal()) ?: '';
3✔
258
    }
259

260
    /**
261
     * Get the checkoutable model's fee total.
262
     */
263
    public function getFeeTotal(): float
264
    {
265
        $value = $this->getFees()->sum(static function (LineItem $item): float {
×
266
            return $item->getSubtotal();
×
267
        });
×
268

269
        return round($value < 0 ? 0 : $value, 2);
×
270
    }
271

272
    /**
273
     * Get the formatted fee total.
274
     */
275
    public function getFormattedFeeTotal(): string
276
    {
NEW
277
        return $this->checkoutable->getCurrency()->format($this->getFeeTotal());
×
278
    }
279

280
    /**
281
     * Get the tax.
282
     */
283
    public function getTax(): float
284
    {
285
        $value = $this->getTaxables()->sum(static function (Taxable $item): float {
4✔
286
            return $item->getTaxTotal();
4✔
287
        });
4✔
288

289
        return round($value, 2);
4✔
290
    }
291

292
    /**
293
     * Get the formatted tax.
294
     */
295
    public function getFormattedTax(): string
296
    {
297
        return $this->checkoutable?->getCurrency()?->format($this->getTax()) ?: '';
3✔
298
    }
299

300
    /**
301
     * Calculate the tax.
302
     */
303
    public function calculateTax(): float
304
    {
305
        $value = $this->getTaxables()->each(static function (Taxable $item): void {
14✔
306
            $item->calculateTaxes();
14✔
307
        })->sum(static function (Taxable $item): float {
14✔
308
            return $item->getTaxTotal();
14✔
309
        });
14✔
310

311
        return round($value, 2);
14✔
312
    }
313

314
    /**
315
     * Find an item by its attributes or make a new instance.
316
     */
317
    public function findItem(array $attributes): ?Item
318
    {
319

320
        $attributes = array_merge(['properties' => null], $attributes, [
14✔
321
            'checkoutable_id' => $this->getKey(),
14✔
322
            'checkoutable_type' => static::class,
14✔
323
        ]);
14✔
324

325
        return $this->items->first(static function (Item $item) use ($attributes): bool {
14✔
326
            return empty(array_diff(
14✔
327
                array_filter(Arr::dot($attributes)),
14✔
328
                array_filter(Arr::dot(array_merge(['properties' => null], $item->withoutRelations()->toArray())))
14✔
329
            ));
14✔
330
        });
14✔
331
    }
332

333
    /**
334
     * Merge the given item into the collection.
335
     */
336
    public function mergeItem(Item $item): Item
337
    {
338
        $stored = $this->findItem(
14✔
339
            $item->only(['properties', 'buyable_id', 'buyable_type'])
14✔
340
        );
14✔
341

342
        if (is_null($stored)) {
14✔
343
            $item->checkoutable()->associate($this);
14✔
344

345
            $item->setRelation('checkoutable', $this->withoutRelations());
14✔
346

347
            $this->items->push($item);
14✔
348

349
            return $item;
14✔
350
        }
351

352
        $stored->quantity += $item->quantity;
1✔
353

354
        return $stored;
1✔
355
    }
356

357
    /**
358
     * Sync the items.
359
     */
360
    public function syncItems(): void
361
    {
362
        $this->items->each(static function (Item $item): void {
×
363
            if ($item->isLineItem() && ! is_null($item->checkoutable)) {
×
NEW
364
                $data = $item->buyable->toItem($item->checkoutable, $item->only('properties'))->only(['price']);
×
365

366
                $item->fill($data)->save();
×
367
                $item->calculateTaxes();
×
368
            }
369
        });
×
370
    }
371

372
    /**
373
     * Determine whether the checkoutable models needs payment.
374
     */
375
    public function needsPayment(): bool
376
    {
NEW
377
        return $this->getTotal() > 0;
×
378
    }
379

380
    /**
381
     * Apply a coupon to the checkoutable model.
382
     */
383
    public function applyCoupon(string|Coupon $coupon): void
384
    {
NEW
385
        $coupon = match (true) {
×
NEW
386
            is_string($coupon) => Coupon::query()->where('bazar_coupons.code', $coupon)->available()->firstOrFail(),
×
NEW
387
            default => $coupon,
×
NEW
388
        };
×
389

NEW
390
        $coupon->apply($this);
×
391
    }
392

393
    /**
394
     * Get the discount.
395
     */
396
    public function getDiscount(): float
397
    {
NEW
398
        return $this->coupons->sum('pivot.value');
×
399
    }
400

401
    /**
402
     * Get the formatted discount.
403
     */
404
    public function getFormattedDiscount(): string
405
    {
NEW
406
        return 0;
×
407
    }
408

409
    /**
410
     * Get the discount rate.
411
     */
412
    public function getDiscountRate(): float
413
    {
NEW
414
        return 0;
×
415
    }
416

417
    /**
418
     * Get the formatted discount rate.
419
     */
420
    public function getFormattedDiscountRate(): string
421
    {
NEW
422
        return 0;
×
423
    }
424
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc