diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6511b82 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + commit-message: + # Prefix all commit messages with "chore: " + prefix: 'chore' + schedule: + interval: 'monthly' + open-pull-requests-limit: 10 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..1b149a1 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,52 @@ +name: Run lint and tests +on: + pull_request: + types: + - opened + - reopened + - ready_for_review + workflow_dispatch: + +env: + PHP_VERSION: 8.3 + PHP_EXTENSIONS: mbstring + PHP_TOOLS: composer:v2, phpunit:11 + +permissions: + id-token: write + contents: read + +jobs: + run-tests: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install PHP ${{ env.PHP_VERSION }} + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ env.PHP_VERSION }} + extensions: ${{ env.PHP_EXTENSIONS }} + tools: ${{ env.PHP_TOOLS }} + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer update --no-interaction --no-progress + + - name: Static analysis + run: composer phpstan + + - name: Run unit tests + run: composer test:unit diff --git a/.gitignore b/.gitignore index 5d13a24..12129b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ composer.lock vendor .idea +/.settings/ +/.project +/.buildpath +/.vscode +/cache \ No newline at end of file diff --git a/README.md b/README.md index 7e96dfe..2981da9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ SDK API-3.0 PHP * [x] Com autorização na primeira recorrência. * [x] Com autorização a partir da primeira recorrência. * [x] Pagamentos por cartão de débito. +* [x] Pagamentos por pix. * [x] Pagamentos por boleto. * [x] Pagamentos por transferência eletrônica. * [x] Cancelamento de autorização. @@ -21,7 +22,7 @@ Por envolver a interface de usuário da aplicação, o SDK funciona apenas como ## Dependências -* PHP >= 5.6 +* PHP >= 8.3 ## Instalando o SDK @@ -29,20 +30,25 @@ Se já possui um arquivo `composer.json`, basta adicionar a seguinte dependênci ```json "require": { - "developercielo/api-3.0-php": "^1.0" + "developercielo/api-3.0-php": "^2.0.0" } ``` -Com a dependência adicionada ao `composer.json`, basta executar: +Adicionar o `repositories` no `composer.json` -``` -composer install +```json +"repositories": [ + { + "type": "vcs", + "url": "https://github.com/edson-nascimento/API-3.0-PHP" + } +], ``` -Alternativamente, você pode executar diretamente em seu terminal: +Com a dependência adicionada ao `composer.json`, basta executar: ``` -composer require "developercielo/api-3.0-php" +composer update ``` ## Produtos e Bandeiras suportadas e suas constantes @@ -424,6 +430,23 @@ try { } ``` +### Criando uma venda com PIX + +```php +customer('Fulano de Tal'); + +$payment = $sale->payment(15700); +$payment->pix(); +$payment->setCapture(true); + +$sale = (new CieloEcommerce($merchant, $environment))->createSale($sale); +// ... +``` + ### Tokenizando um cartão ```php diff --git a/composer.json b/composer.json index 60c692e..5700d8e 100644 --- a/composer.json +++ b/composer.json @@ -1,21 +1,42 @@ { - "autoload": { - "psr-0": { - "Cielo": "src" - } - }, "name": "developercielo/api-3.0-php", "description": "Integração com a API 3.0 da Cielo", - "license": "MIT", "type": "library", "require": { - "php": ">=5.6", + "php": "^8.2", "ext-curl": "*", "ext-json": "*", - "psr/log":"^1.1" + "psr/log": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.1.2", + "phpstan/phpstan": "^1.10.67", + "kint-php/kint": "^5.1.0" + }, + "autoload": { + "psr-0": { + "Cielo": "src" + } + }, + "autoload-dev": { + "psr-4": { + "TestApp\\": "tests/" + } }, "suggest": { "monolog/monolog": "Allows more advanced logging of the application flow" }, - "homepage": "https://github.com/DeveloperCielo/API-3.0-PHP" -} + "homepage": "https://github.com/DeveloperCielo/API-3.0-PHP", + "scripts": { + "phpstan": "phpstan analyse -c phpstan.neon", + "phpunit": "phpunit --configuration phpunit.xml --testdox --exclude-group payment", + "test": [ + "@phpstan", + "@phpunit" + ], + "test:unit": "phpunit tests/Unit --configuration phpunit.xml --testdox", + "test:e2e": "phpunit tests/E2E --configuration phpunit.xml --testdox" + }, + "license": "MIT", + "minimum-stability": "stable" +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0f3cb1d --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,17 @@ +parameters: + level: 5 + phpVersion: 80300 + checkFunctionNameCase: true + checkInternalClassCaseSensitivity: true + reportMaybesInPropertyPhpDocTypes: true + checkExplicitMixedMissingReturn: true + reportMaybesInMethodSignatures: true + reportStaticMethodSignatures: true + checkTooWideReturnTypesInProtectedAndPublicMethods: true + checkDynamicProperties: true + reportAlwaysTrueInLastCondition: true + paths: + - ./ + excludePaths: + - ./vendor/* + ignoreErrors: \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..6d6460d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + tests + + + \ No newline at end of file diff --git a/src/Cielo/API30/Ecommerce/Address.php b/src/Cielo/API30/Ecommerce/Address.php index d16e71e..4777909 100644 --- a/src/Cielo/API30/Ecommerce/Address.php +++ b/src/Cielo/API30/Ecommerce/Address.php @@ -26,10 +26,7 @@ class Address implements CieloSerializable private $district; - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } diff --git a/src/Cielo/API30/Ecommerce/CieloEcommerce.php b/src/Cielo/API30/Ecommerce/CieloEcommerce.php index f297d73..2d6b18c 100644 --- a/src/Cielo/API30/Ecommerce/CieloEcommerce.php +++ b/src/Cielo/API30/Ecommerce/CieloEcommerce.php @@ -27,10 +27,7 @@ class CieloEcommerce * requests will be send * * @param Merchant $merchant - * The merchant credentials - * @param Environment environment - * The environment: {@link Environment::production()} or - * {@link Environment::sandbox()} + * @param Environment $environment * @param LoggerInterface|null $logger */ public function __construct(Merchant $merchant, Environment $environment = null, LoggerInterface $logger = null) @@ -117,7 +114,7 @@ public function getRecurrentPayment($recurrentPaymentId) * @param integer $amount * Order value in cents * - * @return Sale The Sale with authorization, tid, etc. returned by Cielo. + * @return \Cielo\API30\Ecommerce\Payment * * @throws \Cielo\API30\Ecommerce\Request\CieloRequestException if anything gets wrong. * diff --git a/src/Cielo/API30/Ecommerce/CieloSerializable.php b/src/Cielo/API30/Ecommerce/CieloSerializable.php index 05d13a7..8a8e873 100644 --- a/src/Cielo/API30/Ecommerce/CieloSerializable.php +++ b/src/Cielo/API30/Ecommerce/CieloSerializable.php @@ -12,7 +12,7 @@ interface CieloSerializable extends \JsonSerializable /** * @param \stdClass $data * - * @return mixed + * @return void */ public function populate(\stdClass $data); } diff --git a/src/Cielo/API30/Ecommerce/CreditCard.php b/src/Cielo/API30/Ecommerce/CreditCard.php index e83b4b8..1078a16 100644 --- a/src/Cielo/API30/Ecommerce/CreditCard.php +++ b/src/Cielo/API30/Ecommerce/CreditCard.php @@ -111,10 +111,7 @@ public function populate(\stdClass $data) $this->customerName = isset($data->CustomerName) ? $data->CustomerName : null; } - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } diff --git a/src/Cielo/API30/Ecommerce/Customer.php b/src/Cielo/API30/Ecommerce/Customer.php index c24dc69..479950a 100644 --- a/src/Cielo/API30/Ecommerce/Customer.php +++ b/src/Cielo/API30/Ecommerce/Customer.php @@ -34,10 +34,7 @@ public function __construct($name = null) $this->setName($name); } - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } diff --git a/src/Cielo/API30/Ecommerce/Payment.php b/src/Cielo/API30/Ecommerce/Payment.php index 574f88a..09559ef 100644 --- a/src/Cielo/API30/Ecommerce/Payment.php +++ b/src/Cielo/API30/Ecommerce/Payment.php @@ -18,6 +18,8 @@ class Payment implements \JsonSerializable const PAYMENTTYPE_BOLETO = 'Boleto'; + const PAYMENTTYPE_PIX = 'Pix'; + const PROVIDER_BRADESCO = 'Bradesco'; const PROVIDER_BANCO_DO_BRASIL = 'BancoDoBrasil'; @@ -107,6 +109,12 @@ class Payment implements \JsonSerializable private $identification; private $instructions; + + private $acquirerTransactionId; + + private $qrcodeBase64Image; + + private $qrCodeString; /** * Payment constructor. @@ -194,12 +202,13 @@ public function populate(\stdClass $data) $this->demonstrative = isset($data->Demonstrative) ? $data->Demonstrative : null; $this->identification = isset($data->Identification) ? $data->Identification : null; $this->instructions = isset($data->Instructions) ? $data->Instructions : null; + + $this->acquirerTransactionId = isset($data->AcquirerTransactionId) ? $data->AcquirerTransactionId : null; + $this->qrcodeBase64Image = isset($data->QrcodeBase64Image) ? $data->QrcodeBase64Image : null; + $this->qrCodeString = isset($data->QrCodeString) ? $data->QrCodeString : null; } - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } @@ -264,6 +273,18 @@ public function recurrentPayment($authorizeNow = true) return $recurrentPayment; } + + /** + * + * @return $this + */ + public function pix() + { + $this->setType(self::PAYMENTTYPE_PIX); + $this->setCapture(true); + + return $this; + } /** * @return mixed @@ -1104,4 +1125,66 @@ public function setInstructions($instructions) return $this; } + + /** + * @return string|NULL + */ + public function getAcquirerTransactionId() + { + return $this->acquirerTransactionId; + } + + /** + * @param $acquirerTransactionId + * + * @return $this + */ + public function setAcquirerTransactionId($acquirerTransactionId) + { + $this->acquirerTransactionId = $acquirerTransactionId; + + return $this; + } + + /** + * @return string|NULL + */ + public function getQrcodeBase64Image() + { + return $this->qrcodeBase64Image; + } + + /** + * @param $qrcodeBase64Image + * + * @return $this + * + */ + public function setQrcodeBase64Image($qrcodeBase64Image) + { + $this->qrcodeBase64Image = $qrcodeBase64Image; + + return $this; + } + + /** + * @return string|NULL + */ + public function getQrCodeString() + { + return $this->qrCodeString; + } + + /** + * @param $qrCodeString + * + * @return $this + */ + public function setQrCodeString($qrCodeString) + { + $this->qrCodeString = $qrCodeString; + + return $this; + } + } diff --git a/src/Cielo/API30/Ecommerce/RecurrentPayment.php b/src/Cielo/API30/Ecommerce/RecurrentPayment.php index 088ccf8..6f2be69 100644 --- a/src/Cielo/API30/Ecommerce/RecurrentPayment.php +++ b/src/Cielo/API30/Ecommerce/RecurrentPayment.php @@ -97,10 +97,7 @@ public function populate(\stdClass $data) $this->status = isset($data->Status) ? $data->Status : null; } - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } diff --git a/src/Cielo/API30/Ecommerce/Request/AbstractRequest.php b/src/Cielo/API30/Ecommerce/Request/AbstractRequest.php index 4c4da2a..c53cb60 100644 --- a/src/Cielo/API30/Ecommerce/Request/AbstractRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/AbstractRequest.php @@ -49,7 +49,7 @@ protected function sendRequest($method, $url, \JsonSerializable $content = null) { $headers = [ 'Accept: application/json', - 'Accept-Encoding: gzip', + //'Accept-Encoding: gzip', 'User-Agent: CieloEcommerce/3.0 PHP SDK', 'MerchantId: ' . $this->merchant->getId(), 'MerchantKey: ' . $this->merchant->getKey(), @@ -104,7 +104,9 @@ protected function sendRequest($method, $url, \JsonSerializable $content = null) if (curl_errno($curl)) { $message = sprintf('cURL error[%s]: %s', curl_errno($curl), curl_error($curl)); - $this->logger->error($message); + if ($this->logger !== null) { + $this->logger->error($message); + } throw new \RuntimeException($message); } @@ -135,17 +137,23 @@ protected function readResponse($statusCode, $responseBody) $exception = null; $response = json_decode($responseBody); - foreach ($response as $error) { - $cieloError = new CieloError($error->Message, $error->Code); - $exception = new CieloRequestException('Request Error', $statusCode, $exception); - $exception->setCieloError($cieloError); + if (is_array($response) && count($response) > 0) { + foreach ($response as $error) { + $cieloError = new CieloError($error->Message, $error->Code); + $exception = new CieloRequestException('Request Error', $statusCode, $exception); + $exception->setCieloError($cieloError); + } + } else { + $exception = new CieloRequestException("Request Error $statusCode, response: $responseBody", $statusCode); } throw $exception; + case 401: + throw new CieloRequestException('HTTP 401 NotAuthorized', $statusCode, null); case 404: throw new CieloRequestException('Resource not found', 404, null); default: - throw new CieloRequestException('Unknown status', $statusCode); + throw new CieloRequestException("Unknown statusCode $statusCode, response: $responseBody", $statusCode); } return $unserialized; diff --git a/src/Cielo/API30/Ecommerce/Request/CreateSaleRequest.php b/src/Cielo/API30/Ecommerce/Request/CreateSaleRequest.php index f3c9659..ebe93a5 100644 --- a/src/Cielo/API30/Ecommerce/Request/CreateSaleRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/CreateSaleRequest.php @@ -34,7 +34,7 @@ public function __construct(Merchant $merchant, Environment $environment, Logger /** * @param $sale * - * @return null + * @return Sale * @throws \Cielo\API30\Ecommerce\Request\CieloRequestException * @throws \RuntimeException */ diff --git a/src/Cielo/API30/Ecommerce/Request/QueryRecurrentPaymentRequest.php b/src/Cielo/API30/Ecommerce/Request/QueryRecurrentPaymentRequest.php index 8309772..b9fe577 100644 --- a/src/Cielo/API30/Ecommerce/Request/QueryRecurrentPaymentRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/QueryRecurrentPaymentRequest.php @@ -34,7 +34,7 @@ public function __construct(Merchant $merchant, Environment $environment, Logger /** * @param $recurrentPaymentId * - * @return null + * @return RecurrentPayment * @throws \Cielo\API30\Ecommerce\Request\CieloRequestException * @throws \RuntimeException */ diff --git a/src/Cielo/API30/Ecommerce/Request/QuerySaleRequest.php b/src/Cielo/API30/Ecommerce/Request/QuerySaleRequest.php index e7953b2..cb99400 100644 --- a/src/Cielo/API30/Ecommerce/Request/QuerySaleRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/QuerySaleRequest.php @@ -34,7 +34,7 @@ public function __construct(Merchant $merchant, Environment $environment, Logger /** * @param $paymentId * - * @return null + * @return Sale * @throws \Cielo\API30\Ecommerce\Request\CieloRequestException * @throws \RuntimeException */ diff --git a/src/Cielo/API30/Ecommerce/Request/TokenizeCardRequest.php b/src/Cielo/API30/Ecommerce/Request/TokenizeCardRequest.php index 722e367..e300902 100644 --- a/src/Cielo/API30/Ecommerce/Request/TokenizeCardRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/TokenizeCardRequest.php @@ -1,5 +1,4 @@ merchant = $merchant; + $this->merchant = $merchant; $this->environment = $environment; } /** + * * @inheritdoc */ public function execute($param) @@ -45,6 +50,7 @@ public function execute($param) } /** + * * @inheritdoc */ protected function unserialize($json) diff --git a/src/Cielo/API30/Ecommerce/Request/UpdateSaleRequest.php b/src/Cielo/API30/Ecommerce/Request/UpdateSaleRequest.php index 54b9b02..4fafc21 100644 --- a/src/Cielo/API30/Ecommerce/Request/UpdateSaleRequest.php +++ b/src/Cielo/API30/Ecommerce/Request/UpdateSaleRequest.php @@ -26,7 +26,7 @@ class UpdateSaleRequest extends AbstractRequest /** * UpdateSaleRequest constructor. * - * @param Merchant $type + * @param string $type * @param Merchant $merchant * @param Environment $environment * @param LoggerInterface|null $logger @@ -42,7 +42,7 @@ public function __construct($type, Merchant $merchant, Environment $environment, /** * @param $paymentId * - * @return null + * @return Payment * @throws \Cielo\API30\Ecommerce\Request\CieloRequestException * @throws \RuntimeException */ diff --git a/src/Cielo/API30/Ecommerce/Sale.php b/src/Cielo/API30/Ecommerce/Sale.php index 07639e7..547b569 100644 --- a/src/Cielo/API30/Ecommerce/Sale.php +++ b/src/Cielo/API30/Ecommerce/Sale.php @@ -19,7 +19,7 @@ class Sale implements \JsonSerializable /** * Sale constructor. * - * @param null $merchantOrderId + * @param string|int $merchantOrderId */ public function __construct($merchantOrderId = null) { @@ -33,7 +33,7 @@ public function __construct($merchantOrderId = null) */ public static function fromJson($json) { - $object = json_decode($json); + $object = json_decode($json) ?: json_decode(gzdecode($json)); $sale = new Sale(); $sale->populate($object); @@ -63,10 +63,7 @@ public function populate(\stdClass $data) } } - /** - * @return array - */ - public function jsonSerialize() + public function jsonSerialize(): mixed { return get_object_vars($this); } diff --git a/tests/Unit/SaleTest.php b/tests/Unit/SaleTest.php new file mode 100644 index 0000000..2bfb105 --- /dev/null +++ b/tests/Unit/SaleTest.php @@ -0,0 +1,52 @@ +assertSame('123', $sale->getMerchantOrderId()); + + $payment = $sale->payment(15800); + $this->assertSame($payment, $sale->getPayment()); + $this->assertSame(15800, $payment->getAmount()); + $this->assertSame(1, $payment->getInstallments()); + + $payment = $sale->payment(15800, 7); + $this->assertSame(7, $payment->getInstallments()); + } + + public function testSalePaymentType(): void + { + $sale = new Sale('123'); + $payment = $sale->payment(27400); + + $payment->creditCard("123", CreditCard::VISA) + ->setExpirationDate("12/2018") + ->setCardNumber("0000000000000001") + ->setHolder("Fulano de Tal") + ->setSaveCard(true); + $this->assertSame(Payment::PAYMENTTYPE_CREDITCARD, $payment->getType()); + $this->assertFalse($payment->getCapture()); + + $payment->debitCard("123", CreditCard::VISA) + ->setExpirationDate("12/2018") + ->setCardNumber("0000000000000001") + ->setHolder("Fulano de Tal") + ->setSaveCard(true); + $this->assertSame(Payment::PAYMENTTYPE_DEBITCARD, $payment->getType()); + $this->assertFalse($payment->getCapture()); + + $payment->pix(); + $this->assertSame(Payment::PAYMENTTYPE_PIX, $payment->getType()); + $this->assertTrue($payment->getCapture()); + } +} \ No newline at end of file