['label'=>'Mensal','days'=>30,'price'=>20.00], 'quarterly' => ['label'=>'Trimestral','days'=>90,'price'=>50.00], 'annual' => ['label'=>'Anual','days'=>365,'price'=>100.00], ]; public function accessToken(): string { return trim((string) env('MERCADOPAGO_ACCESS_TOKEN','')); } public function webhookSecret(): string { return trim((string) env('MERCADOPAGO_WEBHOOK_SECRET','')); } public function createCheckout(int $userId, string $plan): array { if (!isset(self::PLANS[$plan])) throw new \InvalidArgumentException('Plano Premium inválido.'); $token=$this->accessToken(); if ($token==='') throw new \RuntimeException('MERCADOPAGO_ACCESS_TOKEN não configurado no .env.'); $user=DB::table('users')->where('id',$userId)->first(); if(!$user) throw new \RuntimeException('Usuário não encontrado.'); $cfg=self::PLANS[$plan]; $localRef='GXPREM-'.$userId.'-'.strtoupper($plan).'-'.Str::upper(Str::random(12)); $idem=(string) Str::uuid(); $base=rtrim((string) config('app.url'),'/'); $payload=[ 'type'=>'online', 'processing_mode'=>'manual', 'capture_mode'=>'automatic_async', 'total_amount'=>number_format($cfg['price'],2,'.',''), 'external_reference'=>$localRef, 'description'=>'GIGAX Arena Premium '.$cfg['label'], 'payer'=>['email'=>(string)($user->email ?? '')], 'items'=>[[ 'title'=>'GIGAX Arena Premium '.$cfg['label'], 'unit_price'=>number_format($cfg['price'],2,'.',''), 'quantity'=>1, ]], 'config'=>['online'=>[ 'success_url'=>$base.'/premium/retorno?result=success', 'failure_url'=>$base.'/premium/retorno?result=failure', 'pending_url'=>$base.'/premium/retorno?result=pending', 'auto_return'=>'all', ]], ]; $res=Http::timeout(20)->withToken($token) ->withHeaders(['X-Idempotency-Key'=>$idem,'Accept'=>'application/json']) ->post('https://api.mercadopago.com/v1/orders',$payload); if(!$res->successful()){ throw new \RuntimeException('Mercado Pago recusou a criação do checkout. HTTP '.$res->status().' - '.substr($res->body(),0,500)); } $data=$res->json(); if(empty($data['id']) || empty($data['checkout_url'])) throw new \RuntimeException('Resposta do Mercado Pago sem order/checkout_url.'); DB::table('gigax_premium_orders')->insert([ 'user_id'=>$userId,'plan_code'=>$plan,'days'=>$cfg['days'],'amount'=>$cfg['price'], 'external_reference'=>$localRef,'mp_order_id'=>(string)$data['id'],'status'=>(string)($data['status'] ?? 'created'), 'idempotency_key'=>$idem,'created_at'=>now(),'updated_at'=>now(), ]); return $data; } public function fetchOrder(string $orderId): array { $token=$this->accessToken(); if($token==='') throw new \RuntimeException('MERCADOPAGO_ACCESS_TOKEN não configurado.'); $res=Http::timeout(20)->withToken($token)->acceptJson()->get('https://api.mercadopago.com/v1/orders/'.rawurlencode($orderId)); if(!$res->successful()) throw new \RuntimeException('Falha ao consultar order Mercado Pago. HTTP '.$res->status()); return (array)$res->json(); } public function syncOrder(string $orderId): array { $remote=$this->fetchOrder($orderId); $local=DB::table('gigax_premium_orders')->where('mp_order_id',$orderId)->first(); if(!$local) return ['activated'=>false,'reason'=>'order_local_not_found','remote'=>$remote]; $remoteRef=(string)($remote['external_reference'] ?? ''); $remoteAmount=(float)($remote['total_amount'] ?? 0); if(!hash_equals((string)$local->external_reference,$remoteRef) || abs($remoteAmount-(float)$local->amount)>0.001){ throw new \RuntimeException('Order Mercado Pago não corresponde ao pedido local.'); } $status=strtolower((string)($remote['status'] ?? 'unknown')); DB::table('gigax_premium_orders')->where('id',$local->id)->update(['status'=>$status,'updated_at'=>now()]); if($status!=='processed') return ['activated'=>false,'status'=>$status,'remote'=>$remote]; if(!empty($local->activated_at)) return ['activated'=>true,'already'=>true,'remote'=>$remote]; DB::transaction(function() use($local){ $fresh=DB::table('gigax_premium_orders')->where('id',$local->id)->lockForUpdate()->first(); if(!$fresh || !empty($fresh->activated_at)) return; $u=DB::table('users')->where('id',$fresh->user_id)->lockForUpdate()->first(); if(!$u) throw new \RuntimeException('Usuário do pedido não encontrado.'); $now=now(); $current=!empty($u->premium_until) ? Carbon::parse($u->premium_until) : null; $start=($current && $current->greaterThan($now)) ? $current : $now->copy(); $until=$start->copy()->addDays((int)$fresh->days); $upd=['is_premium'=>1,'premium_until'=>$until]; if(Schema::hasColumn('users','updated_at')) $upd['updated_at']=$now; DB::table('users')->where('id',$fresh->user_id)->update($upd); DB::table('gigax_premium_orders')->where('id',$fresh->id)->update(['activated_at'=>$now,'updated_at'=>$now]); }); return ['activated'=>true,'remote'=>$remote]; } public function validPremium($user): bool { if(!$user || empty($user->is_premium)) return false; if(!Schema::hasColumn('users','premium_until')) return true; if(empty($user->premium_until)) return true; // concessão administrativa sem prazo try { return Carbon::parse($user->premium_until)->isFuture(); } catch(\Throwable $e){ return false; } } public function verifyWebhookSignature(string $signature, string $requestId, string $dataId): bool { $secret=$this->webhookSecret(); if($secret==='') return false; $parts=[]; foreach(explode(',',$signature) as $part){ $x=explode('=',trim($part),2); if(count($x)===2)$parts[$x[0]]=$x[1]; } if(empty($parts['ts']) || empty($parts['v1'])) return false; $manifest='id:'.strtolower($dataId).';request-id:'.$requestId.';ts:'.$parts['ts'].';'; return hash_equals($parts['v1'],hash_hmac('sha256',$manifest,$secret)); } }