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

conedevelopment / bazar / 20693753035

04 Jan 2026 01:38PM UTC coverage: 68.632% (+4.5%) from 64.117%
20693753035

Pull #236

github

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

257 of 314 new or added lines in 18 files covered. (81.85%)

105 existing lines in 40 files now uncovered.

1676 of 2442 relevant lines covered (68.63%)

25.11 hits per line

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

78.21
/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
    }
160

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

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

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

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

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

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

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

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

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

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