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

conedevelopment / bazar / 20693413327

04 Jan 2026 01:11PM UTC coverage: 66.68% (+2.6%) from 64.117%
20693413327

Pull #236

github

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

213 of 312 new or added lines in 18 files covered. (68.27%)

105 existing lines in 40 files now uncovered.

1627 of 2440 relevant lines covered (66.68%)

18.61 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Cone\Bazar\Models;
6

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

22
class DiscountRule extends Model implements Contract
23
{
24
    use InteractsWithProxy;
25

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

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

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

59
    /**
60
     * The table associated with the model.
61
     *
62
     * @var string
63
     */
64
    protected $table = 'bazar_discount_rules';
65

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

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

82
    /**
83
     * Get the discountable types.
84
     */
85
    public static function getDiscountableTypes(): array
70✔
86
    {
87
        return [
70✔
88
            Cart::getProxiedClass(),
70✔
89
            Shipping::getProxiedClass(),
70✔
90
            Product::getProxiedClass(),
70✔
91
            Variant::getProxiedClass(),
70✔
92
        ];
70✔
93
    }
94

95
    /**
96
     * {@inheritdoc}
97
     */
98
    protected function casts(): array
23✔
99
    {
100
        return [
23✔
101
            'active' => 'boolean',
23✔
102
            'rules' => 'json',
23✔
103
            'stackable' => 'boolean',
23✔
104
            'value_type' => DiscountRuleValueType::class,
23✔
105
        ];
23✔
106
    }
107

108
    /**
109
     * Get the users associated with the discount rule.
110
     */
111
    public function users(): BelongsToMany
14✔
112
    {
113
        return $this->belongsToMany(
14✔
114
            User::getProxiedClass(),
14✔
115
            'bazar_discount_rule_user',
14✔
116
            'discount_rule_id',
14✔
117
            'user_id'
14✔
118
        )->withTimestamps();
14✔
119
    }
120

121
    /**
122
     * Get the discountables associated with the discount rule.
123
     */
NEW
124
    public function discountables(): MorphToMany
×
125
    {
NEW
126
        return $this->morphedByMany(
×
NEW
127
            $this->discountable_type ?: static::getDiscountableTypes()[0],
×
NEW
128
            'discountable',
×
NEW
129
            'bazar_discountables',
×
NEW
130
            'discount_rule_id',
×
NEW
131
            'discountable_id'
×
NEW
132
        )->using(DiscountablePivot::class);
×
133
    }
134

135
    /**
136
     * Validate the discount rule for the given discountable.
137
     */
NEW
138
    public function validate(Discountable $model): bool
×
139
    {
NEW
140
        $type = match (true) {
×
NEW
141
            $model instanceof Item => $model->buyable_type,
×
NEW
142
            default => $model::class,
×
NEW
143
        };
×
144

NEW
145
        return $this->active
×
NEW
146
            && in_array($type, static::getDiscountableTypes())
×
NEW
147
            && $this->discountable_type === $type;
×
148
    }
149

150
    /**
151
     * Calculate the discount for the given discountable.
152
     */
NEW
153
    public function calculate(Discountable $model): float
×
154
    {
NEW
155
        $value = match ($this->value_type) {
×
NEW
156
            DiscountRuleValueType::TOTAL => $model->getDiscountBase(),
×
NEW
157
            DiscountRuleValueType::QUANTITY => $model->getDiscountableQuantity(),
×
NEW
158
            default => 0,
×
NEW
159
        };
×
160

NEW
161
        if ($value <= 0) {
×
NEW
162
            return 0.0;
×
163
        }
164

NEW
165
        $rule = Collection::make($this->rules)
×
NEW
166
            ->filter(static function (array $rule) use ($model): bool {
×
NEW
167
                return is_null($rule['currency'] ?? null)
×
NEW
168
                    || $rule['currency'] === $model->getDiscountableCurrency()->value;
×
NEW
169
            })
×
NEW
170
            ->sortByDesc('value')
×
NEW
171
            ->first(static function (array $rule) use ($value): bool {
×
NEW
172
                return $value >= ($rule['value'] ?? 0);
×
NEW
173
            });
×
174

NEW
175
        if (is_null($rule)) {
×
NEW
176
            return 0.0;
×
177
        }
178

NEW
179
        return (float) match ($rule['type'] ?? null) {
×
NEW
180
            DiscountType::FIX->value => ($rule['discount'] ?? 0),
×
NEW
181
            DiscountType::PERCENT->value => ($model->getDiscountBase() * ((float) ($rule['discount'] ?? 0) / 100)),
×
NEW
182
            default => 0.0,
×
NEW
183
        };
×
184
    }
185

186
    /**
187
     * Apply the discount rule to the given discountable.
188
     */
NEW
189
    public function apply(Discountable $model): void
×
190
    {
NEW
191
        if (! $this->validate($model)) {
×
NEW
192
            throw new DiscountException('The discount rule is not valid for this discountable model.');
×
193
        }
194

NEW
195
        $value = $this->calculate($model);
×
196

NEW
197
        if ($value <= 0) {
×
NEW
198
            throw new DiscountException('The discount rule is not valid for this discountable model.');
×
199
        }
200

NEW
201
        $model->discounts()->syncWithoutDetaching([
×
NEW
202
            $this->getKey() => ['value' => $value],
×
NEW
203
        ]);
×
204
    }
205

206
    /**
207
     * Scope a query to only include active discount rules.
208
     */
209
    #[Scope]
14✔
210
    protected function active(Builder $query, bool $value = true): Builder
211
    {
212
        return $query->where($query->qualifyColumn('active'), $value);
14✔
213
    }
214
}
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