• 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

83.87
/src/Models/Order.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Bazar\Models;
6

7
use Cone\Bazar\Database\Factories\OrderFactory;
8
use Cone\Bazar\Enums\Currency;
9
use Cone\Bazar\Events\OrderStatusChanged;
10
use Cone\Bazar\Exceptions\TransactionFailedException;
11
use Cone\Bazar\Interfaces\Models\Order as Contract;
12
use Cone\Bazar\Notifications\OrderDetails;
13
use Cone\Bazar\Support\Facades\Gateway;
14
use Cone\Bazar\Traits\Addressable;
15
use Cone\Bazar\Traits\AsOrder;
16
use Cone\Root\Traits\InteractsWithProxy;
17
use Illuminate\Database\Eloquent\Builder;
18
use Illuminate\Database\Eloquent\Casts\Attribute;
19
use Illuminate\Database\Eloquent\Concerns\HasUuids;
20
use Illuminate\Database\Eloquent\Factories\HasFactory;
21
use Illuminate\Database\Eloquent\Model;
22
use Illuminate\Database\Eloquent\Relations\HasMany;
23
use Illuminate\Database\Eloquent\Relations\HasOne;
24
use Illuminate\Database\Eloquent\SoftDeletes;
25
use Illuminate\Notifications\Notification;
26
use Illuminate\Support\Collection;
27
use Illuminate\Support\Facades\Notification as Notifier;
28

