Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.85% covered (success)
98.85%
86 / 87
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommissionCalculatorService
98.85% covered (success)
98.85%
86 / 87
83.33% covered (warning)
83.33%
5 / 6
14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 syncForOrder
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
6
 approveEligibleForecasted
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 calculate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cancelOrReverse
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
3
 basePayload
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Services;
4
5use App\Enums\CommissionStatus;
6use App\Enums\OrderStatus;
7use App\Models\Commission;
8use App\Models\CommissionAdjustment;
9use App\Models\Order;
10use Illuminate\Support\Facades\DB;
11
12class CommissionCalculatorService
13{
14    public function __construct(private readonly GamificationService $gamificationService)
15    {
16    }
17
18    public function syncForOrder(Order $order): ?Commission
19    {
20        if (! $order->influencer_id || ! $order->coupon_id) {
21            return null;
22        }
23
24        return DB::transaction(function () use ($order) {
25            $order->loadMissing('influencer', 'commission');
26            $commission = $order->commission;
27
28            if ($order->status === OrderStatus::Pending) {
29                return Commission::updateOrCreate(
30                    ['order_id' => $order->id],
31                    $this->basePayload($order) + [
32                        'commission_amount' => 0,
33                        'status' => CommissionStatus::Pending->value,
34                    ]
35                );
36            }
37
38            if (in_array($order->status, [OrderStatus::Cancelled, OrderStatus::Refunded], true)) {
39                return $this->cancelOrReverse($order, $commission);
40            }
41
42            if ($order->status !== OrderStatus::Paid) {
43                return $commission;
44            }
45
46            $basePercentage = (float) $order->influencer->base_commission_percentage;
47            $bonusPercentage = $this->gamificationService->bonusForOrderPeriod($order);
48            $finalPercentage = $basePercentage + $bonusPercentage;
49            $baseAmount = (float) $order->commission_base_amount;
50            $amount = $this->calculate($baseAmount, $finalPercentage);
51
52            return Commission::updateOrCreate(
53                ['order_id' => $order->id],
54                $this->basePayload($order) + [
55                    'commission_base_amount' => $baseAmount,
56                    'commission_percentage' => $finalPercentage,
57                    'gamification_bonus_percentage' => $bonusPercentage,
58                    'commission_amount' => $amount,
59                    'status' => CommissionStatus::Forecasted->value,
60                    'forecasted_at' => $commission?->forecasted_at ?? now(),
61                    'metadata' => [
62                        'base_percentage' => $basePercentage,
63                        'bonus_percentage' => $bonusPercentage,
64                        'calculated_from_order_status' => $order->status->value,
65                    ],
66                ]
67            );
68        });
69    }
70
71    public function approveEligibleForecasted(): int
72    {
73        $validationDays = (int) config('services.commission.validation_days', 7);
74
75        return Commission::query()
76            ->where('status', CommissionStatus::Forecasted->value)
77            ->where('forecasted_at', '<=', now()->subDays($validationDays))
78            ->update([
79                'status' => CommissionStatus::Approved->value,
80                'approved_at' => now(),
81                'updated_at' => now(),
82            ]);
83    }
84
85    public function calculate(float $baseAmount, float $percentage): float
86    {
87        return round($baseAmount * ($percentage / 100), 2);
88    }
89
90    private function cancelOrReverse(Order $order, ?Commission $commission): ?Commission
91    {
92        if (! $commission) {
93            return null;
94        }
95
96        if (in_array($commission->status, [CommissionStatus::Paid, CommissionStatus::Released, CommissionStatus::Approved], true)) {
97            $commission->update([
98                'status' => CommissionStatus::Reversed->value,
99                'reversed_at' => now(),
100            ]);
101
102            CommissionAdjustment::firstOrCreate(
103                [
104                    'commission_id' => $commission->id,
105                    'order_id' => $order->id,
106                    'type' => 'reversal',
107                ],
108                [
109                    'tenant_id' => $order->tenant_id,
110                    'store_id' => $order->store_id,
111                    'influencer_id' => $order->influencer_id,
112                    'amount' => -abs((float) $commission->commission_amount),
113                    'reason' => 'Estorno automático por cancelamento/reembolso após aprovação ou pagamento.',
114                    'approved_at' => now(),
115                    'metadata' => ['order_status' => $order->status->value],
116                ]
117            );
118
119            return $commission;
120        }
121
122        $commission->update([
123            'status' => CommissionStatus::Cancelled->value,
124            'cancelled_at' => now(),
125        ]);
126
127        return $commission;
128    }
129
130    private function basePayload(Order $order): array
131    {
132        return [
133            'tenant_id' => $order->tenant_id,
134            'store_id' => $order->store_id,
135            'influencer_id' => $order->influencer_id,
136            'coupon_id' => $order->coupon_id,
137            'commission_base_amount' => $order->commission_base_amount,
138            'commission_percentage' => $order->influencer ? (float) $order->influencer->base_commission_percentage : 0,
139        ];
140    }
141}