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

conedevelopment / bazar / 20694116184

04 Jan 2026 02:08PM UTC coverage: 68.615% (+4.5%) from 64.117%
20694116184

push

github

iamgergo
version

1679 of 2447 relevant lines covered (68.61%)

25.06 hits per line

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

82.54
/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
        'discount',
57
        'formatted_discount',
58
        'formatted_subtotal',
59
        'formatted_tax',
60
        'formatted_total',
61
        'status_name',
62
        'subtotal',
63
        'tax',
64
        'total',
65
    ];
66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

257
        $this->transactions->push($transaction);
3✔
258

259
        return $transaction;
3✔
260
    }
261

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

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

277
        $this->transactions->push($transaction);
3✔
278

279
        return $transaction;
3✔
280
    }
281

282
    /**
283
     * Get the label for the order.
284
     */
285
    public function getLabel(): string
×
286
    {
287
        return __('Order #:id', ['id' => $this->getKey()]);
×
288
    }
289

290
    /**
291
     * Get the total paid amount.
292
     */
293
    public function getTotalPaid(): float
4✔
294
    {
295
        return $this->payments->filter->completed()->sum('amount');
4✔
296
    }
297

298
    /**
299
     * Get the total refunded amount.
300
     */
301
    public function getTotalRefunded(): float
4✔
302
    {
303
        return $this->refunds->filter->completed()->sum('amount');
4✔
304
    }
305

306
    /**
307
     * Get the total payable amount.
308
     */
309
    public function getTotalPayable(): float
3✔
310
    {
311
        return max($this->getTotal() - $this->getTotalPaid(), 0);
3✔
312
    }
313

314
    /**
315
     * Get the total refundabke amount.
316
     */
317
    public function getTotalRefundable(): float
4✔
318
    {
319
        return max($this->getTotalPaid() - $this->getTotalRefunded(), 0);
4✔
320
    }
321

322
    /**
323
     * Determine if the order is fully paid.
324
     */
325
    public function paid(): bool
3✔
326
    {
327
        return $this->payments->filter->completed()->isNotEmpty()
3✔
328
            && $this->getTotal() <= $this->getTotalPaid();
3✔
329
    }
330

331
    /**
332
     * Determine if the order is partially paid.
333
     */
334
    public function partiallyPaid(): bool
×
335
    {
336
        return $this->payments->filter->completed()->isNotEmpty()
×
337
            && $this->getTotal() > $this->getTotalPaid();
×
338
    }
339

340
    /**
341
     * Determine if the order is payable.
342
     */
343
    public function payable(): bool
3✔
344
    {
345
        return in_array($this->status, [static::ON_HOLD, static::PENDING, static::CANCELLED])
3✔
346
            && $this->getTotalPayable() > 0
3✔
347
            && ! $this->paid();
3✔
348
    }
349

350
    /**
351
     * Determine if the order is fully refunded.
352
     */
353
    public function refunded(): bool
3✔
354
    {
355
        return $this->refunds->filter->completed()->isNotEmpty()
3✔
356
            && $this->getTotalPaid() <= $this->getTotalRefunded();
3✔
357
    }
358

359
    /**
360
     * Determine if the order is refundable.
361
     */
362
    public function refundable(): bool
3✔
363
    {
364
        return $this->getTotalRefundable() > 0 && ! $this->refunded();
3✔
365
    }
366

367
    /**
368
     * Determine if the orderis partially refunded.
369
     */
370
    public function partiallyRefunded(): bool
×
371
    {
372
        return $this->refunds->filter->completed()->isNotEmpty()
×
373
            && $this->getTotalPaid() > $this->getTotalRefunded();
×
374
    }
375

376
    /**
377
     * Set the status by the given value.
378
     */
379
    public function markAs(string $status): void
3✔
380
    {
381
        if ($this->status !== $status) {
3✔
382
            $from = $this->status;
3✔
383

384
            $this->setAttribute('status', $status)->save();
3✔
385

386
            OrderStatusChanged::dispatch($this, $status, $from);
3✔
387
        }
388
    }
389

390
    /**
391
     * Get the notifiable object.
392
     */
393
    public function getNotifiable(): object
×
394
    {
395
        return match (true) {
396
            is_null($this->user) => Notifier::route('mail', [$this->address->email => $this->address->name]),
×
397
            default => $this->user,
×
398
        };
399
    }
400

401
    /**
402
     * Send the given notification.
403
     */
404
    public function sendNotification(Notification $notification): void
×
405
    {
406
        $this->getNotifiable()->notify($notification);
×
407
    }
408

409
    /**
410
     * Send the order details notification.
411
     */
412
    public function sendOrderDetailsNotification(): void
×
413
    {
414
        $this->sendNotification(new OrderDetails($this));
×
415
    }
416

417
    /**
418
     * Scope a query to only include orders with the given status.
419
     */
420
    public function scopeStatus(Builder $query, string $status): Builder
1✔
421
    {
422
        return $query->where($query->qualifyColumn('status'), $status);
1✔
423
    }
424

425
    /**
426
     * Scope the query to the given user.
427
     */
428
    public function scopeUser(Builder $query, int $value): Builder
1✔
429
    {
430
        return $query->whereHas('user', static function (Builder $query) use ($value): Builder {
1✔
431
            return $query->whereKey($value);
1✔
432
        });
1✔
433
    }
434
}
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