diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index 5e5e2b9..51bb95d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,62 @@ # Created by .ignore support plugin (hsz.mobi) +### macOS template +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### Composer template +composer.phar +/vendor/ + +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +composer.lock +### Windows template +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk ### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion - -*.iml - -## Directory-based project format: .idea/ -# if you remove the above rule, at least ignore the following: - -# User-specific stuff: -# .idea/workspace.xml -# .idea/tasks.xml -# .idea/dictionaries - -# Sensitive or high-churn files: -# .idea/dataSources.ids -# .idea/dataSources.xml -# .idea/sqlDataSources.xml -# .idea/dynamic.xml -# .idea/uiDesigner.xml - -# Gradle: -# .idea/gradle.xml -# .idea/libraries - -# Mongo Explorer plugin: -# .idea/mongoSettings.xml ## File-based project format: -*.ipr *.iws ## Plugin-specific files: @@ -46,33 +74,5 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties - - -### Node template -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git -node_modules +fabric.properties +.env diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f667112 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ionut Calara + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Paylike/Adapter.php b/Paylike/Adapter.php deleted file mode 100644 index 676ddce..0000000 --- a/Paylike/Adapter.php +++ /dev/null @@ -1,91 +0,0 @@ -setApiKey($privateApiKey); - } else { - trigger_error('Private Key is missing!', E_USER_ERROR); - - return null; - } - } - - /** - * @param $key - * set the api key. - */ - public function setApiKey($key) - { - $this->apiKey = $key; - } - - /** - * @param $url this is required, do not use the full url, - * only prepend the params eg: transactions/' . $transactionId . '/captures' - * @param $data this is optional - * Actual call to the api via curl. - * - * @return bool|mixed - */ - public function request($url, $data = null, $httpVerb = 'post') - { - $url = $this->apiUrl . '/' . $url; - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Accept: application/json', - 'Content-Type: application/json', - 'X-Client: PHP ' . phpversion() - )); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERPWD, ":" . $this->apiKey); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - switch ($httpVerb) { - case 'post': - curl_setopt($ch, CURLOPT_POST, true); - if ($data) { - $encoded = json_encode($data); - curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); - } - break; - case 'get': - // can add args here for future use - break; - } - $result = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - $output = json_decode($result, true); - if ($httpCode >= 200 && $httpCode <= 299) { - return $output; - } else { - return false; - } - } - - } -} diff --git a/Paylike/Card.php b/Paylike/Card.php deleted file mode 100644 index 13628eb..0000000 --- a/Paylike/Card.php +++ /dev/null @@ -1,31 +0,0 @@ -request( 'cards/' . $cardId, $data = null, $httpVerb = 'get' ); - } - } -} diff --git a/Paylike/Client.php b/Paylike/Client.php deleted file mode 100644 index 0c99122..0000000 --- a/Paylike/Client.php +++ /dev/null @@ -1,50 +0,0 @@ -request( 'merchants/' . $merchantId . '/transactions', $data ); - } - - /** - * @param $transactionId - * - * @return int|mixed - * Return the transaction data - * - */ - public static function fetch( $transactionId ) { - $adapter = Client::getAdapter(); - if ( ! $adapter ) { - trigger_error( 'Adapter not set!', E_USER_ERROR ); - } - - return $adapter->request( 'transactions/' . $transactionId, $data = null, $httpVerb = 'get' ); - } - - /** - * @param $transactionId - * Capture a transaction that has been authorized. - * This also returns the transaction data. - * - * @param $data - * - * @return bool|int|mixed - */ - public static function capture( $transactionId, $data ) { - $adapter = Client::getAdapter(); - if ( ! $adapter ) { - trigger_error( 'Adapter not set!', E_USER_ERROR ); - } - - return $adapter->request( 'transactions/' . $transactionId . '/captures', $data ); - } - - /** - * @param $transactionId - * You can void a certain amount of a transaction that - * has been authorized but not captured. - * - * @param $data - * - * @return bool|int|mixed - */ - public static function void( $transactionId, $data ) { - $adapter = Client::getAdapter(); - if ( ! $adapter ) { - trigger_error( 'Adapter not set!', E_USER_ERROR ); - } - - return $adapter->request( 'transactions/' . $transactionId . '/voids', $data ); - } - - /** - * @param $transactionId - * You can return a certain amount of a transaction - * that has been captured. - * - * @param $data - * - * @return bool|int|mixed - */ - public static function refund( $transactionId, $data ) { - $adapter = Client::getAdapter(); - if ( ! $adapter ) { - trigger_error( 'Adapter not set!', E_USER_ERROR ); - } - - return $adapter->request( 'transactions/' . $transactionId . '/refunds', $data ); - } - - } -} diff --git a/README.md b/README.md index aadac92..6946faf 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,127 @@ -# Paylike PHP Api Wrapper +# Paylike client (PHP) -This is the PHP wrapper for the Paylike [sdk](https://github.com/paylike/sdk) - -[Sign up for a free merchant account (free and instant)](https://paylike.io) - -- Table of contents - - [Getting an API key](#getting-an-api-key) - - [Requirements](#requirements) - - [Examples](#examples) - - [Getting started](#getting-started) - - [Capturing a transaction](#capturing-a-transaction) - - [Refund a transaction](#refund-a-transaction) - - [Void a transaction](#void-a-transaction) - - [Fetch a transaction](#fetch-a-transaction) - - [Create a transaction](#create-a-transaction) - - [Fetch a card](#fetch-a-card) +You can sign up for a Paylike account at [https://paylike.io](https://paylike.io). ## Getting an API key An API key can be obtained by creating a merchant and adding an app through -our [dashboard](https://app.paylike.io). +Paylike [dashboard](https://app.paylike.io). ## Requirements PHP 5.3.3 and later. -## Examples - -Examples are available in the `examples.php` file. +## Install -## Getting started +You can install the package via [Composer](http://getcomposer.org/). Run the following command: -Download the latest release and include the `Client.php` file in your `php` application. - -```php -require_once('/path/to/Paylike/Client.php'); -$privateAppKey = 'your-private-key-goes-here'; -\Paylike\Client::setKey( $privateAppKey ); +```bash +composer require paylike/php-api ``` - -## Capturing a transaction -**Every operation requires a transaction id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** +If you don't use Composer, you can download the [latest release](https://github.com/paylike/php-api/releases) and include the `init.php` file. ```php - $data = array( - 'amount' => $amount, //value must be in cents - 'currency' => $currency //see available formats https://github.com/paylike/currencies - ); - $transaction = \Paylike\Transaction::capture( $transactionId, $data ); - // you will now have the transaction data in the $transaction variable. +require_once('/path/to/php-api/init.php'); ``` +## Dependencies -## Refund a transaction -**Every operation requires a transaction id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** - +The bindings require the following extension in order to work properly: -```php - $data = array( - 'amount' => $amount, //value must be in cents - 'descriptor' => $reason //is optional see https://github.com/paylike/descriptor for format and restrictions. - ); - $transaction = \Paylike\Transaction::refund( $transactionId, $data ); - // you will now have the transaction data in the $transaction variable. -``` +- [`curl`](https://secure.php.net/manual/en/book.curl.php) -## Void a transaction -**Every operation requires a transaction id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** +If you use Composer, these dependencies should be handled automatically. If you install manually, you'll want to make sure that these extensions are available. +If you don't want to use curl, you can create your own client to extend from `HttpClientInterface` and send that as a parameter when instantiating the `Paylike` class. +## Example ```php - $data = array( - 'amount' => $amount //value must be in cents - ); - $transaction = \Paylike\Transaction::void( $transactionId, $data ); - // you will now have the transaction data in the $transaction variable. -``` - -## Fetch a transaction -**Every operation requires a transaction id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** - - +$paylike = new \Paylike\Paylike($private_api_key); + +// fetch a card +$cards = $paylike->cards(); +$card = $cards->fetch($card_id); + +// capture a transaction +$transactions = $paylike->transactions(); +$transaction = $transactions->capture($transaction_id, array( + 'amount' => 100, + 'currency' => 'EUR' +)); +``` + +## Methods ```php - $transaction = \Paylike\Transaction::fetch( $transactionId); - // you will now have the transaction data in the $transaction variable. -``` - -## Create a transaction -**Every operation requires a transaction id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** - +$paylike = new \Paylike\Paylike($private_api_key); + +$apps = $paylike->apps(); +$apps->create($args); +$apps->fetch(); + +$merchants = $paylike->merchants(); +$merchants->create($args); +$merchants->fetch($merchant_id); +$merchants->update($merchant_id, $args); + +$cards = $paylike->cards(); +$cards->create($merchant_id, $args); +$cards->fetch($card_id); + +$transactions = $paylike->transactions(); +$transactions->create($merchant_id, $args); +$transactions->fetch($transaction_id); +$transactions->capture($transaction_id, $args); +$transactions->void($transaction_id, $args); +$transactions->refund($transaction_id, $args); +``` + +## Error handling + +The api wrapper will throw errors when things do not fly. All errors inherit from +`ApiException`. A very verbose example of catching all types of errors: ```php - $data = array( - 'amount' => $amount, //value must be in cents - 'currency' => $currency //see available formats https://github.com/paylike/currencies - ); - $transaction = \Paylike\Transaction::create( $merchantId, $data ); - // you will now have the transaction data in the $transaction variable. +$paylike = new \Paylike\Paylike($private_api_key); +try { + $transactions = $paylike->transactions(); + $transactions->capture($transaction_id, array( + 'amount' => 100, + 'currency' => 'EUR' + )); +} catch (\Paylike\Exception\NotFound $e) { + // The transaction was not found +} catch (\Paylike\Exception\InvalidRequest $e) { + // Bad (invalid) request - see $e->getJsonBody() for the error +} catch (\Paylike\Exception\Forbidden $e) { + // You are correctly authenticated but do not have access. +} catch (\Paylike\Exception\Unauthorized $e) { + // You need to provide credentials (an app's API key) +} catch (\Paylike\Exception\Conflict $e) { + // Everything you submitted was fine at the time of validation, but something changed in the meantime and came into conflict with this (e.g. double-capture). +} catch (\Paylike\Exception\ApiConnection $e) { + // Network error on connecting via cURL +} catch (\Paylike\Exception\ApiException $e) { + // Unknown api error +} +``` + +In most cases catching `NotFound` and `InvalidRequest` as client errors +and logging `ApiException` would suffice. + +## Development + +Install dependencies: + +``` bash +composer install ``` -## Fetch a card -**Every operation requires a card id that is obtained by using the javascript [sdk](https://github.com/paylike/sdk).** +## Tests +Install dependencies as mentioned above (which will resolve [PHPUnit](http://packagist.org/packages/phpunit/phpunit)), then you can run the test suite: -```php - $card = \Paylike\Card::fetch( $cardId ); - // you will now have the card data in the $card variable. +```bash +./vendor/bin/phpunit ``` + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..35e3bd5 --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "paylike/php-api", + "description": "Demo", + "license": "MIT", + "authors": [ + { + "name": "Ionut Calara", + "email": "ionut@derikon.com" + } + ], + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Paylike\\": "src", + "Paylike\\Tests\\": "tests" + } + }, + "require": { + "php": ">=5.3", + "ext-curl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + } +} diff --git a/css/main.css b/css/main.css deleted file mode 100644 index ebd0ebd..0000000 --- a/css/main.css +++ /dev/null @@ -1,282 +0,0 @@ -/*! HTML5 Boilerplate v5.3.0 | MIT License | https://html5boilerplate.com/ */ - -/* - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. - */ - -/* ========================================================================== - Base styles: opinionated defaults - ========================================================================== */ - -html { - color: #222; - font-size: 1em; - line-height: 1.4; -} - -/* - * Remove text-shadow in selection highlight: - * https://twitter.com/miketaylr/status/12228805301 - * - * These selection rule sets have to be separate. - * Customize the background color to match your design. - */ - -::-moz-selection { - background: #b3d4fc; - text-shadow: none; -} - -::selection { - background: #b3d4fc; - text-shadow: none; -} - -/* - * A better looking default horizontal rule - */ - -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -/* - * Remove the gap between audio, canvas, iframes, - * images, videos and the bottom of their containers: - * https://github.com/h5bp/html5-boilerplate/issues/440 - */ - -audio, -canvas, -iframe, -img, -svg, -video { - vertical-align: middle; -} - -/* - * Remove default fieldset styles. - */ - -fieldset { - border: 0; - margin: 0; - padding: 0; -} - -/* - * Allow only vertical resizing of textareas. - */ - -textarea { - resize: vertical; -} - -/* ========================================================================== - Browser Upgrade Prompt - ========================================================================== */ - -.browserupgrade { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; -} - -/* ========================================================================== - Author's custom styles - ========================================================================== */ - - - - - - - - - - - - - - - - - -/* ========================================================================== - Helper classes - ========================================================================== */ - -/* - * Hide visually and from screen readers - */ - -.hidden { - display: none !important; -} - -/* - * Hide only visually, but have it available for screen readers: - * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility - */ - -.visuallyhidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -/* - * Extends the .visuallyhidden class to allow the element - * to be focusable when navigated to via the keyboard: - * https://www.drupal.org/node/897638 - */ - -.visuallyhidden.focusable:active, -.visuallyhidden.focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - width: auto; -} - -/* - * Hide visually and from screen readers, but maintain layout - */ - -.invisible { - visibility: hidden; -} - -/* - * Clearfix: contain floats - * - * For modern browsers - * 1. The space content is one way to avoid an Opera bug when the - * `contenteditable` attribute is included anywhere else in the document. - * Otherwise it causes space to appear at the top and bottom of elements - * that receive the `clearfix` class. - * 2. The use of `table` rather than `block` is only necessary if using - * `:before` to contain the top-margins of child elements. - */ - -.clearfix:before, -.clearfix:after { - content: " "; /* 1 */ - display: table; /* 2 */ -} - -.clearfix:after { - clear: both; -} - -/* ========================================================================== - EXAMPLE Media Queries for Responsive Design. - These examples override the primary ('mobile first') styles. - Modify as content requires. - ========================================================================== */ - -@media only screen and (min-width: 35em) { - /* Style adjustments for viewports that meet the condition */ -} - -@media print, - (-webkit-min-device-pixel-ratio: 1.25), - (min-resolution: 1.25dppx), - (min-resolution: 120dpi) { - /* Style adjustments for high resolution devices */ -} - -/* ========================================================================== - Print styles. - Inlined to avoid the additional HTTP request: - http://www.phpied.com/delay-loading-your-print-css/ - ========================================================================== */ - -@media print { - *, - *:before, - *:after, - *:first-letter, - *:first-line { - background: transparent !important; - color: #000 !important; /* Black prints faster: - http://www.sanbeiji.com/archives/953 */ - box-shadow: none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - /* - * Don't show links that are fragment identifiers, - * or use the `javascript:` pseudo protocol - */ - - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - /* - * Printing Tables: - * http://css-discuss.incutio.com/wiki/Printing_Tables - */ - - thead { - display: table-header-group; - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } -} diff --git a/css/normalize.css b/css/normalize.css deleted file mode 100644 index 5e5e3c8..0000000 --- a/css/normalize.css +++ /dev/null @@ -1,424 +0,0 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS and IE text size adjust after device orientation change, - * without disabling user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability of focused elements when they are also in an - * active/hover state. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - box-sizing: content-box; /* 2 */ -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/examples.php b/examples.php deleted file mode 100644 index fc59839..0000000 --- a/examples.php +++ /dev/null @@ -1,303 +0,0 @@ - $amount ); - $response = \Paylike\Transaction::void( $transactionId, $data ); - break; - case "voidTransactionHalf": - $data = array( 'amount' => $amount / 2 ); - $response = \Paylike\Transaction::void( $transactionId, array( 'amount' => $data ) ); - break; - case "createTransaction": - $transaction = \Paylike\Transaction::fetch( $transactionId ); - $merchantId = $transaction['transaction']['merchantId']; - $data = array( - 'transactionId' => $transactionId, - 'amount' => $amount, - 'currency' => $currency, - 'custom' => array( - 'email' => 'ionut@derikon.com' - ) - ); - $response = \Paylike\Transaction::create( $merchantId, $data ); - break; - case "fetchTransaction": - $response = \Paylike\Transaction::fetch( $transactionId ); - break; - case "captureTransactionFull": - $data = array( - 'amount' => $amount / 2, - 'currency' => $currency - ); - $response = \Paylike\Transaction::capture( $transactionId, $data ); - break; - case "captureTransactionHalf": - $data = array( - 'amount' => $amount / 2, - 'currency' => $currency - ); - $response = \Paylike\Transaction::capture( $transactionId, $data ); - break; - case "refundTransactionFull": - $data = array( - 'amount' => $amount, - 'descriptor' => $_POST['reason'] - ); - $response = \Paylike\Transaction::refund( $transactionId, $data ); - break; - case "refundTransactionHalf": - $data = array( - 'amount' => $amount / 2, - 'descriptor' => $_POST['reason'] - ); - $response = \Paylike\Transaction::refund( $transactionId, $data ); - break; - case "fetchCard": - $response = \Paylike\Card::fetch( $cardId ); - break; - } -} -if ( ! isset( $transactionId ) ) { - if ( isset( $_SESSION['transactionId'] ) ) { - $transactionId = $_SESSION['transactionId']; - } -} -if ( ! isset( $cardId ) ) { - if ( isset( $_SESSION['cardId'] ) ) { - $cardId = $_SESSION['cardId']; - } -} -?> - - - Paylike test - - - - -

