├── .github ├── FUNDING.yml └── workflows │ └── php.yml ├── src ├── Events │ ├── NullDispatcher.php │ ├── TransactionCreating.php │ ├── TransactionCompleted.php │ └── TransactionCreated.php ├── Exceptions │ ├── ClientException.php │ ├── NetworkException.php │ ├── ServerException.php │ ├── UnknownException.php │ ├── TransbankException.php │ └── HandlesException.php ├── Services │ ├── WrapsDetail.php │ ├── Transactions │ │ ├── TransactionDetail.php │ │ ├── Response.php │ │ ├── Transaction.php │ │ └── DynamicallyAccess.php │ ├── HandlesCredentials.php │ ├── DebugsTransactions.php │ ├── FiresEvents.php │ ├── SendsRequests.php │ ├── Webpay.php │ ├── WebpayMall.php │ └── OneclickMall.php ├── ApiRequest.php ├── Credentials │ ├── Credentials.php │ └── Container.php ├── Transbank.php └── Http │ └── Connector.php ├── phpunit.xml ├── LICENSE ├── composer.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Help me support this package 2 | 3 | ko_fi: DarkGhostHunter 4 | custom: ['https://paypal.me/darkghosthunter'] 5 | -------------------------------------------------------------------------------- /src/Events/NullDispatcher.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | src 10 | 11 | 12 | src/Events/NullDispatcher.php 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Services/Transactions/TransactionDetail.php: -------------------------------------------------------------------------------- 1 | data['response_code']) && $this->data['response_code'] === 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/HandlesCredentials.php: -------------------------------------------------------------------------------- 1 | transbank->isProduction()) { 24 | return $this->container->getProductionCredentials(static::SERVICE_NAME); 25 | } 26 | 27 | // If we're running on integration, there is no harm on creating new credentials for each request. 28 | return Credentials::integrationCredentials($overrideServiceName); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/TransbankException.php: -------------------------------------------------------------------------------- 1 | token; 32 | } 33 | 34 | /** 35 | * Returns the transaction URL where the transaction can be retrieved. 36 | * 37 | * @return string 38 | */ 39 | public function getUrl(): string 40 | { 41 | return $this->url; 42 | } 43 | 44 | /** 45 | * Transforms the Response into a String for Webpay GET redirects. 46 | * 47 | * @return string 48 | */ 49 | public function __toString(): string 50 | { 51 | return $this->url . '?' . http_build_query([$this->tokenName => $this->token]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/DebugsTransactions.php: -------------------------------------------------------------------------------- 1 | transbank->logger->debug($message, $context); 18 | } 19 | 20 | /** 21 | * Debugs a transaction before creating it. 22 | * 23 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 24 | */ 25 | protected function logCreating(ApiRequest $apiRequest): void 26 | { 27 | $this->transbank->logger->debug('Creating transaction', ['api_request' => $apiRequest]); 28 | } 29 | 30 | /** 31 | * Debugs a given operation. 32 | * 33 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 34 | * @param array $rawResponse 35 | * @param string|null $token 36 | */ 37 | protected function logResponse(ApiRequest $apiRequest, array $rawResponse, string $token = null): void 38 | { 39 | $context = ['api_request' => $apiRequest, 'raw_response' => $rawResponse]; 40 | 41 | if ($token) { 42 | $context['token'] = $token; 43 | } 44 | 45 | $this->transbank->logger->debug('Response received', $context); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkghosthunter/transbank", 3 | "description": "Easy-to-use Transbank SDK for PHP.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "payments", 7 | "transbank", 8 | "api", 9 | "sdk", 10 | "webpay" 11 | ], 12 | "config": { 13 | "sort-packages": true 14 | }, 15 | "require": { 16 | "php": "^8.0", 17 | "ext-json": "*", 18 | "psr/http-client": "1.*", 19 | "psr/log": "1.*|2.*|3.*", 20 | "psr/event-dispatcher": "1.*", 21 | "nyholm/psr7": "^1.4" 22 | }, 23 | "require-dev": { 24 | "roave/security-advisories": "dev-latest", 25 | "phpunit/phpunit": "^9.5", 26 | "mockery/mockery": "^1.4", 27 | "guzzlehttp/guzzle": "^7.4" 28 | }, 29 | "suggest": { 30 | "guzzlehttp/guzzle": "HTTP Client for contacting Transbank servers.", 31 | "symfony/http-client": "HTTP Client for contacting Transbank servers.", 32 | "monolog/monolog": "Allows advanced logging for this SDK operations.", 33 | "symfony/event-dispatcher": "Allows for hearing transactions created and completed.", 34 | "league/event": "Allows for hearing transactions created and completed." 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "DarkGhostHunter\\Transbank\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "test": "vendor/bin/phpunit --coverage-clover build/logs/clover.xml", 48 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.0, 8.1] 15 | dependency-version: [prefer-stable, prefer-lowest] 16 | 17 | name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php }} 27 | extensions: mbstring, intl, json 28 | coverage: xdebug 29 | 30 | - name: Cache dependencies 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.composer/cache/files 34 | key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 35 | restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer- 36 | 37 | - name: Install dependencies 38 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress --no-suggest 39 | 40 | - name: Run Tests 41 | run: composer run-script test 42 | 43 | - name: Upload Coverage to Coveralls 44 | env: 45 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | COVERALLS_SERVICE_NAME: github 47 | run: | 48 | rm -rf composer.* vendor/ 49 | composer require php-coveralls/php-coveralls 50 | vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml 51 | -------------------------------------------------------------------------------- /src/Services/FiresEvents.php: -------------------------------------------------------------------------------- 1 | transbank->event->dispatch(new TransactionCreating($apiRequest)); 22 | } 23 | 24 | /** 25 | * Fires a Transaction Created event. 26 | * 27 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 28 | * @param \DarkGhostHunter\Transbank\Services\Transactions\Response $response 29 | */ 30 | protected function fireCreated(ApiRequest $apiRequest, Response $response): void 31 | { 32 | $this->transbank->event->dispatch(new TransactionCreated($apiRequest, $response)); 33 | } 34 | 35 | /** 36 | * Fires a Transaction Completed event. 37 | * 38 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 39 | * @param \DarkGhostHunter\Transbank\Services\Transactions\Transaction $transaction 40 | */ 41 | protected function fireCompleted(ApiRequest $apiRequest, Transaction $transaction): void 42 | { 43 | $this->transbank->event->dispatch(new TransactionCompleted($apiRequest, $transaction)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Exceptions/HandlesException.php: -------------------------------------------------------------------------------- 1 | apiRequest; 39 | } 40 | 41 | /** 42 | * Returns the Server Request sent to Transbank, if any. 43 | * 44 | * @return \Psr\Http\Message\ServerRequestInterface|null 45 | */ 46 | public function getServerRequest(): ?ServerRequestInterface 47 | { 48 | return $this->request; 49 | } 50 | 51 | /** 52 | * Returns the Response from Transbank, if any. 53 | * 54 | * @return \Psr\Http\Message\ResponseInterface|null 55 | */ 56 | public function getResponse(): ?ResponseInterface 57 | { 58 | return $this->response; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Services/SendsRequests.php: -------------------------------------------------------------------------------- 1 | transbank->connector->send( 36 | $method, 37 | $this->buildEndpoint($endpoint, $replace), 38 | $apiRequest, 39 | $this->getEnvironmentCredentials($action), 40 | $options 41 | ); 42 | } 43 | 44 | /** 45 | * Builds the endpoint, depending on the environment, and replaces keys from it. 46 | * 47 | * @param string $endpoint 48 | * @param array $replace 49 | * 50 | * @return string 51 | */ 52 | protected function buildEndpoint(string $endpoint, array $replace = []): string 53 | { 54 | $endpoint = $this->transbank->isProduction() 55 | ? Connector::PRODUCTION_ENDPOINT . $endpoint 56 | : Connector::INTEGRATION_ENDPOINT . $endpoint; 57 | 58 | return str_replace(array_keys($replace), $replace, $endpoint); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ApiRequest.php: -------------------------------------------------------------------------------- 1 | attributes)) { 31 | return ''; 32 | } 33 | 34 | return json_encode($this->jsonSerialize(), $options); 35 | } 36 | 37 | /** 38 | * Specify data which should be serialized to JSON. 39 | * 40 | * @return mixed 41 | */ 42 | public function jsonSerialize(): array 43 | { 44 | return $this->attributes; 45 | } 46 | 47 | /** 48 | * Whether an offset exists. 49 | * 50 | * @param mixed $offset 51 | * @return bool 52 | */ 53 | public function offsetExists(mixed $offset): bool 54 | { 55 | return isset($this->attributes[$offset]); 56 | } 57 | 58 | /** 59 | * Offset to retrieve. 60 | * 61 | * @param mixed $offset 62 | * @return mixed 63 | */ 64 | public function offsetGet(mixed $offset): mixed 65 | { 66 | return $this->attributes[$offset]; 67 | } 68 | 69 | /** 70 | * Offset to set. 71 | * 72 | * @param mixed $offset 73 | * @param mixed $value 74 | * @return void 75 | */ 76 | public function offsetSet(mixed $offset, mixed $value): void 77 | { 78 | $this->attributes[$offset] = $value; 79 | } 80 | 81 | /** 82 | * Offset to unset. 83 | * 84 | * @param mixed $offset 85 | * @return void 86 | */ 87 | public function offsetUnset(mixed $offset): void 88 | { 89 | unset($this->attributes[$offset]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Credentials/Credentials.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public const INTEGRATION_KEYS = [ 15 | 'webpay' => 597055555532, 16 | 'webpayMall' => 597055555535, 17 | 'webpayMall.capture' => 597055555531, 18 | 'oneclickMall' => 597055555541, 19 | 'oneclickMall.capture' => 597055555547, 20 | 'fullTransaction' => 597055555530, 21 | 'fullTransaction.capture' => 597055555531, 22 | 'fullTransactionMall' => 597055555551, 23 | 'fullTransactionMall.capture' => 597055555531, 24 | ]; 25 | 26 | /** 27 | * Integration shared secret. 28 | * 29 | * @var string 30 | */ 31 | public const INTEGRATION_SECRET = '579B532A7440BB0C9079DED94D31EA1615BACEB56610332264630D42D0A36B1C'; 32 | 33 | /** 34 | * Service key, usually the Commerce Code. 35 | * 36 | * @var string|null 37 | */ 38 | public ?string $key = null; 39 | 40 | /** 41 | * Service shared secret. 42 | * 43 | * @var string|null 44 | */ 45 | public ?string $secret = null; 46 | 47 | /** 48 | * Create a new Credentials instance. 49 | * 50 | * @param string|null $key 51 | * @param string|null $secret 52 | */ 53 | public function __construct(?string $key = null, ?string $secret = null) 54 | { 55 | $this->secret = $secret; 56 | $this->key = $key; 57 | } 58 | 59 | /** 60 | * Instance a new Credential object with the key and secret. 61 | * 62 | * @param string $key 63 | * @param string $secret 64 | * @return static 65 | */ 66 | public static function make(string $key, string $secret): static 67 | { 68 | return new static($key, $secret); 69 | } 70 | 71 | /** 72 | * Returns integration key for a given service name. 73 | * 74 | * @param string $service 75 | * @return static 76 | */ 77 | public static function integrationCredentials(string $service): static 78 | { 79 | if (!isset(static::INTEGRATION_KEYS[$service])) { 80 | throw new RuntimeException("The integration key for [$service] doesn't exist."); 81 | } 82 | 83 | return static::make(static::INTEGRATION_KEYS[$service], static::INTEGRATION_SECRET); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Credentials/Container.php: -------------------------------------------------------------------------------- 1 | $credentials 42 | * @return void 43 | */ 44 | public function setFromArray(array $credentials): void 45 | { 46 | foreach ($credentials as $service => $credential) { 47 | // Check if the service name exists. If not, bail. 48 | $this->throwIfCredentialsDoesntExist($service); 49 | 50 | // We need the array declaring the key and the secret. If not, bail. 51 | if (!isset($credential['key'], $credential['secret'])) { 52 | throw new LogicException("Credentials for [$service] must have a [key] and [secret]."); 53 | } 54 | 55 | $this->{$service} = Credentials::make($credential['key'], $credential['secret']); 56 | } 57 | } 58 | 59 | /** 60 | * Returns the credentials for a given service. 61 | * 62 | * @param string $service 63 | * 64 | * @return \DarkGhostHunter\Transbank\Credentials\Credentials 65 | */ 66 | public function getProductionCredentials(string $service): Credentials 67 | { 68 | $this->throwIfCredentialsDoesntExist($service); 69 | 70 | return $this->{$service} ?? throw new RuntimeException("Production credentials for [$service] are not set."); 71 | } 72 | 73 | /** 74 | * Checks that credentials for a service name exists. 75 | * 76 | * @param string $service 77 | * @return void 78 | * @throws \LogicException 79 | */ 80 | protected function throwIfCredentialsDoesntExist(string $service): void 81 | { 82 | if (!property_exists($this, $service)) { 83 | throw new LogicException("The Transbank service [$service] doesn't exist for these credentials."); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Services/Transactions/Transaction.php: -------------------------------------------------------------------------------- 1 | hello_world 21 | * $transaction->helloWorld 22 | * $transaction->getHelloWorld() 23 | * $transaction['hello_world'] 24 | * 25 | * @package DarkGhostHunter\Transbank\Services\Transactions 26 | * 27 | * @property-read $details []static An array of details only for Mall transactions. 28 | */ 29 | class Transaction implements ArrayAccess, JsonSerializable 30 | { 31 | use DynamicallyAccess; 32 | 33 | /** 34 | * ApiRequest constructor. 35 | * 36 | * @param string $serviceAction Name of the service and action that created this transaction using dot notation. 37 | * @param array $data Raw response array from Transbank. 38 | */ 39 | public function __construct(public string $serviceAction, protected array $data) 40 | { 41 | // 42 | } 43 | 44 | /** 45 | * Creates a new Transaction with a detail array. 46 | * 47 | * @param string $serviceAction 48 | * @param array $response 49 | * @return static 50 | */ 51 | public static function createWithDetails(string $serviceAction, array $response): static 52 | { 53 | // If the response contains details, add them as a class. 54 | if (isset($response['details']) && is_array($response['details'])) { 55 | foreach ($response['details'] as $index => $detail) { 56 | $response['details'][$index] = new TransactionDetail($detail); 57 | } 58 | } 59 | 60 | return new static($serviceAction, $response); 61 | } 62 | 63 | /** 64 | * Checks if the transaction was successful. 65 | * 66 | * @return bool 67 | */ 68 | public function isSuccessful(): bool 69 | { 70 | // If TBK data has been received, immediately bail out. 71 | if (isset($this->data['TBK_ID_SESSION'], $this->data['TBK_ORDEN_COMPRA'])) { 72 | return false; 73 | } 74 | 75 | // If there is a native response code, return it. 76 | if (isset($this->data['response_code'])) { 77 | return $this->data['response_code'] === 0; 78 | } 79 | 80 | // If it has details, 81 | if (isset($this->data['details'])) { 82 | foreach ($this->data['details'] as $detail) { 83 | if (!$detail->isSuccessful()) { 84 | return false; 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | 94 | /** 95 | * Returns the Credit Card number as an integer, or null if it doesn't exist. 96 | * 97 | * @return int|null 98 | */ 99 | public function getCreditCardNumber(): ?int 100 | { 101 | if (!isset($this->data['card_detail']['card_number'])) { 102 | return null; 103 | } 104 | 105 | // If the card number is a string, then get the last 4 digits. 106 | if (is_string($this->data['card_detail']['card_number'])) { 107 | return (int)substr($this->data['card_detail']['card_number'], -4); 108 | } 109 | 110 | return $this->data['card_detail']['card_number']; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Services/Transactions/DynamicallyAccess.php: -------------------------------------------------------------------------------- 1 | data[$name]) || isset($this->data[$name = self::toSnakeCase($name)])) { 36 | return $this->data[$name]; 37 | } 38 | } 39 | 40 | // Since there is no key matching the name, bail. 41 | throw new BadMethodCallException("Method $method does not exist"); 42 | } 43 | 44 | /** 45 | * This function happily returns the key of a response using snake case. 46 | * 47 | * @param string $method 48 | * 49 | * @return string 50 | */ 51 | protected static function toSnakeCase(string $method): string 52 | { 53 | return strtolower(ltrim(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $method), '_')); 54 | } 55 | 56 | /** 57 | * Dynamically return a key from their properties. 58 | * 59 | * @param string $name 60 | * 61 | * @return mixed 62 | */ 63 | public function __get(string $name) 64 | { 65 | if (isset($this->data[$name])) { 66 | return $this->data[$name]; 67 | } 68 | 69 | // Lets try to use camelCase. 70 | if (ctype_lower($name[0]) && isset($this->data[$snake = self::toSnakeCase($name)])) { 71 | return $this->data[$snake]; 72 | } 73 | 74 | // The property doesn't exist, so bail out. 75 | trigger_error("Undefined property: " . __CLASS__ . '::$' . $name, E_USER_ERROR); 76 | } 77 | 78 | /** 79 | * Disable setting a property. 80 | * 81 | * @param string $name 82 | * @param mixed $value 83 | */ 84 | public function __set(string $name, mixed $value): void 85 | { 86 | // Immutable 87 | } 88 | 89 | /** 90 | * Checks if a property exists. 91 | * 92 | * @param string $name 93 | * 94 | * @return bool 95 | */ 96 | public function __isset(string $name): bool 97 | { 98 | return $this->offsetExists($name) || isset($this->data[self::toSnakeCase($name)]); 99 | } 100 | 101 | /** 102 | * Whether an offset exists. 103 | * 104 | * @param mixed $offset 105 | * @return bool 106 | */ 107 | public function offsetExists(mixed $offset): bool 108 | { 109 | return isset($this->data[$offset]); 110 | } 111 | 112 | /** 113 | * Offset to retrieve. 114 | * 115 | * @param mixed $offset 116 | * @return mixed 117 | */ 118 | public function offsetGet(mixed $offset) 119 | { 120 | return $this->data[$offset]; 121 | } 122 | 123 | /** 124 | * Offset to set. 125 | * 126 | * @param mixed $offset 127 | * @param mixed $value 128 | * @return void 129 | */ 130 | public function offsetSet(mixed $offset, mixed $value): void 131 | { 132 | // Immutable. 133 | } 134 | 135 | /** 136 | * Offset to unset. 137 | * 138 | * @param mixed $offset 139 | * @return void 140 | */ 141 | public function offsetUnset(mixed $offset): void 142 | { 143 | // Immutable 144 | } 145 | 146 | /** 147 | * Specify data which should be serialized to JSON 148 | * 149 | * @return array 150 | */ 151 | public function jsonSerialize(): array 152 | { 153 | return $this->data; 154 | } 155 | 156 | /** 157 | * Transforms this transaction to a JSON string. 158 | * 159 | * @param int $options 160 | * @return string 161 | */ 162 | public function toJson(int $options = 0): string 163 | { 164 | return json_encode($this->jsonSerialize(), $options); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![rawpixel - Unsplash (UL) #SEDqvdbkDQw](https://images.unsplash.com/photo-1614267119077-51bdcbf9f77a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1280&h=400&q=80) 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/darkghosthunter/transbank/v/stable)](https://packagist.org/packages/darkghosthunter/transbank) [![License](https://poser.pugx.org/darkghosthunter/transbank/license)](https://packagist.org/packages/darkghosthunter/transbank) ![](https://img.shields.io/packagist/php-v/darkghosthunter/transbank.svg) [![PHP Composer](https://github.com/DarkGhostHunter/TransbankApi/workflows/PHP%20Composer/badge.svg)](https://github.com/DarkGhostHunter/transbank/actions) [![Coverage Status](https://coveralls.io/repos/github/DarkGhostHunter/Transbank/badge.svg?branch=master)](https://coveralls.io/github/DarkGhostHunter/Transbank?branch=master) 4 | 5 | # Transbank 6 | 7 | Easy-to-use Transbank SDK for PHP. 8 | 9 | Supports Webpay, Webpay Mall and Webpay Oneclick Mall. 10 | 11 | ## Requisites: 12 | 13 | * PHP 8.0 or later. 14 | * `ext-json` 15 | * [HTTP Client](#http-client) 16 | * [Logger](#logger-optional) (optional) 17 | * [Event dispatcher](#event-dispatcher-optional) (optional) 18 | 19 | # Installation 20 | 21 | Require it with [Composer](https://getcomposer.org/): 22 | 23 | composer require darkghosthunter/webpay 24 | 25 | ## Usage 26 | 27 | This SDK mimics all the Webpay, Webpay Mall and Oneclick Mall methods from the official Transbank SDK for PHP. 28 | 29 | You can check the documentation of these services in Transbank Developer's site. 30 | 31 | - [Webpay](https://www.transbankdevelopers.cl/documentacion/webpay-plus#webpay-plus) 32 | - [Webpay Mall](https://www.transbankdevelopers.cl/documentacion/webpay-plus#webpay-plus-mall) 33 | - [Oneclick Mall](https://www.transbankdevelopers.cl/documentacion/oneclick) 34 | 35 | ## Quickstart 36 | 37 | Instance the `Transbank` object. You can do it manually, or with `make()` which will use Guzzle o Symfony HTTP Clients if any of these is already installed. 38 | 39 | ```php 40 | use DarkGhostHunter\Transbank\Transbank; 41 | 42 | $transbank = Transbank::make(); 43 | ``` 44 | 45 | If your project doesn't manage single instances, you can use `getInstance()` to receive a static Transbank instance. 46 | 47 | ```php 48 | use DarkGhostHunter\Transbank\Transbank; 49 | 50 | $transbank = Transbank::getInstance(); 51 | ``` 52 | 53 | ### Environments and credentials 54 | 55 | By default, this SDK starts up in **integration** environment, where all transactions made are fake by using Transbank's own _integration_ server. 56 | 57 | To operate in production mode, where all transaction will be real, you will need to use `toProduction()` along an `array` with the name of the service and their production credentials issued by Transbank to you: `webpay`, `webpayMall` or `oneclickMall`. 58 | 59 | ```php 60 | $transbank->toProduction([ 61 | 'patpass' => ['555876543210','7a7b7d6cce5e...'] 62 | ]); 63 | ``` 64 | 65 | > For Mall operations, the "child" commerce code is only needed when doing the transactions. 66 | 67 | ### Using a Service 68 | 69 | To use a Transbank service, just call the method on the `Transbank` instance: `webpay`, `webpayMall` and `oneclickMall`. 70 | 71 | ```php 72 | use DarkGhostHunter\Transbank\Transbank; 73 | 74 | $transaction = Transbank::singleton() 75 | ->webpay() 76 | ->create('order#123', 9990, 'https://app.com/compra'); 77 | ``` 78 | 79 | ### HTTP Client 80 | 81 | This package is compatible with any PSR-18 compliant HTTP Client. IF you don't have one, you can install [Guzzle](https://docs.guzzlephp.org/) or [Symfony](https://symfony.com/doc/current/http_client.html) 82 | 83 | composer require guzzlehttp/guzzle:>=7.0 84 | 85 | or 86 | 87 | composer require symfony/http-client:>=5.2 88 | 89 | Some PHP platforms already ship with their own HTTP Client, like [Amp](https://amphp.org/http-client/), [ReactPHP](https://reactphp.org/http/), or [Swoole](https://www.swoole.co.uk/docs/modules/swoole-coroutine-http-client). 90 | 91 | ### Logger (optional) 92 | 93 | You can use this package with any PSR-3 compliant Log system, allowing to debug transactions. You can use [Monolog](https://github.com/Seldaek/monolog) if you don't have one. 94 | 95 | composer require monolog/monolog 96 | 97 | All operations are sent to the logger using `debug`. 98 | 99 | ### Event dispatcher (optional) 100 | 101 | You can use this package with any PSR-14 compliant Event Dispatcher, so you will be able to hear transactions started and completed. You can use [Symfony](https://github.com/symfony/event-dispatcher) or [League](https://event.thephpleague.com/) if you don't have one. 102 | 103 | composer require symfony/event-dispatcher 104 | 105 | or 106 | 107 | composer require league/event 108 | 109 | This package sends the following events: 110 | 111 | * `TransactionCreating` before a transaction is created in Transbank. 112 | * `TransactionCreated` after a transaction is created in Transbank, but pending payment. 113 | * `TransactionCompleted` after a transaction or refund is completed in Transbank, regardless of the success. 114 | 115 | ### Excepciones 116 | 117 | All exceptions implement `TransbankException`, so you can easily catch and check what happened. 118 | 119 | > Transactions properly rejected by banks or credit card issuers do not throw exceptions. 120 | 121 | There are 4 types of exceptions: 122 | 123 | * `ClientException`: Any error byproduct of bad transactions, bad configuration, abort, abandonment, timeout or invalid values. 124 | * `NetworkException`: Any communication error from Transbank Server, like network timeouts or wrong endpoints. 125 | * `ServerException`: Any internal Transbank error. 126 | * `UnknownException`: Any other error. 127 | 128 | > Exceptions are **not** logged. 129 | 130 | ## [Run examples locally](examples/README.md) 131 | 132 | # Licence 133 | 134 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 135 | 136 | `Redcompra`, `Webpay`, `Oneclick`, `Onepay`, `Patpass` and `Transbank` are trademarks of [Transbank S.A.](https://www.transbank.cl/). This package and its author are not associated with Transbank S.A. 137 | -------------------------------------------------------------------------------- /src/Transbank.php: -------------------------------------------------------------------------------- 1 | new \GuzzleHttp\Client(), 69 | class_exists(\Symfony\Component\HttpClient\Psr18Client::class) => new \Symfony\Component\HttpClient\Psr18Client(), 70 | default => throw new RuntimeException( 71 | 'The "guzzlehttp/guzzle" or "symfony/http-client" libraries are not present. Install one or use your own PSR-18 HTTP Client.' 72 | ), 73 | }; 74 | 75 | return new static( 76 | new Container(), 77 | new NullLogger(), 78 | new NullDispatcher(), 79 | new Connector($client, $factory = new Psr17Factory(), $factory) 80 | ); 81 | } 82 | 83 | /** 84 | * Returns the Transbank single instance. 85 | * 86 | * @return static 87 | */ 88 | public static function getInstance(): self 89 | { 90 | return static::$instance ??= static::make(); 91 | } 92 | 93 | /** 94 | * Sets the Transbank instance. 95 | * 96 | * @param static|null $instance 97 | * @return void 98 | */ 99 | public static function setInstance(self $instance = null): void 100 | { 101 | static::$instance = $instance; 102 | } 103 | 104 | /** 105 | * Sets all the Transbank services to run in production servers. 106 | * 107 | * Supported services: 108 | * - webpay 109 | * - webpayMall 110 | * - oneclickMall 111 | * - fullTransaction 112 | * - fullTransactionMall 113 | * 114 | * @param array> $credentials 115 | * @return $this 116 | */ 117 | public function toProduction(array $credentials): static 118 | { 119 | if (empty($credentials)) { 120 | throw new LogicException('Cannot set empty credentials for production environment.'); 121 | } 122 | 123 | $this->credentials->setFromArray($credentials); 124 | 125 | $this->production = true; 126 | 127 | $this->logger->debug( 128 | 'Transbank has been set to production environment.', 129 | ['credentials' => array_keys($credentials)] 130 | ); 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Returns the SDK to integration environment. 137 | * 138 | * @return $this 139 | */ 140 | public function toIntegration(): static 141 | { 142 | $this->production = false; 143 | 144 | $this->logger->debug('Transbank has been set to integration environment.'); 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Check if the current Transbank SDK are running in integration environment. 151 | * 152 | * @return bool 153 | */ 154 | public function isIntegration(): bool 155 | { 156 | return !$this->isProduction(); 157 | } 158 | 159 | /** 160 | * Check if the current Transbank SDK are running in production environment. 161 | * 162 | * @return bool 163 | */ 164 | public function isProduction(): bool 165 | { 166 | return $this->production; 167 | } 168 | 169 | /** 170 | * Returns the Webpay service. 171 | * 172 | * @return \DarkGhostHunter\Transbank\Services\Webpay 173 | */ 174 | public function webpay(): Services\Webpay 175 | { 176 | return $this->webpay ??= new Services\Webpay($this, $this->credentials); 177 | } 178 | 179 | /** 180 | * Returns the Webpay Mall service. 181 | * 182 | * @return \DarkGhostHunter\Transbank\Services\WebpayMall 183 | */ 184 | public function webpayMall(): Services\WebpayMall 185 | { 186 | return $this->webpayMall ??= new Services\WebpayMall($this, $this->credentials); 187 | } 188 | 189 | /** 190 | * Returns the Oneclick Mall service. 191 | * 192 | * @return \DarkGhostHunter\Transbank\Services\OneclickMall 193 | */ 194 | public function oneclickMall(): Services\OneclickMall 195 | { 196 | return $this->oneclickMall ??= new Services\OneclickMall($this, $this->credentials); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Services/Webpay.php: -------------------------------------------------------------------------------- 1 | $buyOrder, 71 | 'amount' => $amount, 72 | 'session_id' => $sessionId, 73 | 'return_url' => $returnUrl, 74 | ] 75 | ); 76 | 77 | $this->logCreating($apiRequest); 78 | $this->fireCreating($apiRequest); 79 | 80 | $response = $this->send(static::SERVICE_NAME, $apiRequest, 'post', self::ENDPOINT_CREATE, [], $options); 81 | $transbankResponse = new Transactions\Response($response['token'], $response['url']); 82 | 83 | $this->fireCreated($apiRequest, $transbankResponse); 84 | $this->logResponse($apiRequest, $response); 85 | 86 | return $transbankResponse; 87 | } 88 | 89 | /** 90 | * Commits a transaction immediately 91 | * 92 | * @param string $token 93 | * @param array $options 94 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 95 | * 96 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 97 | */ 98 | public function commit(string $token, array $options = []): Transactions\Transaction 99 | { 100 | $apiRequest = new ApiRequest(static::ACTION_COMMIT); 101 | 102 | $this->log('Committing transaction', ['token' => $token, 'api_request' => $apiRequest]); 103 | 104 | $response = $this->send( 105 | static::SERVICE_NAME, 106 | $apiRequest, 107 | 'put', 108 | static::ENDPOINT_COMMIT, 109 | ['{token}' => $token], 110 | $options 111 | ); 112 | 113 | $transaction = new Transactions\Transaction(static::ACTION_COMMIT, $response); 114 | 115 | $this->logResponse($apiRequest, $response, $token); 116 | $this->fireCompleted($apiRequest, $transaction); 117 | 118 | return $transaction; 119 | } 120 | 121 | /** 122 | * Returns the status of a non-expired transaction by its token. 123 | * 124 | * @param string $token 125 | * @param array $options 126 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 127 | * 128 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 129 | */ 130 | public function status(string $token, array $options = []): Transactions\Transaction 131 | { 132 | $apiRequest = new ApiRequest(self::ACTION_STATUS); 133 | 134 | $this->log('Retrieving transaction status', ['token' => $token, 'api_request' => $apiRequest]); 135 | 136 | $response = $this->send( 137 | self::SERVICE_NAME, 138 | $apiRequest, 139 | 'get', 140 | self::ENDPOINT_STATUS, 141 | ['{token}' => $token], 142 | $options 143 | ); 144 | 145 | $this->logResponse($apiRequest, $response, $token); 146 | 147 | return new Transactions\Transaction(static::ACTION_STATUS, $response); 148 | } 149 | 150 | /** 151 | * Refunds partially or totally a given credit-card charge amount. 152 | * 153 | * @param string $token 154 | * @param int|float $amount 155 | * @param array $options 156 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 157 | * 158 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 159 | */ 160 | public function refund(string $token, int|float $amount, array $options = []): Transactions\Transaction 161 | { 162 | $apiRequest = new ApiRequest(static::ACTION_REFUND, ['amount' => $amount]); 163 | 164 | $this->log('Refunding transaction', ['token' => $token, 'api_request' => $apiRequest]); 165 | 166 | $this->fireCreating($apiRequest); 167 | 168 | $response = $this->send( 169 | static::SERVICE_NAME, 170 | $apiRequest, 171 | 'put', 172 | self::ENDPOINT_REFUND, 173 | ['{token}' => $token], 174 | $options 175 | ); 176 | 177 | $transaction = new Transactions\Transaction(static::ACTION_REFUND, $response); 178 | 179 | $this->logResponse($apiRequest, $response, $token); 180 | 181 | $this->fireCompleted($apiRequest, $transaction); 182 | 183 | return $transaction; 184 | } 185 | 186 | /** 187 | * Creates a Capture ApiRequest on Transbank servers, returns a response. 188 | * 189 | * This transaction type only works for credit cards, and "holds" the amount up to 7 days. 190 | * 191 | * @param string $token 192 | * @param string $buyOrder 193 | * @param int $authorizationCode 194 | * @param int|float $captureAmount 195 | * @param array $options 196 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 197 | * 198 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 199 | */ 200 | public function capture( 201 | string $token, 202 | string $buyOrder, 203 | int $authorizationCode, 204 | int|float $captureAmount, 205 | array $options = [] 206 | ): Transactions\Transaction { 207 | $apiRequest = new ApiRequest( 208 | static::ACTION_CAPTURE, 209 | [ 210 | 'buy_order' => $buyOrder, 211 | 'authorization_code' => $authorizationCode, 212 | 'capture_amount' => $captureAmount, 213 | ] 214 | ); 215 | 216 | $this->log('Capturing transaction', ['token' => $token, 'api_request' => $apiRequest]); 217 | 218 | $response = $this->send( 219 | static::SERVICE_NAME, 220 | $apiRequest, 221 | 'put', 222 | self::ENDPOINT_CAPTURE, 223 | ['{token}' => $token], 224 | $options 225 | ); 226 | 227 | $transaction = new Transactions\Transaction(static::ACTION_CAPTURE, $response); 228 | 229 | $this->logResponse($apiRequest, $response, $token); 230 | $this->fireCompleted($apiRequest, $transaction); 231 | 232 | return $transaction; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Services/WebpayMall.php: -------------------------------------------------------------------------------- 1 | $buyOrder, 64 | 'session_id' => $sessionId, 65 | 'return_url' => $returnUrl, 66 | 'details' => static::wrapDetails($details), 67 | ] 68 | ); 69 | 70 | $this->logCreating($apiRequest); 71 | $this->fireCreating($apiRequest); 72 | 73 | $response = $this->send(self::SERVICE_NAME, $apiRequest, 'post', static::ENDPOINT_CREATE, [], $options); 74 | 75 | $transbankResponse = new Transactions\Response($response['token'], $response['url']); 76 | 77 | $this->logResponse($apiRequest, $response); 78 | $this->fireCreated($apiRequest, $transbankResponse); 79 | 80 | return $transbankResponse; 81 | } 82 | 83 | /** 84 | * Commits a Mall transaction from Transbank servers. 85 | * 86 | * @param string $token 87 | * @param array $options 88 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 89 | * 90 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 91 | */ 92 | public function commit(string $token, array $options = []): Transactions\Transaction 93 | { 94 | $apiRequest = new ApiRequest(static::ACTION_COMMIT); 95 | 96 | $this->log('Committing transaction', ['token' => $token, 'api_request' => $apiRequest]); 97 | 98 | $response = $this->send( 99 | static::SERVICE_NAME, 100 | $apiRequest, 101 | 'put', 102 | static::ENDPOINT_COMMIT, 103 | ['{token}' => $token], 104 | $options 105 | ); 106 | 107 | $transaction = Transactions\Transaction::createWithDetails(static::ACTION_COMMIT, $response); 108 | 109 | $this->logResponse($apiRequest, $response, $token); 110 | $this->fireCompleted($apiRequest, $transaction); 111 | 112 | return $transaction; 113 | } 114 | 115 | 116 | /** 117 | * Returns the transaction status by its token. 118 | * 119 | * @param string $token 120 | * @param array $options 121 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 122 | * 123 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 124 | */ 125 | public function status(string $token, array $options = []): Transactions\Transaction 126 | { 127 | $apiRequest = new ApiRequest(static::ACTION_STATUS); 128 | 129 | $this->log('Retrieving transaction status', ['token' => $token, 'api_request' => $apiRequest]); 130 | 131 | $response = $this->send( 132 | static::SERVICE_NAME, 133 | $apiRequest, 134 | 'get', 135 | Webpay::ENDPOINT_STATUS, 136 | ['{token}' => $token], 137 | $options 138 | ); 139 | 140 | $this->logResponse($apiRequest, $response, $token); 141 | 142 | return Transactions\Transaction::createWithDetails(static::ACTION_STATUS, $response); 143 | } 144 | 145 | /** 146 | * Refunds partially or totally a Mall transaction in Transbank. 147 | * 148 | * @param string|int $commerceCode 149 | * @param string $token 150 | * @param string $buyOrder 151 | * @param int|float $amount 152 | * @param array $options 153 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 154 | * 155 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 156 | */ 157 | public function refund( 158 | string|int $commerceCode, 159 | string $token, 160 | string $buyOrder, 161 | int|float $amount, 162 | array $options = [] 163 | ): Transactions\Transaction { 164 | $apiRequest = new ApiRequest( 165 | static::ACTION_REFUND, 166 | [ 167 | 'commerce_code' => $commerceCode, 168 | 'buy_order' => $buyOrder, 169 | 'amount' => $amount, 170 | ] 171 | ); 172 | 173 | $this->log('Refunding transaction', ['token' => $token, 'api_request' => $apiRequest]); 174 | 175 | $this->fireCreating($apiRequest); 176 | 177 | $response = $this->send( 178 | static::SERVICE_NAME, 179 | $apiRequest, 180 | 'post', 181 | static::ENDPOINT_REFUND, 182 | ['{token}' => $token], 183 | $options 184 | ); 185 | 186 | $transaction = new Transactions\Transaction(static::ACTION_REFUND, $response); 187 | 188 | $this->logResponse($apiRequest, $response, $token); 189 | $this->fireCompleted($apiRequest, $transaction); 190 | 191 | return $transaction; 192 | } 193 | 194 | /** 195 | * Captures an amount of a given transaction by its token. 196 | * 197 | * @param string|int $commerceCode 198 | * @param string $token 199 | * @param string $buyOrder 200 | * @param int|string $authorizationCode 201 | * @param int|float $captureAmount 202 | * @param array $options 203 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 204 | * 205 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 206 | */ 207 | public function capture( 208 | string|int $commerceCode, 209 | string $token, 210 | string $buyOrder, 211 | string|int $authorizationCode, 212 | int|float $captureAmount, 213 | array $options = [] 214 | ): Transactions\Transaction { 215 | $apiRequest = new ApiRequest( 216 | static::ACTION_CAPTURE, 217 | [ 218 | 'commerce_code' => $commerceCode, 219 | 'buy_order' => $buyOrder, 220 | 'authorization_code' => $authorizationCode, 221 | 'capture_amount' => $captureAmount, 222 | ] 223 | ); 224 | 225 | $this->log('Capturing transaction', ['token' => $token, 'api_request' => $apiRequest]); 226 | 227 | // If we are on integration, we need to override the credentials. 228 | $serviceName = $this->transbank->isIntegration() ? static::ACTION_CAPTURE : static::SERVICE_NAME; 229 | 230 | $response = $this->send( 231 | $serviceName, 232 | $apiRequest, 233 | 'put', 234 | Webpay::ENDPOINT_CAPTURE, 235 | ['{token}' => $token], 236 | $options 237 | ); 238 | 239 | $transaction = new Transactions\Transaction(static::ACTION_CAPTURE, $response); 240 | 241 | $this->logResponse($apiRequest, $response, $token); 242 | $this->fireCompleted($apiRequest, $transaction); 243 | 244 | return $transaction; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Http/Connector.php: -------------------------------------------------------------------------------- 1 | requestFactory->createServerRequest($method, $this->setApiVersion($endpoint)); 98 | 99 | return $this->sendRequest($request, $apiRequest, $credentials, $options); 100 | } 101 | 102 | /** 103 | * Replace the API Version from the endpoint. 104 | * 105 | * @param string $endpoint 106 | * 107 | * @return string 108 | */ 109 | protected function setApiVersion(string $endpoint): string 110 | { 111 | return str_replace('{api_version}', static::API_VERSION, $endpoint); 112 | } 113 | 114 | /** 115 | * Prepares the transaction and sends it. 116 | * 117 | * @param \Psr\Http\Message\ServerRequestInterface $request 118 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 119 | * @param \DarkGhostHunter\Transbank\Credentials\Credentials $credentials 120 | * @param array $options 121 | * 122 | * @return array 123 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 124 | */ 125 | protected function sendRequest( 126 | Request $request, 127 | ApiRequest $apiRequest, 128 | Credentials $credentials, 129 | array $options = [] 130 | ): array { 131 | $request = $this->prepareRequest($request, $apiRequest, $credentials, $options); 132 | 133 | try { 134 | $response = $this->client->sendRequest($request); 135 | } catch (NetworkExceptionInterface $exception) { 136 | throw new NetworkException( 137 | 'Could not establish connection with Transbank.', 138 | $apiRequest, 139 | $request, 140 | null, 141 | $exception 142 | ); 143 | } catch (Throwable $exception) { 144 | throw new UnknownException( 145 | 'An error occurred when trying to communicate with Transbank.', 146 | $apiRequest, 147 | $request, 148 | null, 149 | $exception 150 | ); 151 | } 152 | 153 | // If we received a response, check if the response is NOT an error. 154 | $this->throwExceptionOnResponseError($apiRequest, $request, $response); 155 | 156 | return $this->decodeJsonFromContents($apiRequest, $request, $response); 157 | } 158 | 159 | /** 160 | * Prepares the HTTP Request to send to the Transbank servers. 161 | * 162 | * @param \Psr\Http\Message\ServerRequestInterface $request 163 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 164 | * @param \DarkGhostHunter\Transbank\Credentials\Credentials $credentials 165 | * @param array $options 166 | * 167 | * @return \Psr\Http\Message\ServerRequestInterface 168 | */ 169 | protected function prepareRequest( 170 | Request $request, 171 | ApiRequest $apiRequest, 172 | Credentials $credentials, 173 | array $options 174 | ): Request { 175 | // Let the developer override the credentials headers by adding them before anything else. 176 | $request = $request->withHeader(static::HEADER_KEY, $credentials->key); 177 | $request = $request->withHeader(static::HEADER_SECRET, $credentials->secret); 178 | 179 | // Pass any "option" headers to the request. 180 | if ($options && isset($options['headers'])) { 181 | foreach ($options['headers'] as $header => $value) { 182 | $request = $request->withHeader($header, $value); 183 | } 184 | } 185 | 186 | $request = $request->withHeader('Content-Type', 'application/json'); 187 | $request = $request->withHeader('User-Agent', 'php:darkghosthunter/transbank/' . Transbank::VERSION); 188 | 189 | return $request->withBody($this->streamFactory->createStream($apiRequest->toJson())); 190 | } 191 | 192 | /** 193 | * Checks if the Response is an error or not. 194 | * 195 | * @param \Psr\Http\Message\ResponseInterface $response 196 | * @param \Psr\Http\Message\ServerRequestInterface $request 197 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 198 | * @return void 199 | */ 200 | protected function throwExceptionOnResponseError(ApiRequest $apiRequest, Request $request, Response $response): void 201 | { 202 | // Bail out if the response is present but is not JSON. 203 | if ($response->getBody()->getSize() && !in_array('application/json', $response->getHeader('Content-Type'))) { 204 | throw new ServerException('Non-JSON response received.', $apiRequest, $request, $response); 205 | } 206 | 207 | $status = $response->getStatusCode(); 208 | 209 | if ($status > 299) { 210 | if ($status < 400) { 211 | throw new ServerException('A redirection was returned.', $apiRequest, $request, $response); 212 | } 213 | 214 | if ($status < 500) { 215 | throw new ClientException($this->getErrorMessage($response), $apiRequest, $request, $response); 216 | } 217 | 218 | throw new ServerException($this->getErrorMessage($response), $apiRequest, $request, $response); 219 | } 220 | } 221 | 222 | /** 223 | * Returns the error message from the Transbank response. 224 | * 225 | * @param \Psr\Http\Message\ResponseInterface $response 226 | * @return string 227 | */ 228 | protected function getErrorMessage(Response $response): string 229 | { 230 | $contents = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR) ?? []; 231 | 232 | return $contents['error_message'] ?? $response->getBody()->getContents(); 233 | } 234 | 235 | /** 236 | * Parses the JSON from the response, or bails if is malformed. 237 | * 238 | * @param \DarkGhostHunter\Transbank\ApiRequest $apiRequest 239 | * @param \Psr\Http\Message\ServerRequestInterface $request 240 | * @param \Psr\Http\Message\ResponseInterface $response 241 | * 242 | * @return array 243 | */ 244 | protected function decodeJsonFromContents( 245 | ApiRequest $apiRequest, 246 | ServerRequestInterface $request, 247 | ResponseInterface $response 248 | ): array { 249 | if (empty($contents = $response->getBody()->getContents())) { 250 | return []; 251 | } 252 | 253 | try { 254 | return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); 255 | } catch (JsonException $exception) { 256 | throw new ServerException('The response JSON is malformed.', $apiRequest, $request, $response, $exception); 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Services/OneclickMall.php: -------------------------------------------------------------------------------- 1 | $username, 76 | 'email' => $email, 77 | 'response_url' => $responseUrl, 78 | ] 79 | ); 80 | 81 | $this->log('Creating subscription', ['api_request' => $apiRequest,]); 82 | $this->fireCreating($apiRequest); 83 | 84 | $response = $this->send(self::SERVICE_NAME, $apiRequest, 'post', static::ENDPOINT_START, [], $options); 85 | $transaction = new Transactions\Response($response['token'], $response['url_webpay']); 86 | 87 | $this->fireCreated($apiRequest, $transaction); 88 | $this->logResponse($apiRequest, $response); 89 | 90 | return new Transactions\Response($response['token'], $response['url_webpay'], 'TBK_TOKEN'); 91 | } 92 | 93 | /** 94 | * Finishes a subscription process in Transbank. 95 | * 96 | * @param string $token 97 | * @param array $options 98 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 99 | * 100 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 101 | */ 102 | public function finish(string $token, array $options = []): Transactions\Transaction 103 | { 104 | $apiRequest = new ApiRequest(static::ACTION_FINISH); 105 | 106 | $this->log('Finishing subscription', ['token' => $token, 'api_request' => $apiRequest]); 107 | 108 | $response = $this->send( 109 | static::SERVICE_NAME, 110 | $apiRequest, 111 | 'put', 112 | static::ENDPOINT_FINISH, 113 | ['{token}' => $token], 114 | $options 115 | ); 116 | 117 | $transaction = new Transactions\Transaction(static::ACTION_FINISH, $response); 118 | 119 | $this->logResponse($apiRequest, $response, $token); 120 | 121 | $this->fireCompleted($apiRequest, $transaction); 122 | 123 | return $transaction; 124 | } 125 | 126 | /** 127 | * Deletes a subscription. 128 | * 129 | * If the subscription doesn't exist, an exception will be returned. 130 | * 131 | * @param string $tbkUser 132 | * @param string $username 133 | * @param array $options 134 | * @return void 135 | * 136 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 137 | */ 138 | public function delete(string $tbkUser, string $username, array $options = []): void 139 | { 140 | $apiRequest = new ApiRequest(static::ACTION_DELETE, ['tbk_user' => $tbkUser, 'username' => $username]); 141 | 142 | $this->log('Deleting subscription', ['api_request' => $apiRequest]); 143 | 144 | $response = $this->send(static::SERVICE_NAME, $apiRequest, 'delete', static::ENDPOINT_DELETE, [], $options); 145 | 146 | $this->logResponse($apiRequest, $response); 147 | } 148 | 149 | /** 150 | * Authorizes a given set of transactions. 151 | * 152 | * @param string $tbkUser 153 | * @param string $username 154 | * @param string $buyOrder 155 | * @param array $details 156 | * @param array $options 157 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 158 | * 159 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 160 | */ 161 | public function authorize( 162 | string $tbkUser, 163 | string $username, 164 | string $buyOrder, 165 | array $details, 166 | array $options = [] 167 | ): Transactions\Transaction { 168 | $apiRequest = new ApiRequest( 169 | static::ACTION_AUTHORIZE, 170 | [ 171 | 'tbk_user' => $tbkUser, 172 | 'username' => $username, 173 | 'buy_order' => $buyOrder, 174 | 'details' => static::wrapDetails($details), 175 | ] 176 | ); 177 | 178 | $this->log('Authorizing transaction', ['api_request' => $apiRequest]); 179 | 180 | $this->fireCreating($apiRequest); 181 | 182 | $response = $this->send(static::SERVICE_NAME, $apiRequest, 'post', static::ENDPOINT_AUTHORIZE, [], $options); 183 | $transaction = Transactions\Transaction::createWithDetails(static::ACTION_AUTHORIZE, $response); 184 | 185 | $this->logResponse($apiRequest, $response); 186 | $this->fireCompleted($apiRequest, $transaction); 187 | 188 | return $transaction; 189 | } 190 | 191 | /** 192 | * Retrieves a transaction from Transbank. 193 | * 194 | * @param string $buyOrder 195 | * @param array $options 196 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 197 | * 198 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 199 | */ 200 | public function status(string $buyOrder, array $options = []): Transactions\Transaction 201 | { 202 | $apiRequest = new ApiRequest(static::ACTION_STATUS); 203 | 204 | $this->log('Retrieving transaction status', ['buy_order' => $buyOrder, 'api_request' => $apiRequest]); 205 | 206 | $response = $this->send( 207 | static::SERVICE_NAME, 208 | $apiRequest, 209 | 'get', 210 | static::ENDPOINT_STATUS, 211 | ['{buyOrder}' => $buyOrder], 212 | $options 213 | ); 214 | 215 | $this->logResponse($apiRequest, $response); 216 | 217 | return Transactions\Transaction::createWithDetails(static::ACTION_AUTHORIZE, $response); 218 | } 219 | 220 | /** 221 | * Refunds a child transaction. 222 | * 223 | * @param string $buyOrder 224 | * @param string $childCommerceCode 225 | * @param string $childBuyOrder 226 | * @param int|float $amount 227 | * @param array $options 228 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 229 | * 230 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 231 | */ 232 | public function refund( 233 | string $buyOrder, 234 | string $childCommerceCode, 235 | string $childBuyOrder, 236 | int|float $amount, 237 | array $options = [] 238 | ): Transactions\Transaction { 239 | $apiRequest = new ApiRequest( 240 | static::ACTION_REFUND, 241 | [ 242 | 'commerce_code' => $childCommerceCode, 243 | 'detail_buy_order' => $childBuyOrder, 244 | 'amount' => $amount, 245 | ] 246 | ); 247 | 248 | $this->log('Refunding transaction', ['buy_order' => $buyOrder, 'api_request' => $apiRequest,]); 249 | 250 | $this->fireCreating($apiRequest); 251 | 252 | $response = $this->send( 253 | static::SERVICE_NAME, 254 | $apiRequest, 255 | 'post', 256 | static::ENDPOINT_REFUND, 257 | ['{buyOrder}' => $buyOrder], 258 | $options 259 | ); 260 | 261 | $transaction = new Transactions\Transaction(static::ACTION_REFUND, $response); 262 | 263 | $this->log('Response received', ['buy_order' => $buyOrder, 'api_request' => $apiRequest, 'response' => $response]); 264 | $this->fireCompleted($apiRequest, $transaction); 265 | 266 | return $transaction; 267 | } 268 | 269 | /** 270 | * Captures a transaction from Transbank. 271 | * 272 | * @param string $commerceCode 273 | * @param string $buyOrder 274 | * @param string|int $authorizationCode 275 | * @param int|float $captureAmount 276 | * @param array $options 277 | * @return \DarkGhostHunter\Transbank\Services\Transactions\Transaction 278 | * 279 | * @throws \JsonException|\DarkGhostHunter\Transbank\Exceptions\TransbankException 280 | */ 281 | public function capture( 282 | string $commerceCode, 283 | string $buyOrder, 284 | string|int $authorizationCode, 285 | int|float $captureAmount, 286 | array $options = [] 287 | ): Transactions\Transaction { 288 | $apiRequest = new ApiRequest( 289 | static::ACTION_CAPTURE, 290 | [ 291 | 'commerce_code' => $commerceCode, 292 | 'buy_order' => $buyOrder, 293 | 'authorization_code' => $authorizationCode, 294 | 'capture_amount' => $captureAmount, 295 | ] 296 | ); 297 | 298 | $this->log('Capturing transaction', ['api_request' => $apiRequest]); 299 | 300 | // If we are on integration, we need to override the credentials. 301 | $serviceName = $this->transbank->isIntegration() ? static::ACTION_CAPTURE : static::SERVICE_NAME; 302 | 303 | $response = $this->send($serviceName, $apiRequest, 'put', static::ENDPOINT_CAPTURE, [], $options); 304 | $transaction = new Transactions\Transaction(static::ACTION_CAPTURE, $response); 305 | 306 | $this->logResponse($apiRequest, $response); 307 | $this->fireCompleted($apiRequest, $transaction); 308 | 309 | return $transaction; 310 | } 311 | } 312 | --------------------------------------------------------------------------------