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

conedevelopment / bazar / 20311982352

17 Dec 2025 05:42PM UTC coverage: 64.265% (+0.8%) from 63.48%
20311982352

Pull #235

github

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

205 of 265 new or added lines in 23 files covered. (77.36%)

8 existing lines in 1 file now uncovered.

1097 of 1707 relevant lines covered (64.26%)

17.14 hits per line

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

85.0
/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();
23✔
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 [
25✔
87
            'active' => 'boolean',
25✔
88
            'expires_at' => 'datetime',
25✔
89
            'rules' => 'json',
25✔
90
            'stackable' => 'boolean',
25✔
91
            'type' => CouponType::class,
25✔
92
            'value' => 'float',
25✔
93
        ];
25✔
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);
22✔
110
    }
111

112
    /**
113
     * Validate the coupon for the checkoutable model.
114
     */
115
    public function validate(Checkoutable $model): bool
116
    {
117
        if (! $this->active) {
21✔
NEW
118
            return false;
×
119
        }
120

121
        if (! is_null($this->expires_at) && $this->expires_at->isPast()) {
21✔
NEW
122
            return false;
×
123
        }
124

125
        if ($model->coupons->where('stackable', false)->isNotEmpty()) {
21✔
126
            return false;
6✔
127
        }
128

129
        if (! $this->stackable && $model->coupons->isNotEmpty()) {
21✔
NEW
130
            return false;
×
131
        }
132

133
        if ($this->limit() > 0 && $this->applications()->count() >= $this->limit()) {
21✔
NEW
134
            return false;
×
135
        }
136

137
        return true;
21✔
138
    }
139

140
    /**
141
     * Calculate the discount amount for the checkoutable model.
142
     */
143
    public function calculate(Checkoutable $model): float
144
    {
145
        return match ($this->type) {
21✔
146
            CouponType::PERCENT => round($model->getSubtotal() * ($this->value / 100), 2),
21✔
147
            CouponType::FIX => min($this->value, $model->getSubtotal()),
21✔
148
        };
21✔
149
    }
150

151
    /**
152
     * Apply the coupon to the checkoutable model.
153
     */
154
    public function apply(Checkoutable $model): void
155
    {
156
        if (! $this->validate($model)) {
21✔
157
            throw new Exception('The coupon is not valid for this checkoutable model.');
6✔
158
        }
159

160
        $value = $this->calculate($model);
21✔
161

162
        $model->coupons()->syncWithoutDetaching([
21✔
163
            $this->getKey() => ['value' => $value],
21✔
164
        ]);
21✔
165
    }
166

167
    /**
168
     * Scope a query to only include active coupons.
169
     */
170
    #[Scope]
171
    protected function active(Builder $query, bool $active = true): Builder
172
    {
173
        return $query->where($query->qualifyColumn('active'), $active);
2✔
174
    }
175

176
    /**
177
     * Scope a query to only include available coupons.
178
     */
179
    #[Scope]
180
    protected function available(Builder $query, ?DateTimeInterface $date = null): Builder
181
    {
182
        $date ??= Date::now();
2✔
183

184
        return $query->active()
2✔
185
            ->whereNull($query->qualifyColumn('expires_at'))
2✔
186
            ->orWhere($query->qualifyColumn('expires_at'), '>', $date);
2✔
187
    }
188

189
    /**
190
     * Scope the query to only include the coupons with the given code.
191
     */
192
    #[Scope]
193
    protected function code(Builder $query, string $code): Builder
194
    {
195
        return $query->whereRaw('lower(`bazar_coupons`.`code`) like ?', [strtolower($code)]);
2✔
196
    }
197
}
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