- Test Paylike Api Wrapper -

-
-
';
-    print_r( $response );
-    echo '
'; - if ( isset( $_POST['cardId'] ) ) { - if ( isset( $response['card']['id'] ) && $response['card']['id'] ) { - echo '
Card operation was successful.
'; - } else { - echo '
Card operation failed.
'; - } - } else { - if ( isset( $response['transaction']['id'] ) && $response['transaction']['id'] ) { - echo '
Transaction operation was successful.
'; - } else { - echo '
Transaction operation failed.
'; - } - } - echo ''; -} -?> - - - - - -
-
-

- Void the transaction for the entire amount -

- - class="transactionId"> - - -
-
-
-

- Void the transaction for half of amount -

- - class="transactionId"> - - -
-
-
-

- Create a new transaction based on a previous one. -

- - class="transactionId"> - - -
-
-
-

- Fetch transaction. -

- - class="transactionId"> - - -
-
-
-

- Capture the transaction for the full amount -

- - class="transactionId"> - - -
-
-
-

- Capture the transaction for the half of the amount -

- - class="transactionId"> - - -
-
-

- Refund the transaction for the full amount -
- - Only works when you have previously captured the amount. - -

- - class="transactionId">
- - - - -
-
-
-

- Capture the transaction for the half of the amount -
- - Only works when you have previously captured the amount. - -

