Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.60% covered (warning)
86.60%
84 / 97
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
NuvemshopOrderService
86.60% covered (warning)
86.60%
84 / 97
88.89% covered (warning)
88.89%
8 / 9
25.39
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
 syncOrders
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 upsertOrderFromPayload
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
6
 extractCouponCode
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 mapStatus
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 syncItems
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 money
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 date
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 safeMetadata
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services\Nuvemshop;
4
5use App\Enums\OrderStatus;
6use App\Models\InfluencerCoupon;
7use App\Models\Order;
8use App\Models\StoreIntegration;
9use App\Services\CommissionCalculatorService;
10use Carbon\Carbon;
11use Illuminate\Support\Arr;
12use Illuminate\Support\Facades\DB;
13
14class NuvemshopOrderService
15{
16    public function __construct(private readonly CommissionCalculatorService $commissionCalculator)
17    {
18    }
19
20    public function syncOrders(StoreIntegration $integration, ?Carbon $from = null, ?Carbon $to = null): array
21    {
22        $client = new NuvemshopClient($integration);
23        $query = array_filter([
24            'created_at_min' => $from?->toIso8601String(),
25            'created_at_max' => $to?->toIso8601String(),
26            'per_page' => 200,
27        ]);
28
29        $orders = $client->get('/'.$integration->external_store_id.'/orders', $query);
30
31        $created = 0;
32        $updated = 0;
33
34        foreach ($orders as $payload) {
35            $result = $this->upsertOrderFromPayload($integration, $payload);
36            $result['created'] ? $created++ : $updated++;
37        }
38
39        return ['processed' => count($orders), 'created' => $created, 'updated' => $updated];
40    }
41
42    public function upsertOrderFromPayload(StoreIntegration $integration, array $payload): array
43    {
44        return DB::transaction(function () use ($integration, $payload) {
45            $couponCode = $this->extractCouponCode($payload);
46            $normalized = $couponCode ? InfluencerCoupon::normalize($couponCode) : null;
47            $coupon = $normalized
48                ? InfluencerCoupon::query()->where('store_id', $integration->store_id)->where('coupon_code_normalized', $normalized)->first()
49                : null;
50
51            $productsAmount = $this->money(data_get($payload, 'subtotal', data_get($payload, 'products_amount', 0)));
52            $discountAmount = $this->money(data_get($payload, 'discount', data_get($payload, 'discount_amount', 0)));
53            $shippingAmount = $this->money(data_get($payload, 'shipping_cost_owner', data_get($payload, 'shipping_amount', 0)));
54            $paidProductsAmount = max(0, $productsAmount - $discountAmount);
55            $status = $this->mapStatus($payload);
56
57            $order = Order::updateOrCreate(
58                [
59                    'store_id' => $integration->store_id,
60                    'external_id' => (string) data_get($payload, 'id'),
61                ],
62                [
63                    'tenant_id' => $integration->tenant_id,
64                    'store_integration_id' => $integration->id,
65                    'influencer_id' => $coupon?->influencer_id,
66                    'coupon_id' => $coupon?->id,
67                    'order_number' => (string) data_get($payload, 'number', data_get($payload, 'order_number')),
68                    'status' => $status,
69                    'coupon_code_original' => $couponCode,
70                    'coupon_code_normalized' => $normalized,
71                    'products_amount' => $productsAmount,
72                    'discount_amount' => $discountAmount,
73                    'shipping_amount' => $shippingAmount,
74                    'paid_products_amount' => $paidProductsAmount,
75                    'total_amount' => $this->money(data_get($payload, 'total', $paidProductsAmount + $shippingAmount)),
76                    'commission_base_amount' => $paidProductsAmount,
77                    'placed_at' => $this->date(data_get($payload, 'created_at')),
78                    'paid_at' => $status === OrderStatus::Paid ? $this->date(data_get($payload, 'paid_at', data_get($payload, 'created_at'))) : null,
79                    'cancelled_at' => $status === OrderStatus::Cancelled ? now() : null,
80                    'refunded_at' => $status === OrderStatus::Refunded ? now() : null,
81                    'metadata' => $this->safeMetadata($payload),
82                ]
83            );
84
85            $wasRecentlyCreated = $order->wasRecentlyCreated;
86            $this->syncItems($order, Arr::wrap(data_get($payload, 'products', data_get($payload, 'items', []))));
87            $this->commissionCalculator->syncForOrder($order->fresh(['influencer', 'coupon', 'commission']));
88
89            return ['order' => $order, 'created' => $wasRecentlyCreated];
90        });
91    }
92
93    public function extractCouponCode(array $payload): ?string
94    {
95        $coupon = data_get($payload, 'coupon.0.code')
96            ?? data_get($payload, 'coupon.code')
97            ?? data_get($payload, 'discount_coupon')
98            ?? data_get($payload, 'coupon_code');
99
100        return filled($coupon) ? (string) $coupon : null;
101    }
102
103    private function mapStatus(array $payload): OrderStatus
104    {
105        $paymentStatus = strtolower((string) data_get($payload, 'payment_status', ''));
106        $status = strtolower((string) data_get($payload, 'status', ''));
107
108        if (str_contains($status, 'cancel')) {
109            return OrderStatus::Cancelled;
110        }
111
112        if (str_contains($status, 'refund') || str_contains($paymentStatus, 'refund')) {
113            return OrderStatus::Refunded;
114        }
115
116        if (in_array($paymentStatus, ['paid', 'authorized', 'approved'], true) || data_get($payload, 'paid_at')) {
117            return OrderStatus::Paid;
118        }
119
120        return OrderStatus::Pending;
121    }
122
123    private function syncItems(Order $order, array $items): void
124    {
125        $order->items()->delete();
126
127        foreach ($items as $item) {
128            $quantity = (int) data_get($item, 'quantity', 1);
129            $unitPrice = $this->money(data_get($item, 'price', data_get($item, 'unit_price', 0)));
130
131            $order->items()->create([
132                'external_product_id' => (string) data_get($item, 'product_id', data_get($item, 'id')),
133                'external_variant_id' => (string) data_get($item, 'variant_id', ''),
134                'name' => (string) data_get($item, 'name', 'Produto sem nome'),
135                'sku' => data_get($item, 'sku'),
136                'quantity' => max(1, $quantity),
137                'unit_price' => $unitPrice,
138                'total_price' => round($unitPrice * max(1, $quantity), 2),
139                'category_name' => data_get($item, 'category_name'),
140                'metadata' => [
141                    'product_id' => data_get($item, 'product_id', data_get($item, 'id')),
142                    'variant_id' => data_get($item, 'variant_id'),
143                ],
144            ]);
145        }
146    }
147
148    private function money(mixed $value): float
149    {
150        return round((float) str_replace(',', '.', (string) $value), 2);
151    }
152
153    private function date(mixed $value): ?Carbon
154    {
155        return filled($value) ? Carbon::parse($value) : null;
156    }
157
158    private function safeMetadata(array $payload): array
159    {
160        return [
161            'source' => 'nuvemshop',
162            'raw_status' => data_get($payload, 'status'),
163            'raw_payment_status' => data_get($payload, 'payment_status'),
164            'coupon' => data_get($payload, 'coupon'),
165        ];
166    }
167}