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

conedevelopment / bazar / 20270383925

16 Dec 2025 01:55PM UTC coverage: 62.787% (-0.7%) from 63.48%
20270383925

Pull #235

github

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

157 of 248 new or added lines in 23 files covered. (63.31%)

3 existing lines in 2 files now uncovered.

1068 of 1701 relevant lines covered (62.79%)

16.76 hits per line

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

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

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

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

NEW
125
        if ($this->limit() > 0 && $this->applications()->count() >= $this->limit()) {
×
NEW
126
            return false;
×
127
        }
128

NEW
129
        return true;
×
130
    }
131

132
    /**
133
     * Calculate the discount amount for the checkoutable model.
134
     */
135
    public function calculate(Checkoutable $model): float
136
    {
NEW
137
        return match ($this->type) {
×
NEW
138
            CouponType::PERCENT => round($model->getSubtotal() * ($this->value / 100), 2),
×
NEW
139
            CouponType::FIX => min($this->value, $model->getSubtotal()),
×
NEW
140
        };
×
141
    }
142

143
    /**
144
     * Apply the coupon to the checkoutable model.
145
     */
146
    public function apply(Checkoutable $model): void
147
    {
NEW
148
        if (! $this->validate($model)) {
×
NEW
149
            throw new Exception('The coupon is not valid for this checkoutable model.');
×
150
        }
151

NEW
152
        $value = $this->calculate($model);
×
153

NEW
154
        $model->coupons()->syncWithoutDetaching([
×
NEW
155
            $this->getKey() => ['value' => $value],
×
NEW
156
        ]);
×
157
    }
158

159
    /**
160
     * Scope a query to only include active coupons.
161
     */
162
    #[Scope]
163
    protected function active(Builder $query, bool $active = true): Builder
164
    {
NEW
165
        return $query->where($query->qualifyColumn('active'), $active);
×
166
    }
167

168
    /**
169
     * Scope a query to only include available coupons.
170
     */
171
    #[Scope]
172
    protected function available(Builder $query, ?DateTimeInterface $date = null): Builder
173
    {
NEW
174
        $date ??= Date::now();
×
175

NEW
176
        return $query->active()
×
NEW
177
            ->whereNull($query->qualifyColumn('expires_at'))
×
NEW
178
            ->orWhere($query->qualifyColumn('expires_at'), '>', $date);
×
179
    }
180
}
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