Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
86.60% |
84 / 97 |
|
88.89% |
8 / 9 |
CRAP | |
0.00% |
0 / 1 |
| NuvemshopOrderService | |
86.60% |
84 / 97 |
|
88.89% |
8 / 9 |
25.39 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| syncOrders | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| upsertOrderFromPayload | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
6 | |||
| extractCouponCode | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| mapStatus | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
| syncItems | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
| money | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| date | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| safeMetadata | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services\Nuvemshop; |
| 4 | |
| 5 | use App\Enums\OrderStatus; |
| 6 | use App\Models\InfluencerCoupon; |
| 7 | use App\Models\Order; |
| 8 | use App\Models\StoreIntegration; |
| 9 | use App\Services\CommissionCalculatorService; |
| 10 | use Carbon\Carbon; |
| 11 | use Illuminate\Support\Arr; |
| 12 | use Illuminate\Support\Facades\DB; |
| 13 | |
| 14 | class 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 | } |