Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.05% covered (warning)
77.05%
47 / 61
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
GamificationService
77.05% covered (warning)
77.05%
47 / 61
66.67% covered (warning)
66.67%
4 / 6
13.74
0.00% covered (danger)
0.00%
0 / 1
 bonusForOrderPeriod
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 bestRuleForInfluencer
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 progress
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 evaluatePeriod
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 salesAmount
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 activeRules
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services;
4
5use App\Enums\GamificationMetric;
6use App\Enums\OrderStatus;
7use App\Enums\Status;
8use App\Models\GamificationAchievement;
9use App\Models\GamificationRule;
10use App\Models\Influencer;
11use App\Models\Order;
12use Carbon\CarbonImmutable;
13use Illuminate\Database\Eloquent\Collection;
14
15class GamificationService
16{
17    public function bonusForOrderPeriod(Order $order): float
18    {
19        if (! $order->paid_at || ! $order->influencer_id) {
20            return 0.0;
21        }
22
23        $date = CarbonImmutable::parse($order->paid_at);
24        $rule = $this->bestRuleForInfluencer($order->influencer, $date->year, $date->month);
25
26        return $rule ? (float) $rule->bonus_percentage : 0.0;
27    }
28
29    public function bestRuleForInfluencer(Influencer $influencer, int $year, int $month): ?GamificationRule
30    {
31        $salesAmount = $this->salesAmount($influencer, $year, $month);
32
33        return $this->activeRules($influencer)
34            ->where('metric', GamificationMetric::SalesAmount)
35            ->filter(fn (GamificationRule $rule) => $salesAmount >= (float) $rule->threshold_value)
36            ->sortByDesc('bonus_percentage')
37            ->first();
38    }
39
40    public function progress(Influencer $influencer, ?int $year = null, ?int $month = null): array
41    {
42        $now = now();
43        $year ??= (int) $now->year;
44        $month ??= (int) $now->month;
45
46        $salesAmount = $this->salesAmount($influencer, $year, $month);
47        $rules = $this->activeRules($influencer)->where('metric', GamificationMetric::SalesAmount)->sortBy('threshold_value')->values();
48        $current = $rules->filter(fn ($rule) => $salesAmount >= (float) $rule->threshold_value)->sortByDesc('threshold_value')->first();
49        $next = $rules->first(fn ($rule) => $salesAmount < (float) $rule->threshold_value);
50
51        return [
52            'sales_amount' => $salesAmount,
53            'current_rule' => $current,
54            'next_rule' => $next,
55            'percent_to_next' => $next ? min(100, round(($salesAmount / max(1, (float) $next->threshold_value)) * 100, 1)) : 100,
56        ];
57    }
58
59    public function evaluatePeriod(Influencer $influencer, int $year, int $month): ?GamificationAchievement
60    {
61        $rule = $this->bestRuleForInfluencer($influencer, $year, $month);
62
63        if (! $rule || ! $rule->badge_id) {
64            return null;
65        }
66
67        return GamificationAchievement::firstOrCreate(
68            [
69                'influencer_id' => $influencer->id,
70                'badge_id' => $rule->badge_id,
71            ],
72            [
73                'tenant_id' => $influencer->tenant_id,
74                'store_id' => $influencer->store_id,
75                'gamification_rule_id' => $rule->id,
76                'period_year' => $year,
77                'period_month' => $month,
78                'achieved_value' => $this->salesAmount($influencer, $year, $month),
79                'bonus_percentage' => $rule->bonus_percentage,
80                'achieved_at' => now(),
81            ]
82        );
83    }
84
85    public function salesAmount(Influencer $influencer, int $year, int $month): float
86    {
87        return (float) Order::query()
88            ->where('influencer_id', $influencer->id)
89            ->where('status', OrderStatus::Paid->value)
90            ->whereYear('paid_at', $year)
91            ->whereMonth('paid_at', $month)
92            ->sum('commission_base_amount');
93    }
94
95    public function activeRules(Influencer $influencer): Collection
96    {
97        return GamificationRule::query()
98            ->with('badge')
99            ->where('tenant_id', $influencer->tenant_id)
100            ->where('store_id', $influencer->store_id)
101            ->where('status', Status::Active->value)
102            ->where(function ($query) {
103                $query->whereNull('starts_at')->orWhere('starts_at', '<=', now()->toDateString());
104            })
105            ->where(function ($query) {
106                $query->whereNull('ends_at')->orWhere('ends_at', '>=', now()->toDateString());
107            })
108            ->get();
109    }
110}