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

conedevelopment / bazar / 20694042218

04 Jan 2026 02:03PM UTC coverage: 68.615% (+4.5%) from 64.117%
20694042218

Pull #236

github

web-flow
Merge de62d475e into f6c84deae
Pull Request #236: Discount Rules

260 of 319 new or added lines in 19 files covered. (81.5%)

105 existing lines in 40 files now uncovered.

1679 of 2447 relevant lines covered (68.61%)

25.06 hits per line

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

78.48
/src/Models/DiscountRule.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Cone\Bazar\Models;
6

7
use Cone\Bazar\Database\Factories\DiscountRuleFactory;
8
use Cone\Bazar\Enums\DiscountRuleValueType;
9
use Cone\Bazar\Enums\DiscountType;
10
use Cone\Bazar\Exceptions\DiscountException;
11
use Cone\Bazar\Interfaces\Discountable;
12
use Cone\Bazar\Interfaces\Models\DiscountRule as Contract;
13
use Cone\Bazar\Models\Discountable as DiscountablePivot;
14
use Cone\Root\Models\User;
15
use Cone\Root\Traits\InteractsWithProxy;
16
use Illuminate\Database\Eloquent\Attributes\Scope;
17
use Illuminate\Database\Eloquent\Builder;
18
use Illuminate\Database\Eloquent\Factories\HasFactory;
19
use Illuminate\Database\Eloquent\Model;
20
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
21
use Illuminate\Database\Eloquent\Relations\MorphToMany;
22
use Illuminate\Support\Collection;
23

