• 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

94.29
/src/Models/Coupon.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Bazar\Models;
6

7
use Cone\Bazar\Database\Factories\CouponFactory;
8
use Cone\Bazar\Enums\CouponType;
9
use Cone\Bazar\Interfaces\Checkoutable;
10
use Cone\Bazar\Interfaces\Models\Coupon as Contract;
11
use Cone\Root\Traits\InteractsWithProxy;
12
use DateTimeInterface;
13
use Exception;
14
use Illuminate\Database\Eloquent\Attributes\Scope;
15
use Illuminate\Database\Eloquent\Builder;
16
use Illuminate\Database\Eloquent\Factories\HasFactory;
17
use Illuminate\Database\Eloquent\Model;
18
use Illuminate\Database\Eloquent\Relations\HasMany;
19
use Illuminate\Support\Facades\Date;
20

21
class Coupon extends Model implements Contract
22
{
23
    use HasFactory;
24
    use InteractsWithProxy;
25

26
    /**
27
     * The model's default values for attributes.
28
     */
29
    protected $attributes = [
30
        'active' => true,
31
        'code' => null,
32
        'expires_at' => null,
33
        'rules' => '[]',
34
        'stackable' => false,
35
        'type' => CouponType::FIX,
36
        'value' => 0,
37
    ];
38

39
    /**
40
     * The attributes that are mass assignable.
41
     */
42
    protected $fillable = [
43
        'active',
44
        'code',
45
        'expires_at',
46
        'rules',
47
        'stackable',
48
        'type',
49
        'value',
50
    ];
51

52
    /**
53
     * The table associated with the model.
54
     */
55
    protected $table = 'bazar_coupons';
56

57
    /**
58
     * Get the proxied interface.
59
     */
60
    public static function getProxiedInterface(): string
61
    {
62
        return Contract::class;
1✔
63
    }
64

65
    /**
66
     * Create a new factory instance for the model.
67
     */
68
    protected static function newFactory(): CouponFactory
69
    {
70
        return CouponFactory::new();
41✔
71
    }
72

73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function getMorphClass(): string
77
    {
NEW
78
        return static::getProxiedClass();
×
79
    }
80

81
    /**
82
     * {@inheritdoc}
83
     */
84
    public function casts(): array
85
    {
86
        return [
41✔
87
            'active' => 'boolean',
41✔
88
            'expires_at' => 'datetime',
41✔
89
            'rules' => 'json',
41✔
90
            'stackable' => 'boolean',
41✔
91
            'type' => CouponType::class,
41✔
92
            'value' => 'float',
41✔
93
        ];
41✔
94
    }
95

96
    /**
97
     * Get the applications of the coupon.
98
     */
99
    public function applications(): HasMany
100
    {
NEW
101
        return $this->hasMany(AppliedCoupon::getProxiedClass());
×
102
    }
103

104
    /**
105
     * Get the limit of the coupon.
106
     */
107
    public function limit(): int
108
    {
109
        return (int) ($this->rules['limit'] ?? 0);
39✔
110
    }
111

112
    /**
113
     * Validate the coupon for the checkoutable model.
114
     */
115
    public function validate(Checkoutable $model): bool
116
    {
117
        return match (true) {
118
            ! $this->active => false,
38✔
119
            ! is_null($this->expires_at) && $this->expires_at->isPast() => false,
38✔
120
            $model->coupons->where('stackable', false)->isNotEmpty() => false,
38✔
121
            ! $this->stackable && $model->coupons->isNotEmpty() => false,
38✔
122
            $this->limit() > 0 && $this->applications()->count() >= $this->limit() => false,
38✔
123
            default => true,
38✔
124
        };
125
    }
126

127
    /**
128
     * Calculate the discount amount for the checkoutable model.
129
     */
130
    public function calculate(Checkoutable $model): float
131
    {
132
        return match ($this->type) {
38✔
133
            CouponType::PERCENT => round($model->getSubtotal() * ($this->value / 100), 2),
38✔
134
            CouponType::FIX => min($this->value, $model->getSubtotal()),
38✔
135
        };
38✔
136
    }
137

138
    /**
139
     * Apply the coupon to the checkoutable model.
140
     */
141
    public function apply(Checkoutable $model): void
142
    {
143
        if (! $this->validate($model)) {
38✔
144
            throw new Exception('The coupon is not valid for this checkoutable model.');
6✔
145
        }
146

147
        $value = $this->calculate($model);
38✔
148

149
        $model->coupons()->syncWithoutDetaching([
38✔
150
            $this->getKey() => ['value' => $value],
38✔
151
        ]);
38✔
152
    }
153

154
    /**
155
     * Scope a query to only include active coupons.
156
     */
157
    #[Scope]
158
    protected function active(Builder $query, bool $active = true): Builder
159
    {
160
        return $query->where($query->qualifyColumn('active'), $active);
3✔
161
    }
162

163
    /**
164
     * Scope a query to only include available coupons.
165
     */
166
    #[Scope]
167
    protected function available(Builder $query, ?DateTimeInterface $date = null): Builder
168
    {
169
        $date ??= Date::now();
3✔
170

171
        return $query->active()
3✔
172
            ->whereNull($query->qualifyColumn('expires_at'))
3✔
173
            ->orWhere($query->qualifyColumn('expires_at'), '>', $date);
3✔
174
    }
175

176
    /**
177
     * Scope the query to only include the coupons with the given code.
178
     */
179
    #[Scope]
180
    protected function code(Builder $query, string $code): Builder
181
    {
182
        return $query->whereRaw('lower(`bazar_coupons`.`code`) like ?', [strtolower($code)]);
5✔
183
    }
184
}
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