Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
77.05% |
47 / 61 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
| GamificationService | |
77.05% |
47 / 61 |
|
66.67% |
4 / 6 |
13.74 | |
0.00% |
0 / 1 |
| bonusForOrderPeriod | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
| bestRuleForInfluencer | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| progress | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
| evaluatePeriod | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
| salesAmount | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| activeRules | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services; |
| 4 | |
| 5 | use App\Enums\GamificationMetric; |
| 6 | use App\Enums\OrderStatus; |
| 7 | use App\Enums\Status; |
| 8 | use App\Models\GamificationAchievement; |
| 9 | use App\Models\GamificationRule; |
| 10 | use App\Models\Influencer; |
| 11 | use App\Models\Order; |
| 12 | use Carbon\CarbonImmutable; |
| 13 | use Illuminate\Database\Eloquent\Collection; |
| 14 | |
| 15 | class 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 | } |