24
class DiscountRule extends Model implements Contract
25
{
26
    use HasFactory;
27
    use InteractsWithProxy;
28

29
    /**
30
     * The accessors to append to the model's array form.
31
     *
32
     * @var list<string>
33
     */
34
    protected $appends = [];
35

36
    /**
37
     * The attributes that should have default values.
38
     *
39
     * @var array<string, mixed>
40
     */
41
    protected $attributes = [
42
        'active' => true,
43
        'discountable_type' => null,
44
        'rules' => '[]',
45
        'stackable' => false,
46
        'value_type' => DiscountRuleValueType::TOTAL,
47
    ];
48

49
    /**
50
     * The attributes that are mass assignable.
51
     *
52
     * @var list<string>
53
     */
54
    protected $fillable = [
55
        'active',
56
        'discountable_type',
57
        'name',
58
        'rules',
59
        'stackable',
60
    ];
61

62
    /**
63
     * The table associated with the model.
64
     *
65
     * @var string
66
     */
67
    protected $table = 'bazar_discount_rules';
68

69
    /**
70
     * Get the proxied interface.
71
     */
72
    public static function getProxiedInterface(): string
1✔
73
    {
74
        return Contract::class;
1✔
75
    }
76

77
    /**
78
     * Get the morph class for the model.
79
     */
NEW
80
    public function getMorphClass(): string
×
81
    {
NEW
82
        return static::getProxiedClass();
×
83
    }
84

85
    /**
86
     * Create a new factory instance for the model.
87
     */
88
    protected static function newFactory(): DiscountRuleFactory
31✔
89
    {
90
        return DiscountRuleFactory::new();
31✔
91
    }
92

93
    /**
94
     * Get the discountable types.
95
     */
96
    public static function getDiscountableTypes(): array
101✔
97
    {
98
        return [
101✔
99
            Cart::getProxiedClass(),
101✔
100
            Shipping::getProxiedClass(),
101✔
101
            Product::getProxiedClass(),
101✔
102
            Variant::getProxiedClass(),
101✔
103
        ];
101✔
104
    }
105

106
    /**
107
     * {@inheritdoc}
108
     */
109
    protected function casts(): array
54✔
110
    {
111
        return [
54✔
112
            'active' => 'boolean',
54✔
113
            'rules' => 'json',
54✔
114
            'stackable' => 'boolean',
54✔
115
            'value_type' => DiscountRuleValueType::class,
54✔
116
        ];
54✔
117
    }
118

119
    /**
120
     * Get the users associated with the discount rule.
121
     */
122
    public function users(): BelongsToMany
15✔
123
    {
124
        return $this->belongsToMany(
15✔
125
            User::getProxiedClass(),
15✔
126
            'bazar_discount_rule_user',
15✔
127
            'discount_rule_id',
15✔
128
            'user_id'
15✔
129
        )->withTimestamps();
15✔
130
    }
131

132
    /**
133
     * Get the discountables associated with the discount rule.
134
     */
NEW
135
    public function discountables(): MorphToMany
×
136
    {
NEW
137
        return $this->morphedByMany(
×
NEW
138
            $this->discountable_type ?: static::getDiscountableTypes()[0],
×
NEW
139
            'discountable',
×
NEW
140
            'bazar_discountables',
×
NEW
141
            'discount_rule_id',
×
NEW
142
            'discountable_id'
×
NEW
143
        )->using(DiscountablePivot::class);
×
144
    }
145

146
    /**
147
     * Validate the discount rule for the given discountable.
148
     */
149
    public function validate(Discountable $model): bool
5✔
150
    {
151
        $type = match (true) {
5✔
152
            $model instanceof Item => $model->buyable_type,
5✔
153
            default => $model::class,
4✔
154
        };
5✔
155

156
        return $this->active
5✔
157
            && in_array($type, static::getDiscountableTypes())
5✔
158
            && $this->discountable_type === $type
5✔
159
            && ($this->stackable || $model->getAppliedDiscountRules()->isEmpty());
5✔
160
    }
161

162
    /**
163
     * Calculate the discount for the given discountable.
164
     */
165
    public function calculate(Discountable $model): float
6✔
166
    {
167
        $value = match ($this->value_type) {
6✔
168
            DiscountRuleValueType::TOTAL => $model->getDiscountBase(),
6✔
NEW
169
            DiscountRuleValueType::QUANTITY => $model->getDiscountableQuantity(),
×
NEW
170
            default => 0,
×
171
        };
6✔
172

173
        if ($value <= 0) {
6✔
NEW
174
            return 0.0;
×
175
        }
176

177
        $rule = Collection::make($this->rules)
6✔
178
            ->filter(static function (array $rule) use ($model): bool {
6✔
179
                return is_null($rule['currency'] ?? null)
6✔
180
                    || $rule['currency'] === $model->getDiscountableCurrency()->value;
6✔
181
            })
6✔
182
            ->sortByDesc('value')
6✔
183
            ->first(static function (array $rule) use ($value): bool {
6✔
184
                return $value >= ($rule['value'] ?? 0);
6✔
185
            });
6✔
186

187
        if (is_null($rule)) {
6✔
NEW
188
            return 0.0;
×
189
        }
190

191
        return (float) match ($rule['type'] ?? null) {
6✔
192
            DiscountType::FIX->value => ($rule['discount'] ?? 0),
6✔
NEW
193
            DiscountType::PERCENT->value => ($model->getDiscountBase() * ((float) ($rule['discount'] ?? 0) / 100)),
×
194
            default => 0.0,
6✔
195
        };
6✔
196
    }
197

198
    /**
199
     * Apply the discount rule to the given discountable.
200
     */
201
    public function apply(Discountable $model): void
5✔
202
    {
203
        if (! $this->validate($model)) {
5✔
NEW
204
            throw new DiscountException('The discount rule is not valid for this discountable model.');
×
205
        }
206

207
        $value = $this->calculate($model);
5✔
208

209
        if ($value <= 0) {
5✔
NEW
210
            throw new DiscountException('The discount value is 0.');
×
211
        }
212

213
        $model->discounts()->syncWithoutDetaching([
5✔
214
            $this->getKey() => ['value' => $value],
5✔
215
        ]);
5✔
216
    }
217

218
    /**
219
     * Scope a query to only include active discount rules.
220
     */
221
    #[Scope]
14✔
222
    protected function active(Builder $query, bool $value = true): Builder
223
    {
224
        return $query->where($query->qualifyColumn('active'), $value);
14✔
225
    }
226
}
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