29
class Order extends Model implements Contract
30
{
31
    use Addressable;
32
    use AsOrder;
33
    use HasFactory;
34
    use HasUuids;
35
    use InteractsWithProxy;
36
    use SoftDeletes;
37

38
    public const CANCELLED = 'cancelled';
39

40
    public const FULFILLED = 'fulfilled';
41

42
    public const FAILED = 'failed';
43

44
    public const IN_PROGRESS = 'in_progress';
45

46
    public const ON_HOLD = 'on_hold';
47

48
    public const PENDING = 'pending';
49

50
    /**
51
     * The accessors to append to the model's array form.
52
     *
53
     * @var list<string>
54
     */
55
    protected $appends = [
56
        'formatted_subtotal',
57
        'formatted_tax',
58
        'formatted_total',
59
        'subtotal',
60
        'status_name',
61
        'tax',
62
        'total',
63
    ];
64

65
    /**
66
     * The attributes that should have default values.
67
     *
68
     * @var array<string, mixed>
69
     */
70
    protected $attributes = [
71
        'currency' => null,
72
        'status' => self::ON_HOLD,
73
    ];
74

75
    /**
76
     * The attributes that are mass assignable.
77
     *
78
     * @var list<string>
79
     */
80
    protected $fillable = [
81
        'currency',
82
        'status',
83
    ];
84

85
    /**
86
     * The table associated with the model.
87
     *
88
     * @var string
89
     */
90
    protected $table = 'bazar_orders';
91

92
    /**
93
     * Get the proxied interface.
94
     */
95
    public static function getProxiedInterface(): string
96
    {
97
        return Contract::class;
1✔
98
    }
99

100
    /**
101
     * Create a new factory instance for the model.
102
     */
103
    protected static function newFactory(): OrderFactory
104
    {
105
        return OrderFactory::new();
28✔
106
    }
107

108
    /**
109
     * Get the available order statuses.
110
     */
111
    public static function getStatuses(): array
112
    {
113
        return [
65✔
114
            static::PENDING => __('Pending'),
65✔
115
            static::ON_HOLD => __('On Hold'),
65✔
116
            static::IN_PROGRESS => __('In Progress'),
65✔
117
            static::FULFILLED => __('Fulfilled'),
65✔
118
            static::CANCELLED => __('Cancelled'),
65✔
119
            static::FAILED => __('Failed'),
65✔
120
        ];
65✔
121
    }
122

123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function casts(): array
127
    {
128
        return [
29✔
129
            'currency' => Currency::class,
29✔
130
        ];
29✔
131
    }
132

133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function getMorphClass(): string
137
    {
138
        return static::getProxiedClass();
19✔
139
    }
140

141
    /**
142
     * Get the cart for the order.
143
     */
144
    public function cart(): HasOne
145
    {
146
        return $this->hasOne(Cart::getProxiedClass());
1✔
147
    }
148

149
    /**
150
     * Get the transactions for the order.
151
     */
152
    public function transactions(): HasMany
153
    {
154
        return $this->hasMany(Transaction::getProxiedClass());
21✔
155
    }
156

157
    /**
158
     * Get the base transaction for the order.
159
     */
160
    public function transaction(): HasOne
161
    {
162
        return $this->transactions()->one()->ofMany(
1✔
163
            ['id' => 'MIN'],
1✔
164
            static function (Builder $query): Builder {
1✔
165
                return $query->payment();
1✔
166
            }
1✔
167
        );
1✔
168
    }
169

170
    /**
171
     * Get the payments attribute.
172
     *
173
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<\Illuminate\Support\Collection, never>
174
     */
175
    protected function payments(): Attribute
176
    {
177
        return new Attribute(
5✔
178
            get: function (): Collection {
5✔
179
                return $this->transactions->where('type', Transaction::PAYMENT);
4✔
180
            }
5✔
181
        );
5✔
182
    }
183

184
    /**
185
     * Get the refunds attribute.
186
     *
187
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<\Illuminate\Support\Collection, never>
188
     */
189
    protected function refunds(): Attribute
190
    {
191
        return new Attribute(
5✔
192
            get: function (): Collection {
5✔
193
                return $this->transactions->where('type', Transaction::REFUND);
4✔
194
            }
5✔
195
        );
5✔
196
    }
197

198
    /**
199
     * Get the status name attribute.
200
     *
201
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
202
     */
203
    protected function statusName(): Attribute
204
    {
205
        return new Attribute(
3✔
206
            get: static function (mixed $value, array $attributes): string {
3✔
207
                return static::getStatuses()[$attributes['status']] ?? $attributes['status'];
3✔
208
            }
3✔
209
        );
3✔
210
    }
211

212
    /**
213
     * Get the payment status name attribute.
214
     *
215
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
216
     */
217
    protected function paymentStatus(): Attribute
218
    {
219
        return new Attribute(
1✔
220
            get: function (): string {
1✔
221
                return match (true) {
UNCOV
222
                    $this->refunded() => __('Refunded'),
×
UNCOV
223
                    $this->partiallyRefunded() => __('Partially Refunded'),
×
224
                    $this->paid() => __('Paid'),
×
225
                    $this->partiallyPaid() => __('Partially Paid'),
×
226
                    default => __('Pending Payment'),
×
227
                };
228
            }
1✔
229
        );
1✔
230
    }
231

232
    /**
233
     * Get the columns that should receive a unique identifier.
234
     */
235
    public function uniqueIds(): array
236
    {
237
        return ['uuid'];
29✔
238
    }
239

240
    /**
241
     * Create a payment transaction for the order.
242
     */
243
    public function pay(?float $amount = null, ?string $driver = null, array $attributes = []): Transaction
244
    {
245
        if (! $this->payable()) {
3✔
UNCOV
246
            throw new TransactionFailedException("Order #{$this->getKey()} is fully paid.");
×
247
        }
248

249
        $transaction = $this->transactions()->create(array_replace($attributes, [
3✔
250
            'type' => Transaction::PAYMENT,
3✔
251
            'driver' => $driver ?: Gateway::getDefaultDriver(),
3✔
252
            'amount' => is_null($amount) ? $this->getTotalPayable() : min($amount, $this->getTotalPayable()),
3✔
253
        ]));
3✔
254

255
        $this->transactions->push($transaction);
3✔
256

257
        return $transaction;
3✔
258
    }
259

260
    /**
261
     * Create a refund transaction for the order.
262
     */
263
    public function refund(?float $amount = null, ?string $driver = null, array $attributes = []): Transaction
264
    {
265
        if (! $this->refundable()) {
3✔
UNCOV
266
            throw new TransactionFailedException("Order #{$this->getKey()} is fully refunded.");
×
267
        }
268

269
        $transaction = $this->transactions()->create(array_replace($attributes, [
3✔
270
            'type' => Transaction::REFUND,
3✔
271
            'driver' => $driver ?: Gateway::getDefaultDriver(),
3✔
272
            'amount' => is_null($amount) ? $this->getTotalRefundable() : min($amount, $this->getTotalRefundable()),
3✔
273
        ]));
3✔
274

275
        $this->transactions->push($transaction);
3✔
276

277
        return $transaction;
3✔
278
    }
279

280
    /**
281
     * Get the total paid amount.
282
     */
283
    public function getTotalPaid(): float
284
    {
285
        return $this->payments->filter->completed()->sum('amount');
4✔
286
    }
287

288
    /**
289
     * Get the total refunded amount.
290
     */
291
    public function getTotalRefunded(): float
292
    {
293
        return $this->refunds->filter->completed()->sum('amount');
4✔
294
    }
295

296
    /**
297
     * Get the total payable amount.
298
     */
299
    public function getTotalPayable(): float
300
    {
301
        return max($this->getTotal() - $this->getTotalPaid(), 0);
3✔
302
    }
303

304
    /**
305
     * Get the total refundabke amount.
306
     */
307
    public function getTotalRefundable(): float
308
    {
309
        return max($this->getTotalPaid() - $this->getTotalRefunded(), 0);
4✔
310
    }
311

312
    /**
313
     * Determine if the order is fully paid.
314
     */
315
    public function paid(): bool
316
    {
317
        return $this->payments->filter->completed()->isNotEmpty()
3✔
318
            && $this->getTotal() <= $this->getTotalPaid();
3✔
319
    }
320

321
    /**
322
     * Determine if the order is partially paid.
323
     */
324
    public function partiallyPaid(): bool
325
    {
UNCOV
326
        return $this->payments->filter->completed()->isNotEmpty()
×
UNCOV
327
            && $this->getTotal() > $this->getTotalPaid();
×
328
    }
329

330
    /**
331
     * Determine if the order is payable.
332
     */
333
    public function payable(): bool
334
    {
335
        return in_array($this->status, [static::ON_HOLD, static::PENDING, static::CANCELLED])
3✔
336
            && $this->getTotalPayable() > 0
3✔
337
            && ! $this->paid();
3✔
338
    }
339

340
    /**
341
     * Determine if the order is fully refunded.
342
     */
343
    public function refunded(): bool
344
    {
345
        return $this->refunds->filter->completed()->isNotEmpty()
3✔
346
            && $this->getTotalPaid() <= $this->getTotalRefunded();
3✔
347
    }
348

349
    /**
350
     * Determine if the order is refundable.
351
     */
352
    public function refundable(): bool
353
    {
354
        return $this->getTotalRefundable() > 0 && ! $this->refunded();
3✔
355
    }
356

357
    /**
358
     * Determine if the orderis partially refunded.
359
     */
360
    public function partiallyRefunded(): bool
361
    {
UNCOV
362
        return $this->refunds->filter->completed()->isNotEmpty()
×
UNCOV
363
            && $this->getTotalPaid() > $this->getTotalRefunded();
×
364
    }
365

366
    /**
367
     * Set the status by the given value.
368
     */
369
    public function markAs(string $status): void
370
    {
371
        if ($this->status !== $status) {
3✔
372
            $from = $this->status;
3✔
373

374
            $this->setAttribute('status', $status)->save();
3✔
375

376
            OrderStatusChanged::dispatch($this, $status, $from);
3✔
377
        }
378
    }
379

380
    /**
381
     * Get the notifiable object.
382
     */
383
    public function getNotifiable(): object
384
    {
385
        return match (true) {
NEW
UNCOV
386
            is_null($this->user) => Notifier::route('mail', [$this->address->email => $this->address->name]),
×
NEW
387
            default => $this->user,
×
388
        };
389
    }
390

391
    /**
392
     * Send the given notification.
393
     */
394
    public function sendNotification(Notification $notification): void
395
    {
UNCOV
396
        $this->getNotifiable()->notify($notification);
×
397
    }
398

399
    /**
400
     * Send the order details notification.
401
     */
402
    public function sendOrderDetailsNotification(): void
403
    {
UNCOV
404
        $this->sendNotification(new OrderDetails($this));
×
405
    }
406

407
    /**
408
     * Scope a query to only include orders with the given status.
409
     */
410
    public function scopeStatus(Builder $query, string $status): Builder
411
    {
412
        return $query->where($query->qualifyColumn('status'), $status);
1✔
413
    }
414

415
    /**
416
     * Scope the query to the given user.
417
     */
418
    public function scopeUser(Builder $query, int $value): Builder
419
    {
420
        return $query->whereHas('user', static function (Builder $query) use ($value): Builder {
1✔
421
            return $query->whereKey($value);
1✔
422
        });
1✔
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