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

conedevelopment / bazar / 20312410163

17 Dec 2025 05:58PM UTC coverage: 64.395% (+0.9%) from 63.48%
20312410163

push

github

web-flow
Merge pull request #235 from conedevelopment/coupons

Coupons & Discounts

204 of 260 new or added lines in 23 files covered. (78.46%)

8 existing lines in 1 file now uncovered.

1096 of 1702 relevant lines covered (64.39%)

18.27 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
        '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
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
106
    {
107
        return OrderFactory::new();
33✔
108
    }
109

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

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

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

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

151
    /**
152
     * Get the transactions for the order.
153
     */
154
    public function transactions(): HasMany
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
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
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
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
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
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
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
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
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 total paid amount.
284
     */
285
    public function getTotalPaid(): float
286
    {
287
        return $this->payments->filter->completed()->sum('amount');
4✔
288
    }
289

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

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

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

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

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

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

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

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

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

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

376
            $this->setAttribute('status', $status)->save();
3✔
377

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

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

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

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

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

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