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

conedevelopment / bazar / 20213722262

14 Dec 2025 08:30PM UTC coverage: 63.031% (-0.4%) from 63.48%
20213722262

Pull #235

github

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

0 of 3 new or added lines in 2 files covered. (0.0%)

53 existing lines in 11 files now uncovered.

965 of 1531 relevant lines covered (63.03%)

15.16 hits per line

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

83.33
/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\Events\OrderStatusChanged;
9
use Cone\Bazar\Exceptions\TransactionFailedException;
10
use Cone\Bazar\Interfaces\Models\Order as Contract;
11
use Cone\Bazar\Notifications\OrderDetails;
12
use Cone\Bazar\Support\Facades\Gateway;
13
use Cone\Bazar\Traits\Addressable;
14
use Cone\Bazar\Traits\InteractsWithItems;
15
use Cone\Root\Traits\InteractsWithProxy;
16
use Illuminate\Database\Eloquent\Builder;
17
use Illuminate\Database\Eloquent\Casts\Attribute;
18
use Illuminate\Database\Eloquent\Concerns\HasUuids;
19
use Illuminate\Database\Eloquent\Factories\HasFactory;
20
use Illuminate\Database\Eloquent\Model;
21
use Illuminate\Database\Eloquent\Relations\HasMany;
22
use Illuminate\Database\Eloquent\Relations\HasOne;
23
use Illuminate\Database\Eloquent\SoftDeletes;
24
use Illuminate\Notifications\Notification;
25
use Illuminate\Support\Collection;
26
use Illuminate\Support\Facades\Notification as Notifier;
27

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

37
    public const CANCELLED = 'cancelled';
38

39
    public const FULFILLED = 'fulfilled';
40

41
    public const FAILED = 'failed';
42

43
    public const IN_PROGRESS = 'in_progress';
44

45
    public const ON_HOLD = 'on_hold';
46

47
    public const PENDING = 'pending';
48

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

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

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

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

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

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

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

122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function getMorphClass(): string
126
    {
127
        return static::getProxiedClass();
19✔
128
    }
129

130
    /**
131
     * Get the cart for the order.
132
     */
133
    public function cart(): HasOne
134
    {
135
        return $this->hasOne(Cart::getProxiedClass());
1✔
136
    }
137

138
    /**
139
     * Get the transactions for the order.
140
     */
141
    public function transactions(): HasMany
142
    {
143
        return $this->hasMany(Transaction::getProxiedClass());
21✔
144
    }
145

146
    /**
147
     * Get the base transaction for the order.
148
     */
149
    public function transaction(): HasOne
150
    {
151
        return $this->transactions()->one()->ofMany(
1✔
152
            ['id' => 'MIN'],
1✔
153
            static function (Builder $query): Builder {
1✔
154
                return $query->payment();
1✔
155
            }
1✔
156
        );
1✔
157
    }
158

159
    /**
160
     * Get the payments attribute.
161
     *
162
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<\Illuminate\Support\Collection, never>
163
     */
164
    protected function payments(): Attribute
165
    {
166
        return new Attribute(
5✔
167
            get: function (): Collection {
5✔
168
                return $this->transactions->where('type', Transaction::PAYMENT);
4✔
169
            }
5✔
170
        );
5✔
171
    }
172

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

187
    /**
188
     * Get the status name attribute.
189
     *
190
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
191
     */
192
    protected function statusName(): Attribute
193
    {
194
        return new Attribute(
3✔
195
            get: static function (mixed $value, array $attributes): string {
3✔
196
                return static::getStatuses()[$attributes['status']] ?? $attributes['status'];
3✔
197
            }
3✔
198
        );
3✔
199
    }
200

201
    /**
202
     * Get the payment status name attribute.
203
     *
204
     * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never>
205
     */
206
    protected function paymentStatus(): Attribute
207
    {
208
        return new Attribute(
1✔
209
            get: function (): string {
1✔
210
                return match (true) {
UNCOV
211
                    $this->refunded() => __('Refunded'),
×
UNCOV
212
                    $this->partiallyRefunded() => __('Partially Refunded'),
×
UNCOV
213
                    $this->paid() => __('Paid'),
×
UNCOV
214
                    $this->partiallyPaid() => __('Partially Paid'),
×
UNCOV
215
                    default => __('Pending Payment'),
×
216
                };
217
            }
1✔
218
        );
1✔
219
    }
220

221
    /**
222
     * Get the columns that should receive a unique identifier.
223
     */
224
    public function uniqueIds(): array
225
    {
226
        return ['uuid'];
29✔
227
    }
228

229
    /**
230
     * Create a payment transaction for the order.
231
     */
232
    public function pay(?float $amount = null, ?string $driver = null, array $attributes = []): Transaction
233
    {
234
        if (! $this->payable()) {
3✔
UNCOV
235
            throw new TransactionFailedException("Order #{$this->getKey()} is fully paid.");
×
236
        }
237

238
        $transaction = $this->transactions()->create(array_replace($attributes, [
3✔
239
            'type' => Transaction::PAYMENT,
3✔
240
            'driver' => $driver ?: Gateway::getDefaultDriver(),
3✔
241
            'amount' => is_null($amount) ? $this->getTotalPayable() : min($amount, $this->getTotalPayable()),
3✔
242
        ]));
3✔
243

244
        $this->transactions->push($transaction);
3✔
245

246
        return $transaction;
3✔
247
    }
