diff --git a/.gitattributes b/.gitattributes index ef28ab6..e2049be 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,4 @@ /docs export-ignore /UPGRADING.md export-ignore /help export-ignore +/.phpunit.cache export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 6911ad5..2156c06 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: abr4xas +custom: https://angelcruz.dev/donate diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3b07c83..c036916 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Ask a question - url: https://github.com/abr4xas/php-instapago/discussions/new?category=q-a - about: Ask the community for help - - name: Request a feature - url: https://github.com/abr4xas/php-instapago/discussions/new?category=ideas - about: Share ideas for new features - - name: Report a security issue - url: https://github.com/abr4xas/php-instapago/security/policy - about: Learn how to notify us for sensitive bugs - - name: Report a bug - url: https://github.com/abr4xas/php-instapago/issues/new - about: Report a reproducable bug + - name: Ask a question + url: https://github.com/abr4xas/php-instapago/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/abr4xas/php-instapago/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/abr4xas/php-instapago/security/policy + about: Learn how to notify us for sensitive bugs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 30c8a49..39b1580 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,11 @@ updates: schedule: interval: "weekly" labels: - - "dependencies" \ No newline at end of file + - "dependencies" + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 27c23a4..c3ad22d 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -8,6 +8,7 @@ permissions: jobs: dependabot: runs-on: ubuntu-latest + timeout-minutes: 5 if: ${{ github.actor == 'dependabot[bot]' }} steps: diff --git a/.github/workflows/fix-php-code-style-issues-cs-fixer.yml b/.github/workflows/fix-php-code-style-issues-cs-fixer.yml deleted file mode 100644 index 0665951..0000000 --- a/.github/workflows/fix-php-code-style-issues-cs-fixer.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Check & fix styling - -on: [pull_request] - -permissions: - contents: write - -jobs: - php-cs-fixer: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Run PHP CS Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php-cs-fixer.dist.php --allow-risky=yes - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Fix styling diff --git a/.github/workflows/fix-php-code-style-issues-pint.yml b/.github/workflows/fix-php-code-style-issues-pint.yml index 255506b..56d54d3 100644 --- a/.github/workflows/fix-php-code-style-issues-pint.yml +++ b/.github/workflows/fix-php-code-style-issues-pint.yml @@ -11,6 +11,7 @@ permissions: jobs: php-code-styling: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Checkout code diff --git a/.github/workflows/run-tests-pest.yml b/.github/workflows/run-tests-pest.yml index 027816e..4abe28f 100644 --- a/.github/workflows/run-tests-pest.yml +++ b/.github/workflows/run-tests-pest.yml @@ -5,11 +5,12 @@ on: [pull_request] jobs: test: runs-on: ${{ matrix.os }} + timeout-minutes: 5 strategy: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.2, 8.1] + php: [8.2, 8.3, 8.4] stability: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -33,5 +34,8 @@ jobs: - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction + - name: List Installed Dependencies + run: composer show -D + - name: Execute tests - run: vendor/bin/pest + run: vendor/bin/pest --ci diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 4b93116..18ae20e 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,6 +10,7 @@ permissions: jobs: update: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index 97f3bee..740938e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ psalm.xml vendor .php-cs-fixer.cache test.php +.phpunit.cache diff --git a/README.md b/README.md index 3edb301..0720979 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,10 @@ require 'vendor/autoload.php'; use \Instapago\Instapago\Api; use \Instapago\Instapago\Exceptions\{ InstapagoException, - AuthException, - BankRejectException, - InvalidInputException, - TimeoutException, + InstapagoAuthException, + InstapagoBankRejectException, + InstapagoInvalidInputException, + InstapagoTimeoutException, ValidationException, GenericException, }; @@ -66,19 +66,19 @@ try{ echo $e->getMessage(); // manejar el error -}catch(AuthException $e){ +}catch(InstapagoAuthException $e){ echo $e->getMessage(); // manejar el error -}catch(BankRejectException $e){ +}catch(InstapagoBankRejectException $e){ echo $e->getMessage(); // manejar el error -}catch(InvalidInputException $e){ +}catch(InstapagoInvalidInputException $e){ echo $e->getMessage(); // manejar el error -}catch(TimeoutException $e){ +}catch(InstapagoTimeoutException $e){ echo $e->getMessage(); // manejar el error diff --git a/composer.json b/composer.json index a0842b5..0ec31b0 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "instapago/instapago", - "description": ":D", + "description": "Instapago is a technological solution designed for the market of electronic commerce (eCommerce) in Venezuela and Latin America, with the intention of offering a premium product category, which allows people and companies leverage their expansion capabilities, facilitating payment mechanisms for customers with a friendly integration into systems currently used.", "keywords": [ "Instapago", "instapago" @@ -15,12 +15,13 @@ } ], "require": { - "php": "^8.1", - "guzzlehttp/guzzle": "^7.5.1" + "php": "^8.2|^8.3|^8.4", + "guzzlehttp/guzzle": "^7.9.2" }, "require-dev": { - "laravel/pint": "^1.10", - "pestphp/pest": "^2.6" + "laravel/pint": "^1.18.3", + "mockery/mockery": "^1.6", + "pestphp/pest": "^3.7" }, "autoload": { "psr-4": { diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..63b0341 --- /dev/null +++ b/pint.json @@ -0,0 +1,19 @@ +{ + "preset": "psr12", + "rules": { + "blank_line_before_statement": true, + "method_argument_space": true, + "single_trait_insert_per_statement": true, + "types_spaces": { + "space": "single" + }, + "align_multiline_comment": true, + "array_indentation": true, + "array_syntax": true, + "concat_space": { + "spacing": "one" + }, + "explicit_string_variable": true, + "fully_qualified_strict_types": true + } +} diff --git a/src/Api.php b/src/Api.php index 7b76de2..291eee7 100644 --- a/src/Api.php +++ b/src/Api.php @@ -1,16 +1,19 @@ payment('2', $fields); - } catch (AuthException|BankRejectException|GenericException|InstapagoException|InvalidInputException|TimeoutException|Exceptions\ValidationException|GuzzleException $e) { + } catch (InstapagoAuthException | InstapagoBankRejectException | GenericException | InstapagoException | InstapagoInvalidInputException | InstapagoTimeoutException | ValidationException | GuzzleException $e) { return $e->getMessage(); } } @@ -48,11 +51,11 @@ public function directPayment(array $fields): array|string /** * Crear un pago diferido o reservado. */ - public function reservePayment($fields): array|string + public function reservePayment($fields): array | string { try { return $this->payment('1', $fields); - } catch (AuthException|BankRejectException|GenericException|InstapagoException|InvalidInputException|TimeoutException|Exceptions\ValidationException|GuzzleException $e) { + } catch (InstapagoAuthException | InstapagoBankRejectException | GenericException | InstapagoException | InstapagoInvalidInputException | InstapagoTimeoutException | ValidationException | GuzzleException $e) { return $e->getMessage(); } } @@ -62,14 +65,14 @@ public function reservePayment($fields): array|string * Este método funciona para procesar un bloqueo o pre-autorización * para así procesarla y hacer el cobro respectivo. * - * @throws Exceptions\ValidationException + * @throws ValidationException * @throws GenericException * @throws GuzzleException - * @throws TimeoutException + * @throws InstapagoTimeoutException */ - public function completePayment(array $fields): array|string + public function completePayment(array $fields): array | string { - (new Validator())->release()->validate($fields); + (new Validator())->setValidations('release')->validate($fields); $fields = [ 'KeyID' => $this->keyId, //required @@ -78,12 +81,12 @@ public function completePayment(array $fields): array|string 'amount' => $fields['amount'], //required ]; - $obj = $this->curlTransaccion('complete', $fields, 'POST'); + $obj = $this->curlTransaction('complete', $fields, 'POST'); try { return $this->checkResponseCode($obj); - } catch (AuthException|BankRejectException|GenericException|InstapagoException|InvalidInputException $e) { - return $e->getMessage(); + } catch (InstapagoAuthException | InstapagoBankRejectException | GenericException | InstapagoException | InstapagoInvalidInputException $e) { + return $e; } } @@ -92,14 +95,14 @@ public function completePayment(array $fields): array|string * Este método funciona para procesar un bloqueo o pre-autorización * para así procesarla y hacer el cobro respectivo. * - * @throws Exceptions\ValidationException + * @throws ValidationException * @throws GenericException * @throws GuzzleException - * @throws TimeoutException + * @throws InstapagoTimeoutException */ - public function query(string $id_pago): array|string + public function query(string $id_pago): array | string { - (new Validator())->query()->validate([ + (new Validator())->setValidations('query')->validate([ 'id' => $id_pago, ]); @@ -109,11 +112,11 @@ public function query(string $id_pago): array|string 'id' => $id_pago, //required ]; - $obj = $this->curlTransaccion('payment', $fields, 'GET'); + $obj = $this->curlTransaction('payment', $fields, 'GET'); try { return $this->checkResponseCode($obj); - } catch (AuthException|BankRejectException|GenericException|InstapagoException|InvalidInputException $e) { + } catch (InstapagoAuthException | InstapagoBankRejectException | GenericException | InstapagoException | InstapagoInvalidInputException $e) { return $e->getMessage(); } } @@ -122,11 +125,11 @@ public function query(string $id_pago): array|string * Cancelar Pago * Este método funciona para cancelar un pago previamente procesado. * - * @throws Exceptions\ValidationException + * @throws ValidationException */ - public function cancel(string $id_pago): array|string + public function cancel(string $id_pago): array | string { - (new Validator())->query()->validate([ + (new Validator())->setValidations('query')->validate([ 'id' => $id_pago, ]); @@ -137,8 +140,8 @@ public function cancel(string $id_pago): array|string ]; try { - return $this->curlTransaccion('payment', $fields, 'DELETE'); - } catch (GuzzleException|GenericException|TimeoutException $e) { + return $this->curlTransaction('payment', $fields, 'DELETE'); + } catch (GuzzleException | GenericException | InstapagoTimeoutException $e) { return $e->getMessage(); } } @@ -146,19 +149,19 @@ public function cancel(string $id_pago): array|string /** * Crear un pago. * - * @param string $type tipo de pago ('1' o '0') + * @param string $type tipo de pago ('1' o '0') * - * @throws AuthException - * @throws BankRejectException - * @throws Exceptions\ValidationException + * @throws InstapagoAuthException + * @throws InstapagoBankRejectException + * @throws ValidationException * @throws GenericException * @throws InstapagoException - * @throws InvalidInputException - * @throws TimeoutException|GuzzleException + * @throws InstapagoInvalidInputException + * @throws InstapagoTimeoutException|GuzzleException */ private function payment(string $type, array $fields): array { - (new Validator())->payment()->validate($fields); + (new Validator())->setValidations('payment')->validate($fields); $fields = [ 'KeyID' => $this->keyId, @@ -174,24 +177,24 @@ private function payment(string $type, array $fields): array 'IP' => $fields['ip'], ]; - $obj = $this->curlTransaccion('payment', $fields, 'POST'); + $obj = $this->curlTransaction('payment', $fields, 'POST'); return $this->checkResponseCode($obj); } /** - * Realiza Transaccion - * Efectúa y retornar una respuesta a un metodo de pago. + * Realiza Transacción + * Efectúa y retornar una respuesta a un método de pago. * - * @param $url string endpoint a consultar - * @param $method string verbo http de la consulta - * @return array resultados de la transaccion + * @param $url string endpoint a consultar + * @param $method string verbo http de la consulta + * @return array resultados de la transacción * * @throws GenericException - * @throws TimeoutException + * @throws InstapagoTimeoutException * @throws GuzzleException */ - private function curlTransaccion(string $url, array $fields, string $method): array + private function curlTransaction(string $url, array $fields, string $method): array { $client = new Client([ 'base_uri' => 'https://api.instapago.com/', @@ -211,47 +214,33 @@ private function curlTransaccion(string $url, array $fields, string $method): ar return json_decode($body, true); } catch (ConnectException $e) { - throw new TimeoutException('Cannot connect to api.instapago.com'); + throw new InstapagoTimeoutException('Cannot connect to api.instapago.com'); } } /** - * Verifica y retornar el resultado de la transaccion. + * Verifica y retornar el resultado de la transacción. * - * @param array $obj datos de la consulta - * @return array datos de transaccion + * @param array $obj datos de la consulta + * @return array datos de transacción * - * @throws AuthException - * @throws BankRejectException + * @throws InstapagoAuthException + * @throws InstapagoBankRejectException * @throws GenericException * @throws InstapagoException - * @throws InvalidInputException + * @throws InstapagoInvalidInputException */ private function checkResponseCode(array $obj): array { - return match ($obj['code']) { - '400' => throw new InvalidInputException( - 'Error al validar los datos enviados' - ), - '401' => throw new AuthException( - 'Error de autenticación, ha ocurrido un error con las llaves utilizadas' - ), - '403' => throw new BankRejectException( - 'Pago Rechazado por el banco' - ), - '500' => throw new InstapagoException( - 'Ha Ocurrido un error interno dentro del servidor' - ), - '503' => throw new InstapagoException( - 'Ha Ocurrido un error al procesar los parámetros de entrada. Revise los datos enviados y vuelva a intentarlo' - ), + '400' => throw new InstapagoInvalidInputException('Datos inválidos.'), + '401' => throw new InstapagoAuthException('Error de autenticación.'), + '403' => throw new InstapagoBankRejectException('Pago rechazado por el banco.'), + '500' => throw new InstapagoException('Error interno del servidor.'), + '503' => throw new InstapagoException('Error al procesar los parámetros de entrada.'), '201' => $this->getResponse($obj), - default => throw new GenericException( - 'Not implemented yet' - ), + default => throw new GenericException('Respuesta no implementada: ' . $obj['code']), }; - } private function getResponse(array $obj): array diff --git a/src/Exceptions/AuthException.php b/src/Exceptions/AuthException.php deleted file mode 100644 index b1d8b99..0000000 --- a/src/Exceptions/AuthException.php +++ /dev/null @@ -1,12 +0,0 @@ -. @@ -29,74 +31,65 @@ namespace Instapago\Instapago; -/** - * Validator. - * - * Valida las entradas de datos para los métodos del API. - */ +use Instapago\Instapago\Exceptions\ValidationException; + class Validator { - protected array $validations = []; + private array $validations = []; - public function payment(): self + public function setValidations(string $type): self { - $this->validations = [ - 'amount' => [FILTER_VALIDATE_FLOAT], - 'description' => [FILTER_VALIDATE_REGEXP, '/^(.{0,140})$/'], - 'card_holder' => [FILTER_VALIDATE_REGEXP, '/^([a-zA-ZáéíóúñÁÉÍÓÚÑ\ ]+)$/'], - 'card_holder_id' => [FILTER_VALIDATE_REGEXP, '/^(\d{5,8})$/'], - 'card_number' => [FILTER_VALIDATE_REGEXP, '/^(\d{16})$/'], - 'cvc' => [FILTER_VALIDATE_INT], - 'expiration' => [FILTER_VALIDATE_REGEXP, '/^(\d{2})\/(\d{4})$/'], - 'ip' => [FILTER_VALIDATE_IP], - ]; - - return $this; - } - - public function release(): self - { - $this->validations = [ - 'amount' => [FILTER_VALIDATE_FLOAT], - 'id' => [FILTER_VALIDATE_REGEXP, '/^([0-9a-f]{8})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{12})$/'], - ]; - - return $this; - } - - public function query(): self - { - $this->validations = [ - 'id' => [FILTER_VALIDATE_REGEXP, '/^([0-9a-f]{8})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{12})$/'], - ]; + $rules = $this->getValidationRules(); + $this->validations = $rules[$type] ?? []; return $this; } /** - * @throws Exceptions\ValidationException + * @throws ValidationException */ public function validate(array $fields): void { + $errors = []; foreach ($this->validations as $key => $filters) { - if (! $this->_validation($fields[$key], $filters)) { - throw new Exceptions\ValidationException("Error? {$key}: {$fields[$key]}"); + if (! $this->_validation($fields[$key] ?? null, $filters)) { + $errors[$key] = "Invalid value for {$key}"; } } + + if ($errors) { + throw new ValidationException(json_encode($errors)); + } } - private function _validation(string $value, array $filters): bool + private function _validation(mixed $value, array $filters): bool { $filter = $filters[0]; - $flags = []; - if ($filter === FILTER_VALIDATE_REGEXP) { - $flags = [ - 'options' => [ - 'regexp' => $filters[1], - ], - ]; - } + $options = $filter === FILTER_VALIDATE_REGEXP ? ['options' => ['regexp' => $filters[1]]] : []; + + return filter_var($value, $filter, $options) !== false; + } - return filter_var($value, $filter, $flags); + private function getValidationRules(): array + { + return [ + 'payment' => [ + 'amount' => [FILTER_VALIDATE_FLOAT], + 'description' => [FILTER_VALIDATE_REGEXP, '/^(.{0,140})$/'], + 'card_holder' => [FILTER_VALIDATE_REGEXP, '/^([a-zA-ZáéíóúñÁÉÍÓÚÑ\ ]+)$/'], + 'card_holder_id' => [FILTER_VALIDATE_REGEXP, '/^(\d{5,8})$/'], + 'card_number' => [FILTER_VALIDATE_REGEXP, '/^(\d{16})$/'], + 'cvc' => [FILTER_VALIDATE_INT], + 'expiration' => [FILTER_VALIDATE_REGEXP, '/^(\d{2})\/(\d{4})$/'], + 'ip' => [FILTER_VALIDATE_IP], + ], + 'release' => [ + 'amount' => [FILTER_VALIDATE_FLOAT], + 'id' => [FILTER_VALIDATE_REGEXP, '/^([0-9a-f]{8})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{12})$/'], + ], + 'query' => [ + 'id' => [FILTER_VALIDATE_REGEXP, '/^([0-9a-f]{8})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{4})\-([0-9a-f]{12})$/'], + ], + ]; } } diff --git a/tests/ApiInstapagoTest.php b/tests/ApiInstapagoTest.php index c3501bb..0aee7d6 100644 --- a/tests/ApiInstapagoTest.php +++ b/tests/ApiInstapagoTest.php @@ -1,6 +1,7 @@ api = new Api('1E488391-7934-4301-9F8E-17DC99AB49B3', '691f77db9d62c0f2fe191ce69ed9bb41'); @@ -30,7 +31,7 @@ it('can trow an invalid input error', function () { $payment = $this->api->directPayment($this->dataNoOk); - expect($payment)->toBe('Error al validar los datos enviados'); + expect($payment)->toBe('Datos inválidos.'); }); it('can create a direct payment', function () { @@ -76,3 +77,16 @@ expect($payment['message'])->toBe('El pago ha sido anulado'); })->depends('it can create a direct payment'); + +it('throws an exception if keys are missing', function () { + new Api('', 'publicKey'); +})->throws(InstapagoException::class); + +it('throws an exception if public key is missing', function () { + new Api('key', ''); +})->throws(InstapagoException::class); + +it('throws validation exception for missing fields in direct payment', function () { + $payment = $this->api->directPayment([]); + expect($payment)->toBe('{"amount":"Invalid value for amount","card_holder":"Invalid value for card_holder","card_holder_id":"Invalid value for card_holder_id","card_number":"Invalid value for card_number","cvc":"Invalid value for cvc","expiration":"Invalid value for expiration","ip":"Invalid value for ip"}'); +}); diff --git a/tests/Pest.php b/tests/Pest.php index b3d9bbc..2e2b714 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1 +1,14 @@ extend(Instapago\Instapago\Tests\TestCase::class)->in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..4415b81 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +