diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad8ac2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.iml +.idea/ +/vendor/ +composer.lock + + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..254148b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +before_script: + # Update composer + - composer self-update + - composer install --dev + # Install Nette Tester + - travis_retry composer update --no-interaction --prefer-dist $dependencies + +script: ./vendor/bin/tester -p php -c ./tests/php.ini-unix ./tests/ + +after_failure: + # Print *.actual content + - 'for i in $(find ./tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done' + +sudo: false \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7376760..6e5bb45 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015, Metis framework +Copyright (c) 2015, Metis framework by GrowJOB All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 1d0a9bb..e90fcd4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# PayPal -PayPal integration for Nette +MetisFW/PayPal +====== + +[![Build Status](https://travis-ci.org/MetisFW/PayPal.svg?branch=master)](https://travis-ci.org/MetisFW/PayPal) +[![Downloads this Month](https://img.shields.io/packagist/dm/metisfw/paypal.svg)](https://packagist.org/packages/metisfw/paypal) +[![Latest stable](https://img.shields.io/packagist/v/metisfw/paypal.svg)](https://packagist.org/packages/metisfw/paypal) + +About +------------ +PayPal payment integration to Nette framework. +Internally use [paypal/PayPal-PHP-SDK](https://github.com/paypal/PayPal-PHP-SDK) for api requests. + +Inspired by [Kdyby/PayPalExpress](https://github.com/Kdyby/PayPalExpress) + +Requirements +------------ +MetisFW/PayPal requires PHP 5.3.2 or higher with curl, json and openssl (for lower PHP version) extensions. + +- [Nette Framework](https://github.com/nette/nette) + + +Installation +------------ +1) The best way to install MetisFW/PayPal is using [Composer](http://getcomposer.org/): + +```sh +$ composer require metisfw/paypal +``` + +2) Register extension +``` +extensions: + payPal: MetisFW\PayPal\DI\PayPalExtension +``` + +3) Set up extension parameters + +```neon +payPal: + clientId: AUqne4ywvozUaSQ1THTZYKFr88bhtA0SS_fXBoJTfeSTIasDBWuXLiLcFlfmSXRfL-kZ3Z5shvNrT6rP + secret: EDGPDc3a65JBBY7-IKkNak7aGTVTvY-NhJgfhptegSML58fWjfp89U7UKNgGk9UI-UEZ-btfaE2sGST1 + currency: EUR + sdkConfig: + mode: sandbox + log.Enabled: true + log.FileName: '%tempDir%/PayPal.log' + log.LogLevel: DEBUG + validation.level: log + cache.enabled: true + # 'http.CURLOPT_CONNECTTIMEOUT' => 30 + # 'http.headers.PayPal-Partner-Attribution-Id' => '123123123'/ +``` + +sdkConfig is config to [paypal/PayPal-PHP-SDK](https://github.com/paypal/PayPal-PHP-SDK) +see [sdk-config-sample](https://github.com/paypal/PayPal-PHP-SDK/blob/master/sample/sdk_config.ini) + +----- + +Homepage [MetisFW](https://github.com/MetisFW) and repository [MetisFW/PayPal](https://github.com/MetisFW/PayPal). \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..799a2aa --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "metisfw/paypal", + "type": "library", + "description": "PayPal SDK integration for Nette Framework", + "keywords": [ + "nette", + "paypal", + "pay", + "checkout", + "payment" + ], + "homepage": "https://github.com/MetisFW", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "GrowJOB s.r.o", + "email": "dev@growjob.com" + }, + { + "name": "Hynek VilĂ­mek", + "email": "h.vilimek@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "ext-curl": "*", + "ext-json": "*", + "nette/nette": "~2.3", + "paypal/rest-api-sdk-php": "1.5.*" + }, + "require-dev": { + "nette/tester": "@dev", + "mockery/mockery": "^0.9.4" + }, + "support": { + "email": "dev@growjob.com", + "issues": "https://github.com/MetisFW/PayPal/issues" + }, + "autoload": { + "psr-0": { + "MetisFW\\PayPal\\": "src/" + } + } +} diff --git a/src/MetisFW/PayPal/DI/PayPalExtension.php b/src/MetisFW/PayPal/DI/PayPalExtension.php new file mode 100644 index 0000000..0742b44 --- /dev/null +++ b/src/MetisFW/PayPal/DI/PayPalExtension.php @@ -0,0 +1,48 @@ + 'CZK', + ); + + public function loadConfiguration() { + $builder = $this->getContainerBuilder(); + $config = $this->getConfig($this->defaults); + + Validators::assertField($config, 'clientId'); + Validators::assertField($config, 'secret'); + Validators::assertField($config, 'sdkConfig', 'array'); + + $builder->addDefinition($this->prefix('credentials')) + ->setClass('PayPal\Auth\OAuthTokenCredential', array($config['clientId'], $config['secret'])); + + $builder->addDefinition($this->prefix('apiContext')) + ->setClass('PayPal\Rest\ApiContext', array($this->prefix('@credentials'))); + + $builder->addDefinition($this->prefix('PayPal')) + ->setClass('MetisFW\PayPal\PayPalContext', array($this->prefix('@apiContext'))) + ->addSetup('setConfig', array($config['sdkConfig'])) + ->addSetup('setCurrency', array($config['currency'])); + } + + /** + * @param Configurator $configurator + */ + public static function register(Configurator $configurator) { + $configurator->onCompile[] = function ($config, Compiler $compiler) { + $compiler->addExtension('payPal', new PayPalExtension()); + }; + } + +} diff --git a/src/MetisFW/PayPal/PayPalContext.php b/src/MetisFW/PayPal/PayPalContext.php new file mode 100644 index 0000000..bd6f7be --- /dev/null +++ b/src/MetisFW/PayPal/PayPalContext.php @@ -0,0 +1,52 @@ +apiContext = $apiContext; + } + + /** + * @param array $config + */ + public function setConfig(array $config) { + $this->apiContext->setConfig($config); + } + + /** + * @param string $currency + */ + public function setCurrency($currency) { + $this->currency = $currency; + } + + /** + * @return string + */ + public function getCurrency() { + return $this->currency; + } + + /** + * @return ApiContext + */ + public function getApiContext() { + return $this->apiContext; + } + +} diff --git a/src/MetisFW/PayPal/PayPalException.php b/src/MetisFW/PayPal/PayPalException.php new file mode 100644 index 0000000..c8201ff --- /dev/null +++ b/src/MetisFW/PayPal/PayPalException.php @@ -0,0 +1,7 @@ +context = $context; + } + + /** + * @return array array of PayPal\Api\Transaction + */ + abstract protected function getTransactions(); + + /** + * @see http://paypal.github.io/PayPal-PHP-SDK/sample/doc/payments/CreatePaymentUsingPayPal.html + * + * @return Payment + */ + public function getPayment() { + $payer = new Payer(); + $payer->setPaymentMethod('paypal'); + + $payment = new Payment(); + $payment->setIntent("sale") + ->setPayer($payer); + + $transactions = $this->getTransactions(); + $payment->setTransactions($transactions); + + return $payment; + } + + /** + * Execute payment api call + * + * @return Payment + */ + public function createPayment(Payment $payment) { + try { + return $payment->create($this->context->getApiContext()); + } + catch(\Exception $exception) { + throw $this->translateException($exception); + } + } + + /** + * @param string $paymentId + * @param string $payerId + * + * @return void + */ + public function handleReturn($paymentId, $payerId) { + try { + $payment = Payment::get($paymentId, $this->context->getApiContext()); + $execution = new PaymentExecution(); + $execution->setPayerId($payerId); + + $payment->execute($execution, $this->context->getApiContext()); + $paidPayment = Payment::get($paymentId, $this->context->getApiContext()); + } + catch(\Exception $exception) { + throw $this->translateException($exception); + } + + $this->onReturn($this, $paidPayment); + return $paidPayment; + } + + /** + * @return void + */ + public function handleCancel() { + $this->onCancel($this); + } + + /** + * @param \Exception $exception + * @return \Exception + */ + protected function translateException(\Exception $exception) { + if($exception instanceof PayPalConfigurationException || + $exception instanceof PayPalInvalidCredentialException || + $exception instanceof PayPalMissingCredentialException || + $exception instanceof PayPalConnectionException + ) { + return new PayPalException($exception->getMessage(), $exception->getCode(), $exception); + } + + return $exception; + } + +} diff --git a/src/MetisFW/PayPal/Payment/PaymentOperation.php b/src/MetisFW/PayPal/Payment/PaymentOperation.php new file mode 100644 index 0000000..a838de7 --- /dev/null +++ b/src/MetisFW/PayPal/Payment/PaymentOperation.php @@ -0,0 +1,38 @@ +operation = $operation; + } + + public function handleCheckout() { + try { + $payment = $this->operation->getPayment(); + $this->setPaymentParameters($payment); + + $createdPayment = $this->operation->createPayment($payment); + $this->onCheckout($this, $createdPayment); + + $approvalUrl = $createdPayment->getApprovalLink(); + $this->getPresenter()->redirectUrl($approvalUrl); + } + catch(PayPalException $exception) { + $this->errorHandler($exception); + } + } + + public function handleReturn() { + $paymentId = $this->getPresenter()->getParameter('paymentId'); + $payerId = $this->getPresenter()->getParameter('PayerID'); + + try { + $paidPayment = $this->operation->handleReturn($paymentId, $payerId); + } + catch(PayPalException $exception) { + $this->errorHandler($exception); + return; + } + + $this->onSuccess($this, $paidPayment); + } + + public function handleCancel() { + $this->operation->handleCancel(); + $this->onCancel($this); + } + + public function setTemplateFilePath($templateFilePath) { + $this->setTemplateFilePath($templateFilePath); + } + + /** + * @param array $attrs + * @param string $text + */ + public function render($attrs = array(), $text = "Pay") { + $template = $this->template; + $templateFilePath = ($this->templateFilePath ? $this->templateFilePath : $this->getDefaultTemplateFilePath()); + $template->setFile($templateFilePath); + $template->checkoutLink = $this->link('//checkout!'); + $template->text = $text; + $template->attrs = $attrs; + $template->render(); + } + + /** + * @param \Exception $exception + * + * @throws PayPalException + * + * @return void + */ + protected function errorHandler(\Exception $exception) { + if(!$this->onError) { + throw $exception; + } + + $this->onError($this, $exception); + } + + /** + * @param Payment $payment + */ + protected function setPaymentParameters(Payment $payment) { + $redirectUrls = new RedirectUrls(); + $redirectUrls->setReturnUrl($this->link('//return!'))->setCancelUrl($this->link('//cancel!')); + $payment->setRedirectUrls($redirectUrls); + } + + protected function getDefaultTemplateFilePath() { + return __DIR__.'/templates/PaymentControl.latte'; + } + +} diff --git a/src/MetisFW/PayPal/UI/templates/PaymentControl.latte b/src/MetisFW/PayPal/UI/templates/PaymentControl.latte new file mode 100644 index 0000000..ff1670f --- /dev/null +++ b/src/MetisFW/PayPal/UI/templates/PaymentControl.latte @@ -0,0 +1,3 @@ + +
{$text}
+
diff --git a/tests/PayPal/.gitignore b/tests/PayPal/.gitignore new file mode 100755 index 0000000..9632c82 --- /dev/null +++ b/tests/PayPal/.gitignore @@ -0,0 +1,3 @@ +coverage.dat +*.actual +*.expected diff --git a/tests/PayPal/DI/PayPalExtensionTest.phpt b/tests/PayPal/DI/PayPalExtensionTest.phpt new file mode 100644 index 0000000..91fcb33 --- /dev/null +++ b/tests/PayPal/DI/PayPalExtensionTest.phpt @@ -0,0 +1,29 @@ +setTempDirectory(TEMP_DIR); + $config->addParameters(array('container' => array('class' => 'SystemContainer_'.md5(TEMP_DIR)))); + PayPalExtension::register($config); + $config->addConfig(__DIR__.'/../../paypal.config.neon'); + + $container = $config->createContainer(); + $paypal = $container->getByType('MetisFW\PayPal\PayPalContext'); + + Assert::notEqual(null, $paypal); + } + +} + +\run(new PayPalExtensionTest()); diff --git a/tests/PayPal/Helper/TransactionHelper.php b/tests/PayPal/Helper/TransactionHelper.php new file mode 100644 index 0000000..6ba1d3c --- /dev/null +++ b/tests/PayPal/Helper/TransactionHelper.php @@ -0,0 +1,89 @@ +setAmount($amount); + $transaction->setItemList($itemLists); + $transaction->setInvoiceNumber($invoiceNumber); + $transaction->setDescription($description); + return $transaction; + } + + /** + * @param string $currency + * @param int $total + * @param Details $details + * + * @return Amount + */ + static public function createAmount(Details $details, $total, $currency) { + $amount = new Amount(); + $amount->setCurrency($currency); + $amount->setTotal($total); + $amount->setDetails($details); + return $amount; + } + + /** + * @param float $shippingPrice + * @param float $taxPrice + * @param float $subtotal + * + * @return Details + */ + static public function createDetails($shippingPrice, $taxPrice, $subtotal) { + $details = new Details(); + $details->setShipping($shippingPrice); + $details->setTax($taxPrice); + + $details->setSubtotal($subtotal); + return $details; + } + + /** + * @param array $items + * + * @return ItemList + */ + static public function createItemList(array $items) { + $itemList = new ItemList(); + $itemList->setItems($items); + return $itemList; + } + + /** + * @param string $name + * @param int $currency + * @param int $quantity + * @param string $sku + * @param int $price + * @return Item + */ + static public function createItem($name, $currency, $quantity, $sku, $price) { + $item = new Item(); + $item->setName($name); + $item->setCurrency($currency); + $item->setQuantity($quantity); + $item->setSku($sku); + $item->setPrice($price); + return $item; + } +} \ No newline at end of file diff --git a/tests/PayPal/Payment/DummyPaymentOperation.php b/tests/PayPal/Payment/DummyPaymentOperation.php new file mode 100644 index 0000000..4011c4e --- /dev/null +++ b/tests/PayPal/Payment/DummyPaymentOperation.php @@ -0,0 +1,26 @@ +config = array( + 'clientId' => 'AUqne4ywvozUaSQ1THTZYKFr88bhtA0SS_fXBoJTfeSTIasDBWuXLiLcFlfmSXRfL-kZ3Z5shvNrT6rP', + 'secretId' => 'EDGPDc3a65JBBY7-IKkNak7aGTVTvY-NhJgfhptegSML58fWjfp89U7UKNgGk9UI-UEZ-btfaE2sGST1' + ); + } + + public function testHandleReturn() { + $credentials = new OAuthTokenCredential($this->config['clientId'], $this->config['secretId']); + $apiContext = \Mockery::mock('\PayPal\Rest\ApiContext', array($credentials))->makePartial(); + + $context = new PayPalContext($apiContext); + $operation = new DummyPaymentOperation( + $context); + + $paymentId = "123456"; + $payerId = "john.doe"; + + $paymentMock = \Mockery::mock('alias:\PayPal\Api\Payment'); + $approvedPayment = $paymentMock; + $paymentMock->shouldReceive('get')->with($paymentId, $apiContext)->andReturn($approvedPayment); + $approvedPayment->shouldReceive('execute')->with( + \Mockery::on(function (PaymentExecution $actualExecution) use ($payerId) { + return $actualExecution->getPayerId() == $payerId; + }), + \Mockery::on(function (ApiContext $actualApiContext) use ($apiContext) { + return $actualApiContext === $apiContext; + }) + ); + $result = new Payment(); + $paymentMock->shouldReceive('get')->with($paymentId, $payerId)->andReturn($result); + + $payment = $operation->handleReturn($paymentId, $payerId); + + Assert::true($payment === $approvedPayment); + } + + /** + * This method is called after a test is executed. + * + * @return void + */ + protected function tearDown() { + parent::tearDown(); + \Mockery::close(); + } + +} + +\run(new PaymentOperationHandleReturnTest()); diff --git a/tests/PayPal/Payment/PaymentOperationTest.phpt b/tests/PayPal/Payment/PaymentOperationTest.phpt new file mode 100644 index 0000000..47d518f --- /dev/null +++ b/tests/PayPal/Payment/PaymentOperationTest.phpt @@ -0,0 +1,79 @@ +config = array( + 'clientId' => 'AUqne4ywvozUaSQ1THTZYKFr88bhtA0SS_fXBoJTfeSTIasDBWuXLiLcFlfmSXRfL-kZ3Z5shvNrT6rP', + 'secretId' => 'EDGPDc3a65JBBY7-IKkNak7aGTVTvY-NhJgfhptegSML58fWjfp89U7UKNgGk9UI-UEZ-btfaE2sGST1' + ); + } + + public function testGetPayment() { + $apiContext = \Mockery::mock('\PayPal\Rest\ApiContext', array()); + $context = new PayPalContext($apiContext); + $operation = new DummyPaymentOperation($context); + + $payment = $operation->getPayment(); + Assert::equal('123456789', $payment->transactions[0]->invoice_number); + } + + + public function testCreatePayment() { + $credentials = new OAuthTokenCredential($this->config['clientId'], $this->config['secretId']); + $apiContext = \Mockery::mock('\PayPal\Rest\ApiContext', array($credentials))->makePartial(); + $context = new PayPalContext($apiContext); + $operation = new DummyPaymentOperation($context); + $payment = $operation->getPayment(); + + $redirectUrls = new RedirectUrls(); + $redirectUrls->setReturnUrl('http://localhost/return'); + $redirectUrls->setCancelUrl('http://localhost/cancel'); + $payment->setRedirectUrls($redirectUrls); + + $result = $operation->createPayment($payment); + Assert::equal('created', $result->getState()); + Assert::notEqual(null, $result->getApprovalLink()); + } + + /** + * This method is called after a test is executed. + * + * @return void + */ + protected function tearDown() { + parent::tearDown(); + \Mockery::close(); + } + +} + +\run(new PaymentOperationTest()); + + diff --git a/tests/PayPal/UI/PaymentControlTest.phpt b/tests/PayPal/UI/PaymentControlTest.phpt new file mode 100644 index 0000000..01d7d23 --- /dev/null +++ b/tests/PayPal/UI/PaymentControlTest.phpt @@ -0,0 +1,83 @@ +operationMock = \Mockery::mock('\MetisFW\PayPal\Payment\PaymentOperation'); + $this->control = \Mockery::mock( + '\MetisFW\PayPal\UI\PaymentControl', + array($this->operationMock)) + ->makePartial() + ->shouldAllowMockingProtectedMethods(); + } + + public function testCheckout() { + $plainPayment = new Payment(); + $this->operationMock->shouldReceive('getPayment')->andReturn($plainPayment); + + $createdPayment = \Mockery::mock('\PayPal\Api\Payment'); + $this->operationMock->shouldReceive('createPayment')->with($plainPayment)->andReturn($createdPayment); + $approvalLink = "www.example.com"; + $createdPayment->shouldReceive('getApprovalLink')->andReturn($approvalLink); + + $this->control->shouldReceive('setPaymentParameters')->once(); + $this->control->shouldReceive('onCheckout'); + $this->control->shouldReceive('getPresenter->redirectUrl')->with($approvalLink); + $this->control->handleCheckout(); + + Assert::true(true); + } + + public function testHandleReturn() { + $paymentId = "123456"; + $payerId = "654321"; + $this->control->shouldReceive('getPresenter->getParameter')->with('paymentId')->andReturn($paymentId); + $this->control->shouldReceive('getPresenter->getParameter')->with('PayerID')->andReturn($payerId); + + $this->operationMock->shouldReceive('handleReturn'); + $this->control->shouldReceive('onSuccess'); + $this->control->handleReturn($paymentId, $payerId); + } + + public function testHandleCancel() { + $this->operationMock->shouldReceive('handleCancel'); + $this->control->shouldReceive('onCancel'); + $this->control->handleCancel(); + } + + /** + * This method is called after a test is executed. + * + * @return void + */ + protected function tearDown() { + parent::tearDown(); + \Mockery::close(); + } + +} + +run(new PaymentControlTest()); + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000..e3bca3b --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,29 @@ +run(isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : NULL); +} diff --git a/tests/paypal.config.neon b/tests/paypal.config.neon new file mode 100644 index 0000000..d0cdd10 --- /dev/null +++ b/tests/paypal.config.neon @@ -0,0 +1,13 @@ +payPal: + clientId: AUqne4ywvozUaSQ1THTZYKFr88bhtA0SS_fXBoJTfeSTIasDBWuXLiLcFlfmSXRfL-kZ3Z5shvNrT6rP + secret: EDGPDc3a65JBBY7-IKkNak7aGTVTvY-NhJgfhptegSML58fWjfp89U7UKNgGk9UI-UEZ-btfaE2sGST1 + currency: EUR + sdkConfig: + mode: sandbox + log.Enabled: true + log.FileName: './tmp/PayPal.log' + log.LogLevel: DEBUG + validation.level: log + cache.enabled: true + # 'http.CURLOPT_CONNECTTIMEOUT' => 30 + # 'http.headers.PayPal-Partner-Attribution-Id' => '123123123'/ diff --git a/tests/php.ini-unix b/tests/php.ini-unix new file mode 100644 index 0000000..3d137cc --- /dev/null +++ b/tests/php.ini-unix @@ -0,0 +1,3 @@ +extension=json.so +extension=curl.so +extension=openssl.so \ No newline at end of file diff --git a/tests/tmp/.gitignore b/tests/tmp/.gitignore new file mode 100755 index 0000000..816b594 --- /dev/null +++ b/tests/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* \ No newline at end of file