Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.85% |
86 / 87 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
| CommissionCalculatorService | |
98.85% |
86 / 87 |
|
83.33% |
5 / 6 |
14 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| syncForOrder | |
97.44% |
38 / 39 |
|
0.00% |
0 / 1 |
6 | |||
| approveEligibleForecasted | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| calculate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| cancelOrReverse | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
3 | |||
| basePayload | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services; |
| 4 | |
| 5 | use App\Enums\CommissionStatus; |
| 6 | use App\Enums\OrderStatus; |
| 7 | use App\Models\Commission; |
| 8 | use App\Models\CommissionAdjustment; |
| 9 | use App\Models\Order; |
| 10 | use Illuminate\Support\Facades\DB; |
| 11 | |
| 12 | class 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 | } |