diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..f3e9474 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,11 @@ +/dist/ +build/ +/logs/ +/log/ +/cache/ +/.git/ +/vendor/ +/node_modules/ +tests/config/env.test.php +tmp/ +error_log diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 835f898..0028fab 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,7 +24,7 @@ jobs: if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install PHP ${{ env.PHP_VERSION }} uses: shivammathur/setup-php@v2 diff --git a/README.md b/README.md index f9a8e6b..5358332 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ SDK de integração eRede +## ⚠️ Atualização Importante - Nova Autenticação + +A partir de **janeiro de 2026**, a Rede implementou um novo método de autenticação baseado em **OAuth2** para aprimorar a segurança das transações. + +**A versão 2.x deste SDK é compatível com o novo método de autenticação OAuth2**, garantindo uma transição suave e segura para os desenvolvedores. + +Para mais detalhes sobre a nova autenticação e migração, consulte a [documentação oficial da e-Rede](https://developer.userede.com.br/e-rede). + ## Funcionalidades Este SDK possui as seguintes funcionalidades: @@ -27,7 +35,7 @@ Se já possui um arquivo `composer.json`, basta adicionar a seguinte dependênci ```json { "require": { - "silbeckdevs/erede-php": "*" + "silbeckdevs/erede-php": "^2.0.0" } } ``` @@ -67,7 +75,13 @@ export REDE_DEBUG=0 Ou copie o arquivo `tests/config/env.test.php.example` para `tests/config/env.test.php` e adicione as suas credenciais -## Autorizando uma transação +## Configuração da loja + +A configuração da loja é feita através da classe `Store`. Ela possui os seguintes parâmetros: + +- `filiation`: Número de filiação do estabelecimento (ClientId na versão 2.x) +- `token`: Chave de Integração (ClientSecret na versão 2.x) +- `environment`: Ambiente da loja (Production ou Sandbox) ```php creditCard( '5448280000000007', @@ -87,7 +108,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( ); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -98,12 +119,6 @@ Por padrão, a transação é capturada automaticamente; caso seja necessário a ```php creditCard( '5448280000000007', @@ -114,7 +129,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( )->capture(false); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -126,12 +141,6 @@ if ($transaction->getReturnCode() == '00') { ```php creditCard( '5448280000000007', @@ -145,7 +154,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( $transaction->setInstallments(3); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -156,12 +165,6 @@ if ($transaction->getReturnCode() == '00') { ```php creditCard( '5448280000000007', @@ -172,7 +175,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( )->additional(1234, 56); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -183,12 +186,6 @@ if ($transaction->getReturnCode() == '00') { ```php creditCard( '5448280000000007', @@ -207,7 +204,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( ); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -219,12 +216,6 @@ if ($transaction->getReturnCode() == '00') { ```php creditCard( '5448280000000007', @@ -235,7 +226,7 @@ $transaction = (new Transaction(20.99, 'pedido' . time()))->creditCard( )->iata('code123', '250'); // Autoriza a transação -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf("Transação autorizada com sucesso; tid=%s\n", $transaction->getTid()); @@ -246,14 +237,8 @@ if ($transaction->getReturnCode() == '00') { ```php capture((new Transaction(20.99))->setTid('TID123')); +$transaction = $eRedeService->capture((new Transaction(20.99))->setTid('TID123')); if ($transaction->getReturnCode() == '00') { printf("Transação capturada com sucesso; tid=%s\n", $transaction->getTid()); @@ -264,14 +249,8 @@ if ($transaction->getReturnCode() == '00') { ```php cancel((new Transaction(20.99))->setTid('TID123')); +$transaction = $eRedeService->cancel((new Transaction(20.99))->setTid('TID123')); if ($transaction->getReturnCode() == '359') { printf("Transação cancelada com sucesso; tid=%s\n", $transaction->getTid()); @@ -282,13 +261,8 @@ if ($transaction->getReturnCode() == '359') { ```php get('TID123'); +// Consulta a transação pelo ID +$transaction = $eRedeService->get('TID123'); printf("O status atual da autorização é %s\n", $transaction->getAuthorization()->getStatus()); ``` @@ -297,13 +271,8 @@ printf("O status atual da autorização é %s\n", $transaction->getAuthorization ```php getByReference('pedido123'); +// Consulta a transação pela referência +$transaction = $eRedeService->getByReference('pedido123'); printf("O status atual da autorização é %s\n", $transaction->getAuthorization()->getStatus()); ``` @@ -312,13 +281,8 @@ printf("O status atual da autorização é %s\n", $transaction->getAuthorization ```php getRefunds('TID123'); +// Consulta os cancelamentos de uma transação +$transaction = $eRedeService->getRefunds('TID123'); printf("O status atual da autorização é %s\n", $transaction->getAuthorization()->getStatus()); ``` @@ -327,12 +291,6 @@ printf("O status atual da autorização é %s\n", $transaction->getAuthorization ```php debitCard( '5277696455399733', @@ -357,7 +315,7 @@ $transaction->threeDSecure( $transaction->addUrl('https://redirecturl.com/3ds/success', Url::THREE_D_SECURE_SUCCESS); $transaction->addUrl('https://redirecturl.com/3ds/failure', Url::THREE_D_SECURE_FAILURE); -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '220') { printf("Redirecione o cliente para \"%s\" para autenticação\n", $transaction->getThreeDSecure()->getUrl()); @@ -371,7 +329,7 @@ if ($transaction->getReturnCode() == '220') { // Configura a transação para o PIX e passa a data de expiração $transaction = (new Transaction(200.99, 'pedido' . time()))->createQrCode(new \DateTimeImmutable('+ 1 hour')); -$transaction = (new eRede($store))->create($transaction); +$transaction = $eRedeService->create($transaction); if ($transaction->getReturnCode() == '00') { printf( @@ -383,6 +341,33 @@ if ($transaction->getReturnCode() == '00') { ## Observações -- Ao criar uma transação com `$transaction = (new eRede($store))->create($transaction)` não vai retornar o campo `authorization`, para retornar o campo é preciso fazer uma consulta `$transaction = (new eRede($store))->get('TID123')` +- Ao criar uma transação com `$transaction = $eRedeService->create($transaction)` não vai retornar o campo `authorization`, para retornar o campo é preciso fazer uma consulta `$transaction = $eRedeService->get('TID123')` - O campo `$transaction->getAuthorizationCode()` não está retornando nada, use `$transaction->getBrand()?->getAuthorizationCode()` ou `$transaction->getAuthorization()?->getBrand()?->getAuthorizationCode()` - Caso precise acessar o JSON original do response utilize `$transaction?->getHttpResponse()->getBody()` + +### Gerenciamento de Token OAuth2 + +O token de autenticação OAuth2 possui um **tempo de expiração** de 24 minutos. Para otimizar o desempenho e evitar requisições desnecessárias, é recomendado **salvar e reutilizar o token** enquanto ele estiver válido. + +**Exemplo de implementação:** + +```php +getOAuthToken()); + +// Para reutilizar o token, basta decodificar o JSON e setar no store +$store->setOAuthToken((new OAuthToken())->populate(json_decode($cachedToken))); +$eRedeService = new eRede($store); +``` + +**Recomendações:** + +- Armazene o token em cache (Redis, Memcached) ou banco de dados para ambientes de produção +- Sempre verifique a expiração antes de reutilizar o token +- O SDK gerencia automaticamente a renovação quando o token expira diff --git a/composer.json b/composer.json index 57110e2..1629a96 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "silbeckdevs/erede-php", - "version": "1.0.2", + "version": "2.0.0", "description": "e.Rede integration SDK", "minimum-stability": "stable", "license": "MIT", @@ -12,11 +12,11 @@ "psr/log": "*" }, "require-dev": { - "phpunit/phpunit": "^12.1.6", - "phpstan/phpstan": "^1.12.27", - "kint-php/kint": "^6.0.1", + "phpunit/phpunit": "^12.4.5", + "phpstan/phpstan": "^1.12.32", + "kint-php/kint": "^6.1.0", "monolog/monolog": "^3.9.0", - "friendsofphp/php-cs-fixer": "^3.75.0", + "friendsofphp/php-cs-fixer": "^3.91.2", "brainmaestro/composer-git-hooks": "^3.0.0" }, "autoload": { diff --git a/src/Rede/Environment.php b/src/Rede/Environment.php index 542376d..d5bae5f 100644 --- a/src/Rede/Environment.php +++ b/src/Rede/Environment.php @@ -6,22 +6,22 @@ class Environment implements RedeSerializable { public const PRODUCTION = 'https://api.userede.com.br/erede'; - public const SANDBOX = 'https://api.userede.com.br/desenvolvedores'; + public const SANDBOX = 'https://sandbox-erede.useredecloud.com.br'; - public const VERSION = 'v1'; + public const VERSION = 'v2'; private ?string $ip = null; private ?string $sessionId = null; - private string $endpoint; + private string $baseUrl; /** * Creates an environment with its base url and version. */ - private function __construct(string $baseUrl) + private function __construct(string $baseUrl = self::PRODUCTION) { - $this->endpoint = sprintf('%s/%s/', $baseUrl, Environment::VERSION); + $this->baseUrl = sprintf('%s/%s/', $baseUrl, Environment::VERSION); } /** @@ -40,12 +40,27 @@ public static function sandbox(): Environment return new Environment(Environment::SANDBOX); } + public function isProduction(): bool + { + return str_starts_with($this->baseUrl, self::PRODUCTION); + } + + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + public function getBaseUrlOAuth(): string + { + return $this->isProduction() ? 'https://api.userede.com.br/redelabs' : 'https://rl7-sandbox-api.useredecloud.com.br'; + } + /** * @return string Gets the environment endpoint */ public function getEndpoint(string $service): string { - return $this->endpoint . $service; + return $this->baseUrl . $service; } public function getIp(): ?string diff --git a/src/Rede/Http/RedeHttpClient.php b/src/Rede/Http/RedeHttpClient.php new file mode 100644 index 0000000..7ba0287 --- /dev/null +++ b/src/Rede/Http/RedeHttpClient.php @@ -0,0 +1,204 @@ +platform = $platform; + $this->platformVersion = $platformVersion; + + return $this; + } + + /** + * @param string|array|object $body + * @param array $headers + * + * @throws \RuntimeException + */ + protected function request( + string $method, + string $url, + string|array|object $body = '', + array $headers = [], + string $contentType = self::CONTENT_TYPE_JSON, + ): RedeResponse { + $curl = curl_init($url); + + if (!$curl instanceof \CurlHandle) { + throw new \RuntimeException('Was not possible to create a curl instance.'); + } + + curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); + + switch ($method) { + case self::GET: + break; + case self::POST: + curl_setopt($curl, CURLOPT_POST, true); + break; + default: + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); + } + + $requestHeaders = [ + str_replace(' ', ' ', $this->getUserAgent()), + 'Accept: application/json', + 'Transaction-Response: brand-return-opened', + ]; + + if ($this->store->getOAuthToken()) { + $requestHeaders[] = 'Authorization: Bearer ' . $this->store->getOAuthToken()->getAccessToken(); + } + + $parsedBody = $this->parseBody($body, $contentType); + if (!empty($body)) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $parsedBody); + + $requestHeaders[] = "Content-Type: $contentType"; + } else { + $requestHeaders[] = 'Content-Length: 0'; + } + + $customHeaders = []; + if (!empty($headers)) { + foreach ($headers as $key => $value) { + $newHeader = is_numeric($key) ? trim($value) : trim($key) . ': ' . trim($value); + if ($newHeader) { + $customHeaders[] = $newHeader; + } + } + } + + curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge($customHeaders, $requestHeaders)); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + $this->logger?->debug( + trim( + sprintf( + "Request Rede\n%s %s\n%s\n\n%s", + $method, + $url, + implode("\n", $headers), + preg_replace('/"(cardHolderName|cardnumber|securitycode)":"[^"]+"/i', '"\1":"***"', $parsedBody) + ) + ) + ); + + $response = curl_exec($curl); + $httpInfo = curl_getinfo($curl); + + $this->logger?->debug( + sprintf( + "Response Rede\nStatus Code: %s\n\n%s", + $httpInfo['http_code'], + $response + ) + ); + + $this->dumpHttpInfo($httpInfo); + + if (curl_errno($curl)) { + throw new \RuntimeException(sprintf('Curl error[%s]: %s', curl_errno($curl), curl_error($curl))); + } + + if (!is_string($response)) { + throw new \RuntimeException('Error obtaining a response from the API'); + } + + curl_close($curl); + + return new RedeResponse($httpInfo['http_code'], $response); + } + + private function parseBody(mixed $body, string $contentType): string + { + if (empty($body)) { + return ''; + } + + if (is_string($body)) { + return $body; + } + + if (self::CONTENT_TYPE_FORM_URLENCODED === $contentType) { + return http_build_query($body); + } + + return json_encode($body) ?: ''; + } + + private function getUserAgent(): string + { + $userAgent = sprintf( + 'User-Agent: %s', + sprintf( + eRede::USER_AGENT, + phpversion(), + $this->store->getFiliation(), + php_uname('s'), + php_uname('r'), + php_uname('m') + ) + ); + + if (!empty($this->platform) && !empty($this->platformVersion)) { + $userAgent .= sprintf(' %s/%s', $this->platform, $this->platformVersion); + } + + $curlVersion = curl_version(); + + if (is_array($curlVersion)) { + $userAgent .= sprintf( + ' curl/%s %s', + $curlVersion['version'] ?? '', + $curlVersion['ssl_version'] ?? '' + ); + } + + return $userAgent; + } + + /** + * @param array $httpInfo + */ + private function dumpHttpInfo(array $httpInfo): void + { + foreach ($httpInfo as $key => $info) { + if (is_array($info)) { + foreach ($info as $infoKey => $infoValue) { + $this->logger?->debug(sprintf('Curl[%s][%s]: %s', $key, $infoKey, implode(',', $infoValue))); + } + + continue; + } + + $this->logger?->debug(sprintf('Curl[%s]: %s', $key, $info)); + } + } +} diff --git a/src/Rede/Http/RedeResponse.php b/src/Rede/Http/RedeResponse.php index a93e850..e96262f 100644 --- a/src/Rede/Http/RedeResponse.php +++ b/src/Rede/Http/RedeResponse.php @@ -17,4 +17,9 @@ public function getBody(): string { return $this->body; } + + public function isSuccess(): bool + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } } diff --git a/src/Rede/OAuthToken.php b/src/Rede/OAuthToken.php new file mode 100644 index 0000000..2cd1174 --- /dev/null +++ b/src/Rede/OAuthToken.php @@ -0,0 +1,92 @@ +access_token)) { + return false; + } + + if (null !== $this->expires_at && time() >= $this->expires_at) { + return false; + } + + return true; + } + + // gets and sets + public function getTokenType(): ?string + { + return $this->token_type; + } + + public function setTokenType(?string $token_type): static + { + $this->token_type = $token_type; + + return $this; + } + + public function getAccessToken(): ?string + { + return $this->access_token; + } + + public function setAccessToken(?string $access_token): static + { + $this->access_token = $access_token; + + return $this; + } + + public function getExpiresIn(): ?int + { + return $this->expires_in; + } + + public function setExpiresIn(?int $expires_in): static + { + $this->expires_in = $expires_in; + + return $this; + } + + public function getScope(): ?string + { + return $this->scope; + } + + public function setScope(?string $scope): static + { + $this->scope = $scope; + + return $this; + } + + public function getExpiresAt(): ?int + { + return $this->expires_at; + } + + public function setExpiresAt(?int $expires_at): static + { + $this->expires_at = $expires_at; + + return $this; + } +} diff --git a/src/Rede/Service/AbstractService.php b/src/Rede/Service/AbstractService.php index 297660b..321e4a0 100644 --- a/src/Rede/Service/AbstractService.php +++ b/src/Rede/Service/AbstractService.php @@ -3,39 +3,16 @@ namespace Rede\Service; use Psr\Log\LoggerInterface; -use Rede\eRede; use Rede\Exception\RedeException; +use Rede\Http\RedeHttpClient; use Rede\Store; use Rede\Transaction; -abstract class AbstractService +abstract class AbstractService extends RedeHttpClient { - public const GET = 'GET'; - - public const POST = 'POST'; - - public const PUT = 'PUT'; - - private ?string $platform = null; - - private ?string $platformVersion = null; - - /** - * AbstractService constructor. - */ - public function __construct(protected Store $store, protected ?LoggerInterface $logger = null) + public function __construct(Store $store, ?LoggerInterface $logger = null) { - } - - /** - * @return $this - */ - public function platform(?string $platform, ?string $platformVersion): static - { - $this->platform = $platform; - $this->platformVersion = $platformVersion; - - return $this; + parent::__construct($store, $logger); } /** @@ -50,155 +27,10 @@ abstract public function execute(): Transaction; */ protected function sendRequest(string $body = '', string $method = 'GET'): Transaction { - $userAgent = $this->getUserAgent(); - $headers = [ - str_replace( - ' ', - ' ', - $userAgent - ), - 'Accept: application/json', - 'Transaction-Response: brand-return-opened', - ]; - - $curl = curl_init($this->store->getEnvironment()->getEndpoint($this->getService())); - - if (!$curl instanceof \CurlHandle) { - throw new \RuntimeException('Was not possible to create a curl instance.'); - } - - curl_setopt( - $curl, - CURLOPT_USERPWD, - sprintf('%s:%s', $this->store->getFiliation(), $this->store->getToken()) - ); - - curl_setopt($curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); - - switch ($method) { - case 'GET': - break; - case 'POST': - curl_setopt($curl, CURLOPT_POST, true); - break; - default: - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); - } - - if ('' !== $body) { - curl_setopt($curl, CURLOPT_POSTFIELDS, $body); - - $headers[] = 'Content-Type: application/json; charset=utf8'; - } else { - $headers[] = 'Content-Length: 0'; - } - - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - - $this->logger?->debug( - trim( - sprintf( - "Request Rede\n%s %s\n%s\n\n%s", - $method, - $this->store->getEnvironment()->getEndpoint($this->getService()), - implode("\n", $headers), - preg_replace('/"(cardHolderName|cardnumber|securitycode)":"[^"]+"/i', '"\1":"***"', $body) - ) - ) - ); - - $response = curl_exec($curl); - $httpInfo = curl_getinfo($curl); - - $this->logger?->debug( - sprintf( - "Response Rede\nStatus Code: %s\n\n%s", - $httpInfo['http_code'], - $response - ) - ); - - $this->dumpHttpInfo($httpInfo); - - if (curl_errno($curl)) { - throw new \RuntimeException(sprintf('Curl error[%s]: %s', curl_errno($curl), curl_error($curl))); - } - - if (!is_string($response)) { - throw new \RuntimeException('Error obtaining a response from the API'); - } - - curl_close($curl); - - return $this->parseResponse($response, $httpInfo['http_code']); - } - - /** - * Gets the User-Agent string. - */ - private function getUserAgent(): string - { - $userAgent = sprintf( - 'User-Agent: %s', - sprintf( - eRede::USER_AGENT, - phpversion(), - $this->store->getFiliation(), - php_uname('s'), - php_uname('r'), - php_uname('m') - ) - ); - - if (!empty($this->platform) && !empty($this->platformVersion)) { - $userAgent .= sprintf(' %s/%s', $this->platform, $this->platformVersion); - } - - $curlVersion = curl_version(); - - if (is_array($curlVersion)) { - $userAgent .= sprintf( - ' curl/%s %s', - $curlVersion['version'] ?? '', - $curlVersion['ssl_version'] ?? '' - ); - } - - return $userAgent; + return $this->parseResponse($this->request($method, $this->store->getEnvironment()->getEndpoint($this->getService()), $body)); } - /** - * @return string Gets the service that will be used on the request - */ abstract protected function getService(): string; - /** - * Dumps the httpInfo log. - * - * @param array $httpInfo the http info - * - * @noinspection PhpPluralMixedCanBeReplacedWithArrayInspection - */ - private function dumpHttpInfo(array $httpInfo): void - { - foreach ($httpInfo as $key => $info) { - if (is_array($info)) { - foreach ($info as $infoKey => $infoValue) { - $this->logger?->debug(sprintf('Curl[%s][%s]: %s', $key, $infoKey, implode(',', $infoValue))); - } - - continue; - } - - $this->logger?->debug(sprintf('Curl[%s]: %s', $key, $info)); - } - } - - /** - * @param string $response Parses the HTTP response from Rede - * @param int $statusCode The HTTP status code - */ - abstract protected function parseResponse(string $response, int $statusCode): Transaction; + abstract protected function parseResponse(\Rede\Http\RedeResponse $httpResponse): Transaction; } diff --git a/src/Rede/Service/AbstractTransactionsService.php b/src/Rede/Service/AbstractTransactionsService.php index 934fe2f..64ea301 100644 --- a/src/Rede/Service/AbstractTransactionsService.php +++ b/src/Rede/Service/AbstractTransactionsService.php @@ -69,7 +69,7 @@ protected function getService(): string * * @see AbstractService::parseResponse() */ - protected function parseResponse(string $response, int $statusCode): Transaction + protected function parseResponse(\Rede\Http\RedeResponse $httpResponse): Transaction { $previous = null; @@ -78,13 +78,13 @@ protected function parseResponse(string $response, int $statusCode): Transaction } try { - $this->transaction->setHttpResponse(new \Rede\Http\RedeResponse($statusCode, $response)); - $this->transaction->jsonUnserialize($response); + $this->transaction->setHttpResponse($httpResponse); + $this->transaction->jsonUnserialize($httpResponse->getBody()); } catch (\InvalidArgumentException $e) { $previous = $e; } - if ($statusCode >= 400) { + if ($httpResponse->getStatusCode() >= 400) { throw new RedeException($this->transaction->getReturnMessage() ?? 'Error on getting the content from the API', (int) $this->transaction->getReturnCode(), $previous); } diff --git a/src/Rede/Service/OAuthService.php b/src/Rede/Service/OAuthService.php new file mode 100644 index 0000000..a4aa45c --- /dev/null +++ b/src/Rede/Service/OAuthService.php @@ -0,0 +1,44 @@ +store->setOAuthToken(null); + + $headers = [ + 'Authorization: Basic ' . base64_encode($this->store->getFiliation() . ':' . $this->store->getToken()), + ]; + + $httpResponse = $this->request( + method: self::POST, + url: $this->store->getEnvironment()->getBaseUrlOAuth() . '/oauth2/token', + body: ['grant_type' => 'client_credentials'], + headers: $headers, + contentType: self::CONTENT_TYPE_FORM_URLENCODED, + ); + + if (!$httpResponse->isSuccess()) { + throw new RedeException('Failed to generate access token', $httpResponse->getStatusCode()); + } + + $oauthToken = (new OAuthToken())->populate(json_decode($httpResponse->getBody())); + // -60 para garantir que o token ainda está válido + $oauthToken->setExpiresAt(time() + $oauthToken->getExpiresIn() - 60); + + return $oauthToken; + } +} diff --git a/src/Rede/Store.php b/src/Rede/Store.php index 37ee466..ab21b1a 100644 --- a/src/Rede/Store.php +++ b/src/Rede/Store.php @@ -4,19 +4,25 @@ class Store { - /** - * Which environment will this store used for? - */ private Environment $environment; + private ?OAuthToken $oauthToken = null; + /** * Creates a store. * + * @param string $filiation ClientId para autenticação OAuth 2.0 + * @param string $token ClientSecret para autenticação OAuth 2.0 * @param Environment|null $environment if none provided, production will be used */ - public function __construct(private string $filiation, private string $token, ?Environment $environment = null) - { + public function __construct( + private string $filiation, // TODO rename to clientId + private string $token, // TODO rename to clientSecret + ?Environment $environment = null, + ?OAuthToken $oauthToken = null, + ) { $this->environment = $environment ?? Environment::production(); + $this->oauthToken = $oauthToken; } public function getEnvironment(): Environment @@ -63,4 +69,16 @@ public function setToken(string $token): static return $this; } + + public function getOAuthToken(): ?OAuthToken + { + return $this->oauthToken; + } + + public function setOAuthToken(?OAuthToken $oauthToken): static + { + $this->oauthToken = $oauthToken; + + return $this; + } } diff --git a/src/Rede/eRede.php b/src/Rede/eRede.php index e58702f..4939698 100644 --- a/src/Rede/eRede.php +++ b/src/Rede/eRede.php @@ -7,13 +7,11 @@ use Rede\Service\CaptureTransactionService; use Rede\Service\CreateTransactionService; use Rede\Service\GetTransactionService; +use Rede\Service\OAuthService; -/** - * phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps. - */ class eRede { - public const VERSION = '6.1.0'; + public const VERSION = '7.0.0'; public const USER_AGENT = 'eRede/' . eRede::VERSION . ' (PHP %s; Store %s; %s %s) %s'; @@ -21,11 +19,24 @@ class eRede private ?string $platformVersion = null; - /** - * eRede constructor. - */ public function __construct(private readonly Store $store, private readonly ?LoggerInterface $logger = null) { + if (empty($this->store->getOAuthToken()) || !$this->store->getOAuthToken()->isValid()) { + $this->generateOAuthToken(); + } + } + + public function generateOAuthToken(): ?\Rede\OAuthToken + { + $oauthToken = (new OAuthService($this->store, $this->logger))->generateToken(); + $this->store->setOAuthToken($oauthToken); + + return $oauthToken; + } + + public function getOAuthToken(): ?OAuthToken + { + return $this->store->getOAuthToken(); } /** diff --git a/tests/Unit/AuthUnitTest.php b/tests/Unit/AuthUnitTest.php new file mode 100644 index 0000000..1ea5580 --- /dev/null +++ b/tests/Unit/AuthUnitTest.php @@ -0,0 +1,215 @@ +setTokenType('Bearer') + ->setAccessToken('fluent_token_xyz') + ->setExpiresIn(7200) + ->setScope('admin') + ->setExpiresAt(time() + 7200); + + $this->assertSame('Bearer', $oauthToken->getTokenType()); + $this->assertSame('fluent_token_xyz', $oauthToken->getAccessToken()); + $this->assertSame(7200, $oauthToken->getExpiresIn()); + $this->assertSame('admin', $oauthToken->getScope()); + $this->assertNotNull($oauthToken->getExpiresAt()); + + $emptyToken = new OAuthToken(); + $this->assertFalse($emptyToken->isValid()); + + $validTokenNoExpiry = (new OAuthToken()) + ->setAccessToken('valid_token_no_expiry'); + $this->assertTrue($validTokenNoExpiry->isValid()); + + $validToken = (new OAuthToken()) + ->setAccessToken('valid_token_with_expiry') + ->setExpiresAt(time() + 1800); // expira em 30 minutos + $this->assertTrue($validToken->isValid()); + + $expiredToken = (new OAuthToken()) + ->setAccessToken('expired_token') + ->setExpiresAt(time() - 100); // expirou há 100 segundos + $this->assertFalse($expiredToken->isValid()); + + $expiringNowToken = (new OAuthToken()) + ->setAccessToken('expiring_now_token') + ->setExpiresAt(time()); // expira agora + $this->assertFalse($expiringNowToken->isValid()); + + $jsonData = (object) [ + 'token_type' => 'Bearer', + 'access_token' => 'populated_token_abc', + 'expires_in' => 3600, + 'scope' => 'full_access', + ]; + + $populatedToken = (new OAuthToken())->populate($jsonData); + $this->assertSame('Bearer', $populatedToken->getTokenType()); + $this->assertSame('populated_token_abc', $populatedToken->getAccessToken()); + $this->assertSame(3600, $populatedToken->getExpiresIn()); + $this->assertSame('full_access', $populatedToken->getScope()); + + $partialJsonData = (object) [ + 'token_type' => 'Bearer', + 'access_token' => 'partial_token', + 'expires_in' => null, + 'scope' => null, + ]; + + $partialToken = (new OAuthToken())->populate($partialJsonData); + $this->assertSame('Bearer', $partialToken->getTokenType()); + $this->assertSame('partial_token', $partialToken->getAccessToken()); + $this->assertNull($partialToken->getExpiresIn()); + $this->assertNull($partialToken->getScope()); + + $tokenForArray = (new OAuthToken()) + ->setTokenType('Bearer') + ->setAccessToken('array_test_token') + ->setExpiresIn(1800); + + $array = $tokenForArray->toArray(); + $this->assertIsArray($array); + $this->assertArrayHasKey('token_type', $array); + $this->assertArrayHasKey('access_token', $array); + $this->assertArrayHasKey('expires_in', $array); + $this->assertArrayNotHasKey('scope', $array); // null não deve estar no array + $this->assertArrayNotHasKey('expires_at', $array); // null não deve estar no array + $this->assertSame('Bearer', $array['token_type']); + $this->assertSame('array_test_token', $array['access_token']); + $this->assertSame(1800, $array['expires_in']); + + $tokenForJson = (new OAuthToken()) + ->setTokenType('Bearer') + ->setAccessToken('json_test_token') + ->setExpiresIn(3600) + ->setScope('read'); + + $jsonArray = $tokenForJson->jsonSerialize(); + $this->assertIsArray($jsonArray); + $this->assertSame('Bearer', $jsonArray['token_type']); + $this->assertSame('json_test_token', $jsonArray['access_token']); + $this->assertSame(3600, $jsonArray['expires_in']); + $this->assertSame('read', $jsonArray['scope']); + + $oauthResponse = (object) [ + 'token_type' => 'Bearer', + 'access_token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + 'expires_in' => 3600, + 'scope' => 'api_access payment_processing', + ]; + + $realToken = (new OAuthToken())->populate($oauthResponse); + $realToken->setExpiresAt(time() + $realToken->getExpiresIn() - 60); // como no OAuthService + + $this->assertTrue($realToken->isValid()); + $this->assertSame('Bearer', $realToken->getTokenType()); + $this->assertSame('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', $realToken->getAccessToken()); + $this->assertSame(3600, $realToken->getExpiresIn()); + $this->assertSame('api_access payment_processing', $realToken->getScope()); + $this->assertNotNull($realToken->getExpiresAt()); + $this->assertGreaterThan(time(), $realToken->getExpiresAt()); + + $resetToken = (new OAuthToken()) + ->setTokenType('Bearer') + ->setAccessToken('reset_test') + ->setExpiresIn(3600) + ->setScope('test') + ->setExpiresAt(time() + 3600); + + // Reset dos valores + $resetToken->setTokenType(null) + ->setAccessToken(null) + ->setExpiresIn(null) + ->setScope(null) + ->setExpiresAt(null); + + $this->assertNull($resetToken->getTokenType()); + $this->assertNull($resetToken->getAccessToken()); + $this->assertNull($resetToken->getExpiresIn()); + $this->assertNull($resetToken->getScope()); + $this->assertNull($resetToken->getExpiresAt()); + $this->assertFalse($resetToken->isValid()); + + $emptyStringToken = (new OAuthToken()) + ->setAccessToken('') + ->setExpiresAt(time() + 3600); + $this->assertFalse($emptyStringToken->isValid()); // string vazia deve ser inválida + + $zeroExpiryToken = (new OAuthToken()) + ->setAccessToken('valid_token') + ->setExpiresAt(0); + $this->assertFalse($zeroExpiryToken->isValid()); // 0 está no passado + } + + public function testOAuthTokenStore(): void + { + $store = new Store('1234567890', '1234567890'); + $this->assertNull($store->getOAuthToken()); + + $token = (new OAuthToken())->setAccessToken('test_token_123'); + $store->setOAuthToken($token); + + $this->assertInstanceOf(OAuthToken::class, $store->getOAuthToken()); + $this->assertSame('test_token_123', $store->getOAuthToken()->getAccessToken()); + $this->assertTrue($store->getOAuthToken()->isValid()); + + $eRedeService = new eRede($store); + $this->assertSame($token, $eRedeService->getOAuthToken()); + } + + public function testEnvironmentAllFeatures(): void + { + $productionEnv = Environment::production(); + $sandboxEnv = Environment::sandbox(); + + $this->assertInstanceOf(Environment::class, $productionEnv); + $this->assertInstanceOf(Environment::class, $sandboxEnv); + $this->assertTrue($productionEnv->isProduction()); + $this->assertFalse($sandboxEnv->isProduction()); + + $this->assertSame('https://api.userede.com.br/erede/v2/', $productionEnv->getBaseUrl()); + $this->assertSame('https://sandbox-erede.useredecloud.com.br/v2/', $sandboxEnv->getBaseUrl()); + + $this->assertSame('https://api.userede.com.br/redelabs', $productionEnv->getBaseUrlOAuth()); + $this->assertSame('https://rl7-sandbox-api.useredecloud.com.br', $sandboxEnv->getBaseUrlOAuth()); + + $this->assertSame('https://api.userede.com.br/erede/v2/transactions', $productionEnv->getEndpoint('transactions')); + $this->assertSame('https://sandbox-erede.useredecloud.com.br/v2/transactions', $sandboxEnv->getEndpoint('transactions')); + + $this->assertSame('https://api.userede.com.br/erede/v2/transactions/123/capture', $productionEnv->getEndpoint('transactions/123/capture')); + $this->assertSame('https://sandbox-erede.useredecloud.com.br/v2/transactions/456/refunds', $sandboxEnv->getEndpoint('transactions/456/refunds')); + $this->assertSame('https://api.userede.com.br/erede/v2/transactions/789/cancellations', $productionEnv->getEndpoint('transactions/789/cancellations')); + + $this->assertSame('https://api.userede.com.br/erede/v2/', $productionEnv->getEndpoint('')); + $this->assertSame('https://sandbox-erede.useredecloud.com.br/v2//test', $sandboxEnv->getEndpoint('/test')); + + $this->assertSame('https://api.userede.com.br/erede', Environment::PRODUCTION); + $this->assertSame('https://sandbox-erede.useredecloud.com.br', Environment::SANDBOX); + $this->assertSame('v2', Environment::VERSION); + + $envWithData = Environment::production()->setIp('192.168.1.1')->setSessionId('session123'); + $jsonData = $envWithData->jsonSerialize(); + + $this->assertIsArray($jsonData); + $this->assertArrayHasKey('consumer', $jsonData); + $this->assertSame('192.168.1.1', $jsonData['consumer']->ip); + $this->assertSame('session123', $jsonData['consumer']->sessionId); + + $customStore = new Store('123', '456', Environment::sandbox()); + $this->assertFalse($customStore->getEnvironment()->isProduction()); + + $defaultStore = new Store('789', '012'); + $this->assertTrue($defaultStore->getEnvironment()->isProduction()); + } +} diff --git a/tests/Unit/TransactionUnitTest.php b/tests/Unit/TransactionUnitTest.php index 5f556ea..035263c 100644 --- a/tests/Unit/TransactionUnitTest.php +++ b/tests/Unit/TransactionUnitTest.php @@ -137,12 +137,12 @@ private function getJsonTransactionMock(): string { "method": "GET", "rel": "transaction", - "href": "https://sandbox-erede.useredecloud.com.br/v1/transactions/12345678" + "href": "https://sandbox-erede.useredecloud.com.br/v2/transactions/12345678" }, { "method": "POST", "rel": "refund", - "href": "https://sandbox-erede.useredecloud.com.br/v1/transactions/12345678/refunds" + "href": "https://sandbox-erede.useredecloud.com.br/v2/transactions/12345678/refunds" } ] } @@ -188,12 +188,12 @@ private function getJsonAuthorizationMock(): string { "method": "GET", "rel": "refunds", - "href": "https://sandbox-erede.useredecloud.com.br/v1/transactions/12345678/refunds" + "href": "https://sandbox-erede.useredecloud.com.br/v2/transactions/12345678/refunds" }, { "method": "POST", "rel": "refund", - "href": "https://sandbox-erede.useredecloud.com.br/v1/transactions/12345678/refunds" + "href": "https://sandbox-erede.useredecloud.com.br/v2/transactions/12345678/refunds" } ] }