['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 createPix(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(); $amount=number_format((float)$cfg['price'],2,'.',''); $payload=[ 'type'=>'online', 'processing_mode'=>'automatic', 'total_amount'=>$amount, 'external_reference'=>$localRef, 'description'=>'GIGAX Arena Premium '.$cfg['label'], 'payer'=>[ 'email'=>(string)($user->email ?? ''), ], 'transactions'=>[ 'payments'=>[[ 'amount'=>$amount, 'payment_method'=>[ 'id'=>'pix', 'type'=>'bank_transfer', ], ]], ], ]; $res=Http::timeout(20)->withToken($token) ->withHeaders([ 'X-Idempotency-Key'=>$idem, 'Accept'=>'application/json', 'Content-Type'=>'application/json', ]) ->post('https://api.mercadopago.com/v1/orders',$payload); if(!$res->successful()){ throw new \RuntimeException('Mercado Pago recusou o Pix. HTTP '.$res->status().' - '.substr($res->body(),0,700)); } $data=(array)$res->json(); $payment=(array)data_get($data,'transactions.payments.0',[]); $pm=(array)data_get($payment,'payment_method',[]); $qr=(string)($pm['qr_code'] ?? data_get($payment,'point_of_interaction.transaction_data.qr_code','')); $qr64=(string)($pm['qr_code_base64'] ?? data_get($payment,'point_of_interaction.transaction_data.qr_code_base64','')); $ticket=(string)($pm['ticket_url'] ?? data_get($payment,'point_of_interaction.transaction_data.ticket_url','')); if(empty($data['id']) || $qr===''){ throw new \RuntimeException('Mercado Pago criou a cobrança, mas não retornou o QR Code Pix.'); } 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)($payment['status'] ?? $data['status'] ?? 'action_required'), 'idempotency_key'=>$idem, 'created_at'=>now(), 'updated_at'=>now(), ]); return [ 'order_id'=>(string)$data['id'], 'qr_code'=>$qr, 'qr_code_base64'=>$qr64, 'ticket_url'=>$ticket, 'status'=>(string)($payment['status'] ?? $data['status'] ?? 'action_required'), 'amount'=>(float)$cfg['price'], 'label'=>$cfg['label'], ]; } 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')); $paymentStatus=strtolower((string)data_get($remote,'transactions.payments.0.status','')); DB::table('gigax_premium_orders')->where('id',$local->id)->update([ 'status'=>$paymentStatus ?: $status, 'updated_at'=>now() ]); $paid=in_array($status,['processed','approved'],true) || in_array($paymentStatus,['processed','approved'],true); if(!$paid) return ['activated'=>false,'status'=>$paymentStatus ?: $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)); } }