diff --git a/.gitignore b/.gitignore index 3017b2f..49fad9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /.buildpath -.idea/ /composer.phar /vendor/ /private diff --git a/README.md b/README.md index 5e2d0a1..2e2c72b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ APInter-PHP =========== -Biblioteca para a utilização das APIs fornecidas pelo Banco Inter (077). +Projeto com o início de uma biblioteca para a utilização das API fornecidas pelo Banco Inter. -Suporte a boletos (cobrança), extrato, saldo e webhook de boletos. -Não tem suporte a pagamentos DARF ou agendamento de pagamentos de boletos. +Inicialmente apenas a criação de boletos é suportada. Como usar: ---------- @@ -47,6 +46,13 @@ Facilitou sua vida? Se o código do projeto ajudou você em uma tarefa complexa, considere fazer uma doação ao autor pelo PIX abaixo. -![image](https://github.com/allgood/APInter-PHP/assets/6070736/00958122-b35f-47ea-910e-476cb341bd24) +![image](https://user-images.githubusercontent.com/6070736/116247400-317e3680-a741-11eb-9434-9f226eec39b5.png) Chave Pix: 80fd8916-1131-4844-917e-2732eaa2ba74 + +Propaganda do Autor: +-------------------- + +Não relacionado à essa biblioteca diretamente, mas fazendo uso da mesma API, desenvolvi um app para Android que emite e gerencia boletos do Banco Inter. A aplicação é de código fechado e com o objetivo de ser completamente grátis, mantida por publicidade. + +[Para instalar é só clicar aqui!](https://play.google.com/store/apps/details?id=dev.todobom.interbill) diff --git a/src/BancoInter.php b/src/BancoInter.php index bfb3b6d..3040a7c 100644 --- a/src/BancoInter.php +++ b/src/BancoInter.php @@ -95,15 +95,18 @@ public function __construct( string $certificateFile, string $keyFile, TokenRequest $tokenRequest, - #[Deprecated] - array $oAuthTokenData = null + ?array $oAuthTokenData = null ) { $this->accountNumber = $accountNumber; $this->certificateFile = $certificateFile; $this->keyFile = $keyFile; $this->tokenRequest = $tokenRequest; - if ($oAuthTokenData) { - $this->importOAuthToken($oAuthTokenData); + if ($oAuthTokenData !== null) { + trigger_error('Parâmetro $oAuthTokenData está deprecado, use callbacks.', E_USER_DEPRECATED); + if (is_array($oAuthTokenData) + && isset($oAuthTokenData['access_token'], $oAuthTokenData['expires_in'], $oAuthTokenData['timestamp'])) { + $this->importOAuthToken($oAuthTokenData); + } } } @@ -124,7 +127,7 @@ public function getOAuthToken() */ public function exportOAuthToken() { - $this->checkOauthToken(false); + $this->checkOAuthToken(false); return([ "access_token" => $this->oAuthToken, "expires_in" => $this->tokenExpiresIn, @@ -192,7 +195,14 @@ private function controllerInit(array $http_params) curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_SSLCERT, $this->certificateFile); curl_setopt($curl, CURLOPT_SSLKEY, $this->keyFile); - curl_setopt($curl, CURLOPT_CAPATH, "/etc/ssl/certs/"); + if (DIRECTORY_SEPARATOR === '\\') { + $cainfo = 'C:\\xampp\\apache\\bin\\curl-ca-bundle.crt'; + if (file_exists($cainfo)) { + curl_setopt($curl, CURLOPT_CAINFO, $cainfo); + } + } else { + curl_setopt($curl, CURLOPT_CAPATH, "/etc/ssl/certs/"); + } if ($this->keyPassword) { curl_setopt($curl, CURLOPT_KEYPASSWD, $this->keyPassword); } @@ -203,6 +213,8 @@ private function controllerInit(array $http_params) $this->curl = $curl; } + + /** * * @param string $url @@ -214,9 +226,8 @@ private function controllerInit(array $http_params) public function controllerPost( string $url, \JsonSerializable $data, - array $http_params = null, - bool $postJson = true, - bool $methodPut = false + ?array $http_params = null, + bool $postJson = true ) { if ($http_params == null) { @@ -228,6 +239,8 @@ public function controllerPost( if (!($data instanceof TokenRequest)) { $this->checkOAuthToken(); + $http_params[] = 'x-inter-conta-corrente: ' . $this->accountNumber; + $http_params[] = 'x-conta-corrente: ' . $this->accountNumber; } if ($this->oAuthToken) { @@ -245,14 +258,7 @@ public function controllerPost( $this->controllerInit($http_params); curl_setopt($this->curl, CURLOPT_URL, $this->apiBaseURL . $url); - - if ($methodPut) { - curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT'); - } else { - curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST'); - } - - + curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($this->curl, CURLOPT_POSTFIELDS, $prepared_data); $curlReply = curl_exec($this->curl); @@ -280,12 +286,12 @@ public function controllerPost( } if ($http_code < 200 || $http_code > 299) { - throw new BancoInterException("Erro HTTP " . $http_code, $http_code, $reply); + throw new BancoInterException("Erro HTTP " . $http_code, $http_code, $reply, $this->apiBaseURL . $url); } return $reply; } - public function controllerGet(string $url, array $http_params = null) + public function controllerGet(string $url, ?array $http_params = null) { if ($http_params == null) { @@ -297,6 +303,8 @@ public function controllerGet(string $url, array $http_params = null) $this->checkOAuthToken(); $http_params[] = 'Authorization: Bearer ' . $this->oAuthToken; + $http_params[] = 'x-inter-conta-corrente: ' . $this->accountNumber; + $http_params[] = 'x-conta-corrente: ' . $this->accountNumber; $retry = 5; while ($retry > 0) { @@ -330,7 +338,7 @@ public function controllerGet(string $url, array $http_params = null) } if ($http_code < 200 || $http_code > 299) { - throw new BancoInterException("Erro HTTP " . $http_code, $http_code, $reply); + throw new BancoInterException("Erro HTTP " . $http_code, $http_code, $reply, $this->apiBaseURL . $url); } return $reply; @@ -347,25 +355,68 @@ public function createBoleto(Boleto $boleto): Boleto // garante que o boleto tem um controller $boleto->setController($this); - $reply = $this->controllerPost("/cobranca/v2/boletos", $boleto); + $reply = $this->controllerPost("/cobranca/v3/cobrancas", $boleto); + + // DEBUG TEMPORÁRIO + // file_put_contents('debug_api_response.txt', $reply->body); $replyData = json_decode($reply->body); - $boleto->setNossoNumero($replyData->nossoNumero); - $boleto->setCodigoBarras($replyData->codigoBarras); - $boleto->setLinhaDigitavel($replyData->linhaDigitavel); + $boleto->setNossoNumero($replyData->nossoNumero ?? ($replyData->numero ?? null)); + $boleto->setCodigoBarras($replyData->codigoBarras ?? null); + $boleto->setLinhaDigitavel($replyData->linhaDigitavel ?? null); + if (is_object($replyData) && property_exists($replyData, 'codigoSolicitacao')) { + $boleto->setCodigoSolicitacao($replyData->codigoSolicitacao); + } + if (is_object($replyData) && property_exists($replyData, 'pixCopiaECola')) { + $boleto->setPixCopiaECola($replyData->pixCopiaECola); + } elseif (is_object($replyData) && property_exists($replyData, 'qrCode')) { + $boleto->setPixCopiaECola($replyData->qrCode); + } + + // MELHORIA PARA API V3: + // Se a API retornou apenas o UUID (codigoSolicitacao) mas não os dados do boleto, + // consultamos imediatamente para completar o objeto Boleto. + if ( empty($boleto->getNossoNumero()) && !empty($boleto->getCodigoSolicitacao()) ) { + try { + // Pequena pausa para garantir processamento no lado do banco + sleep(1); + + $detalhes = $this->getBoleto($boleto->getCodigoSolicitacao()); + + // Tratamento para estrutura da API V3 onde os dados do boleto ficam dentro de um objeto 'boleto' + $dadosBoleto = isset($detalhes->boleto) ? $detalhes->boleto : $detalhes; + + if (isset($dadosBoleto->nossoNumero)) { + $boleto->setNossoNumero($dadosBoleto->nossoNumero); + } + if (isset($dadosBoleto->codigoBarras)) { + $boleto->setCodigoBarras($dadosBoleto->codigoBarras); + } + if (isset($dadosBoleto->linhaDigitavel)) { + $boleto->setLinhaDigitavel($dadosBoleto->linhaDigitavel); + } + + // Tratamento para PIX se disponível + if (isset($detalhes->pix) && isset($detalhes->pix->pixCopiaECola)) { + $boleto->setPixCopiaECola($detalhes->pix->pixCopiaECola); + } + } catch (\Exception $e) { + // Silencia erro na recuperação extra + } + } return $boleto; } /** * - * @param string $nossoNumero + * @param string $codigoSolicitacao UUID do boleto (Código de Solicitação) * @return \stdClass */ - public function getBoleto(string $nossoNumero): \stdClass + public function getBoleto(string $codigoSolicitacao): \stdClass { - $reply = $this->controllerGet("/cobranca/v2/boletos/" . $nossoNumero); + $reply = $this->controllerGet("/cobranca/v3/cobrancas/" . $codigoSolicitacao); $replyData = json_decode($reply->body); @@ -375,23 +426,23 @@ public function getBoleto(string $nossoNumero): \stdClass /** * Faz download do PDF do boleto * - * @param string $nossoNumero + * @param string $codigoSolicitacao UUID do boleto (Código de Solicitação) * @param string $savePath Pasta a salvar o arquivo (default para a pasta de upload ou tmp) * @throws BancoInterException * @return string Caminho completo do arquivo baixado */ - public function getPdfBoleto(string $nossoNumero, string $savePath = null): string + public function getPdfBoleto(string $codigoSolicitacao, ?string $savePath = null): string { if ($savePath == null) { $savePath = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(); } - $reply = $this->getPdfBoletoBase64($nossoNumero); + $reply = $this->getPdfBoletoBase64($codigoSolicitacao); $filename = tempnam($savePath, "boleto-inter-") . ".pdf"; if (!file_put_contents($filename, base64_decode($reply))) { - throw new BancoInterException("Erro decodificando e salvando PDF", 0, $reply); + throw new BancoInterException("Erro decodificando e salvando PDF", 0, $reply, $this->apiBaseURL . "/cobranca/v3/cobrancas/" . $codigoSolicitacao . "/pdf"); } return $filename; @@ -401,27 +452,34 @@ public function getPdfBoleto(string $nossoNumero, string $savePath = null): stri * Faz download do PDF do boleto e retorna apenas o conteúdo binário * codificado em string base64 * - * @param string $nossoNumero + * @param string $codigoSolicitacao UUID do boleto (Código de Solicitação) * @throws BancoInterException * @return string Conteúdo do PDF codificado em string base64 */ - public function getPdfBoletoBase64(string $nossoNumero): string + public function getPdfBoletoBase64(string $codigoSolicitacao): string { - $reply = $this->controllerGet("/cobranca/v2/boletos/$nossoNumero/pdf"); + $reply = $this->controllerGet("/cobranca/v3/cobrancas/" . $codigoSolicitacao . "/pdf"); if (!$reply->body) { - throw new BancoInterException('Erro ao receber o PDF', 0, $reply); + throw new BancoInterException('Erro ao receber o PDF', 0, $reply, $this->apiBaseURL . "/cobranca/v3/cobrancas/" . $codigoSolicitacao . "/pdf"); } return json_decode($reply->body)->pdf; } - public function baixaBoleto(string $nossoNumero, string $motivo = "ACERTOS") + /** + * Baixa um boleto (Cancela) + * + * @param string $codigoSolicitacao UUID do boleto (Código de Solicitação) + * @param string $motivo Motivo do cancelamento (default: ACERTOS) + * @return mixed + */ + public function baixaBoleto(string $codigoSolicitacao, string $motivo = "ACERTOS") { $data = new StdSerializable(); $data->motivoCancelamento = $motivo; - $reply = $this->controllerPost("/cobranca/v2/boletos/" . $nossoNumero . "/cancelar", $data); + $reply = $this->controllerPost("/cobranca/v3/cobrancas/" . $codigoSolicitacao . "/cancelar", $data); $replyData = json_decode($reply->body); @@ -450,14 +508,14 @@ public function listaBoletos( $inverterOrdem = false ): \stdClass { - $url = "/cobranca/v2/boletos"; + $url = "/cobranca/v3/cobrancas"; $url .= "?dataInicial=" . $dataInicial; $url .= "&dataFinal=" . $dataFinal; if ($filtro) { $url .= "&situacao=" . $filtro; } if ($ordem) { - if (endsWith($ordem, '_DSC')) { + if ((function_exists('str_ends_with') ? str_ends_with($ordem, '_DSC') : substr($ordem, -4) === '_DSC')) { $ordem = str_replace('_DSC', '', $ordem); $inverterOrdem = true; } @@ -525,7 +583,7 @@ public function setTokenLoadCallback(Closure $callback) * @param \DateTime $dataSaldo * @return float */ - public function getSaldo(\DateTime $dataSaldo = null): ?float + public function getSaldo(?\DateTime $dataSaldo = null): ?float { if (!$dataSaldo) { $dataSaldo = new \DateTime(); @@ -557,154 +615,4 @@ public function getExtrato(\DateTime $dataInicio, \DateTime $dataFim): \stdClass return json_decode($reply->body); } - - /** - * Consulta o extrato COMPLETO em um período entre datas específico. Para utilizar esta chamada, - * suas credenciais junto ao Banco Inter precisam ter acesso à permissão "Consulta de extrato - * e saldo", e você precisa declarar o escopo extrato.read ao criar o TokenRequest. - * O extrato completo é paginado (diferente da função extrato) - * - * Referência do extrato completo: https://developers.bancointer.com.br/reference/extratocomplete - * - * @param \DateTime $dataInicio - * @param \DateTime $dataFim - * @param int $pagina Número da página, a primeira página é 0 (zero) - * @param string $tipoOperacao 'C' para crédito, 'D' para débito - * @param string $tipoTransacao PIX, CAMBIO, ESTORNO, etc. - * @return \stdClass - */ - public function getExtratoCompleto( - \DateTime $dataInicio, - \DateTime $dataFim, - int $pagina = 0, - int $tamanhoPagina = 50, - string $tipoOperacao = '', - string $tipoTransacao = '' - ): \stdClass { - $params['dataInicio'] = $dataInicio->format('Y-m-d'); - $params['dataFim'] = $dataFim->format('Y-m-d'); - $params['pagina'] = $pagina; - $params['tamanhoPagina'] = $tamanhoPagina; - $params['tipoOperacao'] = $tipoOperacao; - $params['tipoTransacao'] = $tipoTransacao; - - $url = "/banking/v2/extrato/completo?" . http_build_query($params); - - $reply = $this->controllerGet($url); - - return json_decode($reply->body); - } - - - /** - * Cria o webhook que receberá atualizações automáticos dos boletos (cobranças) - * Referência: https://developers.bancointer.com.br/reference/criarwebhookboleto - * - * @param $url - * @return boolean - */ - - public function createWebhook($webhookUrl): bool - { - $url = "/cobranca/v2/boletos/webhook"; - - $params = new \ctodobom\APInterPHP\StdSerializable(); - - $params->webhookUrl = $webhookUrl; - - //Verifica se a URL do webhook é válida - if (!filter_var($webhookUrl, FILTER_VALIDATE_URL)) { - return false; - } - - try { - $reply = $this->controllerPost($url, $params, null, true, true); - } catch (BancoInterException $e) { - return false; - } - - if ($reply) { - return true; - } else { - return false; - } - } - - /** - * Retorna o webhook cadastrado, se houver - * - * @return string - */ - - public function getWebhook(): string - { - $url = "/cobranca/v2/boletos/webhook"; - - $reply = $this->controllerGet($url); - - return $reply->body; - } - - /** - * Deleta o webhook, se houver. Caso não haja nenhum webhook, retornará o código HTTP 404 - */ - public function deleteWebhook(): string - { - - $url = "/cobranca/v2/boletos/webhook"; - - $reply = $this->controllerDelete($url); - - return $reply->body; - } - - public function controllerDelete( - string $url, - array $http_params = null - ) { - - if ($http_params == null) { - $http_params = array( - 'accept: application/json', - ); - } - - if ($this->oAuthToken) { - $http_params[] = 'Authorization: Bearer ' . $this->oAuthToken; - } - - $retry = 5; - while ($retry > 0) { - $this->controllerInit($http_params); - curl_setopt($this->curl, CURLOPT_URL, $this->apiBaseURL . $url); - - curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); - - $curlReply = curl_exec($this->curl); - if (!$curlReply) { - $curl_error = curl_error($this->curl); - } - $http_code = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); - $header_size = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); - curl_close($this->curl); - $this->curl = null; - - $reply = new \stdClass(); - $reply->header = substr($curlReply, 0, $header_size); - $reply->body = substr($curlReply, $header_size); - - if ($http_code == 503) { - $retry--; - } else { - $retry = 0; - } - } - if ($http_code == 0) { - throw new \Exception("Curl error: " . $curl_error); - } - if ($http_code < 200 || $http_code > 299) { - throw new BancoInterException("Erro HTTP " . $http_code, $http_code, $reply); - } - return $reply; - } } diff --git a/src/BancoInterException.php b/src/BancoInterException.php index e6d0c94..7eab1a3 100644 --- a/src/BancoInterException.php +++ b/src/BancoInterException.php @@ -6,11 +6,13 @@ class BancoInterException extends \Exception { public $http_code; public $reply; + public $url; - public function __construct($message, $http_code, $reply) + public function __construct($message, $http_code, $reply, $url = null) { $this->http_code = $http_code; $this->reply = $reply; + $this->url = $url; parent::__construct($message, 0, null); } diff --git a/src/Cobranca/Boleto.php b/src/Cobranca/Boleto.php index 377405b..62c5aca 100644 --- a/src/Cobranca/Boleto.php +++ b/src/Cobranca/Boleto.php @@ -19,10 +19,13 @@ class Boleto implements \JsonSerializable private $desconto3 = null; private $multa = null; private $mora = null; + private $beneficiarioFinal = null; + private $pixCopiaECola = null; private $nossoNumero = null; private $codigoBarras = null; private $linhaDigitavel = null; + private $codigoSolicitacao = null; private $controller = null; @@ -340,6 +343,36 @@ public function setLinhaDigitavel($linhaDigitavel) $this->linhaDigitavel = $linhaDigitavel; } + public function getCodigoSolicitacao() + { + return $this->codigoSolicitacao; + } + + public function setCodigoSolicitacao($codigo) + { + $this->codigoSolicitacao = $codigo; + } + + public function getBeneficiarioFinal(): ?Pagador + { + return $this->beneficiarioFinal; + } + + public function setBeneficiarioFinal(Pagador $beneficiarioFinal) + { + $this->beneficiarioFinal = $beneficiarioFinal; + } + + public function getPixCopiaECola() + { + return $this->pixCopiaECola; + } + + public function setPixCopiaECola($pix) + { + $this->pixCopiaECola = $pix; + } + /** * * @param BancoInter $controller @@ -361,6 +394,31 @@ public function __construct() public function jsonSerialize(): array { - return get_object_vars($this); + $data = [ + 'seuNumero' => $this->seuNumero, + 'valorNominal' => $this->valorNominal, + 'valorAbatimento' => $this->valorAbatimento, + 'dataVencimento' => $this->dataVencimento, + 'numDiasAgenda' => $this->numDiasAgenda, + 'pagador' => $this->pagador, + 'mensagem' => $this->mensagem + ]; + + if ($this->desconto1->getCodigoDesconto() !== Desconto::NAO_TEM_DESCONTO) { + $data['desconto'] = $this->desconto1; + } + + if ($this->multa->getCodigoMulta() !== Multa::NAO_TEM_MULTA) { + $data['multa'] = $this->multa; + } + + if ($this->mora->getCodigoMora() !== Mora::ISENTO) { + $data['mora'] = $this->mora; + } + + if ($this->beneficiarioFinal) { + $data['beneficiarioFinal'] = $this->beneficiarioFinal; + } + return $data; } } diff --git a/src/Cobranca/Desconto.php b/src/Cobranca/Desconto.php index ce61bc1..ae88941 100644 --- a/src/Cobranca/Desconto.php +++ b/src/Cobranca/Desconto.php @@ -83,6 +83,28 @@ public function setData($data) public function jsonSerialize(): array { - return get_object_vars($this); + if ($this->codigoDesconto == self::NAO_TEM_DESCONTO) { + return [ + 'codigo' => self::PERCENTUAL_FIXO, // "PERCENTUALDATAINFORMADA" + 'taxa' => 0, + 'quantidadeDias' => 0 + ]; + } + + $out = [ + 'codigo' => $this->codigoDesconto, + 'taxa' => $this->taxa, + 'valor' => $this->valor + ]; + + if ($this->data !== '') { + if (is_numeric($this->data)) { + $out['quantidadeDias'] = (int)$this->data; + } else { + $out['data'] = $this->data; + } + } + + return $out; } } diff --git a/src/Cobranca/Mora.php b/src/Cobranca/Mora.php index 70eed9b..5c5596f 100644 --- a/src/Cobranca/Mora.php +++ b/src/Cobranca/Mora.php @@ -79,6 +79,9 @@ public function setData($data) public function jsonSerialize(): array { - return get_object_vars($this); + $out = ['codigo' => $this->codigoMora]; + if ($this->taxa) { $out['taxa'] = $this->taxa; } + if ($this->valor) { $out['valor'] = $this->valor; } + return $out; } } diff --git a/src/Cobranca/Multa.php b/src/Cobranca/Multa.php index e5e714a..a8fdbf3 100644 --- a/src/Cobranca/Multa.php +++ b/src/Cobranca/Multa.php @@ -78,6 +78,9 @@ public function setData($data) public function jsonSerialize(): array { - return get_object_vars($this); + $out = ['codigo' => $this->codigoMulta]; + if ($this->taxa) { $out['taxa'] = $this->taxa; } + if ($this->valor) { $out['valor'] = $this->valor; } + return $out; } }