From cacb926c0333442e824887a55ba3605ef4caa53e Mon Sep 17 00:00:00 2001 From: Amr Ahmed Date: Wed, 27 Nov 2019 23:24:18 +0200 Subject: [PATCH] initial commit --- .gitignore | 6 + .styleci.yml | 4 + .travis.yml | 32 +++ README.md | 15 ++ composer.json | 53 +++++ config/knet.php | 136 +++++++++++++ database/migrations/.gitignore | 2 + phpstan.neon | 5 + public/.gitignore | 2 + resources/lang/.gitignore | 2 + resources/views/.gitignore | 2 + routes/web.php | 22 +++ src/Console/Commands/KnetCommand.php | 42 ++++ src/Events/KnetResponseHandled.php | 28 +++ src/Events/KnetResponseReceived.php | 28 +++ src/Exceptions/IncompletePayment.php | 31 +++ src/Exceptions/KnetException.php | 20 ++ .../SignatureVerificationException.php | 69 +++++++ src/HasKnet.php | 33 ++++ src/Http/Controllers/KnetController.php | 66 +++++++ .../VerifyKnetResponseSignature.php | 32 +++ src/Invoice.php | 164 ++++++++++++++++ src/Knet.php | 183 ++++++++++++++++++ src/KnetClient.php | 72 +++++++ src/KnetResponseHandler.php | 67 +++++++ src/KnetResponseSignature.php | 41 ++++ src/KnetTransaction.php | 56 ++++++ src/Payment.php | 86 ++++++++ src/Providers/KnetServiceProvider.php | 107 ++++++++++ tests/Fixtures/User.php | 10 + tests/Integrations/IntegrationTestCase.php | 29 +++ tests/Integrations/PayTest.php | 21 ++ tests/TestCase.php | 29 +++ tests/Unit/PaymentTest.php | 53 +++++ 34 files changed, 1548 insertions(+) create mode 100644 .gitignore create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/knet.php create mode 100644 database/migrations/.gitignore create mode 100644 phpstan.neon create mode 100644 public/.gitignore create mode 100644 resources/lang/.gitignore create mode 100644 resources/views/.gitignore create mode 100644 routes/web.php create mode 100644 src/Console/Commands/KnetCommand.php create mode 100644 src/Events/KnetResponseHandled.php create mode 100644 src/Events/KnetResponseReceived.php create mode 100644 src/Exceptions/IncompletePayment.php create mode 100644 src/Exceptions/KnetException.php create mode 100644 src/Exceptions/SignatureVerificationException.php create mode 100644 src/HasKnet.php create mode 100644 src/Http/Controllers/KnetController.php create mode 100644 src/Http/Middleware/VerifyKnetResponseSignature.php create mode 100644 src/Invoice.php create mode 100644 src/Knet.php create mode 100644 src/KnetClient.php create mode 100644 src/KnetResponseHandler.php create mode 100644 src/KnetResponseSignature.php create mode 100644 src/KnetTransaction.php create mode 100644 src/Payment.php create mode 100644 src/Providers/KnetServiceProvider.php create mode 100644 tests/Fixtures/User.php create mode 100644 tests/Integrations/IntegrationTestCase.php create mode 100644 tests/Integrations/PayTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/PaymentTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2b4a70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor +composer.lock +/phpunit.xml +.phpunit.result.cache +.idea +.env \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..e04bb81 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +php: + preset: laravel +js: true +css: true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b7cd3a3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: php + +sudo: false + +php: + - 7.1 + - 7.2 + - 7.3 + +env: + matrix: + - LARAVEL=5.8.* + - LARAVEL=^6.0 + - LARAVEL=^7.0 + +matrix: + fast_finish: true + exclude: + - php: 7.1 + env: LARAVEL=^6.0 + - php: 7.1 + env: LARAVEL=^7.0 + allow_failures: + - env: LARAVEL=^7.0 + +before_install: + - phpenv config-rm xdebug.ini || true + +install: + - travis_retry composer require "illuminate/contracts=${LARAVEL}" --prefer-dist --no-interaction --no-suggest + +script: vendor/bin/phpunit --verbose \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3030b52 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# knet + +General information about this package. + +## Installation + +Information about the installation procedure for this package. + +## Using this package + +Information about using this package + +## Contributing + +Information about contributing to this package. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..85eb74c --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "name": "asciisd/knet", + "description": "Knet package is provides an expressive, fluent interface to Knet's payment services.", + "keywords": [ + "Laravel", + "billing", + "Knet", + "Knet Payment" + ], + "authors": [ + { + "name": "Amr Ahmed", + "email": "aemad@asciisd.com" + } + ], + "homepage": "https://github.com/asciisd/knet", + "require": { + "php": "^7.1.3", + "laravel/framework": "5.8.*", + "dompdf/dompdf": "^0.8.0", + "ext-openssl": "*", + "ext-json": "*" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^3.8|^4.0|^5.0", + "phpunit/phpunit": "^7.5|^8.0" + }, + "license": "MIT", + "autoload": { + "psr-4": { + "Asciisd\\Knet\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Asciisd\\Knet\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Asciisd\\Knet\\Providers\\KnetServiceProvider" + ] + } + }, + "scripts": { + "test": "vendor/bin/phpunit -c ./phpunit.xml --colors=always", + "analysis": "vendor/bin/phpstan analyse" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/knet.php b/config/knet.php new file mode 100644 index 0000000..cd3ea8d --- /dev/null +++ b/config/knet.php @@ -0,0 +1,136 @@ + env('KENT_PRODUCTION_URL', 'https://kpay.com.kw/kpg/PaymentHTTP.htm'), + 'development_url' => env('KENT_PRODUCTION_URL', 'https://kpaytest.com.kw/kpg/PaymentHTTP.htm'), + + /* + |-------------------------------------------------------------------------- + | Knet Credentials + |-------------------------------------------------------------------------- + | + | TranPortal Identification Number: The Payment Gateway Bank administrator + | issues the TranPortal ID to identify the merchant and terminal for transaction + | processing. + | + | TranPortal Password: The Payment Gateway Bank administrator issues the + | TranPortal password to authenticate the merchant and terminal. Merchant data + | will be encrypted and password securely hidden as long as the merchant is issuing + | an https post for transmitting the data to Payment Gateway. + | + */ + 'transport' => [ + 'id' => env('KENT_TRANSPORT_ID'), + 'password' => env('KENT_TRANSPORT_PASSWORD'), + ], + + 'resource_key' => env('KENT_RESOURCE_KEY'), + + /* + |-------------------------------------------------------------------------- + | Knet Response url + |-------------------------------------------------------------------------- + | + | The merchant URL where Payment Gateway send the authorization response + | + */ + 'response_url' => env('KENT_RESPONSE_URL', '/knet/response'), + + /* + |-------------------------------------------------------------------------- + | Knet Error url + |-------------------------------------------------------------------------- + | + | The merchant URL where Payment Gateway send the response in case any + | error while processing the transaction. + | + */ + 'error_url' => env('KENT_ERROR_URL', '/knet/error'), + + /* + |-------------------------------------------------------------------------- + | Transaction Action Code + |-------------------------------------------------------------------------- + | + | Transaction Action Type, "1" for Purchase. + | Transaction Action Type, "2" for Refund. + | Transaction Action Type, "3" for Void. + | Transaction Action Type, "8" for Inquiry. + | + */ + 'action_code' => env('KENT_ACTION_CODE', 1), + + /* + |-------------------------------------------------------------------------- + | Language + |-------------------------------------------------------------------------- + | + | The language in which Payment Page has to be presented. + | + | Supported languages: 'AR', 'EN' + | + */ + 'language' => env('KENT_LANGUAGE', 'EN'), + + /* + |-------------------------------------------------------------------------- + | Knet Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Knet's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + 'path' => env('KNET_PATH', 'knet'), + + /* + |-------------------------------------------------------------------------- + | Knet Model + |-------------------------------------------------------------------------- + | + | This is the model in your application that implements the HasKnet trait + | provided by Knet. It will serve as the primary model you use while + | interacting with Knet related methods, and so on. + | + */ + 'model' => env('KNET_MODEL', App\User::class), + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Knet. + | + */ + 'currency' => env('KENT_CURRENCY', 414), + 'decimals' => '3', + + /* + |-------------------------------------------------------------------------- + | Invoice Paper Size + |-------------------------------------------------------------------------- + | + | This option is the default paper size for all invoices generated using + | Knet. You are free to customize this settings based on the usual + | paper size used by the customers using your Laravel applications. + | + | Supported sizes: 'letter', 'legal', 'A4' + | + */ + 'paper' => env('KNET_PAPER', 'letter'), +]; diff --git a/database/migrations/.gitignore b/database/migrations/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/database/migrations/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..a76a832 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - src + - tests \ No newline at end of file diff --git a/public/.gitignore b/public/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/resources/lang/.gitignore b/resources/lang/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/resources/lang/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/resources/views/.gitignore b/resources/views/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/resources/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..884a1ea --- /dev/null +++ b/routes/web.php @@ -0,0 +1,22 @@ +group(function () { + Route::post('/response', 'KnetController@response')->name('knet.response'); + + Route::middleware('auth')->group(function() { + Route::get('/', 'KnetController@index')->name('knet'); + Route::post('/', 'KnetController@charge')->name('knet.charge'); + Route::post('/error', 'KnetController@error')->name('knet.error'); + }); +}); \ No newline at end of file diff --git a/src/Console/Commands/KnetCommand.php b/src/Console/Commands/KnetCommand.php new file mode 100644 index 0000000..e717ecf --- /dev/null +++ b/src/Console/Commands/KnetCommand.php @@ -0,0 +1,42 @@ +payload = $payload; + } +} diff --git a/src/Events/KnetResponseReceived.php b/src/Events/KnetResponseReceived.php new file mode 100644 index 0000000..1573985 --- /dev/null +++ b/src/Events/KnetResponseReceived.php @@ -0,0 +1,28 @@ +payload = $payload; + } +} diff --git a/src/Exceptions/IncompletePayment.php b/src/Exceptions/IncompletePayment.php new file mode 100644 index 0000000..77bc5a9 --- /dev/null +++ b/src/Exceptions/IncompletePayment.php @@ -0,0 +1,31 @@ +transaction = $transaction; + } +} diff --git a/src/Exceptions/KnetException.php b/src/Exceptions/KnetException.php new file mode 100644 index 0000000..51661f4 --- /dev/null +++ b/src/Exceptions/KnetException.php @@ -0,0 +1,20 @@ +setHttpBody($httpBody); + $instance->setSigHeader($sigHeader); + return $instance; + } + + /** + * Gets the HTTP body as a string. + * + * @return string|null + */ + public function getHttpBody() + { + return $this->httpBody; + } + + /** + * Sets the HTTP body as a string. + * + * @param string|null $httpBody + */ + public function setHttpBody($httpBody) + { + $this->httpBody = $httpBody; + } + + /** + * Gets the `Stripe-Signature` HTTP header. + * + * @return string|null + */ + public function getSigHeader() + { + return $this->sigHeader; + } + + /** + * Sets the `Stripe-Signature` HTTP header. + * + * @param string|null $sigHeader + */ + public function setSigHeader($sigHeader) + { + $this->sigHeader = $sigHeader; + } +} diff --git a/src/HasKnet.php b/src/HasKnet.php new file mode 100644 index 0000000..6cdf722 --- /dev/null +++ b/src/HasKnet.php @@ -0,0 +1,33 @@ + Str::uuid(), + 'currency' => 'KWD', + ], $options); + + $options['livemode'] = App::environment(['production']); + $options['amount'] = $amount; + $options['user_id'] = $this->id; + $options['url'] = (new Knet()) + ->setAmt($amount) + ->setTrackId($options['trackid']) + ->url(); + + $payment = new Payment( + KnetTransaction::create($options) + ); + +// $payment->validate(); + + return $payment; + } +} \ No newline at end of file diff --git a/src/Http/Controllers/KnetController.php b/src/Http/Controllers/KnetController.php new file mode 100644 index 0000000..c64b9e1 --- /dev/null +++ b/src/Http/Controllers/KnetController.php @@ -0,0 +1,66 @@ +middleware(VerifyKnetResponseSignature::class); + } + + public function handleKnet(Request $request) + { + KnetResponseReceived::dispatch($request->all()); + + $knetResponseHandler = new KnetResponseHandler(); + + // update transaction + KnetTransaction::findByTrackId($request->input('trackid')) + ->update($knetResponseHandler->toArray()); + + //todo: notify user + //todo: send emails + + KnetResponseHandled::dispatch($knetResponseHandler->toArray()); + + //return success + return $this->successMethod(); + } + + /** + * Handle successful calls on the controller. + * + * @param array $parameters + * @return Response + */ + protected function successMethod($parameters = []) + { + return Response('knet response handled', 200); + } + + /** + * Handle calls to missing methods on the controller. + * + * @param array $parameters + * @return Response + */ + protected function missingMethod($parameters = []) + { + return new Response; + } +} \ No newline at end of file diff --git a/src/Http/Middleware/VerifyKnetResponseSignature.php b/src/Http/Middleware/VerifyKnetResponseSignature.php new file mode 100644 index 0000000..525359b --- /dev/null +++ b/src/Http/Middleware/VerifyKnetResponseSignature.php @@ -0,0 +1,32 @@ +getContent(), + $request->header(), + $request->input('trackid') + ); + } catch (SignatureVerificationException $exception) { + throw new AccessDeniedHttpException($exception->getMessage(), $exception); + } + return $next($request); + } +} \ No newline at end of file diff --git a/src/Invoice.php b/src/Invoice.php new file mode 100644 index 0000000..d66d84c --- /dev/null +++ b/src/Invoice.php @@ -0,0 +1,164 @@ +transaction = $transaction; + } + + /** + * Get a Carbon date for the invoice. + * + * @param DateTimeZone|string $timezone + * @return Carbon + */ + public function date($timezone = null) + { + $carbon = Carbon::createFromTimestampUTC($this->transaction->created ?? $this->transaction->date); + return $timezone ? $carbon->setTimezone($timezone) : $carbon; + } + + /** + * Get the total amount that was paid (or will be paid). + * + * @return string + */ + public function total() + { + return $this->formatAmount($this->rawTotal()); + } + + /** + * Get the raw total amount that was paid (or will be paid). + * + * @return int + */ + public function rawTotal() + { + return $this->transaction->total + $this->rawStartingBalance(); + } + + /** + * Get the total of the transaction (before discounts). + * + * @return string + */ + public function subtotal() + { + return $this->formatAmount($this->transaction->subtotal); + } + + /** + * Format the given amount into a displayable currency. + * + * @param int $amount + * @return string + */ + protected function formatAmount($amount) + { + return $amount . ' KWD'; + } + + /** + * Get the View instance for the transaction. + * + * @param array $data + * @return View + */ + public function view(array $data) + { + return View::make('knet::receipt', array_merge($data, [ + 'transaction' => $this + ])); + } + + /** + * Capture the transaction as a PDF and return the raw bytes. + * + * @param array $data + * @return string + */ + public function pdf(array $data) + { + if (!defined('DOMPDF_ENABLE_AUTOLOAD')) { + define('DOMPDF_ENABLE_AUTOLOAD', false); + } + $dompdf = new Dompdf; + $dompdf->setPaper(config('knet.paper', 'letter')); + $dompdf->loadHtml($this->view($data)->render()); + $dompdf->render(); + return $dompdf->output(); + } + + /** + * Create an transaction download response. + * + * @param array $data + * @return Response + */ + public function download(array $data) + { + $filename = $data['product'] . '_' . $this->date()->month . '_' . $this->date()->year; + return $this->downloadAs($filename, $data); + } + + /** + * Create an transaction download response with a specific filename. + * + * @param string $filename + * @param array $data + * @return Response + */ + public function downloadAs($filename, array $data) + { + return new Response($this->pdf($data), 200, [ + 'Content-Description' => 'File Transfer', + 'Content-Disposition' => 'attachment; filename="' . $filename . '.pdf"', + 'Content-Transfer-Encoding' => 'binary', + 'Content-Type' => 'application/pdf', + 'X-Vapor-Base64-Encode' => 'True', + ]); + } + + /** + * Get the Stripe transaction instance. + * + * @return \Stripe\Invoice + */ + public function asStripeInvoice() + { + return $this->transaction; + } + + /** + * Dynamically get values from the Stripe transaction. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->transaction->{$key}; + } +} \ No newline at end of file diff --git a/src/Knet.php b/src/Knet.php new file mode 100644 index 0000000..cf3a6d3 --- /dev/null +++ b/src/Knet.php @@ -0,0 +1,183 @@ +checkForResourceKey(); + + $this->initiatePaymentConfig(); + } + + private function initiatePaymentConfig() + { + $this->id = config('knet.transport.id'); + $this->tranportalId = config('knet.transport.id'); + $this->password = config('knet.transport.password'); + + $this->action = config('knet.action_code'); + $this->langid = config('knet.language'); + $this->currencycode = config('knet.currency'); + + $this->responseURL = config('knet.response_url'); + $this->errorURL = config('knet.error_url'); + } + + /** + * check for existence of resource key + * + * @throws Throwable + */ + private function checkForResourceKey() + { + throw_if(!config('knet.resource_key'), KnetException::class); + } + + public function url() + { + return $this->getEnvUrl() . '&' . $this->urlParams(); + } + + private function getEnvUrl() + { + $url = config('knet.development_url'); + + if (App::environment(['production'])) { + $url = config('knet.production_url'); + } + + return $url . '?param=paymentInit'; + } + + private function setTranData() + { + $this->trandata = $this->encryptedParams(); + + return $this; + } + + public function setAmt($amount) + { + $this->amt = $amount; + + return $this; + } + + public function setTrackId($trackid) + { + $this->trackid = $trackid; + + return $this; + } + + public function setUDF1($param) + { + $this->udf1 = $param; + + return $this; + } + + public function setUDF2($param) + { + $this->udf2 = $param; + + return $this; + } + + public function setUDF3($param) + { + $this->udf3 = $param; + + return $this; + } + + public function setUDF4($param) + { + $this->udf4 = $param; + + return $this; + } + + public function setUDF5($param) + { + $this->udf5 = $param; + + return $this; + } + + private function encryptedParams() + { + $params = $this->setAsKeyAndValue($this->paramsToEncrypt); + + return $this->encrypt($params); + } + + private function urlParams() + { + $this->setTranData(); + + return $this->setAsKeyAndValue($this->reqParams); + } + + private function setAsKeyAndValue($arrOfKeys) + { + $params = ''; + + foreach ($arrOfKeys as $param) { + if ($this->{$param} != null) + $params = $this->addTo($params, $param, $this->{$param}); + } + + return $params; + } + + public function addTo($param, $key, $value) + { + if ($param === '') { + $param .= "{$key}={$value}"; + } else { + $param .= "&{$key}={$value}"; + } + + return $param; + } + + private function encrypt($params) + { + return $this->encryptAES($params, config('knet.resource_key')); + } +} diff --git a/src/KnetClient.php b/src/KnetClient.php new file mode 100644 index 0000000..1c9ddb2 --- /dev/null +++ b/src/KnetClient.php @@ -0,0 +1,72 @@ +hex2ByteArray(trim($code)); + $code = $this->byteArray2String($code); + $iv = $key; + $code = base64_encode($code); + $decrypted = openssl_decrypt($code, 'AES-128-CBC', $key, OPENSSL_ZERO_PADDING, $iv); + + return $this->pkcs5_unpad($decrypted); + } + + public function hex2ByteArray($hexString) + { + $string = hex2bin($hexString); + + return unpack('C*', $string); + } + + public function byteArray2String($byteArray) + { + $chars = array_map('chr', $byteArray); + + return implode($chars); + } + + public function pkcs5_unpad($text) + { + $pad = ord($text[strlen($text) - 1]); + if ($pad > strlen($text)) { + return ''; + } + if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) { + return ''; + } + + return substr($text, 0, -1 * $pad); + } + + public function encryptAES($str, $key) + { + $str = $this->pkcs5_pad($str); + $encrypted = openssl_encrypt($str, 'AES-128-CBC', $key, OPENSSL_ZERO_PADDING, $key); + $encrypted = base64_decode($encrypted); + $encrypted = unpack('C*', ($encrypted)); + $encrypted = $this->byteArray2Hex($encrypted); + $encrypted = urlencode($encrypted); + + return $encrypted; + } + + public function pkcs5_pad($text) + { + $blockSize = 16; + $pad = $blockSize - (strlen($text) % $blockSize); + + return $text . str_repeat(chr($pad), $pad); + } + + public function byteArray2Hex($byteArray) + { + $chars = array_map('chr', $byteArray); + $bin = implode($chars); + + return bin2hex($bin); + } +} diff --git a/src/KnetResponseHandler.php b/src/KnetResponseHandler.php new file mode 100644 index 0000000..b426008 --- /dev/null +++ b/src/KnetResponseHandler.php @@ -0,0 +1,67 @@ +exists('trandata')) { + foreach ($this->decryptedData() as $datum) { + $temp = explode('=', $datum); + if (isset($temp[1])) { + if ($temp[0] == 'result') { + $temp[1] = implode(' ', explode('+', $temp[1])); + $this->result['paid'] = $temp[1] == 'CAPTURED'; + } + $this->result[Str::snake($temp[0])] = $temp[1]; + } + } + } else { + $this->errors = explode('-', request('ErrorText')); + } + + return $this; + } + + public function __toString() + { + return json_encode($this->result); + } + + public function toArray() + { + return $this->result; + } + + public function hasErrors() + { + return count($this->errors); + } + + public function errors() + { + return $this->hasErrors() ? $this->errors : null; + } + + private function decrypt($tranData) + { + return $this->decryptAES($tranData, config('knet.resource_key')); + } + + private function decryptedData() + { + $tranData = request('trandata'); + return explode('&', $this->decrypt($tranData)); + } + + public function __get($name) + { + return $this->result[$name]; + } +} diff --git a/src/KnetResponseSignature.php b/src/KnetResponseSignature.php new file mode 100644 index 0000000..bf9ec1c --- /dev/null +++ b/src/KnetResponseSignature.php @@ -0,0 +1,41 @@ +first(); + } + + public function owner() + { + return $this->belongsTo('App\User'); + } +} diff --git a/src/Payment.php b/src/Payment.php new file mode 100644 index 0000000..dd5088b --- /dev/null +++ b/src/Payment.php @@ -0,0 +1,86 @@ +transaction = $transaction; + } + + /** + * Get the total amount that will be paid. + * + * @return string + */ + public function amount() + { + return $this->rawAmount() . ' ' . $this->transaction->currency; + } + + /** + * Get the raw total amount that will be paid. + * + * @return int + */ + public function rawAmount() + { + return $this->transaction->amt; + } + + /** + * Determine if the payment was cancelled. + * + * @return bool + */ + public function isCaptured() + { + return $this->transaction->result === self::CAPTURED; + } + + /** + * Determine if the payment was successful. + * + * @return bool + */ + public function isNonCaptured() + { + return $this->transaction->result === self::NOT_CAPTURED; + } + + public function customer() + { + return $this->transaction->user(); + } + + /** + * Dynamically get values from the PaymentIntent. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->transaction->{$key}; + } +} \ No newline at end of file diff --git a/src/Providers/KnetServiceProvider.php b/src/Providers/KnetServiceProvider.php new file mode 100644 index 0000000..36ec808 --- /dev/null +++ b/src/Providers/KnetServiceProvider.php @@ -0,0 +1,107 @@ +publishes([ + // __DIR__.'/../../config/knet.php' => config_path('knet.php'), + // ], 'config'); + + /** + * Routes + * + * Uncomment this function call to load the route files. + * A web.php file has already been generated. + */ + // $this->loadRoutesFrom(__DIR__.'/../../routes/web.php'); + + /** + * Translations + * + * Uncomment the first function call to load the translations. + * Uncomment the second function call to load the JSON translations. + * Uncomment the third function call to make the translations publishable using the 'translations' tag. + */ + // $this->loadTranslationsFrom(__DIR__.'/../../resources/lang', 'knet'); + // $this->loadJsonTranslationsFrom(__DIR__.'/../../resources/lang', 'knet'); + // $this->publishes([ + // __DIR__.'/../../resources/lang' => resource_path('lang/vendor/knet'), + // ], 'translations'); + + /** + * Views + * + * Uncomment the first section to load the views. + * Uncomment the second section to make the view publishable using the 'view' tags. + */ + // $this->loadViewsFrom(__DIR__.'/../../resources/views', 'knet'); + // $this->publishes([ + // __DIR__.'/../../resources/views' => resource_path('views/vendor/knet'), + // ], 'views'); + + /** + * Commands + * + * Uncomment this section to load the commands. + * A basic command file has already been generated in 'src\Console\Commands\MyPackageCommand.php'. + */ + // if ($this->app->runningInConsole()) { + // $this->commands([ + // \Asciisd\Knet\Console\Commands\KnetCommand::class, + // ]); + // } + + /** + * Public assets + * + * Uncomment this functin call to make the public assets publishable using the 'public' tag. + */ + // $this->publishes([ + // __DIR__.'/../../public' => public_path('vendor/knet'), + // ], 'public'); + + /** + * Migrations + * + * Uncomment the first function call to load the migrations. + * Uncomment the second function call to make the migrations publishable using the 'migrations' tags. + */ + // $this->loadMigrationsFrom(__DIR__.'/../../database/migrations'); + // $this->publishes([ + // __DIR__.'/../../database/migrations/' => database_path('migrations') + // ], 'migrations'); + } + + /** + * Register the application services. + * + * @return void + */ + public function register() + { + /** + * Config file + * + * Uncomment this function call to load the config file. + * If the config file is also publishable, it will merge with that file + */ + // $this->mergeConfigFrom( + // __DIR__.'/../../config/knet.php', 'knet' + // ); + } +} diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php new file mode 100644 index 0000000..ac01dcd --- /dev/null +++ b/tests/Fixtures/User.php @@ -0,0 +1,10 @@ +loadLaravelMigrations(); + $this->artisan('migrate')->run(); + } + + protected function createCustomer($description = 'aemaddin'): User + { + return User::create([ + 'email' => "{$description}@knet-test.com", + 'name' => 'Amr Ahmed', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); + } +} \ No newline at end of file diff --git a/tests/Integrations/PayTest.php b/tests/Integrations/PayTest.php new file mode 100644 index 0000000..7d79f1c --- /dev/null +++ b/tests/Integrations/PayTest.php @@ -0,0 +1,21 @@ +createCustomer('customer_can_be_charged'); + + $response = $user->pay(1000); + $this->assertInstanceOf(Payment::class, $response); + $this->assertEquals(1000, $response->rawAmount()); + $this->assertEquals($user->id, $response->customer->id); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..400a5d5 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,29 @@ +result = Payment::CAPTURED; + + $payment = new Payment($transaction); + + $this->assertTrue($payment->isCaptured()); + } + + /** @test */ + public function it_can_return_not_captured() { + $transaction = new KnetTransaction(); + $transaction->result = Payment::NOT_CAPTURED; + + $payment = new Payment($transaction); + + $this->assertTrue($payment->isNonCaptured()); + } + + /** @test */ + public function it_can_return_raw_amount() { + $transaction = new KnetTransaction(); + $transaction->amt = 10; + + $payment = new Payment($transaction); + + $this->assertEquals($payment->rawAmount(), 10); + } + + /** @test */ + public function it_can_return_formatted_amount() { + $transaction = new KnetTransaction(); + $transaction->amt = 10; + $transaction->currency = 'KWD'; + + $payment = new Payment($transaction); + + $this->assertEquals($payment->amount(), '10 KWD'); + } +} \ No newline at end of file