UnknownSec Bypass
403
:
/
mnt
/
lmsestudio-instance-vol002
/
lms_4cd92d08affc
/
app
/
Services
/ [
drwxr-xr-x
]
Menu
Upload
Mass depes
Mass delete
Terminal
Info server
About
name :
PagarMeService.php
<?php namespace EstudioLMS\Services; use Carbon\Carbon; use EstudioLMS\Cart\Cart; use EstudioLMS\Exceptions\Handler; use EstudioLMS\Repositories\Auth\UserRepository; use EstudioLMS\Repositories\Config\PagarmeRecipientInterface; use EstudioLMS\Repositories\Environment\HiredCourseRepository; use EstudioLMS\Repositories\Financial\HireSubscriptionInterface; use EstudioLMS\Repositories\Financial\PayableInterface; use EstudioLMS\Repositories\Financial\PostbackInterface; use EstudioLMS\Repositories\Financial\RecurringInterface; use EstudioLMS\Repositories\Subscription\PeriodicityInterface; use EstudioLMS\Services\Hires\HiringServices; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use PagarMe\Sdk\ClientException; use PagarMe\Sdk\Customer\Address as pgmAddress; use PagarMe\Sdk\Customer\Phone as pgmPhone; use PagarMe\Sdk\PagarMe; use PagarMe\Sdk\SplitRule\SplitRuleCollection; /** * Class PagarMeService * @package EstudioLMS\Services */ class PagarMeService { /** * @var PagarmeRecipientInterface */ private $pagarmeRecipient; /** * @var UserRepository */ private $userRepository; /** * @var PostbackInterface */ private $postback; /** * @var PayableInterface */ private $payable; /** * @var HireSubscriptionInterface */ private $hireSubscription; /** * @var RecurringInterface */ private $recurring; /** * @var PeriodicityInterface */ private $periodicity; /** * @var HiringServices */ private $hiringServices; /** * @var HiredCourseRepository */ private $hiredCourse; /** * PagarMeService constructor. * @param PagarmeRecipientInterface $pagarmeRecipient * @param UserRepository $userRepository * @param PostbackInterface $postback * @param PayableInterface $payable * @param HireSubscriptionInterface $hireSubscription * @param RecurringInterface $recurring * @param PeriodicityInterface $periodicity * @param HiringServices $hiringServices * @param HiredCourseRepository $hiredCourse */ public function __construct( PagarmeRecipientInterface $pagarmeRecipient, UserRepository $userRepository, PostbackInterface $postback, PayableInterface $payable, HireSubscriptionInterface $hireSubscription, RecurringInterface $recurring, PeriodicityInterface $periodicity, HiringServices $hiringServices, HiredCourseRepository $hiredCourse ) { $this->pagarmeRecipient = $pagarmeRecipient; $this->userRepository = $userRepository; $this->postback = $postback; $this->payable = $payable; $this->hireSubscription = $hireSubscription; $this->recurring = $recurring; $this->periodicity = $periodicity; $this->hiringServices = $hiringServices; $this->hiredCourse = $hiredCourse; } /** * @param Request $request * @param string $transferInterval * @param int $transferDay * @return \PagarMe\Sdk\Recipient\Recipient | array */ public function registerRecipient(Request $request, $transferInterval = 'monthly', $transferDay = 15) { $data = $request->all(); $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $bankCode = str_pad($data['bank_id'], 3, '0', STR_PAD_LEFT); $agenciaNumber = $data['agency']; $agenciaDigit = $data['agency_digit'] == '' ? null : $data['agency_digit']; $accountNumber = $data['account']; $accountDigit = $data['account_digit']; $documentNumber = preg_replace('/[^0-9]/is', '', $data['doc']); $legalName = strlen($data['full_name']) > 30 ? substr($data['full_name'], 0, 29) : $data['full_name']; $accountType = $data['account_type']; try { $bankAccount = $pagarMe->bankAccount()->create( $bankCode, $agenciaNumber, $accountNumber, $accountDigit, $documentNumber, $legalName, $agenciaDigit, $accountType ); } catch (\Exception $e) { //\Log::critical($e->getMessage()); app(Handler::class)->report($e); return [ 'error_code' => $e->getCode(), 'error_message' => $e->getMessage() ]; } $transferEnabled = false; $automaticAnticipationEnabled = false; $anticipatableVolumePercentage = 0; try { $recipient = $pagarMe->recipient()->create( $bankAccount, $transferInterval, $transferDay, $transferEnabled, $automaticAnticipationEnabled, $anticipatableVolumePercentage ); } catch (\Exception $e) { //\Log::critical($e->getMessage()); app(Handler::class)->report($e); return [ 'error_code' => $e->getCode(), 'error_message' => $e->getMessage() ]; } return $recipient; } /** * @param Request $request * @return array|\PagarMe\Sdk\Recipient\Recipient */ public function updateRecipient(Request $request) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $data = $request->all(); $recipientId = isset($data['pagarme_recipient_id']) && !empty($data['pagarme_recipient_id']) ? $data['pagarme_recipient_id'] : null; if (empty($recipientId)) { $recipient = $this->registerRecipient($request); $recipientId = $recipient->getId(); } $recipient = $pagarMe->recipient()->get($recipientId); $bankAccount = null; $newAccountId = 0; $account = $recipient->getBankAccount(); $bankCode = str_pad($data['bank_id'], 3, '0', STR_PAD_LEFT); $agenciaNumber = $data['agency']; $agenciaDigit = $data['agency_digit'] == '' ? null : $data['agency_digit']; $accountNumber = $data['account']; $accountDigit = $data['account_digit']; $documentNumber = preg_replace('/[^0-9]/is', '', $data['doc']); $legalName = strlen($data['full_name']) > 30 ? substr($data['full_name'], 0, 29) : $data['full_name']; $accountType = $data['account_type']; if ($account->getBankCode() !== $bankCode || $account->getAgencia() !== $agenciaNumber || $account->getAgenciaDv() !== $agenciaDigit || $account->getConta() !== $accountNumber || $account->getContaDv() !== $accountDigit) { $bankAccount = $pagarMe->bankAccount()->create( $bankCode, $agenciaNumber, $accountNumber, $accountDigit, $documentNumber, $legalName, $agenciaDigit, $accountType ); $newAccountId = $bankAccount->getId(); } $recipient->setTransferInterval('monthly'); $recipient->setTransferDay(16); $recipient->setTransferEnabled(true); $recipient->setAutomaticAnticipationEnabled(false); $recipient->setAnticipatableVolumePercentage(0); if ($newAccountId > 0 && $newAccountId !== $data['pagarme_account_id']) { $recipient->setBankAccount($bankAccount); } $pagarMe->recipient()->update($recipient); $recipient = $pagarMe->recipient()->get($recipientId); return $recipient; } /** * @param $recipientId * @return \ArrayObject */ public function getRecipientByID_Curl($recipientId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey ]; $params = http_build_query($data); $url = "https://api.pagar.me/1/recipients/" . $recipientId . '?'; $charSet = "UTF-8"; $headers = ["Content-Type: application/json"]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . $params); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); $json = json_decode($result, true); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); return $json; } /** * @param $data * @return pgmAddress */ private function createAddress($data) { $address = new pgmAddress([ 'zipcode' => $data['zipcode'], 'street' => $data['street'], 'street_number' => $data['street_number'], 'complementary' => $data['complementary'], 'neighborhood' => $data['neighborhood'], 'city' => $data['city'], 'state' => $data['state'] ]); return $address; } /** * @param $data * @return pgmPhone */ private function createPhone($data) { $phone = new pgmPhone([ "ddd" => $data['ddd'], "number" => $data['number'] ]); return $phone; } /** * @param $customerId * @return \PagarMe\Sdk\Customer\Customer */ public function getCustomerByID($customerId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); return $pagarMe->customer()->get($customerId); } /** * @param $customerId * @return \ArrayObject */ public function getCustomerByID_Curl($customerId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey ]; $params = http_build_query($data); $url = "https://api.pagar.me/1/customers/" . $customerId . '?'; $charSet = "UTF-8"; $headers = ["Content-Type: application/json"]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . $params); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); $json = json_decode($result, true); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); return $json; } /** * @param $cardHash * @param $customerId * @return mixed */ public function storeCardByHashCURL($cardHash, $customerId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey, 'customer_id' => $customerId, 'card_hash' => $cardHash ]; $data = json_encode($data); $url = "https://api.pagar.me/1/cards"; $charSet = "UTF-8"; $headers = ["Content-Type: application/json"]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); $json = json_decode($result, true); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); return $json; } /** * @param $cardId * @return \PagarMe\Sdk\Card\Card */ public function getCardByID($cardId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $card = $pagarMe->card()->get($cardId); return $card; } /** * @param $customerId * @return \ArrayObject */ public function getAllCustomerCards($customerId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/customers/{$customerId}/cards"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter cartões: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter cartões: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } // Retorna apenas o array de cartões return $response['data'] ?? []; } /** * @param $cardHashId * @param Cart $cart * @param $subscriptionHash * @param null $userId * @param null $postbackUrl * @param string $softDescriptor * @return \PagarMe\Sdk\Transaction\BoletoTransaction|\PagarMe\Sdk\Transaction\CreditCardTransaction */ // DESCONTINUADO V4 /*public function cardTransaction($cardHashId, Cart $cart, $subscriptionHash = null, $userId = null, $postbackUrl = null, $softDescriptor = '' ) { $userId = is_null($userId) ? Auth::user()->id : $userId; $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $recipient = $this->pagarmeRecipient->firstRecord(); $user = $this->userRepository->with([ 'customer' => function ($q) use ($cardHashId) { $q->where('card_id', '=', $cardHashId); } ])->find($userId); $cardCvv = Crypt::decrypt($user->customer[0]->hash); //$cardCvv = '658'; $card = $this->getCardByID($user->customer[0]->card_id); $customer = $this->getCustomerByID(intval($user->customer[0]->customer_id)); $recipientCli = $pagarMe->recipient()->get($recipient->pagarme_recipient_id); $recipientAdm = $pagarMe->recipient()->get(config('pagar_me.PAGAR_ME_RECIPIENT_ID')); $splitRules = new SplitRuleCollection(); $price = ($cart->getGrossAmount() - $cart->getDiscountAmount()) + $cart->getShippingAmount() + $cart->getInstallmentInterest(); $installments = $cart->get('installments'); $adminPerc = Session::get('planLimite.is_free') == true ? 2.4 : 1.4; $adminValue = round((($adminPerc * $price) / 100), 2); $adminValue += 0.5; $valueClient = $price - $adminValue; $adminValue = strval($adminValue * 100); $valueClient = strval($valueClient * 100); $ruleCli = $pagarMe->splitRule()->monetaryRule($valueClient, $recipientCli, true, true, true); $ruleAdm = $pagarMe->splitRule()->monetaryRule($adminValue, $recipientAdm, false, false, false); $splitRules[0] = $ruleCli; $splitRules[1] = $ruleAdm; $extraAttributes = [ 'split_rules' => $splitRules ]; $metaData = [ 'user_email' => $user->email, 'user_id' => $user->id, 'recipient_id' => $recipientCli->getId(), 'instance_email' => Session::get('planLimite.email'), 'instance_url' => Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'), 'product_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id') ]; $https = Session::get('planLimite.ssl_activated') == 1 ? 'https://' : 'http://'; $onlineUrl = $https . Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'); if (!is_null($subscriptionHash)) { $metaData['subscription_hash'] = $subscriptionHash; $onlineUrl .= '/gateway/pagarme/postback'; } else { $onlineUrl .= '/gateway/pagarme/single/postback'; } $postbackUrl = is_null($postbackUrl) ? env('PAGAR_ME_POSTBACK_URL', $onlineUrl) : $postbackUrl; $amount = strval($price * 100); $transaction = $pagarMe->transaction()->creditCardTransaction( $amount, $card, $customer, $cardCvv, $installments, true, $postbackUrl, $metaData, $extraAttributes, $softDescriptor ); $result = null; $result = $this->getTransactionByID($transaction->getId()); while (!method_exists($result, 'getStatus')) { $result = $this->getTransactionByID($transaction->getId()); } while ($result->getStatus() == 'processing') { $result = $this->getTransactionByID($transaction->getId()); } return $result; }*/ /** * @param Cart $cart * @param $subscriptionHash * @param null $userId * @param null $postbackUrl * @return \PagarMe\Sdk\Transaction\BoletoTransaction|\PagarMe\Sdk\Transaction\CreditCardTransaction */ // DESCONTINUADO V4 /*public function boletoTransaction(Cart $cart, $subscriptionHash = null, $userId = null, $postbackUrl = null ) { $userId = is_null($userId) ? Auth::user()->id : $userId; $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $recipient = $this->pagarmeRecipient->firstRecord(); $user = $this->userRepository->with(['customer'])->find($userId); $customerId = intval($user->customer[0]->customer_id); $customer = $this->getCustomerByID($customerId); $recipient98 = $pagarMe->recipient()->get($recipient->pagarme_recipient_id); $recipient2 = $pagarMe->recipient()->get(config('pagar_me.PAGAR_ME_RECIPIENT_ID')); $splitRules = new SplitRuleCollection(); $adminPerc = Session::get('planLimite.is_free') == true ? 3 : 2; $price = ($cart->getGrossAmount() - $cart->getDiscountAmount()) + $cart->getShippingAmount(); $adminValue = round((($adminPerc * $price) / 100), 2); $valueClient = $price - $adminValue; $adminValue = strval($adminValue * 100); $valueClient = strval($valueClient * 100); $rule_98 = $pagarMe->splitRule()->monetaryRule($valueClient, $recipient98, true, true, true); $rule_2 = $pagarMe->splitRule()->monetaryRule($adminValue, $recipient2, false, false, false); $splitRules[0] = $rule_98; $splitRules[1] = $rule_2; $extraAttributes = [ 'split_rules' => $splitRules ]; $metaData = [ 'user_email' => $user->email, 'user_id' => $user->id, 'recipient_id' => $recipient98->getId(), 'instance_email' => Session::get('planLimite.email'), 'instance_url' => Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'), 'product_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id') ]; $https = Session::get('planLimite.ssl_activated') == 1 ? 'https://' : 'http://'; $onlineUrl = $https . Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'); if (!is_null($subscriptionHash)) { $metaData['subscription_hash'] = $subscriptionHash; $onlineUrl .= '/gateway/pagarme/postback'; } else { $onlineUrl .= '/gateway/pagarme/single/postback'; } $postbackUrl = is_null($postbackUrl) ? env('PAGAR_ME_POSTBACK_URL', $onlineUrl) : $postbackUrl; $amount = strval((($cart->getGrossAmount() - $cart->getDiscountAmount()) + $cart->getShippingAmount()) * 100); $transaction = $pagarMe->transaction()->boletoTransaction( $amount, $customer, $postbackUrl, $metaData, $extraAttributes ); $result = null; $result = $this->getTransactionByID($transaction->getId()); while (!method_exists($result, 'getStatus')) { $result = $this->getTransactionByID($transaction->getId()); } while ($result->getStatus() == 'processing') { $result = $this->getTransactionByID($transaction->getId()); } return $result; }*/ /** * @param $id * @return \PagarMe\Sdk\Transaction\BoletoTransaction|\PagarMe\Sdk\Transaction\CreditCardTransaction */ // DESCONTINUADO V4 /*public function getTransactionByID($id) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $pagarMe = new PagarMe($apiKey); $transaction = null; try { $transaction = $pagarMe->transaction()->get($id); } catch (ClientException $e) { app(Handler::class)->report($e); Log::info($e->getMessage()); $transaction = null; } return $transaction; }*/ /** * Recebe postbacks diretos das APIs v4 e v5 do PagarMe * * @param Request $request * @return bool */ public function postback(Request $request) { Log::info('Recebendo postback'); // Tentar obter payload como array ou JSON $payload = $request->all(); // Se payload estiver vazio, tentar obter do corpo da requisição if (empty($payload)) { $payload = json_decode($request->getContent(), true); } // Se payload for array com uma string JSON, decodificar if (is_array($payload) && count($payload) === 1 && is_string($payload[0])) { $payload = json_decode($payload[0], true); } // Se ainda for string JSON, decodificar if (is_string($payload)) { $payload = json_decode($payload, true); } // NOVO: Detectar se é um registro do banco de dados (tem 'payload' como campo) if (is_array($payload) && isset($payload['payload']) && isset($payload['postback_id'])) { Log::info('Detectado registro do banco de dados - extraindo payload'); $actualPayload = json_decode($payload['payload'], true); if ($actualPayload) { $payload = $actualPayload; } else { Log::error('Erro ao decodificar payload do registro do banco'); return false; } } if (!is_array($payload)) { Log::error('Formato de payload inválido - não é array nem JSON válido'); return false; } $received = Carbon::now(); // Identificar versão da API e extrair dados essenciais $apiVersion = $this->identifyApiVersion($payload); $postbackData = $this->extractPostbackData($payload, $apiVersion); if (!$postbackData) { Log::error('Formato de postback inválido'); return false; } Log::info('Postback identificado', [ 'version' => $apiVersion, 'type' => $postbackData['type'], 'id' => $postbackData['postback_id'] ]); // Verificar duplicidade $existing = $this->postback->findWhere([ ['postback_id', '=', $postbackData['postback_id']] ])->first(); if ($existing) { Log::info('Postback duplicado ignorado: ' . $postbackData['postback_id']); return false; } // Criar registro $this->postback->create([ 'gateway_id' => 1, 'subscription_id' => $postbackData['transaction_id'], 'postback_id' => $postbackData['postback_id'], 'payload' => json_encode($payload), 'processed' => false, 'received_at' => $received->toDateTimeString() ]); Log::info('Postback criado: ' . $postbackData['postback_id']); return true; } /** * Identifica a versão da API baseada na estrutura do payload * * @param array $payload * @return string */ private function identifyApiVersion(array $payload) { // v5: tem 'type' (como 'order.created', 'charge.paid') e estrutura com 'data' if (isset($payload['type']) && isset($payload['data'])) { return 'v5'; } // v4: tem 'object' (como 'transaction') e 'current_status' if (isset($payload['object']) && isset($payload['current_status'])) { return 'v4'; } return 'unknown'; } /** * Envia notificação por email para postbacks da API v4 * * @param array $payload */ private function sendV4PostbackNotification(array $payload) { try { Log::info('Enviando notificação de postback v4'); // Extrair dados principais da transação v4 $transactionId = $payload['id'] ?? 'N/A'; $status = $payload['current_status'] ?? 'N/A'; $amount = isset($payload['amount']) ? ($payload['amount'] / 100) : 'N/A'; $paymentMethod = $payload['payment_method'] ?? 'N/A'; $object = $payload['object'] ?? 'N/A'; $createdAt = $payload['date_created'] ?? 'N/A'; // Extrair metadata se disponível $metadata = $payload['metadata'] ?? []; // Formatar mensagem de texto simples $message = "NOTIFICAÇÃO - POSTBACK API V4 RECEBIDO\n\n"; $message .= "=== DADOS PRINCIPAIS DA TRANSAÇÃO ===\n"; $message .= "ID da Transação: {$transactionId}\n"; $message .= "Tipo de Objeto: {$object}\n"; $message .= "Status Atual: {$status}\n"; $message .= "Valor: R$ {$amount}\n"; $message .= "Método de Pagamento: {$paymentMethod}\n"; $message .= "Data de Criação: {$createdAt}\n"; $message .= "Data da Notificação: " . Carbon::now()->format('d/m/Y H:i:s') . "\n\n"; // Adicionar metadata se existir if (!empty($metadata)) { $message .= "=== METADATA ===\n"; foreach ($metadata as $key => $value) { $message .= "{$key}: {$value}\n"; } $message .= "\n"; } $message .= "=== PAYLOAD COMPLETO ===\n"; $message .= json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); // Dados para o email $emailData = [ 'subject' => 'Notificação Postback v4 - Transação ' . $transactionId, 'message' => $message, 'transaction_id' => $transactionId, 'status' => $status ]; // Enviar email Mail::raw($message, function ($mail) use ($emailData) { $mail->to('moises@estudiosite.com.br') ->subject($emailData['subject']); }); Log::info('Notificação v4 enviada com sucesso', [ 'transaction_id' => $transactionId, 'status' => $status ]); } catch (\Exception $e) { Log::error('Erro ao enviar notificação v4: ' . $e->getMessage(), [ 'payload_id' => $payload['id'] ?? 'unknown', 'error' => $e->getTraceAsString() ]); } } /** * Extrai dados essenciais do postback baseado na versão * * @param array $payload * @param string $version * @return array|null */ private function extractPostbackData(array $payload, string $version) { switch ($version) { case 'v5': // Para v5, o transaction_id está no charge ou na transação do boleto $transactionId = null; if (isset($payload['data']['charges'][0]['last_transaction']['id'])) { $transactionId = $payload['data']['charges'][0]['last_transaction']['id']; } elseif (isset($payload['data']['charges'][0]['id'])) { $transactionId = $payload['data']['charges'][0]['id']; } elseif (isset($payload['data']['id'])) { $transactionId = $payload['data']['id']; } return [ 'type' => $payload['type'] ?? 'unknown', 'postback_id' => $payload['id'] ?? null, 'transaction_id' => $transactionId ]; case 'v4': return [ 'type' => $payload['object'] . '.' . ($payload['current_status'] ?? 'unknown'), 'postback_id' => $payload['id'] ?? null, 'transaction_id' => $payload['transaction']['id'] ?? $payload['id'] ?? null ]; default: return null; } } /** * @param null $postBackId * Faz o processamento dos postbacks do pagar.me * Invocado pelo listener "created" do model Postback */ public function processPostbacks($postBackId = null) { try { $postback = $this->postback->findWhere([ ['processed', '=', 0], ['postback_id', '=', $postBackId] ])->first(); if (!$postback) { Log::error('Postback não encontrado: ' . $postBackId); return; } $today = Carbon::now()->format('Y-m-d'); $recipientESId = config('pagar_me.PAGAR_ME_RECIPIENT_ID'); $payload = json_decode($postback['payload'], true); if (!$payload) { Log::error('Erro ao decodificar payload do postback: ' . $postBackId); return; } // Identificar versão da API $apiVersion = $this->identifyApiVersion($payload); // NOVO: Enviar notificação por email para postbacks v4 if ($apiVersion === 'v4') { $this->sendV4PostbackNotification($payload); } // Verificar se é assinatura $isSubscription = isset($payload['data']['metadata']['subscription_hash']); Log::info('Tipo de postback identificado', [ 'api_version' => $apiVersion, 'is_subscription' => $isSubscription, 'payload_type' => $payload['type'] ?? 'unknown' ]); if ($isSubscription) { $status = $this->processSubscriptionPostback($payload); $postback->processed = $status; } else { $status = $this->processTrasactionPostback($payload); $postback->processed = $status; } $postback->save(); } catch (\Exception $e) { Log::error('Erro ao processar postback: ' . $e->getMessage()); app(Handler::class)->report($e); } } /** * Processa postbacks de assinaturas da API v5 * @param array $payload * @return int */ public function processSubscriptionPostback($payload) { try { Log::info('Processando postback de assinatura v5', ['type' => $payload['type'] ?? 'unknown']); // Verificar se é um webhook válido da API v5 para assinaturas if (!isset($payload['type']) || strpos($payload['type'], 'order.') !== 0) { Log::error('Postback de assinatura inválido - não é um webhook de order da API v5'); return 0; } // Processar apenas webhooks relevantes para assinaturas if (!in_array($payload['type'], ['order.created', 'order.paid', 'order.payment_failed', 'order.canceled'])) { Log::info('Webhook de assinatura ignorado - tipo não processado: ' . $payload['type']); return 2; // Não processado, mas não é erro } // Validar estrutura do payload if (!isset($payload['data']) || !isset($payload['data']['charges']) || empty($payload['data']['charges'])) { Log::error('Payload v5 de assinatura inválido - sem charges'); return 0; } $order = $payload['data']; $charge = $order['charges'][0]; $chargeId = $charge['id']; $orderId = $order['id']; // Este é o payment_code correto para assinaturas $paymentMethod = $charge['payment_method'] ?? 'unknown'; // Extrair dados da assinatura $subscriptionHash = $order['metadata']['subscription_hash'] ?? null; $userEmail = $order['metadata']['user_email'] ?? null; if (!$subscriptionHash || !$userEmail) { Log::error('Dados de assinatura incompletos', ['subscription_hash' => $subscriptionHash, 'user_email' => $userEmail]); return 0; } Log::info('Processando assinatura', [ 'subscription_hash' => $subscriptionHash, 'charge_id' => $chargeId, 'order_id' => $orderId, 'payment_method' => $paymentMethod ]); // Buscar usuário $user = $this->userRepository->findByField('email', $userEmail)->first(); if (!$user) { Log::error('Usuário não encontrado', ['user_email' => $userEmail]); return 0; } // Buscar assinatura na tabela hire_subscriptions $subscription = $this->findSubscription($chargeId, $orderId); if (!$subscription) { // Fallback: buscar por subscription_hash $subscription = $this->hireSubscription->findByField('subscription_hash', $subscriptionHash)->first(); if (!$subscription) { Log::error('Assinatura não encontrada', [ 'subscription_hash' => $subscriptionHash, 'charge_id' => $chargeId, 'order_id' => $orderId ]); return 0; } } Log::info('Assinatura encontrada', [ 'subscription_id' => $subscription->id, 'subscription_hash' => $subscription->subscription_hash, 'payment_code' => $subscription->payment_code ]); // LÓGICA SIMPLIFICADA: Cartão vs Boleto if ($paymentMethod === 'credit_card') { // CARTÃO: Apenas verificação de segurança (processamento já feito em tempo real) Log::info('Postback de cartão de crédito - APENAS verificação de segurança'); // Verificação simples: assinatura paga + recorrências existem? if ($subscription->status == 1) { $recurrenceCount = $this->recurring->countSubscriptionRecurrences($subscriptionHash); Log::info('Verificação de segurança simplificada', [ 'subscription_status' => $subscription->status, 'recurrence_count' => $recurrenceCount ]); if ($recurrenceCount >= 2) { Log::info('✅ Verificação de segurança APROVADA - assinatura funcionando corretamente'); // CORREÇÃO: Para assinaturas, atualizar subscription e recorrências (não hiring) Log::info('Atualizando taxas da assinatura via postback de segurança'); // Calcular taxas via postback (agora disponíveis) $totalFeeAmount = $this->calculateFeeAmount($chargeId); $netAmount = ($charge['amount'] / 100) - $totalFeeAmount; // Atualizar assinatura com taxas corretas $subscription->fee_amount = $totalFeeAmount; $subscription->net_amount = $netAmount; $subscription->save(); Log::info('Assinatura atualizada com taxas via postback', [ 'subscription_id' => $subscription->id, 'fee_amount' => $totalFeeAmount, 'net_amount' => $netAmount ]); // Atualizar primeira recorrência com taxas corretas $firstRecurrence = $this->recurring->findWhere([ ['subscription_hash', '=', $subscription->subscription_hash], ['status', '=', 1] // Paga ])->first(); if ($firstRecurrence && $firstRecurrence->fee_amount == 0) { $firstRecurrence->fee_amount = $totalFeeAmount; $firstRecurrence->net_amount = $netAmount; $firstRecurrence->save(); Log::info('Primeira recorrência atualizada com taxas via postback', [ 'recurrence_id' => $firstRecurrence->id, 'fee_amount' => $totalFeeAmount, 'net_amount' => $netAmount ]); } return 1; // Tudo OK } else { Log::warning('⚠️ Verificação de segurança detectou falta de recorrências', [ 'expected_min' => 2, 'actual_count' => $recurrenceCount ]); // CRÍTICO: Se comando não criou próxima recorrência, criar via postback if ($recurrenceCount < 2) { Log::info('Criando recorrências faltantes via postback de segurança'); $cardId = isset($charge['last_transaction']['card']['id']) ? $charge['last_transaction']['card']['id'] : null; $totalFeeAmount = $this->calculateFeeAmount($charge['id']); $netAmount = ($charge['amount'] / 100) - $totalFeeAmount; $this->processRecurrences($user, $subscription, $order, $charge, 'paid', $totalFeeAmount, $netAmount, $cardId); Log::info('✅ Recorrências criadas via postback de segurança'); } return 1; // Corrigido } } else { Log::warning('❌ Assinatura não está paga - verificação de segurança falhou', [ 'subscription_status' => $subscription->status ]); return 2; // Problema na assinatura } } else { // BOLETO: Processamento completo (assíncrono) Log::info('Postback de boleto - processamento completo'); return $this->processSubscriptionPostbackNormal($subscription, $order, $charge, $user, $payload['type'], $subscriptionHash); } } catch (\Exception $e) { Log::error('Erro ao processar postback de assinatura v5: ' . $e->getMessage(), [ 'exception' => $e->getTraceAsString(), 'payload_type' => $payload['type'] ?? 'unknown' ]); app(Handler::class)->report($e); return 0; } } /** * Verifica consistência da assinatura de cartão de crédito (verificação de segurança) */ private function performSubscriptionSecurityCheck($subscription, $order, $charge, $user, $webhookType) { try { $chargeId = $charge['id']; $orderId = $order['id']; $currentStatus = $charge['status'] ?? 'unknown'; $paymentMethod = $charge['payment_method'] ?? 'unknown'; Log::info('Verificação de segurança da assinatura', [ 'subscription_id' => $subscription->id, 'current_status' => $currentStatus, 'webhook_type' => $webhookType ]); // NOVA LÓGICA SIMPLIFICADA: Verificar recorrências independente de payment_code Log::info('Verificando estado das recorrências para assinatura paga'); // Verificar se as recorrências foram criadas (prioridade máxima) $recurrenceCount = $this->recurring->countSubscriptionRecurrences($subscription->subscription_hash); Log::info('Estado das recorrências', [ 'subscription_hash' => $subscription->subscription_hash, 'recurrence_count' => $recurrenceCount, 'subscription_status' => $subscription->status, 'current_payment_code' => $subscription->payment_code, 'postback_order_id' => $orderId ]); if ($recurrenceCount >= 2) { Log::info('Recorrências suficientes já existem - verificação completa', ['count' => $recurrenceCount]); return 1; } // CRÍTICO: Se recorrências insuficientes E assinatura está paga, SEMPRE criar recorrências if ($subscription->status == 1 && $currentStatus === 'paid') { Log::info('Assinatura paga mas recorrências insuficientes - criando automaticamente', [ 'current_count' => $recurrenceCount, 'subscription_status' => $subscription->status, 'payment_status' => $currentStatus ]); $cardId = ($paymentMethod === 'credit_card' && isset($charge['last_transaction']['card']['id'])) ? $charge['last_transaction']['card']['id'] : null; // Calcular taxas para recorrências $totalFeeAmount = $this->calculateFeeAmount($chargeId); $netAmount = ($charge['amount'] / 100) - $totalFeeAmount; // Processar recorrências faltantes - SEMPRE try { $this->processRecurrences($user, $subscription, $order, $charge, $currentStatus, $totalFeeAmount, $netAmount, $cardId); Log::info('✅ Recorrências criadas com sucesso via verificação de segurança'); return 1; } catch (\Exception $e) { Log::error('❌ Erro ao criar recorrências na verificação de segurança: ' . $e->getMessage()); return 2; // Erro na criação, mas continuar } } // Se chegou aqui, algo está inconsistente Log::warning('Estado inconsistente da assinatura', [ 'subscription_status' => $subscription->status, 'payment_status' => $currentStatus, 'recurrence_count' => $recurrenceCount, 'expected_payment_code' => $orderId, 'actual_payment_code' => $subscription->payment_code ]); return 2; // Aviso de inconsistência } catch (\Exception $e) { Log::error('Erro na verificação de segurança da assinatura: ' . $e->getMessage()); return 0; } } /** * Processa postback de assinatura normal (boleto - assíncrono) */ private function processSubscriptionPostbackNormal($subscription, $order, $charge, $user, $webhookType, $subscriptionHash) { try { // MONITORAMENTO: Início do processamento $startTime = microtime(true); $chargeId = $charge['id']; $orderId = $order['id']; $paymentMethod = $charge['payment_method'] ?? 'unknown'; $cardId = ($paymentMethod === 'credit_card' && isset($charge['last_transaction']['card']['id'])) ? $charge['last_transaction']['card']['id'] : null; // Determinar status $status = $this->getStatusFromWebhook($webhookType, $charge); Log::info('Status da assinatura determinado', [ 'status' => $status, 'webhook_type' => $webhookType, 'payment_method' => $paymentMethod, 'subscription_hash' => $subscriptionHash ]); // Processar taxas se pago $totalFeeAmount = 0.00; $netAmount = 0.00; if ($status === 'paid') { $totalFeeAmount = $this->calculateFeeAmount($chargeId); $netAmount = ($charge['amount'] / 100) - $totalFeeAmount; // Atualizar dados da assinatura $hireSubscriptionData = [ 'payment_code' => $orderId, // Usar order_id (or_) para assinaturas 'payment_type' => $paymentMethod, 'card_id' => $cardId, 'gross_amount' => $charge['amount'] / 100, 'fee_amount' => $totalFeeAmount, 'net_amount' => $netAmount, 'status' => $this->translateSubscriptionStatus($status) // Usar função específica para assinaturas ]; $subscription->fill($hireSubscriptionData); $subscription->save(); Log::info('Assinatura atualizada', [ 'subscription_id' => $subscription->id, 'status_code' => $hireSubscriptionData['status'], 'status_description' => $this->getSubscriptionStatusDescription($hireSubscriptionData['status']) ]); } // Processar recorrências $this->processRecurrences($user, $subscription, $order, $charge, $status, $totalFeeAmount, $netAmount, $cardId); // MONITORAMENTO: Tempo total de processamento $totalTime = microtime(true) - $startTime; Log::info('Processamento de assinatura concluído', [ 'subscription_hash' => $subscriptionHash, 'payment_method' => $paymentMethod, 'status' => $status, 'total_time' => round($totalTime, 2) . 's', 'is_slow' => $totalTime > 5.0 // Marcar se demorou mais que 5 segundos ]); return 1; } catch (\Exception $e) { $totalTime = microtime(true) - $startTime; Log::error('Erro no processamento normal da assinatura: ' . $e->getMessage(), [ 'subscription_hash' => $subscriptionHash ?? 'unknown', 'payment_method' => $paymentMethod ?? 'unknown', 'processing_time' => round($totalTime, 2) . 's', 'exception' => $e->getTraceAsString() ]); return 0; } } /** * Processa as recorrências da assinatura */ private function processRecurrences($user, $subscription, $order, $charge, $status, $totalFeeAmount, $netAmount, $cardId) { $today = Carbon::now()->format('Y-m-d'); $subscriptionHash = $subscription->subscription_hash; $chargeId = $charge['id']; $orderId = $order['id']; // Para recorrências, usar order_id $paymentMethod = $charge['payment_method'] ?? 'unknown'; $recurring = $this->recurring->getLatestRecurrence($subscriptionHash); $countRecurrences = $this->recurring->countSubscriptionRecurrences($subscriptionHash); if ($recurring) { Log::info('Recorrência existente encontrada', ['recurring_id' => $recurring->id, 'status' => $recurring->status]); // Se existe recorrência pendente e o pagamento foi aprovado if (in_array($recurring->status, [0, 2, 3]) && $status === 'paid') { $recurring->status = 1; // Pago $recurring->attempts = $recurring->attempts + 1; $recurring->payment_date = $today; $recurring->fee_amount = $totalFeeAmount; $recurring->net_amount = $netAmount; $recurring->save(); // Verificar se precisa do payment_code if (empty($recurring->payment_code)) { $recurring->payment_code = $orderId; // Usar order_id para recorrências $recurring->save(); } // Gerar próxima recorrência $this->generateNextRecurrence( $user, $order, $charge, $recurring->due_date, 0, // Status pendente 0, 0, $cardId, $subscription->periodicity_id, $countRecurrences + 1 ); Log::info('Recorrência paga e próxima gerada'); } elseif ($recurring->status == 1 && $status === 'paid') { // CORREÇÃO: Recorrência já foi paga (pelo comando), mas verificar se próxima existe Log::info('Recorrência já processada - verificando se próxima existe', [ 'recurring_id' => $recurring->id, 'status' => $recurring->status, 'count_recurrences' => $countRecurrences ]); // Verificar se próxima recorrência existe $nextDueDate = $this->getDueDate($recurring->due_date, $subscription->periodicity_id); $nextRecurrence = $this->recurring->findWhere([ ['subscription_hash', '=', $subscriptionHash], ['due_date', '=', $nextDueDate->format('Y-m-d')] ])->first(); if (!$nextRecurrence) { Log::info('Próxima recorrência não existe - criando', [ 'next_due_date' => $nextDueDate->format('Y-m-d'), 'periodicity_id' => $subscription->periodicity_id ]); // Gerar próxima recorrência $this->generateNextRecurrence( $user, $order, $charge, $recurring->due_date, 0, // Status pendente 0, 0, $cardId, $subscription->periodicity_id, $countRecurrences + 1 ); Log::info('Próxima recorrência criada via postback'); } else { Log::info('Próxima recorrência já existe', ['next_recurring_id' => $nextRecurrence->id]); } } elseif (in_array($recurring->status, [0, 2, 3]) && $status !== 'paid') { // Pagamento falhou, atualizar para próxima tentativa $nextAttemptDate = Carbon::parse($recurring->next_attempt_date)->addDay()->format('Y-m-d'); $recurring->attempts = $recurring->attempts + 1; if ($recurring->next_attempt_date <= Carbon::tomorrow()->toDateString()) { $recurring->next_attempt_date = $nextAttemptDate; } $recurring->payment_code = $orderId; // Usar order_id para recorrências $recurring->status = $paymentMethod === 'credit_card' ? 2 : 3; // Falha cartão ou boleto $recurring->save(); Log::info('Recorrência com falha atualizada para próxima tentativa'); } } else { // Não existe recorrência, criar a primeira Log::info('Criando primeira recorrência para assinatura'); $retroDate = Carbon::parse($subscription->created_at)->format('Y-m-d'); if ($paymentMethod === 'credit_card') { // Cartão: criar recorrência atual paga e próxima pendente $this->generateFirstRecurrence($user, $order, $charge, $retroDate, 1, $totalFeeAmount, $netAmount, $cardId); usleep(100000); // OTIMIZADO: Reduzido de 1 segundo para 100ms $this->generateNextRecurrence( $user, $order, $charge, $retroDate, 0, 0, 0, $cardId, $subscription->periodicity_id, 2 ); Log::info('Recorrências de cartão criadas (atual e próxima)'); } else { // Boleto: criar apenas recorrência atual $this->generateFirstRecurrence($user, $order, $charge, $retroDate, 3); // Status boleto Log::info('Recorrência de boleto criada'); } } } /** * @param $date * @param $periodicityId * @return Carbon */ private function getDueDate($date, $periodicityId) { $periodicity = $this->periodicity->find($periodicityId); switch ($periodicity->periodicity) { case 30: $nextDueDate = Carbon::parse($date)->addMonth(); break; case 90: $nextDueDate = Carbon::parse($date)->addMonth(3); break; case 180: $nextDueDate = Carbon::parse($date)->addMonth(6); break; case 365: $nextDueDate = Carbon::parse($date)->addYear(); break; } return $nextDueDate; } /** * Gera a primeira recorrência para uma assinatura * @param object $user * @param array $order * @param array $charge * @param string $today * @param int $status * @param float $sumFeeAmount * @param float $netAmount * @param string|null $cardId */ private function generateFirstRecurrence($user, $order, $charge, $today, $status, $sumFeeAmount = 0, $netAmount = 0, $cardId = null) { try { $tomorrow = Carbon::now()->addDay()->format('Y-m-d'); $paymentMethod = $charge['payment_method'] ?? 'unknown'; $orderId = $order['id']; // Usar order_id como payment_code $subscriptionHash = $order['metadata']['subscription_hash'] ?? null; if (!$subscriptionHash) { Log::error('Subscription hash não encontrado para primeira recorrência'); return; } $recurrenceData = [ 'user_id' => $user->id, 'subscription_hash' => $subscriptionHash, 'gateway_id' => 1, 'due_date' => $today, 'next_attempt_date' => $paymentMethod === 'credit_card' ? $today : $tomorrow, 'payment_code' => $orderId, // Usar order_id em vez de charge_id 'payment_type' => $paymentMethod, 'card_id' => $cardId, 'payment_date' => $paymentMethod === 'credit_card' ? $today : null, 'attempts' => $paymentMethod === 'credit_card' ? 1 : 0, 'gross_amount' => ($charge['amount'] / 100), 'discount_amount' => null, 'fee_amount' => $sumFeeAmount, 'extra_amount' => null, 'net_amount' => $netAmount, 'charge_number' => 1, 'status' => $status ]; $recurrence = $this->recurring->create($recurrenceData); Log::info('Primeira recorrência criada', [ 'recurrence_id' => $recurrence->id, 'status' => $status, 'payment_code' => $orderId ]); return $recurrence; } catch (\Exception $e) { Log::error('Erro ao criar primeira recorrência: ' . $e->getMessage()); app(Handler::class)->report($e); return null; } } /** * Gera a próxima recorrência para uma assinatura * @param object $user * @param array $order * @param array $charge * @param string $dueDate * @param int $status * @param float $sumFeeAmount * @param float $netAmount * @param string|null $cardId * @param int $periodicityId * @param int $chargeNumber */ private function generateNextRecurrence($user, $order, $charge, $dueDate, $status = 0, $sumFeeAmount = 0, $netAmount = 0, $cardId = null, $periodicityId = 0, $chargeNumber = 1) { try { $paymentMethod = $charge['payment_method'] ?? 'unknown'; $subscriptionHash = $order['metadata']['subscription_hash'] ?? null; if (!$subscriptionHash) { Log::error('Subscription hash não encontrado para próxima recorrência'); return; } $nextDueDate = $this->getDueDate($dueDate, $periodicityId); $recurrenceData = [ 'user_id' => $user->id, 'subscription_hash' => $subscriptionHash, 'gateway_id' => 1, 'due_date' => $nextDueDate, 'next_attempt_date' => $nextDueDate, 'payment_type' => $paymentMethod, 'card_id' => $cardId, 'payment_date' => null, 'attempts' => 0, 'gross_amount' => ($charge['amount'] / 100), 'discount_amount' => null, 'fee_amount' => $sumFeeAmount, 'extra_amount' => null, 'net_amount' => $netAmount, 'charge_number' => $chargeNumber, 'status' => $status // Nota: payment_code será definido quando o pagamento for processado ]; $recurrence = $this->recurring->create($recurrenceData); Log::info('Próxima recorrência criada', [ 'recurrence_id' => $recurrence->id, 'due_date' => $nextDueDate, 'charge_number' => $chargeNumber, 'subscription_hash' => $subscriptionHash ]); return $recurrence; } catch (\Exception $e) { Log::error('Erro ao criar próxima recorrência: ' . $e->getMessage(), [ 'subscription_hash' => $subscriptionHash ?? 'unknown', 'periodicity_id' => $periodicityId ]); app(Handler::class)->report($e); return null; } } /** * @param $payload * @return int */ public function processTrasactionPostback($payload) { try { Log::info('Processando postback de transação avulsa v5', ['type' => $payload['type'] ?? 'unknown']); // Verificar se é um webhook válido da API v5 if (!isset($payload['type']) || strpos($payload['type'], 'order.') !== 0) { Log::error('Postback inválido - não é um webhook de order da API v5'); return 0; } // SIMPLIFICAÇÃO: Com tempo real para cartão, reduzir processamento de postback // Processar apenas casos que não são tratados em tempo real $allowedTypes = [ // Boleto (sempre assíncrono) 'order.created', // Boleto criado 'order.paid', // Boleto pago // Chargeback e estornos (sempre assíncronos) 'charge.refund.created', // Estorno criado 'charge.chargeback.created', // Chargeback criado // Falhas que precisam de tratamento 'order.payment_failed', // Falha no pagamento 'order.canceled' // Cancelamento ]; if (!in_array($payload['type'], $allowedTypes)) { Log::info('Webhook ignorado - tipo não processado na nova lógica: ' . $payload['type']); return 2; // Não processado, mas válido } // Validar estrutura do payload if (!isset($payload['data']) || !isset($payload['data']['charges']) || empty($payload['data']['charges'])) { Log::error('Payload v5 inválido - sem charges'); return 0; } $order = $payload['data']; $charge = $order['charges'][0]; // Primeira cobrança $chargeId = $charge['id']; Log::info('Processando charge', ['charge_id' => $chargeId, 'order_id' => $order['id']]); // Extrair dados necessários $userEmail = $order['metadata']['user_email'] ?? null; $paymentMethod = $charge['payment_method'] ?? 'unknown'; $cardId = ($paymentMethod === 'credit_card' && isset($charge['last_transaction']['card']['id'])) ? $charge['last_transaction']['card']['id'] : null; // Determinar status baseado no tipo do webhook e status da charge $status = $this->getStatusFromWebhook($payload['type'], $charge); Log::info('Status determinado', ['status' => $status, 'webhook_type' => $payload['type'], 'payment_method' => $paymentMethod]); // PROTEÇÃO CONTRA DUPLO PROCESSAMENTO: // Se for cartão de crédito com status 'paid', verificar se já foi processado em tempo real if ($paymentMethod === 'credit_card' && $status === 'paid') { Log::info('Cartão pago - verificando se já foi processado em tempo real', [ 'charge_id' => $chargeId, 'order_id' => $order['id'] ]); // Buscar hiring para verificar se já foi processado $hiring = $this->findHiring($chargeId, $order['id']); if ($hiring && $hiring->status == 3) { // Status 3 = pago Log::info('Transação já processada em tempo real - ignorando postback', [ 'hiring_id' => $hiring->id, 'hiring_status' => $hiring->status ]); //return 1; // Sucesso - já processado // COMENTADO PARA TESTES } } // Buscar o hiring relacionado $hiring = $this->findHiring($chargeId, $order['id']); if (!$hiring) { // Para boletos, é normal não existir hiring ainda no order.created if ($paymentMethod === 'boleto' && $payload['type'] === 'order.created') { Log::info('Boleto criado - hiring será criado quando necessário'); return 1; } Log::error('Hiring não encontrado', ['charge_id' => $chargeId, 'order_id' => $order['id']]); return 0; } Log::info('Hiring encontrado', ['hiring_id' => $hiring->id, 'user_id' => $hiring->user_id]); // Processar payables apenas para transações pagas $totalFeeAmount = 0.00; if ($status === 'paid') { $totalFeeAmount = $this->calculateFeeAmount($chargeId); } // Atualizar dados do hiring $hiringData = [ 'fee_amount' => $totalFeeAmount, 'net_amount' => ($hiring->gross_amount - ($hiring->discount_amount + $totalFeeAmount)) + ($hiring->installment_interest ?? 0), 'status' => $this->translateStatus($status) ]; $hiring->fill($hiringData)->save(); Log::info('Hiring atualizado', ['hiring_id' => $hiring->id, 'new_status' => $hiringData['status']]); // Atualizar ou criar hired_course $this->updateHiredCourse($hiring, $status); return 1; } catch (\Exception $e) { Log::error('Erro ao processar transaction postback v5: ' . $e->getMessage(), [ 'exception' => $e->getTraceAsString(), 'payload_type' => $payload['type'] ?? 'unknown' ]); app(Handler::class)->report($e); return 0; } } /** * Determina o status baseado no tipo de webhook e dados da charge */ private function getStatusFromWebhook($webhookType, $charge) { switch ($webhookType) { case 'order.paid': return 'paid'; case 'order.canceled': return 'canceled'; case 'order.payment_failed': return 'failed'; case 'charge.refund.created': return 'refunded'; case 'order.created': // Verificar status específico da charge $chargeStatus = $charge['status'] ?? 'pending'; if ($chargeStatus === 'paid') { return 'paid'; } elseif ($chargeStatus === 'failed') { return 'failed'; } elseif ($chargeStatus === 'canceled') { return 'canceled'; } else { return 'created'; // order.created = aguardando pagamento } default: // Para outros tipos, verificar status da charge $chargeStatus = $charge['status'] ?? 'created'; return $chargeStatus; } } /** * Busca o hiring relacionado ao postback * PADRONIZAÇÃO: Tanto assinaturas quanto vendas avulsas usam order_id (or_) como payment_code */ private function findHiring($chargeId, $orderId) { // NOVA PADRONIZAÇÃO: buscar sempre por order_id primeiro $hiring = $this->hiringServices->getByPaymentCode($orderId); if (!$hiring) { // Fallback: buscar por charge_id para transações antigas que ainda usavam ch_ $hiring = $this->hiringServices->getByPaymentCode($chargeId); } // OTIMIZADO: Reduzir tentativas e delay para boletos (assinaturas) if (!$hiring) { $counter = 0; $maxRetries = 3; // Reduzido de 10 para 3 tentativas $delay = 0.5; // Reduzido de 1 segundo para 0.5 segundos while (!$hiring && $counter < $maxRetries) { usleep($delay * 1000000); // Usar microsegundos para controle mais preciso // Priorizar order_id na busca com retry $hiring = $this->hiringServices->getByPaymentCode($orderId); if (!$hiring) { $hiring = $this->hiringServices->getByPaymentCode($chargeId); } $counter++; // Log para monitoramento if (!$hiring) { Log::debug('Retry busca hiring', [ 'attempt' => $counter, 'order_id' => $orderId, 'charge_id' => $chargeId ]); } } } return $hiring; } /** * Busca a assinatura relacionada ao postback * Para assinaturas busca na tabela hire_subscriptions por order_id (or_) */ private function findSubscription($chargeId, $orderId) { // Para assinaturas, buscar na tabela hire_subscriptions por order_id $subscription = $this->hireSubscription->findWhere([ ['payment_code', '=', $orderId] ])->first(); if (!$subscription) { // Fallback: buscar pelo charge_id (assinaturas antigas) $subscription = $this->hireSubscription->findWhere([ ['payment_code', '=', $chargeId] ])->first(); } // OTIMIZADO: Reduzir tentativas para evitar timeout em assinaturas if (!$subscription) { $counter = 0; $maxRetries = 2; // Apenas 2 tentativas para assinaturas $delay = 0.3; // Delay muito reduzido while (!$subscription && $counter < $maxRetries) { usleep($delay * 1000000); $subscription = $this->hireSubscription->findWhere([ ['payment_code', '=', $orderId] ])->first(); if (!$subscription) { $subscription = $this->hireSubscription->findWhere([ ['payment_code', '=', $chargeId] ])->first(); } $counter++; // Log para monitoramento de assinaturas if (!$subscription) { Log::debug('Retry busca subscription', [ 'attempt' => $counter, 'order_id' => $orderId, 'charge_id' => $chargeId ]); } } } return $subscription; } /** * Calcula o valor das taxas para transações pagas * IMPORTANTE: Este método deve ser usado APENAS em processamento de postbacks * NÃO usar em processamento tempo real devido ao delay da API */ private function calculateFeeAmount($chargeId) { try { // OTIMIZADO: Timeout específico para cálculo de taxas $startTime = microtime(true); $payables = $this->getChargePayables($chargeId); $duration = microtime(true) - $startTime; // Log tempo de resposta da API Log::debug('API response time', [ 'charge_id' => $chargeId, 'duration' => round($duration, 2) . 's' ]); $recipientESId = config('pagar_me.PAGAR_ME_RECIPIENT_ID'); $recipient = $this->pagarmeRecipient->firstRecord(); $recipientCLIid = $recipient->pagarme_recipient_id; $totalFeeAmount = 0; foreach ($payables as $payable) { if (isset($payable['recipient_id']) && $payable['recipient_id'] == $recipientESId) { $totalFeeAmount += $payable['amount'] ?? 0; } elseif (isset($payable['recipient_id']) && $payable['recipient_id'] == $recipientCLIid) { $totalFeeAmount += $payable['fee'] ?? 0; } } Log::debug('TAXA CALCULADA: ', [ 'valor_taxa' => $totalFeeAmount ]); return $totalFeeAmount / 100; // Converter de centavos para reais } catch (\Exception $e) { Log::error('Erro ao calcular fee amount (timeout possível): ' . $e->getMessage(), [ 'charge_id' => $chargeId, 'error_type' => get_class($e) ]); // Retornar valor padrão para não travar o processamento return 0.00; } } /** * Atualiza ou cria o registro de hired_course */ private function updateHiredCourse($hiring, $status) { // Se o hiring já tem dados de start/end, usar eles; senão calcular $startDate = $hiring->start ?? date('Y-m-d'); $endDate = $hiring->end; // Se não tem end date, calcular baseado no plano if (!$endDate) { $course = DB::table('courses') ->join('course_plans', 'courses.id', '=', 'course_plans.course_id') ->join('plans', 'course_plans.plan_id', '=', 'plans.id') ->join('durations', 'plans.duration_id', '=', 'durations.id') ->where('courses.id', $hiring->course_id) ->where('course_plans.plan_id', $hiring->plan_id) ->select('durations.duration') ->first(); if ($course) { $addDate = '+' . $course->duration . ' months'; $endDate = date('Y-m-d 23:59:59', strtotime($addDate, strtotime($startDate))); Log::info('End date calculado para hired_course', [ 'hiring_id' => $hiring->id, 'duration_months' => $course->duration, 'end_date' => $endDate ]); } } $hiredCourseData = [ 'user_id' => $hiring->user_id, 'course_id' => $hiring->course_id, 'plan_id' => $hiring->plan_id, 'hirings_id' => $hiring->id, 'status' => $this->translateStatus($status), 'start' => $startDate, 'end' => $endDate, 'is_free' => false ]; $existingHiredCourse = $this->hiredCourse->findWhere([ ['user_id', '=', $hiring->user_id], ['course_id', '=', $hiring->course_id] ])->first(); if ($existingHiredCourse) { $existingHiredCourse->fill($hiredCourseData)->save(); Log::info('Hired course atualizado', [ 'hired_course_id' => $existingHiredCourse->id, 'end_date' => $endDate ]); } else { $newHiredCourse = $this->hiredCourse->create($hiredCourseData); Log::info('Hired course criado', [ 'hired_course_id' => $newHiredCourse->id, 'end_date' => $endDate ]); } } /** * Helper para traduzir status do gateway para os códigos da tabela hirings (vendas avulsas) * @param string $status * @return int */ private function translateStatus($status) { switch ($status) { case 'paid': return 3; // pedido pago case 'created': case 'pending': case 'waiting_payment': return 1; // pedido criado ou aguardando pagamento case 'canceled': return 7; // cancelado case 'refunded': return 6; // estornado case 'awaiting_refund': return 5; // aguardando estorno case 'failed': return 2; // falha no pagamento (mantendo código existente) default: return 0; // status default } } /** * Helper para traduzir status do gateway para os códigos da tabela hire_subscriptions (assinaturas) * @param string $status * @return int */ private function translateSubscriptionStatus($status) { switch ($status) { case 'paid': return 1; // pago case 'created': case 'pending': case 'waiting_payment': return 2; // aguardando case 'suspended': return 3; // suspenso case 'canceled': return 4; // cancelado case 'failed': return 2; // aguardando (volta para aguardando em caso de falha) default: return 2; // aguardando (status default) } } /** * Retorna descrição textual do status de assinatura * @param int $statusCode * @return string */ private function getSubscriptionStatusDescription($statusCode) { switch ($statusCode) { case 1: return 'pago'; case 2: return 'aguardando'; case 3: return 'suspenso'; case 4: return 'cancelado'; default: return 'status desconhecido'; } } /** * Reenvio de segunda via de boleto - API v5 * * @param string $email * @param string $paymentCode Order ID ou Charge ID * @return array */ public function collectPayment(string $email, string $paymentCode) { try { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // Primeiro, tentar buscar como Order ID $orderUrl = "{$baseUrl}/orders/{$paymentCode}"; $orderResult = $this->makeApiRequest($orderUrl, $headers, 'GET'); $boletoUrl = null; $orderData = null; if ($orderResult && !isset($orderResult['errors'])) { // É um Order ID válido $orderData = $orderResult; // Procurar por cobrança de boleto no pedido if (isset($orderData['charges']) && is_array($orderData['charges'])) { foreach ($orderData['charges'] as $charge) { if (isset($charge['payment_method']) && $charge['payment_method'] === 'boleto') { if (isset($charge['last_transaction']['url'])) { $boletoUrl = $charge['last_transaction']['url']; break; } } } } } else { // Se falhou como Order, tentar como Charge ID $chargeUrl = "{$baseUrl}/charges/{$paymentCode}"; $chargeResult = $this->makeApiRequest($chargeUrl, $headers, 'GET'); if ($chargeResult && !isset($chargeResult['errors'])) { // É um Charge ID válido if (isset($chargeResult['payment_method']) && $chargeResult['payment_method'] === 'boleto') { if (isset($chargeResult['last_transaction']['url'])) { $boletoUrl = $chargeResult['last_transaction']['url']; // Buscar dados do order para ter informações completas if (isset($chargeResult['order_id'])) { $orderData = $this->makeApiRequest("{$baseUrl}/orders/{$chargeResult['order_id']}", $headers, 'GET'); } } } } } if (!$boletoUrl) { return [ 'success' => false, 'error' => 'Boleto não encontrado ou pagamento não é do tipo boleto', 'message' => 'Não foi possível localizar a URL do boleto para reenvio.' ]; } // Enviar email com a segunda via do boleto $emailSent = $this->sendBoletoEmail($email, $boletoUrl, $orderData); if ($emailSent) { return [ 'success' => true, 'message' => 'Segunda via do boleto enviada com sucesso para ' . $email, 'boleto_url' => $boletoUrl, 'email' => $email ]; } else { return [ 'success' => false, 'error' => 'Erro ao enviar email', 'message' => 'Boleto encontrado, mas houve erro no envio do email.', 'boleto_url' => $boletoUrl ]; } } catch (\Exception $e) { Log::error('Erro no reenvio de boleto v5: ' . $e->getMessage(), [ 'email' => $email, 'payment_code' => $paymentCode, 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'error' => 'Erro interno', 'message' => 'Erro ao processar reenvio de boleto: ' . $e->getMessage() ]; } } /** * Fazer requisição HTTP para API * * @param string $url * @param array $headers * @param string $method * @param array|null $data * @return array|null */ private function makeApiRequest(string $url, array $headers, string $method = 'GET', ?array $data = null) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($method === 'POST' && $data) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { Log::error('Erro na requisição cURL: ' . curl_error($ch)); curl_close($ch); return null; } curl_close($ch); $response = json_decode($result, true); if ($httpCode >= 400) { Log::warning('API retornou erro HTTP: ' . $httpCode, ['response' => $response]); } return $response; } /** * Enviar email com segunda via do boleto * * @param string $email * @param string $boletoUrl * @param array|null $orderData * @return bool */ private function sendBoletoEmail(string $email, string $boletoUrl, ?array $orderData = null) { try { // Preparar dados para o email $data = [ 'boleto_url' => $boletoUrl, 'email' => $email, 'order_data' => $orderData, 'site_name' => config('app.name', 'EstudioLMS'), 'sent_at' => \Carbon\Carbon::now()->format('d/m/Y H:i:s') ]; // Tentar enviar email usando o sistema de emails do Laravel Mail::send('emails.cart.pagarme_boleto_resend', $data, function ($message) use ($email, $data) { $message->to($email) ->subject('Segunda via do boleto'); }); return true; } catch (\Exception $e) { Log::error('Erro ao enviar email de boleto: ' . $e->getMessage(), [ 'email' => $email, 'boleto_url' => $boletoUrl, 'trace' => $e->getTraceAsString() ]); return false; } } /** * @return mixed */ public function balance() { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey ]; $params = http_build_query($data); $recipient = $this->pagarmeRecipient->firstRecord(); $recipientId = $recipient->pagarme_recipient_id; $url = "https://api.pagar.me/1/recipients/" . $recipientId . "/balance?"; $charSet = "UTF-8"; $headers = ["Content-Type: application/json"]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . $params); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); $json = json_decode($result, true); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); return $json; } /** * @return bool|mixed|string */ public function getBalanceOperations() { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $dataCurl = [ 'api_key' => $apiKey, 'count' => 1000, 'page' => 1 ]; $recipient = $this->pagarmeRecipient->firstRecord(); $recipientId = $recipient->pagarme_recipient_id; $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => "https://api.pagar.me/1/recipients/$recipientId/balance/operations", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30000, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_POSTFIELDS => json_encode($dataCurl), CURLOPT_HTTPHEADER => array( "content-type: application/json", ), )); $response = curl_exec($curl); $response = json_decode($response, true); return $response; } /** * @return bool|mixed|string */ public function getBalanceSpecificOperation($balanceId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $dataCurl = [ 'api_key' => $apiKey, 'count' => 1000, 'page' => 1 ]; $recipient = $this->pagarmeRecipient->firstRecord(); $recipientId = $recipient->pagarme_recipient_id; $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => "https://api.pagar.me/1/recipients/$recipientId/balance/operations/$balanceId", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30000, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_POSTFIELDS => json_encode($dataCurl), CURLOPT_HTTPHEADER => array( "content-type: application/json", ), )); $response = curl_exec($curl); $response = json_decode($response, true); return $response; } /** * @param $amount * @param $freeInstalments * @param $maxInstalments * @param $interestRate * @return mixed */ public function installments($amount, $freeInstalments, $maxInstalments, $interestRate) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey, 'amount' => $amount, 'free_installments' => $freeInstalments, 'max_installments' => $maxInstalments, 'interest_rate' => $interestRate ]; $url = "https://api.pagar.me/1/transactions/calculate_installments_amount/"; $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30000, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => array( "content-type: application/json", ), )); $response = curl_exec($curl); $response = json_decode($response, true); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); return $response; } /** * Cancela ou estorna uma transação na API v5 do pagar.me * * @param string $chargeId ID da charge na API v5 (ch_xxx ou or_xxx) * @param array|null $bankAccount Dados da conta bancária para estorno (opcional) * @param string $type Tipo de operação: 'cancel' ou 'refund' * @param float|null $amount Valor do estorno (opcional, para estorno parcial) * @return array */ public function cancelTransaction($chargeId, $bankAccount = null, $type = 'refund', $amount = null) { try { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // API v5 unificou cancelamento e estorno em um único endpoint $url = "{$baseUrl}/charges/{$chargeId}"; $method = 'DELETE'; $data = []; // Se valor específico for informado, adicionar ao payload if ($amount !== null) { $data['amount'] = (int)($amount * 100); // Converter para centavos } // Se dados bancários forem fornecidos, adicionar ao payload (necessário para boleto) if (isset($bankAccount)) { $data['bank_account'] = [ 'holder_name' => $bankAccount['legal_name'], 'holder_type' => strlen(preg_replace('/[^0-9]/is', '', $bankAccount['document_number'])) == 11 ? 'individual' : 'company', 'holder_document' => preg_replace('/[^0-9]/is', '', $bankAccount['document_number']), 'bank' => str_pad($bankAccount['bank_id'], 3, '0', STR_PAD_LEFT), 'branch_number' => $bankAccount['agency'], 'branch_check_digit' => $bankAccount['agency_digit'] ?? null, 'account_number' => $bankAccount['account'], 'account_check_digit' => $bankAccount['account_digit'], 'type' => $bankAccount['account_type'] === 'conta_corrente' ? 'checking' : 'savings' ]; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $error = curl_error($ch); curl_close($ch); Log::error('Erro no cancelTransaction v5: ' . $error, [ 'charge_id' => $chargeId, 'type' => $type ]); return [ 'errors' => [['message' => 'Erro de conexão: ' . $error]] ]; } curl_close($ch); $response = json_decode($result, true); if ($httpCode >= 400) { // Tratamento específico para erro de documento diferente do pagador if ($httpCode == 412 && isset($response['message']) && strpos($response['message'], 'documentos distintos ao do pagador') !== false) { Log::warning('Tentativa de estorno com documento diferente do pagador', [ 'charge_id' => $chargeId, 'response' => $response, 'type' => $type ]); return [ 'errors' => [[ 'message' => 'Este boleto não pode ser estornado para documentos distintos ao do pagador.', 'code' => 'document_mismatch' ]] ]; } Log::warning('Erro HTTP no cancelTransaction v5: ' . $httpCode, [ 'charge_id' => $chargeId, 'response' => $response, 'type' => $type, 'method' => $method, 'url' => $url ]); } else { Log::info('Cancelamento/estorno realizado com sucesso', [ 'charge_id' => $chargeId, 'type' => $type, 'http_code' => $httpCode ]); } return $response; } catch (\Exception $e) { Log::error('Exceção no cancelTransaction v5: ' . $e->getMessage(), [ 'charge_id' => $chargeId, 'type' => $type, 'trace' => $e->getTraceAsString() ]); return [ 'errors' => [['message' => 'Erro interno: ' . $e->getMessage()]] ]; } } /** * Busca o charge_id correto para cancelamento/estorno baseado no payment_code * * @param string $paymentCode Payment code da transação (order_id ou transaction_id) * @return string|null */ public function getChargeIdFromPaymentCode($paymentCode) { try { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // Se o payment_code já é um charge_id (ch_), retornar diretamente if (strpos($paymentCode, 'ch_') === 0) { return $paymentCode; } // Se é um order_id (or_), buscar os charges do pedido if (strpos($paymentCode, 'or_') === 0) { $url = "{$baseUrl}/orders/{$paymentCode}"; $result = $this->makeApiRequest($url, $headers, 'GET'); if ($result && isset($result['charges']) && !empty($result['charges'])) { // Retornar o primeiro charge encontrado return $result['charges'][0]['id']; } } // Fallback: tentar buscar diretamente como charge_id $url = "{$baseUrl}/charges/{$paymentCode}"; $result = $this->makeApiRequest($url, $headers, 'GET'); if ($result && isset($result['id'])) { return $result['id']; } Log::warning('Charge ID não encontrado para payment_code: ' . $paymentCode); return null; } catch (\Exception $e) { Log::error('Erro ao buscar charge_id: ' . $e->getMessage(), [ 'payment_code' => $paymentCode, 'trace' => $e->getTraceAsString() ]); return null; } } /** * @param $documentNumber * @return mixed */ public function getBankAccountByDocument($documentNumber) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey, 'count' => 1, 'document_number' => $documentNumber ]; $url = "https://api.pagar.me/1/bank_accounts"; $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30000, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => array( "content-type: application/json", ), )); $response = curl_exec($curl); $response = json_decode($response, true); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); return $response; } /** * @param $transactionId * @return mixed */ public function getRefundTransaction($transactionId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $data = [ 'api_key' => $apiKey, 'transaction_id' => $transactionId ]; $url = "https://api.pagar.me/1/refunds"; $curl = curl_init(); curl_setopt_array($curl, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30000, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => "GET", CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => array( "content-type: application/json", ), )); $response = curl_exec($curl); $response = json_decode($response, true); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); return $response; } /** * Cria um pedido com suas cobranças na API v5 do Pagar.me * * @param array $orderData * @return array Objeto de resposta do pedido */ public function createOrder(array $orderData) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/orders"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($orderData)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao criar pedido: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao criar pedido: '; foreach ($response['errors'] as $field => $errors) { if (is_array($errors)) { foreach ($errors as $error) { $errorMsg .= $error . '; '; } } else { $errorMsg .= $errors . '; '; } } throw new \Exception($errorMsg); } return $response; } /** * Cria um cartão na API v5 do Pagar.me * * @param array $cardData * @return array Objeto do cartão */ public function createCard(array $cardData) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // Preparar dados do cartão $cardPayload = [ 'number' => $cardData['card_number'], 'holder_name' => $cardData['holder_name'], 'exp_month' => $cardData['exp_month'], 'exp_year' => $cardData['exp_year'], 'cvv' => $cardData['cvv'] ]; // Adicionar billing_address se fornecido if (isset($cardData['billing_address']) && !empty($cardData['billing_address'])) { $billingAddress = $cardData['billing_address']; $cardPayload['billing_address'] = [ 'line_1' => $billingAddress['street'] . ', ' . ($billingAddress['street_number'] ?? $billingAddress['number'] ?? ''), 'line_2' => $billingAddress['complement'] ?? $billingAddress['complementary'] ?? '', 'zip_code' => $billingAddress['zipcode'] ?? $billingAddress['zip_code'] ?? '', 'city' => $billingAddress['city'], 'state' => $billingAddress['state'], 'country' => $billingAddress['country'] ?? 'BR' ]; } // Criar o cartão diretamente $url = "{$baseUrl}/customers/{$cardData['customer_id']}/cards"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($cardPayload)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao criar cartão: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); // Log da resposta completa para debug Log::info('Resposta createCard API v5', [ 'response' => $response, 'raw_result' => $result ]); if (isset($response['errors'])) { $errorMsg = 'Erro ao criar cartão: '; foreach ($response['errors'] as $field => $errors) { if (is_array($errors)) { foreach ($errors as $error) { $errorMsg .= $error . '; '; } } else { $errorMsg .= $errors . '; '; } } throw new \Exception($errorMsg); } // Verificar se o ID está presente na resposta if (!isset($response['id'])) { Log::error('ID não encontrado na resposta do cartão', [ 'response' => $response, 'card_data' => $cardData ]); throw new \Exception('ID do cartão não retornado pela API. Resposta: ' . json_encode($response)); } return $response; } /** * Registra um cliente no Pagar.me v5 * * @param object $user * @return array Objeto do cliente */ public function registerCustomer($user) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); // Limpar CPF removendo pontos, traços e outros caracteres não numéricos $cleanCpf = preg_replace('/[^0-9]/is', '', $user->cpf); $customerData = [ 'name' => $user->name, 'email' => $user->email, 'type' => strlen($cleanCpf) == 11 ? 'individual' : 'corporation', 'document' => $cleanCpf, ]; // Adicionar endereço se disponível if (isset($user->address) && $user->address) { $customerData['address'] = [ 'street' => $user->address->street, 'number' => $user->address->number, 'complement' => $user->address->complement, 'neighborhood' => $user->address->neighborhood, 'zip_code' => $user->address->zip_code, 'city' => $user->address->city, 'state' => $user->address->state, 'country' => 'BR' ]; // Adicionar telefone se disponível if ($user->address->phone) { // Extrair DDD e número do telefone $phone = preg_replace('/[^0-9]/', '', $user->address->phone); if (strlen($phone) >= 10) { $ddd = substr($phone, 0, 2); $number = substr($phone, 2); $customerData['phones'] = [ 'mobile_phone' => [ 'country_code' => '55', 'area_code' => $ddd, 'number' => $number ] ]; } } } $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/customers"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($customerData)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao registrar cliente: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao registrar cliente: '; foreach ($response['errors'] as $field => $errors) { if (is_array($errors)) { foreach ($errors as $error) { $errorMsg .= $error . '; '; } } else { $errorMsg .= $errors . '; '; } } throw new \Exception($errorMsg); } return $response; } /** * Obtém os recebíveis de uma cobrança na API v5 * Utiliza o endpoint correto: GET /payables com filtro charge_id * * @param string $chargeId * @return array Lista de recebíveis */ public function getChargePayables($chargeId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // Endpoint correto: /payables com filtro charge_id $queryParams = [ 'charge_id' => $chargeId, 'size' => 100 // Limite suficiente para cobrir todas as parcelas ]; $url = "{$baseUrl}/payables?" . http_build_query($queryParams); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter recebíveis: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter recebíveis: '; foreach ($response['errors'] as $field => $errors) { if (is_array($errors)) { foreach ($errors as $error) { $errorMsg .= $error . '; '; } } else { $errorMsg .= $errors . '; '; } } throw new \Exception($errorMsg); } return $response['data'] ?? []; } /** * Obtém um cliente na API v5 do Pagar.me * * @param string $customerId * @return array Objeto do cliente */ public function getCustomerByID_v5($customerId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; // Prepara a autenticação Basic Auth $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/customers/{$customerId}"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter cliente: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter cliente: '; foreach ($response['errors'] as $field => $errors) { if (is_array($errors)) { foreach ($errors as $error) { $errorMsg .= $error . '; '; } } else { $errorMsg .= $errors . '; '; } } throw new \Exception($errorMsg); } return $response; } /** * Obtém a URL da instância de forma adequada tanto para web quanto para comandos * * @return string */ private function getInstanceUrl() { if (app()->runningInConsole()) { // Em comandos artisan: PRIORITARIAMENTE buscar dados da sessão if (Session::has('planLimite.url') && Session::has('planLimite.domain')) { return Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'); } // Fallback 1: tentar obter da configuração APP_URL $url = env('APP_URL'); if ($url) { $parsedUrl = parse_url($url, PHP_URL_HOST); if ($parsedUrl) { return $parsedUrl; } } // Fallback 2: tentar obter do tenant_id se disponível $tenantId = config('config.tenant_id'); if ($tenantId) { return $tenantId; } // Último recurso para comandos return 'localhost'; } else { // Em contexto web/navegador: PRIORITARIAMENTE usar URL corrente de execução try { $currentHost = request()->getHost(); if ($currentHost && $currentHost !== 'localhost') { return $currentHost; } } catch (\Exception $e) { // Se falhar ao obter request, continuar para fallbacks } // Fallback 1: usar dados da sessão planLimite if (Session::has('planLimite.url') && Session::has('planLimite.domain')) { return Session::get('planLimite.url') . '.' . Session::get('planLimite.domain'); } // Último recurso para contexto web return 'localhost'; } } /** * Cria uma transação de boleto para assinatura usando API v5 * * @param Cart $cart * @param string $subscriptionHash * @param int $userId * @return array Dados da transação */ public function boletoSubscriptionTransaction(Cart $cart, $subscriptionHash = null, $userId = null) { $userId = is_null($userId) ? Auth::user()->id : $userId; $user = $this->userRepository->find($userId); // ROTINA DE SEGURANÇA: Verificar se customer existe na API v5 $customerId = $this->ensureCustomerExistsV5($user); // Calcular preço com validação (mesma lógica do cardSubscriptionTransaction) $grossAmount = $cart->getGrossAmount() ?: 0; $discountAmount = $cart->getDiscountAmount() ?: 0; $shippingAmount = $cart->getShippingAmount() ?: 0; $price = ($grossAmount - $discountAmount) + $shippingAmount; // Validar se o preço é válido if ($price <= 0) { Log::error('Valor inválido para assinatura boleto', [ 'gross_amount' => $grossAmount, 'discount_amount' => $discountAmount, 'shipping_amount' => $shippingAmount, 'final_price' => $price, 'cart_data' => [ 'course_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id'), 'price' => $cart->get('price'), 'name' => $cart->get('name') ] ]); throw new \Exception('Valor da assinatura deve ser maior que R$ 0,01'); } // Determinar se é plano gratuito para split rules $isFree = Session::get('planLimite.is_free') == true; // CORREÇÃO: Calcular valor em centavos de forma consistente $totalCents = round($price * 100); // Mesmo cálculo usado nos splits // Criar split rules para boleto de assinatura $splitRules = $this->createSplitRulesV5($price, $isFree, 'boleto'); $orderData = [ 'items' => [ [ 'amount' => $totalCents, // Usar o mesmo valor calculado nos splits 'description' => 'Assinatura ID #' . $cart->get('course_id'), 'quantity' => 1, 'code' => (string)$cart->get('course_id') ] ], 'customer_id' => $customerId, 'payments' => [ [ 'payment_method' => 'boleto', 'boleto' => [ 'instructions' => 'Pagar até a data de vencimento', 'due_at' => Carbon::now()->addDays(3)->format('Y-m-d\TH:i:s') ], 'split' => $splitRules ] ], 'metadata' => [ 'user_email' => $user->email, 'user_id' => $user->id, 'instance_email' => Session::get('planLimite.email'), 'instance_url' => $this->getInstanceUrl(), 'product_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id'), 'subscription_hash' => $subscriptionHash ] ]; $order = $this->createOrder($orderData); if (!isset($order['charges']) || empty($order['charges'])) { throw new \Exception('Erro ao criar cobrança de boleto'); } $charge = $order['charges'][0]; $transaction = $charge['last_transaction'] ?? null; if (!$transaction || !isset($transaction['url'])) { throw new \Exception('Erro ao gerar boleto'); } // Criar um objeto mock compatível com a API v4 para manter compatibilidade return new class($charge, $transaction, $order) { private $charge; private $transaction; private $order; public function __construct($charge, $transaction, $order) { $this->charge = $charge; $this->transaction = $transaction; $this->order = $order; } public function getId() { return $this->charge['id']; } public function getStatus() { return $this->charge['status']; } public function getPaymentMethod() { return 'boleto'; } public function getBoletoUrl() { return $this->transaction['url']; } public function getMetadata() { return $this->order['metadata']; } // ADICIONADO: Método getOrderId para compatibilidade com comando public function getOrderId() { $orderId = $this->order['id'] ?? null; if (!$orderId) { throw new \Exception('Order ID não encontrado no objeto mock de boleto'); } return $orderId; } }; } /** * Cria uma transação de cartão para assinatura usando API v5 * * @param string $cardId * @param Cart $cart * @param string $subscriptionHash * @param int $userId * @param string $softDescriptor * @return array Dados da transação */ public function cardSubscriptionTransaction($cardId, Cart $cart, $subscriptionHash = null, $userId = null, $softDescriptor = '') { $userId = is_null($userId) ? Auth::user()->id : $userId; $user = $this->userRepository->find($userId); // ROTINA DE SEGURANÇA: Verificar se customer existe na API v5 $customerId = $this->ensureCustomerExistsV5($user); // Calcular preço com validação $grossAmount = $cart->getGrossAmount() ?: 0; $discountAmount = $cart->getDiscountAmount() ?: 0; $shippingAmount = $cart->getShippingAmount() ?: 0; $price = ($grossAmount - $discountAmount) + $shippingAmount; // Validar se o preço é válido if ($price <= 0) { Log::error('Valor inválido para assinatura', [ 'gross_amount' => $grossAmount, 'discount_amount' => $discountAmount, 'shipping_amount' => $shippingAmount, 'final_price' => $price, 'cart_data' => [ 'course_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id'), 'price' => $cart->get('price'), 'name' => $cart->get('name') ] ]); throw new \Exception('Valor da assinatura deve ser maior que R$ 0,01'); } // Determinar se é plano gratuito para split rules $isFree = Session::get('planLimite.is_free') == true; // CORREÇÃO: Calcular valor em centavos de forma consistente $totalCents = round($price * 100); // Mesmo cálculo usado nos splits // Criar split rules para cartão de assinatura $splitRules = $this->createSplitRulesV5($price, $isFree, 'credit_card'); $orderData = [ 'items' => [ [ 'amount' => $totalCents, // Usar o mesmo valor calculado nos splits 'description' => 'Assinatura ID #' . ($cart->get('course_id') ?: 'N/A'), 'quantity' => 1, 'code' => (string)($cart->get('course_id') ?: '') ] ], 'customer_id' => $customerId, 'payments' => [ [ 'payment_method' => 'credit_card', 'credit_card' => [ 'installments' => 1, 'statement_descriptor' => $softDescriptor, 'card_id' => $cardId ], 'split' => $splitRules ] ], 'metadata' => [ 'user_email' => $user->email, 'user_id' => $user->id, 'instance_email' => Session::get('planLimite.email'), 'instance_url' => $this->getInstanceUrl(), 'product_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id'), 'subscription_hash' => $subscriptionHash ] ]; $order = $this->createOrder($orderData); if (!isset($order['charges']) || empty($order['charges'])) { throw new \Exception('Erro ao criar cobrança de cartão'); } $charge = $order['charges'][0]; $transaction = $charge['last_transaction'] ?? null; if (!$transaction) { throw new \Exception('Erro ao processar transação'); } // Criar um objeto mock compatível com a API v4 para manter compatibilidade return new class($charge, $transaction, $order) { private $charge; private $transaction; private $order; public function __construct($charge, $transaction, $order) { $this->charge = $charge; $this->transaction = $transaction; $this->order = $order; } public function getId() { return $this->charge['id']; } public function getStatus() { return $this->charge['status']; } public function getPaymentMethod() { return 'credit_card'; } public function getRefuseReason() { return $this->transaction['acquirer_message'] ?? 'Transação não autorizada'; } public function getMetadata() { return $this->order['metadata']; } // CRÍTICO: Garantir que order_id esteja sempre disponível public function getOrderId() { $orderId = $this->order['id'] ?? null; if (!$orderId) { throw new \Exception('Order ID não encontrado no objeto mock de cartão'); } return $orderId; } }; } /** * Cria uma transação de cartão de crédito em tempo real com processamento completo * * @param string $cardId * @param Cart $cart * @param string|null $subscriptionHash * @param int|null $userId * @param string $softDescriptor * @param bool $realTimeProcessing * @return array */ public function createCreditCardOrderRealTime($cardId, Cart $cart, $subscriptionHash = null, $userId = null, $softDescriptor = '', $realTimeProcessing = true) { Log::info('Iniciando transação cartão de crédito tempo real', ['card_id' => $cardId, 'real_time' => $realTimeProcessing]); $userId = is_null($userId) ? Auth::user()->id : $userId; $user = $this->userRepository->find($userId); // ROTINA DE SEGURANÇA: Verificar se customer existe na API v5 $customerId = $this->getCustomerIdForUser($user); // Preparar dados do pedido $grossAmount = $cart->getGrossAmount(); $discountAmount = $cart->getDiscountAmount(); $shippingAmount = $cart->getShippingAmount(); $installmentInterest = $cart->getInstallmentInterest() ?? 0; $price = ($grossAmount - $discountAmount) + $shippingAmount + $installmentInterest; $installments = $cart->get('installments') ?? 1; if ($price <= 0) { throw new \Exception('Valor da transação deve ser maior que R$ 0,01'); } // Determinar se é plano gratuito para split rules $isFree = Session::get('planLimite.is_free') == true; // CORREÇÃO: Calcular valor em centavos de forma consistente $totalCents = round($price * 100); // Mesmo cálculo usado nos splits // Criar split rules para transação em tempo real $splitRules = $this->createSplitRulesV5($price, $isFree, 'credit_card'); $orderData = [ 'items' => [ [ 'amount' => $totalCents, // Usar o mesmo valor calculado nos splits 'description' => $subscriptionHash ? 'Assinatura ID #' . $cart->get('course_id') : 'Curso ID #' . $cart->get('course_id'), 'quantity' => 1, 'code' => (string)$cart->get('course_id') ] ], 'customer_id' => $customerId, 'payments' => [ [ 'payment_method' => 'credit_card', 'credit_card' => [ 'installments' => $installments, 'statement_descriptor' => $softDescriptor, 'card_id' => $cardId ], 'split' => $splitRules, ], ], 'metadata' => [ 'user_email' => $user->email, 'user_id' => $user->id, 'instance_email' => Session::get('planLimite.email'), 'instance_url' => $this->getInstanceUrl(), 'product_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id') ] ]; if ($subscriptionHash) { $orderData['metadata']['subscription_hash'] = $subscriptionHash; } //dd($orderData); try { // Criar o pedido $order = $this->createOrder($orderData); //dd($order); if (!isset($order['charges']) || empty($order['charges'])) { throw new \Exception('Erro ao criar cobrança de cartão'); } $charge = $order['charges'][0]; $chargeId = $charge['id']; $orderId = $order['id']; Log::info('Pedido criado', ['order_id' => $orderId, 'charge_id' => $chargeId]); if ($realTimeProcessing) { // Aguardar status final em tempo real $finalStatus = $this->waitForCreditCardAuthorization($chargeId, 30); Log::info('Status final obtido', ['status' => $finalStatus, 'charge_id' => $chargeId]); if ($finalStatus === 'paid') { // Processar tudo imediatamente sem aguardar postback $this->processCreditCardRealTime($order, $charge, $cart, $user, $subscriptionHash, $installmentInterest); return [ 'status' => 'approved', 'order' => $order, 'charge' => $charge, 'message' => 'Transação aprovada com sucesso!' ]; } else { $errorMsg = $this->getCreditCardErrorMessage($charge); return [ 'status' => 'rejected', 'order' => $order, 'charge' => $charge, 'message' => $errorMsg, 'reason' => $finalStatus ]; } } else { // Retornar status atual (fallback para postback) return [ 'status' => $charge['status'], 'order' => $order, 'charge' => $charge, 'message' => 'Transação criada, aguardando confirmação via postback' ]; } } catch (\Exception $e) { Log::error('Erro ao criar transação cartão tempo real: ' . $e->getMessage()); throw $e; } } /** * Aguarda autorização do cartão de crédito com polling otimizado * * @param string $chargeId * @param int $timeout * @return string */ private function waitForCreditCardAuthorization($chargeId, $timeout = 30) { $startTime = time(); $maxAttempts = $timeout / 2; // Verificar a cada 2 segundos $attempt = 0; Log::info('Iniciando polling para autorização cartão', ['charge_id' => $chargeId, 'timeout' => $timeout]); while ((time() - $startTime) < $timeout && $attempt < $maxAttempts) { try { $charge = $this->getChargeById($chargeId); $status = $charge['status'] ?? 'processing'; Log::debug('Polling status', ['attempt' => $attempt + 1, 'status' => $status, 'charge_id' => $chargeId]); // Status finais que indicam conclusão do processamento if (in_array($status, ['paid', 'failed', 'canceled', 'pending_refund', 'refunded'])) { Log::info('Status final obtido via polling', ['status' => $status, 'attempts' => $attempt + 1]); return $status; } $attempt++; if ($attempt < $maxAttempts) { sleep(2); // Aguardar 2 segundos antes da próxima tentativa } } catch (\Exception $e) { Log::warning('Erro durante polling: ' . $e->getMessage(), ['attempt' => $attempt + 1]); $attempt++; if ($attempt < $maxAttempts) { sleep(2); } } } Log::warning('Timeout no polling da autorização', ['charge_id' => $chargeId, 'timeout' => $timeout]); return 'timeout'; // Timeout - continuar com postback } /** * Obtém uma cobrança específica da API v5 * * @param string $chargeId * @return array */ public function getChargeById($chargeId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/charges/{$chargeId}"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter cobrança: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter cobrança: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } return $response; } /** * Obtém um pedido específico da API v5 * * @param string $orderId * @return array */ public function getOrderById($orderId) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $url = "{$baseUrl}/orders/{$orderId}"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter pedido: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter pedido: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } return $response; } /** * Obtém dados do boleto a partir de um order_id * * @param string $orderId * @return array|null Retorna dados do boleto ou null se não encontrado */ public function getBoletoDataFromOrder($orderId) { try { // Buscar o pedido $order = $this->getOrderById($orderId); if (!isset($order['charges']) || empty($order['charges'])) { Log::warning('Pedido sem cobranças', ['order_id' => $orderId]); return null; } // Procurar cobrança de boleto foreach ($order['charges'] as $charge) { if (isset($charge['payment_method']) && $charge['payment_method'] === 'boleto') { $chargeId = $charge['id']; $lastTransaction = $charge['last_transaction'] ?? null; if (!$lastTransaction) { Log::warning('Cobrança de boleto sem transação', ['charge_id' => $chargeId]); continue; } // Retornar dados estruturados do boleto return [ 'charge_id' => $chargeId, 'order_id' => $orderId, 'status' => $charge['status'] ?? 'unknown', 'amount' => $charge['amount'] ?? 0, 'paid_amount' => $charge['paid_amount'] ?? 0, 'boleto_url' => $lastTransaction['url'] ?? null, 'barcode' => $lastTransaction['barcode'] ?? null, 'due_at' => $lastTransaction['due_at'] ?? null, 'paid_at' => $lastTransaction['paid_at'] ?? null, 'created_at' => $charge['created_at'] ?? null, 'updated_at' => $charge['updated_at'] ?? null, 'payment_method' => 'boleto', 'gateway_id' => $lastTransaction['gateway_id'] ?? null, 'acquirer_name' => $lastTransaction['acquirer_name'] ?? null, 'acquirer_message' => $lastTransaction['acquirer_message'] ?? null, 'acquirer_return_code' => $lastTransaction['acquirer_return_code'] ?? null ]; } } Log::warning('Nenhuma cobrança de boleto encontrada no pedido', ['order_id' => $orderId]); return null; } catch (\Exception $e) { Log::error('Erro ao obter dados do boleto: ' . $e->getMessage(), [ 'order_id' => $orderId, 'exception' => $e->getTraceAsString() ]); return null; } } /** * Processa transação de cartão de crédito em tempo real (sem postback) * * @param array $order * @param array $charge * @param Cart $cart * @param object $user * @param string|null $subscriptionHash * @param float $installmentInterest */ private function processCreditCardRealTime($order, $charge, $cart, $user, $subscriptionHash = null, $installmentInterest = 0) { try { $chargeId = $charge['id']; $orderId = $order['id']; $paymentMethod = $charge['payment_method']; $cardId = isset($charge['last_transaction']['card']['id']) ? $charge['last_transaction']['card']['id'] : null; Log::info('Processando cartão tempo real SEM cálculo de taxas', [ 'charge_id' => $chargeId, 'order_id' => $orderId, 'is_subscription' => !is_null($subscriptionHash) ]); // NOVA ESTRATÉGIA: NÃO calcular taxas em tempo real devido ao delay da API // Taxas serão calculadas e atualizadas no postback $totalFeeAmount = 0.00; // Será atualizado no postback $grossAmount = $charge['amount'] / 100; $netAmount = $grossAmount; // Inicial = gross, será recalculado no postback Log::info('Taxas definidas para atualização posterior via postback', [ 'gross_amount' => $grossAmount, 'fee_amount_initial' => $totalFeeAmount, 'net_amount_initial' => $netAmount ]); if ($subscriptionHash) { // Processar assinatura $this->processSubscriptionRealTime($order, $charge, $cart, $user, $subscriptionHash, $totalFeeAmount, $netAmount, $cardId); } else { // Processar venda avulsa $this->processSingleTransactionRealTime($order, $charge, $cart, $user, $totalFeeAmount, $netAmount, $cardId, $installmentInterest); } Log::info('Processamento tempo real concluído - taxas serão atualizadas via postback', ['charge_id' => $chargeId]); } catch (\Exception $e) { Log::error('Erro no processamento tempo real: ' . $e->getMessage()); throw $e; } } /** * Processa assinatura em tempo real */ private function processSubscriptionRealTime($order, $charge, $cart, $user, $subscriptionHash, $totalFeeAmount, $netAmount, $cardId) { try { $orderId = $order['id']; $chargeId = $charge['id']; $paymentMethod = $charge['payment_method']; Log::info('Iniciando processamento de assinatura tempo real', [ 'subscription_hash' => $subscriptionHash, 'order_id' => $orderId, 'charge_id' => $chargeId, 'user_id' => $user->id ]); // Buscar assinatura existente $subscription = $this->hireSubscription->findByField('subscription_hash', $subscriptionHash)->first(); if (!$subscription) { Log::error('Assinatura não encontrada para processamento tempo real', [ 'subscription_hash' => $subscriptionHash, 'user_id' => $user->id ]); throw new \Exception('Assinatura não encontrada: ' . $subscriptionHash); } Log::info('Assinatura encontrada', [ 'subscription_id' => $subscription->id, 'current_status' => $subscription->status, 'periodicity_id' => $subscription->periodicity_id ]); // Verificar se a assinatura tem periodicity_id if (!$subscription->periodicity_id) { Log::error('Assinatura sem periodicity_id definido', [ 'subscription_id' => $subscription->id ]); throw new \Exception('Assinatura sem periodicidade definida'); } // Atualizar assinatura existente $hireSubscriptionData = [ 'payment_code' => $orderId, 'payment_type' => $paymentMethod, 'card_id' => $cardId, 'gross_amount' => $charge['amount'] / 100, 'fee_amount' => $totalFeeAmount, 'net_amount' => $netAmount, 'status' => 1 // pago ]; $subscription->fill($hireSubscriptionData); $subscription->save(); Log::info('Assinatura atualizada tempo real', [ 'subscription_id' => $subscription->id, 'payment_code' => $orderId, 'gross_amount' => $hireSubscriptionData['gross_amount'] ]); // Processar recorrências - CRÍTICO: Este é o ponto onde as recorrências devem ser criadas Log::info('Iniciando criação de recorrências para assinatura'); $this->processRecurrencesRealTime($user, $subscription, $order, $charge, $totalFeeAmount, $netAmount, $cardId); Log::info('Processamento de assinatura tempo real concluído com sucesso', [ 'subscription_id' => $subscription->id ]); } catch (\Exception $e) { Log::error('Erro no processamento de assinatura tempo real: ' . $e->getMessage(), [ 'subscription_hash' => $subscriptionHash ?? 'unknown', 'exception' => $e->getTraceAsString() ]); throw $e; } } /** * Processa venda avulsa em tempo real */ private function processSingleTransactionRealTime($order, $charge, $cart, $user, $totalFeeAmount, $netAmount, $cardId, $installmentInterest) { $orderId = $order['id']; $chargeId = $charge['id']; $paymentMethod = $charge['payment_method']; // Obter informações do curso e plano para calcular data de fim $courseId = $cart->get('course_id'); $planId = $cart->get('plan_id'); // Buscar curso com planos e duração $course = DB::table('courses') ->join('course_plans', 'courses.id', '=', 'course_plans.course_id') ->join('plans', 'course_plans.plan_id', '=', 'plans.id') ->join('durations', 'plans.duration_id', '=', 'durations.id') ->where('courses.id', $courseId) ->where('course_plans.plan_id', $planId) ->select('durations.duration') ->first(); if (!$course) { Log::error('Curso ou plano não encontrado para cálculo de data fim', [ 'course_id' => $courseId, 'plan_id' => $planId ]); throw new \Exception('Não foi possível obter informações do curso para calcular período de acesso'); } // Calcular data de início e fim $startDate = Carbon::now()->toDateString(); $addDate = '+' . $course->duration . ' months'; $endDate = date('Y-m-d 23:59:59', strtotime($addDate, strtotime($startDate))); Log::info('Período de acesso calculado', [ 'course_id' => $courseId, 'plan_id' => $planId, 'duration_months' => $course->duration, 'start_date' => $startDate, 'end_date' => $endDate ]); // Criar ou atualizar hiring $grossAmount = $cart->getGrossAmount(); $discountAmount = $cart->getDiscountAmount(); $shippingAmount = $cart->getShippingAmount(); $price = ($grossAmount - $discountAmount) + $shippingAmount + $installmentInterest; $hiringData = [ 'user_id' => $user->id, 'gateway_id' => 'PagarMe', 'payment_code' => $orderId, // Padronização: usar order_id (or_) para todos os tipos 'payment_method' => $paymentMethod, 'gross_amount' => $grossAmount, 'discount_amount' => $discountAmount, 'fee_amount' => $totalFeeAmount, 'shipping_amount' => $shippingAmount, 'extra_amount' => $cart->getExtraAmount(), 'net_amount' => $netAmount, 'shipping_type' => $cart->getShippingCode(), 'status' => 3, // pago 'course_id' => $courseId, 'plan_id' => $planId, 'start' => $startDate, 'end' => $endDate, // Agora calculado corretamente 'coupon_title' => $cart->get('coupon_name'), 'coupon_code' => $cart->get('coupon_code'), 'coupon_discount' => $cart->get('coupon_discount'), 'installments' => $cart->get('installments') ?? 1, 'installment_interest' => $installmentInterest ]; $hiring = $this->hiringServices->store($hiringData); Log::info('Hiring criado tempo real', ['hiring_id' => $hiring['id'], 'end_date' => $endDate]); // Criar hired_course $this->createHiredCourseRealTime($hiring, $cart, $user, $startDate, $endDate); } /** * Cria hired_course em tempo real */ private function createHiredCourseRealTime($hiring, $cart, $user, $startDate, $endDate) { $hiredCourseData = [ 'user_id' => $user->id, 'course_id' => $cart->get('course_id'), 'plan_id' => $cart->get('plan_id'), 'hirings_id' => $hiring['id'], 'status' => 3, // pago 'start' => $startDate, 'end' => $endDate, // Usar data calculada corretamente 'is_free' => false ]; // Verificar se já existe $existingHiredCourse = $this->hiredCourse->findWhere([ ['user_id', '=', $user->id], ['course_id', '=', $cart->get('course_id')] ])->first(); if ($existingHiredCourse) { $existingHiredCourse->fill($hiredCourseData)->save(); Log::info('Hired course atualizado tempo real', [ 'hired_course_id' => $existingHiredCourse->id, 'end_date' => $endDate ]); } else { $newHiredCourse = $this->hiredCourse->create($hiredCourseData); Log::info('Hired course criado tempo real', [ 'hired_course_id' => $newHiredCourse->id, 'end_date' => $endDate ]); } } /** * Processa recorrências em tempo real para assinaturas */ private function processRecurrencesRealTime($user, $subscription, $order, $charge, $totalFeeAmount, $netAmount, $cardId) { try { $today = Carbon::now()->format('Y-m-d'); $subscriptionHash = $subscription->subscription_hash; $orderId = $order['id']; Log::info('Iniciando processamento de recorrências tempo real SEM cálculo de taxas', [ 'subscription_hash' => $subscriptionHash, 'order_id' => $orderId, 'total_fee_amount_initial' => $totalFeeAmount, 'net_amount_initial' => $netAmount ]); $recurring = $this->recurring->getLatestRecurrence($subscriptionHash); $countRecurrences = $this->recurring->countSubscriptionRecurrences($subscriptionHash); Log::info('Estado atual das recorrências', [ 'existing_recurrence' => $recurring ? $recurring->id : null, 'count_recurrences' => $countRecurrences, 'subscription_hash' => $subscriptionHash ]); if ($recurring) { // Atualizar recorrência existente se ainda não foi paga if (in_array($recurring->status, [0, 2, 3])) { $recurring->status = 1; // Pago $recurring->attempts = $recurring->attempts + 1; $recurring->payment_date = $today; $recurring->fee_amount = $totalFeeAmount; $recurring->net_amount = $netAmount; $recurring->payment_code = $orderId; $recurring->save(); Log::info('Recorrência existente atualizada', ['recurring_id' => $recurring->id]); // Gerar próxima recorrência $this->generateNextRecurrence( $user, $order, $charge, $recurring->due_date, 0, // Status pendente 0, 0, $cardId, $subscription->periodicity_id, $countRecurrences + 1 ); Log::info('Próxima recorrência gerada após atualizar existente'); } else { Log::info('Recorrência existente já está paga', ['recurring_id' => $recurring->id, 'status' => $recurring->status]); } } else { // Não existe recorrência, criar primeira (paga) e próxima (pendente) Log::info('Nenhuma recorrência existente - criando primeira e próxima'); $retroDate = Carbon::parse($subscription->created_at)->format('Y-m-d'); Log::info('Dados para criação de recorrências', [ 'retro_date' => $retroDate, 'periodicity_id' => $subscription->periodicity_id, 'subscription_id' => $subscription->id, 'user_id' => $user->id ]); // Criar primeira recorrência já paga Log::info('Criando primeira recorrência (paga)...'); $firstRecurrence = $this->generateFirstRecurrence($user, $order, $charge, $retroDate, 1, $totalFeeAmount, $netAmount, $cardId); if ($firstRecurrence) { Log::info('✅ Primeira recorrência criada com sucesso', [ 'recurrence_id' => $firstRecurrence->id, 'status' => $firstRecurrence->status, 'payment_code' => $firstRecurrence->payment_code ]); } else { Log::error('❌ Falha na criação da primeira recorrência'); throw new \Exception('Não foi possível criar a primeira recorrência'); } // OTIMIZADO: Reduzir delay para evitar timeout usleep(100000); // 100ms em vez de 1 segundo // Criar próxima recorrência pendente Log::info('Criando próxima recorrência (pendente)...'); $nextRecurrence = $this->generateNextRecurrence( $user, $order, $charge, $retroDate, 0, // Status pendente 0, 0, $cardId, $subscription->periodicity_id, 2 // Número da recorrência ); if ($nextRecurrence) { Log::info('✅ Próxima recorrência criada com sucesso', [ 'recurrence_id' => $nextRecurrence->id, 'due_date' => $nextRecurrence->due_date, 'charge_number' => $nextRecurrence->charge_number ]); } else { Log::error('❌ Falha na criação da próxima recorrência'); // Não é crítico se a próxima recorrência falhar, mas deve ser logado } Log::info('🎉 Recorrências inicial e próxima criadas tempo real com sucesso'); } // Verificar se as recorrências foram criadas corretamente $finalCount = $this->recurring->countSubscriptionRecurrences($subscriptionHash); Log::info('Verificação final de recorrências', [ 'subscription_hash' => $subscriptionHash, 'final_count' => $finalCount, 'initial_count' => $countRecurrences ]); } catch (\Exception $e) { Log::error('Erro ao processar recorrências tempo real: ' . $e->getMessage(), [ 'subscription_hash' => $subscriptionHash ?? 'unknown', 'exception' => $e->getTraceAsString() ]); throw $e; } } /** * Obtém customer_id para um usuário com verificação de segurança v5 */ private function getCustomerIdForUser($user) { // ROTINA DE SEGURANÇA: Garantir que o customer existe na API v5 return $this->ensureCustomerExistsV5($user); } /** * Obtém mensagem de erro específica para cartão de crédito */ private function getCreditCardErrorMessage($charge) { $lastTransaction = $charge['last_transaction'] ?? []; $acquirerMessage = $lastTransaction['acquirer_message'] ?? null; $gatewayResponse = $lastTransaction['gateway_response'] ?? null; if ($acquirerMessage) { return $acquirerMessage; } if ($gatewayResponse && isset($gatewayResponse['errors'])) { $errors = $gatewayResponse['errors']; if (is_array($errors) && !empty($errors)) { return $errors[0]['message'] ?? 'Transação não autorizada'; } } return 'Transação não autorizada pelo banco emissor'; } /** * Verifica se deve usar processamento em tempo real baseado na configuração */ public function shouldUseRealTimeProcessing($paymentMethod) { // Configuração que pode ser movida para config ou banco de dados $realTimeEnabled = config('pagar_me.REAL_TIME_PROCESSING', true); $realTimeMethods = config('pagar_me.REAL_TIME_METHODS', [ 'credit_card' => true, 'pix' => false, 'boleto' => false ]); return $realTimeEnabled && isset($realTimeMethods[$paymentMethod]) && $realTimeMethods[$paymentMethod]; } /** * Cria as regras de split (rateio) para API v5 do Pagar.me * * @param float $price Valor total da transação * @param bool $isFree Se é plano gratuito * @param string $paymentMethod Método de pagamento (credit_card, boleto) * @return array Array com as regras de split */ private function createSplitRulesV5($price, $isFree = false, $paymentMethod = 'credit_card') { $recipient = $this->pagarmeRecipient->firstRecord(); if (!$recipient) { throw new \Exception('Recipient do cliente não encontrado para split rules'); } // Percentuais baseados na API v4 if ($paymentMethod === 'boleto') { $adminPerc = $isFree ? 3 : 2; } else { $adminPerc = $isFree ? 2.4 : 1.4; } // CORREÇÃO: Calcular tudo em centavos para evitar problemas de arredondamento $totalCents = round($price * 100); // Total em centavos, arredondado // Calcular valor admin em centavos $adminValueCents = round((($adminPerc * $totalCents) / 100)); if ($paymentMethod === 'credit_card') { $adminValueCents += 100; // Taxa fixa de R$ 1,00 para cartão } // Garantir que o cliente receba o restante exato $valueClientCents = $totalCents - $adminValueCents; // Validar se os valores são positivos if ($valueClientCents <= 0 || $adminValueCents <= 0) { throw new \Exception('Valores de split inválidos: Cliente=' . $valueClientCents . ', Admin=' . $adminValueCents); } // Validar se a soma bate com o total if (($valueClientCents + $adminValueCents) !== $totalCents) { throw new \Exception('Soma dos splits não confere com o total: Split=' . ($valueClientCents + $adminValueCents) . ', Total=' . $totalCents); } Log::info('Split rules calculados v2', [ 'price' => $price, 'payment_method' => $paymentMethod, 'is_free' => $isFree, 'admin_perc' => $adminPerc, 'total_cents' => $totalCents, 'admin_value_cents' => $adminValueCents, 'value_client_cents' => $valueClientCents, 'sum_check' => ($valueClientCents + $adminValueCents), 'sum_matches' => ($valueClientCents + $adminValueCents) === $totalCents ]); return [ [ 'recipient_id' => $recipient->pagarme_recipient_id, 'type' => 'flat', 'amount' => $valueClientCents, 'options' => [ 'charge_processing_fee' => true, 'charge_remainder_fee' => true, 'liable' => true ] ], [ 'recipient_id' => config('pagar_me.PAGAR_ME_RECIPIENT_ID'), 'type' => 'flat', 'amount' => $adminValueCents, 'options' => [ 'charge_processing_fee' => false, 'charge_remainder_fee' => false, 'liable' => false ] ] ]; } /** * Cria um pedido (order) com split rules incluídos * * @param array $orderData Dados básicos do pedido * @param float $price Valor total da transação * @param string $paymentMethod Método de pagamento (credit_card, boleto) * @param bool $includeSplitRules Se deve incluir split rules automaticamente * @return array Resposta da API */ public function createOrderWithSplitRules(array $orderData, $price, $paymentMethod = 'boleto', $includeSplitRules = true) { if ($includeSplitRules && $price > 0) { // Determinar se é plano gratuito $isFree = Session::get('planLimite.is_free') == true; // Criar split rules $splitRules = $this->createSplitRulesV5($price, $isFree, $paymentMethod); // Adicionar split rules dentro do array payments (conforme documentação) if (isset($orderData['payments']) && !empty($orderData['payments'])) { foreach ($orderData['payments'] as &$payment) { $payment['split'] = $splitRules; } } Log::info('Split rules adicionados ao pagamento', [ 'payment_method' => $paymentMethod, 'price' => $price, 'is_free' => $isFree, 'split_rules_count' => count($splitRules) ]); } return $this->createOrder($orderData); } /** * Obtém o saldo do recipient usando API v5 * * @return array */ public function balanceV5() { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $recipient = $this->pagarmeRecipient->firstRecord(); $recipientId = $recipient->pagarme_recipient_id; $url = "{$baseUrl}/recipients/{$recipientId}/balance"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter saldo: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter saldo: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } return $response; } /** * Obtém as operações de saldo do recipient usando API v5 * * @return array */ public function getBalanceOperationsV5() { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; $recipient = $this->pagarmeRecipient->firstRecord(); $recipientId = $recipient->pagarme_recipient_id; $url = "{$baseUrl}/balance/operations?recipient_id={$recipientId}&status=transferred"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter operações de saldo: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter operações de saldo: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } return $response['data'] ?? []; } /** * Obtém os recebíveis (payables) do recipient usando API v5 * * @param array $filters Filtros opcionais (recipient_id, charge_id, status, etc.) * @param int $page Página para paginação * @param int $size Quantidade de itens por página * @return array */ public function getPayablesV5($filters = [], $page = 1, $size = 50) { $apiKey = config('pagar_me.PAGAR_ME_API_KEY'); $baseUrl = 'https://api.pagar.me/core/v5'; $auth = base64_encode($apiKey . ":"); $headers = [ 'Content-Type: application/json', 'Authorization: Basic ' . $auth ]; // Construir query string com filtros $queryParams = [ 'page' => $page, 'size' => $size ]; // Adicionar recipient_id se não estiver nos filtros if (!isset($filters['recipient_id'])) { $recipient = $this->pagarmeRecipient->firstRecord(); $queryParams['recipient_id'] = $recipient->pagarme_recipient_id; } // Mesclar com filtros adicionais $queryParams = array_merge($queryParams, $filters); $url = "{$baseUrl}/payables?" . http_build_query($queryParams); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $result = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('Erro ao obter payables: ' . curl_error($ch)); } curl_close($ch); $response = json_decode($result, true); if (isset($response['errors'])) { $errorMsg = 'Erro ao obter payables: '; foreach ($response['errors'] as $error) { $errorMsg .= $error['message'] . '; '; } throw new \Exception($errorMsg); } return $response; } /** * Garante que o customer existe na API v5 do Pagar.me * Se não existir (customer da API v4), cria um novo e atualiza na tabela * * @param object $user * @return string customer_id válido para API v5 */ public function ensureCustomerExistsV5($user) { try { Log::info('Verificando existência do customer na API v5', ['user_id' => $user->id]); // Carregar relacionamento customer se não estiver carregado if (!$user->relationLoaded('customer')) { $user->load('customer'); } $customerRecord = $user->customer()->first(); if (!$customerRecord) { Log::info('Nenhum customer encontrado - criando novo na API v5', ['user_id' => $user->id]); return $this->createNewCustomerV5($user); } $existingCustomerId = $customerRecord->customer_id; Log::info('Customer encontrado na tabela - verificando na API v5', [ 'user_id' => $user->id, 'customer_id' => $existingCustomerId ]); // Tentar buscar o customer na API v5 try { $customerData = $this->getCustomerByID_v5($existingCustomerId); if ($customerData && isset($customerData['id'])) { Log::info('Customer existe na API v5 - tudo OK', [ 'user_id' => $user->id, 'customer_id' => $existingCustomerId ]); return $existingCustomerId; } } catch (\Exception $e) { Log::warning('Customer não encontrado na API v5 - migrando da v4', [ 'user_id' => $user->id, 'old_customer_id' => $existingCustomerId, 'error' => $e->getMessage() ]); } // Se chegou aqui, o customer não existe na API v5 (é da v4) // Criar novo customer na API v5 e atualizar na tabela $newCustomerId = $this->createNewCustomerV5($user, $customerRecord); Log::info('Customer migrado com sucesso da v4 para v5', [ 'user_id' => $user->id, 'old_customer_id' => $existingCustomerId, 'new_customer_id' => $newCustomerId ]); return $newCustomerId; } catch (\Exception $e) { Log::error('Erro crítico na verificação do customer v5: ' . $e->getMessage(), [ 'user_id' => $user->id, 'exception' => $e->getTraceAsString() ]); throw new \Exception('Não foi possível validar customer para pagamento: ' . $e->getMessage()); } } /** * Cria um novo customer na API v5 e atualiza/cria registro na tabela * * @param object $user * @param object|null $existingCustomerRecord * @return string novo customer_id */ private function createNewCustomerV5($user, $existingCustomerRecord = null) { try { Log::info('Criando novo customer na API v5', ['user_id' => $user->id]); // Criar customer na API v5 $customerData = $this->registerCustomer($user); if (!isset($customerData['id'])) { throw new \Exception('API v5 não retornou ID do customer criado'); } $newCustomerId = $customerData['id']; Log::info('Customer criado na API v5', [ 'user_id' => $user->id, 'new_customer_id' => $newCustomerId ]); // Atualizar ou criar registro na tabela customers if ($existingCustomerRecord) { // Atualizar registro existente $existingCustomerRecord->customer_id = $newCustomerId; $existingCustomerRecord->save(); Log::info('Registro customer atualizado na tabela', [ 'user_id' => $user->id, 'customer_record_id' => $existingCustomerRecord->id, 'new_customer_id' => $newCustomerId ]); } else { // Criar novo registro na tabela $newCustomerRecord = $this->userRepository->find($user->id)->customer()->create([ 'customer_id' => $newCustomerId, 'card_id' => null, 'hash' => null ]); Log::info('Novo registro customer criado na tabela', [ 'user_id' => $user->id, 'customer_record_id' => $newCustomerRecord->id, 'new_customer_id' => $newCustomerId ]); } return $newCustomerId; } catch (\Exception $e) { Log::error('Erro ao criar novo customer v5: ' . $e->getMessage(), [ 'user_id' => $user->id, 'exception' => $e->getTraceAsString() ]); throw new \Exception('Falha ao criar customer na API v5: ' . $e->getMessage()); } } /** * Obtém payables para API v5 usando order_id * * @param string $paymentCode Order ID da API v5 * @return array|null */ public function getPayablesForPaymentCode($paymentCode) { try { return $this->getPayablesFromOrderV5($paymentCode); } catch (\Exception $e) { Log::error('Erro ao obter payables para payment_code: ' . $e->getMessage(), [ 'payment_code' => $paymentCode ]); return null; } } /** * Obtém payables a partir de um order_id da API v5 * * @param string $orderId * @return array|null */ private function getPayablesFromOrderV5($orderId) { try { // Buscar o pedido $order = $this->getOrderById($orderId); if (!isset($order['charges'])) { Log::warning('Pedido sem cobranças', ['order_id' => $orderId]); return null; } // Buscar a primeira cobrança $charge = $order['charges'][0]; $chargeId = $charge['id']; // Obter payables da cobrança $payables = $this->getChargePayables($chargeId); // Adaptar formato para compatibilidade com código existente $adaptedPayables = []; foreach ($payables as $payable) { $adaptedPayables[] = [ 'id' => $payable['id'], 'amount' => $payable['amount'], // Já em centavos na v5 'fee' => $payable['fee'], // Já em centavos na v5 'status' => $payable['status'], 'payment_date' => $payable['payment_date'], 'recipient_id' => $payable['recipient_id'], 'installment' => $payable['installment'] ?? null ]; } return $adaptedPayables; } catch (\Exception $e) { Log::error('Erro ao obter payables do order v5: ' . $e->getMessage(), [ 'order_id' => $orderId ]); return null; } } }
Copyright © 2026 - UnknownSec