248

249
    /**
250
     * Create a refund transaction for the order.
251
     */
252
    public function refund(?float $amount = null, ?string $driver = null, array $attributes = []): Transaction
253
    {
254
        if (! $this->refundable()) {
3✔
UNCOV
255
            throw new TransactionFailedException("Order #{$this->getKey()} is fully refunded.");
×
256
        }
257

258
        $transaction = $this->transactions()->create(array_replace($attributes, [
3✔
259
            'type' => Transaction::REFUND,
3✔
260
            'driver' => $driver ?: Gateway::getDefaultDriver(),
3✔
261
            'amount' => is_null($amount) ? $this->getTotalRefundable() : min($amount, $this->getTotalRefundable()),
3✔
262
        ]));
3✔
263

264
        $this->transactions->push($transaction);
3✔
265

266
        return $transaction;
3✔
267
    }
268

269
    /**
270
     * Get the total paid amount.
271
     */
272
    public function getTotalPaid(): float
273
    {
274
        return $this->payments->filter->completed()->sum('amount');
4✔
275
    }
276

277
    /**
278
     * Get the total refunded amount.
279
     */
280
    public function getTotalRefunded(): float
281
    {
282
        return $this->refunds->filter->completed()->sum('amount');
4✔
283
    }
284

285
    /**
286
     * Get the total payable amount.
287
     */
288
    public function getTotalPayable(): float
289
    {
290
        return max($this->getTotal() - $this->getTotalPaid(), 0);
3✔
291
    }
292

293
    /**
294
     * Get the total refundabke amount.
295
     */
296
    public function getTotalRefundable(): float
297
    {
298
        return max($this->getTotalPaid() - $this->getTotalRefunded(), 0);
4✔
299
    }
300

301
    /**
302
     * Determine if the order is fully paid.
303
     */
304
    public function paid(): bool
305
    {
306
        return $this->payments->filter->completed()->isNotEmpty()
3✔
307
            && $this->getTotal() <= $this->getTotalPaid();
3✔
308
    }
309

310
    /**
311
     * Determine if the order is partially paid.
312
     */
313
    public function partiallyPaid(): bool
314
    {
UNCOV
315
        return $this->payments->filter->completed()->isNotEmpty()
×
UNCOV
316
            && $this->getTotal() > $this->getTotalPaid();
×
317
    }
318

319
    /**
320
     * Determine if the order is payable.
321
     */
322
    public function payable(): bool
323
    {
324
        return in_array($this->status, [static::ON_HOLD, static::PENDING, static::CANCELLED])
3✔
325
            && $this->getTotalPayable() > 0
3✔
326
            && ! $this->paid();
3✔
327
    }
328

329
    /**
330
     * Determine if the order is fully refunded.
331
     */
332
    public function refunded(): bool
333
    {
334
        return $this->refunds->filter->completed()->isNotEmpty()
3✔
335
            && $this->getTotalPaid() <= $this->getTotalRefunded();
3✔
336
    }
337

338
    /**
339
     * Determine if the order is refundable.
340
     */
341
    public function refundable(): bool
342
    {
343
        return $this->getTotalRefundable() > 0 && ! $this->refunded();
3✔
344
    }
345

346
    /**
347
     * Determine if the orderis partially refunded.
348
     */
349
    public function partiallyRefunded(): bool
350
    {
UNCOV
351
        return $this->refunds->filter->completed()->isNotEmpty()
×
UNCOV
352
            && $this->getTotalPaid() > $this->getTotalRefunded();
×
353
    }
354

355
    /**
356
     * Set the status by the given value.
357
     */
358
    public function markAs(string $status): void
359
    {
360
        if ($this->status !== $status) {
3✔
361
            $from = $this->status;
3✔
362

363
            $this->setAttribute('status', $status)->save();
3✔
364

365
            OrderStatusChanged::dispatch($this, $status, $from);
3✔
366
        }
367
    }
368

369
    /**
370
     * Get the notifiable object.
371
     */
372
    public function getNotifiable(): object
373
    {
374
        return match (true) {
NEW
UNCOV
375
            is_null($this->user) => Notifier::route('mail', [$this->address->email => $this->address->name]),
×
NEW
UNCOV
376
            default => $this->user,
×
377
        };
378
    }
379

380
    /**
381
     * Send the given notification.
382
     */
383
    public function sendNotification(Notification $notification): void
384
    {
UNCOV
385
        $this->getNotifiable()->notify($notification);
×
386
    }
387

388
    /**
389
     * Send the order details notification.
390
     */
391
    public function sendOrderDetailsNotification(): void
392
    {
UNCOV
393
        $this->sendNotification(new OrderDetails($this));
×
394
    }
395

396
    /**
397
     * Scope a query to only include orders with the given status.
398
     */
399
    public function scopeStatus(Builder $query, string $status): Builder
400
    {
401
        return $query->where($query->qualifyColumn('status'), $status);
1✔
402
    }
403

404
    /**
405
     * Scope the query to the given user.
406
     */
407
    public function scopeUser(Builder $query, int $value): Builder
408
    {
409
        return $query->whereHas('user', static function (Builder $query) use ($value): Builder {
1✔
410
            return $query->whereKey($value);
1✔
411
        });
1✔
412
    }
413
}
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