- - class="transactionId">
- - - - -
-
-
-

- Fetch card. -

- - class="cardId"> - - -
- - diff --git a/init.php b/init.php new file mode 100644 index 0000000..75a51b9 --- /dev/null +++ b/init.php @@ -0,0 +1,28 @@ + + + + ./tests + + + diff --git a/src/Exception/ApiConnection.php b/src/Exception/ApiConnection.php new file mode 100644 index 0000000..64e67ea --- /dev/null +++ b/src/Exception/ApiConnection.php @@ -0,0 +1,13 @@ +http_status = $http_status; + $this->http_body = $http_body; + $this->json_body = $json_body; + $this->http_headers = $http_headers; + } + + public function getHttpStatus() + { + return $this->http_status; + } + + public function getHttpBody() + { + return $this->http_body; + } + + public function getJsonBody() + { + return $this->json_body; + } + + public function getHttpHeaders() + { + return $this->http_headers; + } +} diff --git a/src/Exception/Conflict.php b/src/Exception/Conflict.php new file mode 100644 index 0000000..a66ec2e --- /dev/null +++ b/src/Exception/Conflict.php @@ -0,0 +1,13 @@ +api_key = $api_key; + $this->base_url = $base_url; + } + + /** + * Performs the underlying HTTP request. It takes care of handling the + * connection errors, parsing the headers and the response body. + * + * @param string $http_verb The HTTP verb to use: get, post + * @param string $method The API method to be called + * @param array $args Assoc array of parameters to be passed + * + * @return ApiResponse + * @throws ApiException + */ + public function request( + $http_verb, + $method, + $args = array() + ) { + + $timeout = self::TIMEOUT; + $url = $this->base_url . '/' . $method; + $ch = curl_init(); + + // Create a callback to capture HTTP headers for the response + $response_headers = array(); + $headerCallback = function ($ch, $header_line) use (&$response_headers + ) { + // Ignore the HTTP request line (HTTP/1.1 200 OK) + if (strpos($header_line, ":") === false) { + return strlen($header_line); + } + list($key, $value) = explode(":", trim($header_line), 2); + $response_headers[trim($key)] = trim($value); + + return strlen($header_line); + }; + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept: application/vnd.api+json', + 'Content-Type: application/vnd.api+json' + )); + curl_setopt($ch, CURLOPT_USERAGENT, 'PHP 0.2.0 (php' . phpversion() .')'); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl); + curl_setopt($ch, CURLOPT_USERPWD, ":" . $this->api_key); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, $headerCallback); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $http_verb = strtoupper($http_verb); + switch ($http_verb) { + case 'POST': + curl_setopt($ch, CURLOPT_POST, true); + $encoded = json_encode($args); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); + break; + case 'GET': + $query = http_build_query($args, '', '&'); + if ($query) { + curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); + } + break; + case 'DELETE': + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + case 'PATCH': + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); + $encoded = json_encode($args); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); + break; + case 'PUT': + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + $encoded = json_encode($args); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); + break; + } + + $response_body = curl_exec($ch); + if ($response_body === false) { + $errno = curl_errno($ch); + $message = curl_error($ch); + curl_close($ch); + $this->handleCurlError($url, $errno, $message); + } + + $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // + $json = $this->parseResponse($response_body, $response_code, + $response_headers); + $api_response = new ApiResponse($response_body, $response_code, + $response_headers, $json); + + return $api_response; + } + + /** + * @param $response_body + * @param $response_code + * @param $response_headers + * + * @return mixed + * @throws ApiException + */ + private function parseResponse( + $response_body, + $response_code, + $response_headers + ) { + $resp = null; + if ($response_body) { + $resp = json_decode($response_body, true); + $jsonError = json_last_error(); + if ($resp === null && $jsonError !== JSON_ERROR_NONE) { + $msg = "Invalid response body: $response_body " + . "(HTTP response code: $response_code, json_last_error: $jsonError)"; + throw new ApiException($msg, $response_code, + $response_body); + } + } + + if ($response_code < 200 || $response_code >= 300) { + $this->handleApiError($response_body, $response_code, + $response_headers, $resp); + } + + return $resp; + } + + + /** + * @param $url + * @param $errno + * @param $message + * + * @throws ApiConnection + */ + private function handleCurlError($url, $errno, $message) + { + switch ($errno) { + case CURLE_SSL_CACERT: + case CURLE_SSL_PEER_CERTIFICATE: + $msg + = "Could not verify Paylike's SSL certificate."; // highly unlikely + break; + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_OPERATION_TIMEOUTED: + $msg + = "Could not connect to Paylike ($url). Please check your internet connection and try again."; + break; + default: + $msg = "Unexpected error communicating with Paylike."; + } + + $msg .= "\n\n(Network error [errno $errno]: $message)"; + throw new ApiConnection($msg); + } + + + /** + * @param $response_body + * @param $response_code + * @param $response_headers + * @param $json_resp + * + * @throws ApiException + * @throws Conflict + * @throws Forbidden + * @throws InvalidRequest + * @throws Unauthorized + */ + private function handleApiError( + $response_body, + $response_code, + $response_headers, + $json_resp + ) { + + switch ($response_code) { + case 400: + // format for the errors: + // - [{"field":"amount","message":"Can refund at most GBP 0"}] + // - [{"code":2,"text":"Invalid card details", "client": true, "merchant": false}] + $message = "Bad (invalid) request"; + // @TODO - extract error parsing logic + if ($json_resp && is_array($json_resp) && ! empty($json_resp)) { + if (isset($json_resp[0]['message'])) { + $message = $json_resp[0]['message']; + } else if (isset($json_resp[0]['text'])) { + $message = $json_resp[0]['text']; + } + } + throw new InvalidRequest($message, + $response_code, $response_body, $json_resp, + $response_headers); + case 401: + throw new Unauthorized("You need to provide credentials (an app's API key).", + $response_code, + $response_body, $json_resp, + $response_headers); + case 403: + throw new Forbidden("You are correctly authenticated but do not have access.", + $response_code, $response_body, + $json_resp, + $response_headers); + case 404: + throw new NotFound("Resource not found.", + $response_code, $response_body, + $json_resp, + $response_headers); + case 409: + throw new Conflict("Everything you submitted was fine at the time of validation, but something changed in the meantime and came into conflict with this (e.g. double-capture).", + $response_code, $response_body, + $json_resp, + $response_headers); + default: + throw new ApiException("Unknown api error", + $response_code, + $response_body, + $json_resp, + $response_headers); + } + } +} diff --git a/src/HttpClient/HttpClientInterface.php b/src/HttpClient/HttpClientInterface.php new file mode 100644 index 0000000..8311b27 --- /dev/null +++ b/src/HttpClient/HttpClientInterface.php @@ -0,0 +1,19 @@ +api_key = $api_key; + $this->client = $client ? $client + : new CurlClient($this->api_key, self::BASE_URL); + } + + /** + * @return string + */ + public function getApiKey() + { + return $this->api_key; + } + + + /** + * @return Apps + */ + public function apps() + { + return new Apps($this); + } + + /** + * @return Merchants + */ + public function merchants() + { + return new Merchants($this); + } + + /** + * @return Transactions + */ + public function transactions() + { + return new Transactions($this); + } + + /** + * @return Cards + */ + public function cards() + { + return new Cards($this); + } +} diff --git a/src/Resource/Apps.php b/src/Resource/Apps.php new file mode 100644 index 0000000..c0e5276 --- /dev/null +++ b/src/Resource/Apps.php @@ -0,0 +1,40 @@ +paylike->client->request('POST', $url, $args); + + return $api_response->json['app']; + } + + /** + * @link https://github.com/paylike/api-docs#fetch-current-app + * @return array + */ + public function fetch() + { + $url = 'me'; + + $api_response = $this->paylike->client->request('GET', $url); + + return $api_response->json['identity']; + } +} diff --git a/src/Resource/Cards.php b/src/Resource/Cards.php new file mode 100644 index 0000000..888a1b0 --- /dev/null +++ b/src/Resource/Cards.php @@ -0,0 +1,44 @@ +paylike->client->request('POST', $url, $args); + + return $api_response->json['card']['id']; + } + + /** + * @link https://github.com/paylike/api-docs#fetch-a-card + * + * @param $card_id + * + * @return array + */ + public function fetch($card_id) + { + $url = 'cards/' . $card_id; + + $api_response = $this->paylike->client->request('GET', $url); + + return $api_response->json['card']; + } +} diff --git a/src/Resource/Merchants.php b/src/Resource/Merchants.php new file mode 100644 index 0000000..5112e02 --- /dev/null +++ b/src/Resource/Merchants.php @@ -0,0 +1,57 @@ +paylike->client->request('POST', $url, $args); + + return $api_response->json['merchant']['id']; + } + + /** + * @link https://github.com/paylike/api-docs#fetch-a-merchant + * + * @param $merchant_id + * + * @return mixed + */ + public function fetch($merchant_id) + { + $url = 'merchants/' . $merchant_id; + + $api_response = $this->paylike->client->request('GET', $url); + + return $api_response->json['merchant']; + } + + /** + * https://github.com/paylike/api-docs#update-a-merchant + * @param $merchant_id + * @param $args + * + * @return void + */ + public function update($merchant_id, $args) + { + $url = 'merchants/' . $merchant_id; + + $this->paylike->client->request('PUT', $url, $args); + } +} diff --git a/src/Resource/Resource.php b/src/Resource/Resource.php new file mode 100644 index 0000000..e22dd7d --- /dev/null +++ b/src/Resource/Resource.php @@ -0,0 +1,26 @@ +paylike = $paylike; + } +} diff --git a/src/Resource/Transactions.php b/src/Resource/Transactions.php new file mode 100644 index 0000000..bd517d5 --- /dev/null +++ b/src/Resource/Transactions.php @@ -0,0 +1,95 @@ +paylike->client->request('POST', $url, $args); + + return $api_response->json['transaction']['id']; + } + + /** + * @link https://github.com/paylike/api-docs#fetch-a-transaction + * + * @param $transaction_id + * + * @return array + */ + public function fetch($transaction_id) + { + $url = 'transactions/' . $transaction_id; + + $api_response = $this->paylike->client->request('GET', $url); + + return $api_response->json['transaction']; + } + + /** + * @link https://github.com/paylike/api-docs#capture-a-transaction + * + * @param $transaction_id + * @param $args array + * + * @return array + */ + public function capture($transaction_id, $args) + { + $url = 'transactions/' . $transaction_id . '/captures'; + + $api_response = $this->paylike->client->request('POST', $url, $args); + + return $api_response->json['transaction']; + } + + /** + * @link https://github.com/paylike/api-docs#void-a-transaction + * + * @param $transaction_id + * @param $args array + * + * @return array + */ + public function void($transaction_id, $args) + { + $url = 'transactions/' . $transaction_id . '/voids'; + + $api_response = $this->paylike->client->request('POST', $url, $args); + + return $api_response->json['transaction']; + } + + /** + * @link https://github.com/paylike/api-docs#refund-a-transaction + * + * @param $transaction_id + * @param $args array + * + * @return array + */ + public function refund($transaction_id, $args) + { + $url = 'transactions/' . $transaction_id . '/refunds'; + + $api_response = $this->paylike->client->request('POST', $url, $args); + + return $api_response->json['transaction']; + } +} diff --git a/src/Response/ApiResponse.php b/src/Response/ApiResponse.php new file mode 100644 index 0000000..eeac120 --- /dev/null +++ b/src/Response/ApiResponse.php @@ -0,0 +1,36 @@ +body = $body; + $this->code = $code; + $this->headers = $headers; + $this->json = $json; + } +} diff --git a/tests/AppsTest.php b/tests/AppsTest.php new file mode 100644 index 0000000..f098cc2 --- /dev/null +++ b/tests/AppsTest.php @@ -0,0 +1,36 @@ +apps = $this->paylike->apps(); + } + + + public function testCreate() + { + $app_identity = $this->apps->create(array( + 'name' => 'Test App Name' + )); + + $this->assertNotEmpty($app_identity, 'app identity'); + } + + public function testFetch() + { + $app = $this->apps->fetch(); + + $this->assertEquals($app['id'], $this->app_id, 'app id'); + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php new file mode 100644 index 0000000..a5c9ab6 --- /dev/null +++ b/tests/BaseTest.php @@ -0,0 +1,25 @@ +paylike = new Paylike("dbcf01af-8667-4967-9791-56101ca87ac8"); + $this->app_id = "594d3cde5be12d547cbe2ec2"; + $this->transaction_id = "594d3d2cbe4728547d40150e"; + $this->merchant_id = "594d3c455be12d547cbe2ebe"; + } +} diff --git a/tests/CardsTest.php b/tests/CardsTest.php new file mode 100644 index 0000000..abe9f82 --- /dev/null +++ b/tests/CardsTest.php @@ -0,0 +1,54 @@ +cards = $this->paylike->cards(); + } + + + public function testCreate() + { + $transaction_id = $this->transaction_id; + $merchant_id = $this->merchant_id; + + $card_id = $this->cards->create($merchant_id, array( + 'transactionId' => $transaction_id + )); + + $this->assertNotEmpty($card_id, 'primary key'); + $this->assertInternalType('string', $card_id, 'primary key type'); + } + + public function testFetch() + { + $transaction_id = $this->transaction_id; + $merchant_id = $this->merchant_id; + + $card_id = $this->cards->create($merchant_id, array( + 'transactionId' => $transaction_id + )); + + $card = $this->cards->fetch($card_id); + + $this->assertEquals($card['id'], $card_id, 'primary key'); + } + + public function testFailFetch() + { + $this->setExpectedException(NotFound::class); + $this->cards->fetch('wrong id'); + } +} diff --git a/tests/MerchantsTest.php b/tests/MerchantsTest.php new file mode 100644 index 0000000..903db88 --- /dev/null +++ b/tests/MerchantsTest.php @@ -0,0 +1,55 @@ +merchants = $this->paylike->merchants(); + } + + + public function testCreate() + { + $merchant_id = $this->merchants->create(array( + 'company' => array( + 'country' => 'DK' + ), + 'currency' => 'DKK', + 'email' => 'john@example.com', + 'website' => 'https://example.com', + 'descriptor' => 'Test Merchant Name', + 'test' => true, + )); + + $this->assertNotEmpty($merchant_id, 'primary key'); + $this->assertInternalType('string', $merchant_id, 'primary key type'); + } + + public function testFetch() + { + $merchant_id = $this->merchant_id; + + $merchant = $this->merchants->fetch($merchant_id); + + $this->assertEquals($merchant['id'], $merchant_id, 'primary key'); + } + + public function testUpdate() + { + $merchant_id = $this->merchant_id; + + $this->merchants->update($merchant_id, array( + 'name' => 'Updated Merchant Name' + )); + } +} diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php new file mode 100644 index 0000000..8447872 --- /dev/null +++ b/tests/TransactionsTest.php @@ -0,0 +1,151 @@ +transactions = $this->paylike->transactions(); + } + + public function testCreate() + { + $merchant_id = $this->merchant_id; + $transaction_id = $this->transaction_id; + + $new_transaction_id = $this->transactions->create($merchant_id, array( + 'transactionId' => $transaction_id, + 'currency' => 'EUR', + 'amount' => 200, + 'custom' => array( + 'source' => 'php client test' + ) + )); + + $this->assertNotEmpty($new_transaction_id, 'primary key'); + $this->assertInternalType('string', $new_transaction_id, 'primary key type'); + } + + public function testFetch() + { + $transaction_id = $this->transaction_id; + + $transaction = $this->transactions->fetch($transaction_id); + + $this->assertEquals($transaction['id'], $transaction_id, 'primary key'); + } + + public function testFailFetch() + { + $this->setExpectedException(NotFound::class); + $this->transactions->fetch('wrong id'); + } + + public function testCapture() + { + $new_transaction_id = $this->createNewTransactionForTest(); + + $transaction = $this->transactions->capture($new_transaction_id, array( + 'currency' => 'EUR', + 'amount' => 100 + )); + + $this->assertEquals($transaction['capturedAmount'], 100, + 'captured amount'); + $this->assertEquals($transaction['pendingAmount'], 200, + 'pending amount'); + + $trail = $transaction['trail']; + $this->assertCount(1, $trail, 'length of trail'); + $this->assertEquals($trail[0]['capture'], true, 'type of trail'); + $this->assertEquals($trail[0]['amount'], 100, 'amount in capture trail'); + } + + public function testCaptureBiggerAmount() + { + $this->setExpectedException(InvalidRequest::class); + + $new_transaction_id = $this->createNewTransactionForTest(); + $this->transactions->capture($new_transaction_id, array( + 'currency' => 'EUR', + 'amount' => 400 + )); + } + + public function testRefund() + { + $new_transaction_id = $this->createNewTransactionForTest(); + + $this->transactions->capture($new_transaction_id, array( + 'currency' => 'EUR', + 'amount' => 200 + )); + + $transaction = $this->transactions->refund($new_transaction_id, array( + 'amount' => 120 + )); + + $this->assertEquals($transaction['capturedAmount'], 200, + 'captured amount'); + $this->assertEquals($transaction['pendingAmount'], 100, + 'pending amount'); + $this->assertEquals($transaction['refundedAmount'], 120, + 'refunded amount'); + + $trail = $transaction['trail']; + $this->assertCount(2, $trail, 'length of trail'); + $this->assertEquals($trail[0]['capture'], true, 'type of trail'); + $this->assertEquals($trail[0]['amount'], 200, 'amount in capture trail'); + $this->assertEquals($trail[1]['refund'], true, 'type of trail'); + $this->assertEquals($trail[1]['amount'], 120, 'amount in refund trail'); + } + + public function testVoid() + { + $new_transaction_id = $this->createNewTransactionForTest(); + + $transaction = $this->transactions->void($new_transaction_id, array( + 'amount' => 200 + )); + + $this->assertEquals($transaction['voidedAmount'], 200, 'voided amount'); + $this->assertEquals($transaction['pendingAmount'], 100, + 'pending amount'); + + $trail = $transaction['trail']; + $this->assertCount(1, $trail, 'length of trail'); + $this->assertEquals($trail[0]['void'], true, 'type of trail'); + $this->assertEquals($trail[0]['amount'], 200, 'amount in void trail'); + } + + /** + * @return bool|mixed + */ + private function createNewTransactionForTest() + { + $merchant_id = $this->merchant_id; + $transaction_id = $this->transaction_id; + + $new_transaction_id = $this->transactions->create($merchant_id, array( + 'transactionId' => $transaction_id, + 'currency' => 'EUR', + 'amount' => 300, + 'custom' => array( + 'source' => 'php client test' + ) + )); + + return $new_transaction_id; + } +}