├── src ├── resources │ ├── assets │ │ └── logo │ │ │ ├── ccnow.png │ │ │ ├── payfast.png │ │ │ ├── paypal.png │ │ │ └── 2checkout.png │ └── config │ │ └── subscription.php ├── Exceptions │ ├── SubscriptionException.php │ └── TransactionException.php ├── Facades │ └── Subscription.php ├── Classes │ ├── ProcessorInfo.php │ ├── TransactionResult.php │ ├── SubscriptionProduct.php │ └── SubscriptionConsumer.php ├── Subscription.php ├── Contracts │ ├── Service.php │ ├── Consumer.php │ └── Product.php ├── SubscriptionServiceProvider.php ├── Mocks │ └── MockService.php ├── SubscriptionFactory.php └── Services │ ├── CCNow.php │ ├── TwoCheckout.php │ ├── Paypal.php │ └── PayFast.php ├── .travis.yml ├── phpunit.xml ├── composer.json ├── LICENSE ├── tests ├── SubscriptionTest.php └── SubscriptionFactoryTest.php └── README.md /src/resources/assets/logo/ccnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navneetrai/laravel-subscription/HEAD/src/resources/assets/logo/ccnow.png -------------------------------------------------------------------------------- /src/resources/assets/logo/payfast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navneetrai/laravel-subscription/HEAD/src/resources/assets/logo/payfast.png -------------------------------------------------------------------------------- /src/resources/assets/logo/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navneetrai/laravel-subscription/HEAD/src/resources/assets/logo/paypal.png -------------------------------------------------------------------------------- /src/resources/assets/logo/2checkout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/navneetrai/laravel-subscription/HEAD/src/resources/assets/logo/2checkout.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | 6 | #branches: 7 | # only: 8 | # - master 9 | 10 | before_script: 11 | - composer self-update 12 | - composer install 13 | 14 | script: 15 | - phpunit 16 | -------------------------------------------------------------------------------- /src/Exceptions/SubscriptionException.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navneetrai/laravel-subscription", 3 | "type": "library", 4 | "description": "Subscription Billing manager for Laravel 5.2", 5 | "keywords": ["laravel", "subscription", "billing", "paypal", "2checkout"], 6 | "homepage":"https://github.com/navneetrai/laravel-subscription", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Navneet Rai", 11 | "email": "navneetrai@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~4.0", 19 | "orchestra/testbench": "~3.2", 20 | "mockery/mockery": "^0.9.4" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Userdesk\\Subscription\\": "src/", 25 | "Userdesk\\Tests\\Subscription\\": "tests/" 26 | } 27 | }, 28 | "minimum-stability": "dev" 29 | } -------------------------------------------------------------------------------- /src/Classes/ProcessorInfo.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->logo = $logo; 17 | $this->url = $url; 18 | } 19 | 20 | /** 21 | * Get Processor name. 22 | * 23 | * @return string 24 | */ 25 | public function getName(){ 26 | return $this->name; 27 | } 28 | 29 | 30 | /** 31 | * Get Processor logo. 32 | * 33 | * @return string 34 | */ 35 | public function getLogo(){ 36 | return $this->logo; 37 | } 38 | 39 | /** 40 | * Get Processor url. 41 | * 42 | * @return string 43 | */ 44 | public function getUrl(){ 45 | return $this->url; 46 | } 47 | } -------------------------------------------------------------------------------- /src/Subscription.php: -------------------------------------------------------------------------------- 1 | subscriptionFactory = $subscriptionFactory; 20 | } 21 | 22 | /** 23 | * @param string $service 24 | * 25 | * @return \Userdesk\Subscription\Contracts\Service 26 | */ 27 | 28 | public function processor($service){ 29 | return $this->subscriptionFactory->createService($service); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Contracts/Service.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'ccnow'=>[ 18 | 'login'=>'', 19 | 'key'=>'', 20 | 'url'=>env('APP_URL', '') 21 | ], 22 | 23 | '2checkout'=>[ 24 | 'sid'=>'', 25 | 'secret'=>'' 26 | ], 27 | 28 | 'paypal'=>[ 29 | 'email'=>'', 30 | 'logo'=>'', 31 | 'auth'=>'' 32 | ], 33 | 34 | 'payfast'=>[ 35 | 'merchant_id'=>'', 36 | 'merchant_key'=>'', 37 | 'passphrase'=>'', 38 | 'auth_token'=>'', 39 | 'sandbox'=>0 40 | ], 41 | ] 42 | 43 | ]; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 navneetrai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Contracts/Consumer.php: -------------------------------------------------------------------------------- 1 | publishes([ 15 | __DIR__.'/resources/assets' => public_path('vendor/laravel-subscription'), 16 | ], 'public'); 17 | 18 | $this->publishes([ 19 | __DIR__.'/resources/config/subscription.php' => config_path('subscription.php'), 20 | ], 'config'); 21 | } 22 | 23 | /** 24 | * Indicates if loading of the provider is deferred. 25 | * 26 | * @var bool 27 | */ 28 | protected $defer = false; 29 | 30 | /** 31 | * Register the service provider. 32 | * 33 | * @return void 34 | */ 35 | public function register(){ 36 | $this->app->bind('subscription', function(){ 37 | return new Subscription; 38 | }); 39 | } 40 | 41 | 42 | /** 43 | * Get the services provided by the provider. 44 | * 45 | * @return array 46 | */ 47 | public function provides(){ 48 | return ['subscription']; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Contracts/Product.php: -------------------------------------------------------------------------------- 1 | 'Userdesk\Subscription\Facades\Subscription' 31 | ); 32 | } 33 | 34 | /** 35 | * @covers Userdesk\Subscription\Subscription::processor 36 | */ 37 | public function testCreateProcessor() 38 | { 39 | Config::set('subscription.services.paypal.email', 'test@best.com'); 40 | 41 | $subscriptionFactory = Mockery::mock('Userdesk\Subscription\SubscriptionFactory[createService]'); 42 | $subscriptionFactory->shouldReceive('createService')->passthru(); 43 | 44 | $subscription = new Subscription($subscriptionFactory); 45 | $processor = $subscription->processor('paypal'); 46 | $this->assertInstanceOf('Userdesk\Subscription\Services\Paypal', $processor); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Classes/TransactionResult.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->ident = $ident; 23 | $this->amount = $amount; 24 | $this->status = $status; 25 | $this->action = $action; 26 | $this->data = $data; 27 | } 28 | 29 | /** 30 | * Get Transaction Result Id. Should be same as id sent to complete function 31 | * 32 | * @return string 33 | */ 34 | public function getId(){ 35 | return $this->id; 36 | } 37 | 38 | /** 39 | * Get Transaction Ident. 40 | * 41 | * @return string 42 | */ 43 | public function getIdent(){ 44 | return $this->ident; 45 | } 46 | 47 | /** 48 | * Get Transaction Amount. 49 | * 50 | * @return float 51 | */ 52 | public function getAmount(){ 53 | return $this->amount; 54 | } 55 | 56 | /** 57 | * Get Transaction Result Status. 58 | * 59 | * @return string 60 | */ 61 | public function getStatus(){ 62 | return $this->status; 63 | } 64 | 65 | /** 66 | * Get Transaction Result Name Action. 67 | * 68 | * @return string payment|refund|cancel|signup|fail|demo 69 | */ 70 | public function getAction(){ 71 | return $this->action; 72 | } 73 | 74 | /** 75 | * Get Transaction Result Data. 76 | * 77 | * @return array 78 | */ 79 | public function getData(){ 80 | return $this->data; 81 | } 82 | } -------------------------------------------------------------------------------- /src/Mocks/MockService.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | /** 19 | * Create Redirect response to complete cart. 20 | * 21 | * @param int $id 22 | * @param \Userdesk\Subscription\Contracts\Product $product 23 | * @param \Userdesk\Subscription\Contracts\Consumer $consumer 24 | * @return \Illuminate\Http\Response|null 25 | */ 26 | public function complete(int $id, SubscriptionProductContract $product, SubscriptionConsumerContract $consumer = null){ 27 | $redir_url = 'https://www.google.com'; 28 | return redirect()->away($redir_url); 29 | } 30 | 31 | /** 32 | * Handle IPN data. 33 | * 34 | * @param array $input 35 | * @return \Userdesk\Subscription\Class\TransactionResult|null 36 | */ 37 | public function ipn(array $input){ 38 | $item_number = str_random(12); 39 | $txn_id = str_random(12); 40 | 41 | $action = 'test'; 42 | $status = 'mock'; 43 | $amount = array_get($input, 'mc_gross', 0); 44 | 45 | return new TransactionResult($item_number, $txn_id, $amount, $status, $action, $input); 46 | } 47 | 48 | /** 49 | * Handle PDT data. 50 | * 51 | * @param array $input 52 | * @return \Userdesk\Subscription\Class\TransactionResult|null 53 | */ 54 | public function pdt(array $input){ 55 | $item_number = str_random(12); 56 | $subscr_id = str_random(12); 57 | 58 | $action = 'test'; 59 | $status = 'mock'; 60 | $amount = array_get($input, 'mc_gross', 0); 61 | 62 | 63 | return new TransactionResult($item_number, $subscr_id, 0, $payment_status, $action, $keys->get()); 64 | 65 | } 66 | 67 | /** 68 | * Return Processor Info. 69 | * 70 | * @return \Userdesk\Subscription\Classes\ProcessorInfo|null 71 | */ 72 | public function info(){ 73 | new ProcessorInfo('Mock', '/vendor/laravel-subscription/logo/mock.png', 'http://www.example.com'); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Classes/SubscriptionProduct.php: -------------------------------------------------------------------------------- 1 | title = $title; 30 | $this->price = $price; 31 | $this->ipnUrl = $ipnUrl; 32 | $this->returnUrl = $returnUrl; 33 | $this->cancelUrl = $cancelUrl; 34 | $this->discount = $discount; 35 | $this->description = $description; 36 | $this->frequency = $frequency; 37 | $this->recurrence = $recurrence; 38 | } 39 | 40 | /** 41 | * Get Product Title. 42 | * 43 | * @return string 44 | */ 45 | public function getTitle(){ 46 | return $this->title; 47 | } 48 | 49 | /** 50 | * Get Product Description. 51 | * 52 | * @return string 53 | */ 54 | public function getDescription(){ 55 | return $this->description; 56 | } 57 | 58 | /** 59 | * Create Product Recurrence Frequency. 60 | * 61 | * @return string 62 | */ 63 | public function getFrequency(){ 64 | return $this->frequency; 65 | } 66 | 67 | /** 68 | * Get Product Recurrence Type. 69 | * 70 | * @return string 'year'|'month'|'day'|'none' 71 | */ 72 | public function getRecurrence(){ 73 | return $this->recurrence; 74 | } 75 | 76 | /** 77 | * Get Product Price. 78 | * 79 | * @return float 80 | */ 81 | public function getPrice(){ 82 | return $this->price; 83 | } 84 | 85 | /** 86 | * Get Product Discount. 87 | * 88 | * @return float 89 | */ 90 | public function getDiscount(){ 91 | return $this->discount; 92 | } 93 | 94 | /** 95 | * Get Product First Price. 96 | * 97 | * @return float 98 | */ 99 | public function getFirstPrice(){ 100 | return ($this->getDiscount() > 0)?(sprintf("%.02d", $this->getPrice() - $this->getDiscount())):$this->getPrice(); 101 | } 102 | 103 | /** 104 | * Get Product IPN Url. 105 | * 106 | * @return string 107 | */ 108 | public function getIpnUrl(){ 109 | return $this->ipnUrl; 110 | } 111 | 112 | /** 113 | * Get Product Return Url. 114 | * 115 | * @return string 116 | */ 117 | public function getReturnUrl(){ 118 | return $this->returnUrl; 119 | } 120 | 121 | /** 122 | * Get Product Cancel Url. 123 | * 124 | * @return string 125 | */ 126 | public function getCancelUrl(){ 127 | return $this->cancelUrl; 128 | } 129 | } -------------------------------------------------------------------------------- /src/Classes/SubscriptionConsumer.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->address = $address; 29 | $this->city = $city; 30 | $this->state = $state; 31 | $this->zip = $zip; 32 | $this->country = $country; 33 | $this->email = $email; 34 | $this->phone = $phone; 35 | } 36 | 37 | /** 38 | * Create Consumer's Name. 39 | * 40 | * @return string 41 | */ 42 | public function getName(){ 43 | return $this->name; 44 | } 45 | 46 | 47 | /** 48 | * Create Consumer's Name. 49 | * 50 | * @return string 51 | */ 52 | public function getFirstName(){ 53 | return $this->splitName()[0]; 54 | } 55 | 56 | 57 | /** 58 | * Create Consumer's Name. 59 | * 60 | * @return string 61 | */ 62 | public function getLastName(){ 63 | return $this->splitName()[1]; 64 | } 65 | 66 | /** 67 | * Create Consumer's Address. 68 | * 69 | * @return string 70 | */ 71 | public function getAddress(){ 72 | return $this->address; 73 | } 74 | 75 | /** 76 | * Create Consumer's City. 77 | * 78 | * @return string 79 | */ 80 | public function getCity(){ 81 | return $this->city; 82 | } 83 | 84 | /** 85 | * Create Consumer's State. 86 | * 87 | * @return string 88 | */ 89 | public function getState(){ 90 | return $this->state; 91 | } 92 | 93 | /** 94 | * Create Consumer's Zip Code. 95 | * 96 | * @return string 97 | */ 98 | public function getZip(){ 99 | return $this->zip; 100 | } 101 | 102 | /** 103 | * Create Consumer's Country Code. 104 | * 105 | * @return string 106 | */ 107 | public function getCountry(){ 108 | return $this->country; 109 | } 110 | 111 | /** 112 | * Create Consumer's Email Id. 113 | * 114 | * @return string 115 | */ 116 | public function getEmail(){ 117 | return $this->email; 118 | } 119 | 120 | /** 121 | * Create Consumer's Phone number. 122 | * 123 | * @return string 124 | */ 125 | public function getPhone(){ 126 | return $this->phone; 127 | } 128 | 129 | private function splitName() { 130 | $name = trim($this->name); 131 | $last_name = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); 132 | $first_name = trim( preg_replace('#'.$last_name.'#', '', $name ) ); 133 | return array($first_name, $last_name); 134 | } 135 | } -------------------------------------------------------------------------------- /src/SubscriptionFactory.php: -------------------------------------------------------------------------------- 1 | registerServiceAlias('TwoCheckout', '2checkout'); 14 | $this->registerServiceAlias('CCNow', 'ccnow'); 15 | } 16 | 17 | /** 18 | * Builds and returns payment services 19 | * 20 | * It will first try to build an payment service 21 | * 22 | * @param string $serviceName Name of service to create 23 | * 24 | * @return \Userdesk\Subscription\Contracts\Service 25 | */ 26 | 27 | public function registerServiceAlias($serviceName, $alias){ 28 | $this->registerService($alias, 'Userdesk\\Subscription\\Services\\'.$serviceName); 29 | } 30 | 31 | /** 32 | * Builds and returns payment services 33 | * 34 | * It will first try to build an payment service 35 | * 36 | * @param string $serviceName Name of service to create 37 | * 38 | * @return \Userdesk\Subscription\Contracts\Service 39 | */ 40 | public function createService($serviceName){ 41 | $config = Config::get(sprintf("subscription.services.%s", $serviceName)); 42 | 43 | if(empty($config)){ 44 | throw new SubscriptionException(sprintf('No Config exists for Service %s.', $serviceName)); 45 | } 46 | 47 | $fullyQualifiedServiceName = $this->getFullyQualifiedServiceName($serviceName); 48 | if (class_exists($fullyQualifiedServiceName)) { 49 | return $this->buildService($fullyQualifiedServiceName, $config); 50 | } 51 | 52 | return null; 53 | } 54 | 55 | /** 56 | * Register a custom service to classname mapping. 57 | * 58 | * @param string $serviceName Name of the service 59 | * @param string $className Class to instantiate 60 | * 61 | * @return SubscriptionFactory 62 | * 63 | * @throws Exception If the class is nonexistent or does not implement a valid ServiceInterface 64 | */ 65 | public function registerService($serviceName, $className) { 66 | if (!class_exists($className)) { 67 | throw new SubscriptionException(sprintf('Service class %s does not exist.', $className)); 68 | } 69 | $reflClass = new \ReflectionClass($className); 70 | 71 | if ($reflClass->implementsInterface('Userdesk\\Subscription\\Contracts\\Service')) { 72 | $this->serviceClassMap[ucfirst($serviceName)] = $className; 73 | return $this; 74 | } 75 | 76 | throw new SubscriptionException(sprintf('Service class %s must implement ServiceInterface.', $className)); 77 | } 78 | 79 | /** 80 | * Gets the fully qualified name of the service 81 | * 82 | * @param string $serviceName The name of the service of which to get the fully qualified name 83 | * 84 | * @return string The fully qualified name of the service 85 | */ 86 | private function getFullyQualifiedServiceName($serviceName) { 87 | $serviceName = ucfirst($serviceName); 88 | if (isset($this->serviceClassMap[$serviceName])) { 89 | return $this->serviceClassMap[$serviceName]; 90 | } 91 | return '\\Userdesk\\Subscription\\Services\\' . $serviceName; 92 | } 93 | 94 | /** 95 | * Builds payment services 96 | * 97 | * @param string $serviceName The fully qualified service name 98 | * 99 | * @return PaymentInterface 100 | */ 101 | private function buildService($serviceName, $config) { 102 | return new $serviceName($config); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Services/CCNow.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | /** 19 | * Create Redirect response to complete cart. 20 | * 21 | * @param int $id 22 | * @param \Userdesk\Subscription\Contracts\Product $product 23 | * @param \Userdesk\Subscription\Contracts\Consumer $consumer 24 | * @return \Illuminate\Http\Response|null 25 | */ 26 | public function complete(int $id, SubscriptionProductContract $product, SubscriptionConsumerContract $consumer = null){ 27 | $login = array_get($this->config, 'login'); 28 | $key = array_get($this->config, 'key'); 29 | $hash = md5(sprintf("%s^x_login^x_fp_arg_list^x_fp_sequence^x_amount^x_currency_code^%s^%s^USD^%s", $login, $id, $product->getPrice(), $key)); 30 | 31 | $frequency = $product->getFrequency(); 32 | $recurrence = $product->getRecurrence(); 33 | 34 | $params = [ 35 | 'x_version'=>'1.0', 36 | 'x_fp_arg_list'=>'x_login^x_fp_arg_list^x_fp_sequence^x_amount^x_currency_code', 37 | 'x_currency_code'=>'USD', 38 | 'x_method'=>'CC', 39 | 'x_shipping_amount'=>'0.00', 40 | 'x_subscription_type'=>'A', 41 | 'x_login'=>array_get($this->config, 'login'), 42 | 'x_fp_sequence'=>$id, 43 | 'x_invoice_num'=>$id, 44 | 'x_ship_to_name'=>$consumer->getName(), 45 | 'x_ship_to_address'=>$consumer->getAddress(), 46 | 'x_ship_to_address2'=>'', 47 | 'x_ship_to_city'=>$consumer->getCity(), 48 | 'x_ship_to_state'=>$consumer->getState(), 49 | 'x_ship_to_zip'=>$consumer->getZip(), 50 | 'x_ship_to_country'=>$consumer->getCountry(), 51 | 'x_ship_to_phone'=>$consumer->getPhone(), 52 | 'x_email'=>$consumer->getEmail(), 53 | 'x_product_sku_1'=>$id, 54 | 'x_product_title_1'=>$product->getTitle(), 55 | 'x_product_quantity_1'=>'1', 56 | 'x_product_unitprice_1'=>$product->getPrice(), 57 | 'x_product_url_1'=>array_get($this->config, 'url'), 58 | 'x_amount'=>$product->getPrice(), 59 | 'x_subscription_freq'=>($recurrence=='month') ? sprintf('%sM', $frequency) : (($recurrence=='year') ? sprintf('%sY', $frequency):''), 60 | 'x_fp_hash'=>$hash 61 | ]; 62 | 63 | $url = sprintf('https://www.ccnow.com/cgi-local/transact.cgi?%s', http_build_query($params)); 64 | return redirect()->away($url); 65 | } 66 | 67 | /** 68 | * Handle IPN data. 69 | * 70 | * @param array $input 71 | * @return \Userdesk\Subscription\Class\TransactionResult|null 72 | */ 73 | public function ipn(array $input){ 74 | $item_number = array_get($input, 'x_invoice_num'); 75 | if(empty($item_number)){ 76 | $item_number = array_get($input, 'x_product_sku_1'); 77 | } 78 | if(!empty($item_number)){ 79 | $txn_id = array_get($input, 'x_orderid'); 80 | $subscr_id = array_get($input, 'x_orderid'); 81 | $amount = array_get($input, 'x_amount_usd', array_get($input, 'x_amount')); 82 | $status = array_get($input, 'x_status'); 83 | 84 | if (preg_match ('/pending|shipped|chargeback\_reversal/', $status)) { 85 | $action = 'payment'; 86 | } elseif (preg_match ('/refunded|chargeback|partial\_refund/', $status)) { 87 | $action = 'refund'; 88 | $amount = -1 * abs($amount); 89 | } elseif (preg_match ('/received/', $status)) { 90 | $action = 'signup'; 91 | } elseif (preg_match ('/canceled|declined|rejected/', $status)) { 92 | $action = 'cancel'; 93 | } 94 | 95 | return new TransactionResult($item_number, $txn_id, $amount, $status, $action, $input); 96 | } 97 | 98 | throw new TransactionException('Cart Not Found for CCNow IPN'); 99 | } 100 | 101 | /** 102 | * Handle PDT data. 103 | * 104 | * @param array $input 105 | * @return \Userdesk\Subscription\Class\TransactionResult|null 106 | */ 107 | public function pdt(array $input){ 108 | $item_number = array_get($input, 'cart_id'); 109 | $subscr_id = array_get($input, 'order_id'); 110 | $payment_amount = array_get($input, 'amount'); 111 | 112 | return new TransactionResult($item_number, $subscr_id, $payment_amount, 'signup', 'signup', $input); 113 | } 114 | 115 | /** 116 | * Return Processor Info. 117 | * 118 | * @return \Userdesk\Subscription\Class\ProcessorInfo|null 119 | */ 120 | public function info(){ 121 | return new ProcessorInfo('CCNow', '/vendor/laravel-subscription/logo/ccnow.png', 'http://www.ccnow.com'); 122 | } 123 | } -------------------------------------------------------------------------------- /src/Services/TwoCheckout.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | /** 19 | * Create Redirect response to complete cart. 20 | * 21 | * @param int $id 22 | * @param \Userdesk\Subscription\Contracts\Product $product 23 | * @param \Userdesk\Subscription\Contracts\Consumer $consumer 24 | * @return \Illuminate\Http\Response|null 25 | */ 26 | public function complete(int $id, SubscriptionProductContract $product, SubscriptionConsumerContract $consumer){ 27 | $params = [ 28 | 'mode'=>'2CO', 29 | 'sid'=>array_get($this->config, 'sid'), 30 | 'li_0_type'=>'product', 31 | 'li_0_name'=>$product->getTitle(), 32 | 'li_0__description'=>$product->getDescription(), 33 | 'li_0_price'=>$product->getPrice(), 34 | 'li_0_recurrence'=>($product->getFrequency()=='month') ? '1 Month' : (($product->getFrequency()=='year')?'1 Year':''), 35 | 'li_0_duration'=>(($product->getFrequency()=='year')||($product->getFrequency()=='month')) ? 'Forever':'', 36 | 'li_0_tangible'=>'N', 37 | 'li_0_quantity'=>'1', 38 | 'merchant_order_id'=>$id, 39 | 'card_holder_name'=>$consumer->getName(), 40 | 'street_address'=>$consumer->getAddress(), 41 | 'street_address2'=>'', 42 | 'city'=>$consumer->getCity(), 43 | 'state'=>$consumer->getState(), 44 | 'zip'=>$consumer->getZip(), 45 | 'country'=>$consumer->getCountry(), 46 | 'email'=>$consumer->getEmail(), 47 | 'phone'=>$consumer->getPhone(), 48 | 'purchase_step'=>'payment-method', 49 | 'x_receipt_link_url'=>$product->getReturnUrl(), 50 | 'ipn_url'=>$product->getIpnUrl(), 51 | 'submit'=>'Checkout' 52 | ]; 53 | 54 | if($product->getDiscount() > 0){ 55 | $params = array_merge($params, [ 56 | 'li_0_startup_fee'=>$product->getDiscount(), 57 | ]); 58 | } 59 | 60 | $url = sprintf('https://www.2checkout.com/checkout/purchase?%s', http_build_query($params)); 61 | return redirect()->away($url); 62 | } 63 | 64 | /** 65 | * Handle IPN data. 66 | * 67 | * @param array $input 68 | * @return \Userdesk\Subscription\Class\TransactionResult|null 69 | */ 70 | public function ipn(array $input){ 71 | $item_number = array_get($input, 'vendor_order_id'); 72 | if(!empty($item_number)){ 73 | $txn_type = array_get($input, 'message_type'); 74 | $txn_id = array_get($input, 'invoice_id', str_random(12)); 75 | $subscr_id = array_get($input, 'sale_id'); 76 | 77 | $amount = array_get($input, 'invoice_usd_amount', array_get($input, 'item_usd_amount_1')); 78 | 79 | if (preg_match ('/ORDER_CREATED|FRAUD_STATUS_CHANGED|INVOICE_STATUS_CHANGED|RECURRING_INSTALLMENT_SUCCESS/', $txn_type)) { 80 | $fraud_status = array_get($input, 'fraud_status'); 81 | 82 | $action = 'payment'; 83 | 84 | if(($fraud_status=='fail')||($fraud_status=='declined')){ 85 | $action = 'fail'; 86 | } 87 | } elseif (preg_match ('/REFUND_ISSUED/', $txn_type)) { 88 | $action = 'refund'; 89 | $amount = -1 * abs($amount); 90 | } elseif (preg_match ('/subscr_cancel|subscr_eot|subscr_failed|RECURRING_STOPPED|RECURRING_COMPLETE|RECURRING_INSTALLMENT_FAILED/', $txn_type)) { 91 | $action = 'cancel'; 92 | } 93 | 94 | return new TransactionResult($item_number, $txn_id, $amount, $txn_type, $action, $input); 95 | } 96 | 97 | throw new TransactionException('Cart Not Found for 2Checkout IPN'); 98 | } 99 | 100 | /** 101 | * Handle PDT data. 102 | * 103 | * @param array $input 104 | * @return \Userdesk\Subscription\Class\TransactionResult|null 105 | */ 106 | public function pdt(array $input){ 107 | $demo_order = array_get($input, 'demo') == 'Y'; 108 | $subscr_id = $demo_order ? 0 : array_get($input, 'order_number'); 109 | $item_number = array_get($input, 'merchant_order_id'); 110 | $payment_status = array_get($input, 'credit_card_processed'); 111 | $payment_amount = array_get($input, 'total'); 112 | $txn_id = array_get($input, 'order_number'); 113 | $payer_email = array_get($input, 'email'); 114 | $status = 'Unknown'; 115 | 116 | $key = strtoupper(md5(sprintf('%s%s%s%s', array_get($this->config, 'secret'), array_get($this->config, 'sid'), $subscr_id, $payment_amount))); 117 | 118 | if ($key == array_get($input, 'key')) { 119 | if ($demo_order){ 120 | $status = 'Demo Order'; 121 | $action = 'demo'; 122 | } else { 123 | if(preg_match('/^Y|K$/', $payment_status)){ 124 | $action = 'signup'; 125 | $status = 'Success'; 126 | }else{ 127 | $status = 'Fail'; 128 | $action = 'fail'; 129 | } 130 | } 131 | } 132 | 133 | return new TransactionResult($item_number, $subscr_id, $payment_amount, $status, $action, $keys->get()); 134 | } 135 | 136 | /** 137 | * Return Processor Info. 138 | * 139 | * @return \Userdesk\Subscription\Class\ProcessorInfo|null 140 | */ 141 | public function info(){ 142 | return new ProcessorInfo('2Checkout', '/vendor/laravel-subscription/logo/2checkout.png', 'https://www.2checkout.com'); 143 | } 144 | } -------------------------------------------------------------------------------- /tests/SubscriptionFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectException('\\Userdesk\Subscription\Exceptions\SubscriptionException'); 19 | $factory = new SubscriptionFactory(); 20 | $service = $factory->createService('paypal'); 21 | } 22 | 23 | /** 24 | * @covers Userdesk\Subscription\SubscriptionFactory::createService 25 | * @covers Userdesk\Subscription\SubscriptionFactory::fullyQualifiedServiceName 26 | * @covers Userdesk\Subscription\SubscriptionFactory::buildService 27 | */ 28 | public function testCreateServiceNonExistentService() { 29 | Config::set('subscription.services.foo.email', 'test@best.com'); 30 | 31 | $factory = new SubscriptionFactory(); 32 | $service = $factory->createService('foo'); 33 | 34 | $this->assertNull($service); 35 | } 36 | 37 | /** 38 | * @covers Userdesk\Subscription\SubscriptionFactory::createService 39 | * @covers Userdesk\Subscription\SubscriptionFactory::fullyQualifiedServiceName 40 | * @covers Userdesk\Subscription\SubscriptionFactory::buildService 41 | */ 42 | public function testCreateServicePreLoaded(){ 43 | Config::set('subscription.services.paypal.email', 'test@best.com'); 44 | 45 | $factory = new SubscriptionFactory(); 46 | $service = $factory->createService('paypal'); 47 | 48 | $this->assertInstanceOf('Userdesk\\Subscription\\Services\\Paypal', $service); 49 | } 50 | 51 | /** 52 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 53 | */ 54 | public function testRegisterServiceThrowsExceptionIfNonExistentClass(){ 55 | $this->expectException('\\Userdesk\Subscription\Exceptions\SubscriptionException'); 56 | $factory = new SubscriptionFactory(); 57 | $factory->registerService('foo', 'bar'); 58 | } 59 | 60 | /** 61 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 62 | */ 63 | public function testRegisterServiceThrowsExceptionIfClassNotFulfillsContract(){ 64 | $this->expectException('\\Userdesk\Subscription\Exceptions\SubscriptionException'); 65 | $factory = new SubscriptionFactory(); 66 | $factory->registerService('foo', 'Userdesk\\Subscription\\SubscriptionFactory'); 67 | } 68 | 69 | /** 70 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 71 | */ 72 | public function testRegisterServiceSuccessIfClassFulfillsContract(){ 73 | $factory = new SubscriptionFactory(); 74 | $this->assertInstanceOf( 75 | 'Userdesk\\Subscription\\SubscriptionFactory', 76 | $factory->registerService('foo', 'Userdesk\\Subscription\\Mocks\\MockService') 77 | ); 78 | } 79 | 80 | /** 81 | * @covers Userdesk\Subscription\SubscriptionFactory::registerServiceAlias 82 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 83 | */ 84 | public function testRegisterServiceAlias(){ 85 | Config::set('subscription.services.paypal.email', 'test@best.com'); 86 | Config::set('subscription.services.alias.email', 'test@best.com'); 87 | 88 | $factory = new SubscriptionFactory(); 89 | $service = $factory->createService('alias'); 90 | 91 | $this->assertNull($service); 92 | 93 | $factory->registerServiceAlias('paypal', 'alias'); 94 | 95 | $newService = $factory->createService('alias'); 96 | 97 | $this->assertInstanceOf('Userdesk\\Subscription\\Services\\Paypal', $newService); 98 | } 99 | 100 | /** 101 | * @covers Userdesk\Subscription\SubscriptionFactory::createService 102 | * @covers Userdesk\Subscription\SubscriptionFactory::fullyQualifiedServiceName 103 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 104 | * @covers Userdesk\Subscription\SubscriptionFactory::buildService 105 | */ 106 | public function testCreateServiceUserRegistered(){ 107 | Config::set('subscription.services.foo.email', 'test@best.com'); 108 | 109 | $factory = new SubscriptionFactory(); 110 | $service = $factory->createService('foo'); 111 | 112 | $this->assertNull($service); 113 | 114 | $factory->registerService('foo', 'Userdesk\Subscription\Mocks\MockService'); 115 | 116 | $newService = $factory->createService('foo'); 117 | 118 | $this->assertInstanceOf('Userdesk\\Subscription\\Contracts\\Service', $newService); 119 | } 120 | 121 | /** 122 | * @covers Userdesk\Subscription\SubscriptionFactory::createService 123 | * @covers Userdesk\Subscription\SubscriptionFactory::fullyQualifiedServiceName 124 | * @covers Userdesk\Subscription\SubscriptionFactory::registerService 125 | * @covers Userdesk\Subscription\SubscriptionFactory::buildService 126 | */ 127 | public function testCreateServiceUserRegisteredOverridesPreLoaded(){ 128 | Config::set('subscription.services.paypal.email', 'test@best.com'); 129 | 130 | $factory = new SubscriptionFactory(); 131 | $factory->registerService('paypal', 'Userdesk\Subscription\Mocks\MockService'); 132 | 133 | $service = $factory->createService('paypal'); 134 | 135 | $this->assertInstanceOf('Userdesk\\Subscription\\Mocks\\MockService', $service); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Services/Paypal.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | /** 19 | * Create Redirect response to complete cart. 20 | * 21 | * @param int $id 22 | * @param \Userdesk\Subscription\Contracts\Product $product 23 | * @param \Userdesk\Subscription\Contracts\Consumer $consumer 24 | * @return \Illuminate\Http\Response|null 25 | */ 26 | public function complete(int $id, SubscriptionProductContract $product, SubscriptionConsumerContract $consumer = null){ 27 | $params = [ 28 | 'business'=>array_get($this->config, 'email'), 29 | 'currency_code'=>'USD', 30 | 'no_shipping'=>1, 31 | 'no_note'=>1, 32 | 'item_name'=>$product->getTitle(), 33 | 'item_number'=>$id, 34 | 'cpp_header_image'=>array_get($this->config, 'logo'), 35 | 'cpp_header_color'=>'FFFFFF', 36 | 'notify_url'=>$product->getIpnUrl(), 37 | 'return'=>$product->getReturnUrl(), 38 | 'cancel_return'=>$product->getCancelUrl(), 39 | 'submit'=>'Pay Using PayPal' 40 | ]; 41 | 42 | $recur = $this->getRecurrenceString($product->getRecurrence()); 43 | 44 | if($product->getRecurrence() != 'none'){ 45 | $params = array_merge($params, [ 46 | 'cmd'=>'_xclick-subscriptions', 47 | 'a3'=>$product->getPrice(), 48 | 'p3'=>$product->getFrequency(), 49 | 't3'=>$recur, 50 | 'src'=>1 51 | ]); 52 | 53 | if($product->getDiscount() > 0){ 54 | $params = array_merge($params, [ 55 | 'a1'=>$product->getFirstPrice(), 56 | 'p1'=>$product->getFrequency(), 57 | 't1'=>$recur 58 | ]); 59 | } 60 | }else{ 61 | $params = array_merge($params, [ 62 | 'cmd'=>'_xclick', 63 | 'amount'=>$product->getPrice() 64 | ]); 65 | } 66 | 67 | $redir_url = sprintf('https://www.paypal.com/cgi-bin/webscr?%s', http_build_query($params)); 68 | return redirect()->away($redir_url); 69 | } 70 | 71 | /** 72 | * Handle IPN data. 73 | * 74 | * @param array $input 75 | * @return \Userdesk\Subscription\Class\TransactionResult|null 76 | */ 77 | public function ipn(array $input){ 78 | $item_number = array_get($input, 'item_number'); 79 | if(!empty($item_number)){ 80 | $txn_id = array_get($input, 'txn_id', str_random(12)); 81 | $subscr_id = array_get($input, 'subscr_id', $txn_id); 82 | $txn_type = array_get($input, 'txn_type', 'subscr_payment'); 83 | 84 | $amount = array_get($input, 'mc_gross', 0); 85 | 86 | if (preg_match ('/subscr_payment|web_accept/', $txn_type)) { 87 | $status = array_get($input, 'payment_status', 'Unknown'); 88 | if (preg_match('/Completed|Processed|Canceled\_Reversal/', $status)) { 89 | $action = 'payment'; 90 | } elseif (preg_match('/Refunded|Reversed/', $status)) { 91 | $action = 'refund'; 92 | $amount = -1 * abs($amount); 93 | } elseif (preg_match('/Created|Pending/', $status)) { 94 | $action = 'signup'; 95 | } 96 | } else{ 97 | $status = $txn_type; 98 | if (preg_match ('/expire|eot|cancel|fail/', $txn_type)) { 99 | $action = 'cancel'; 100 | } elseif (preg_match ('/signup/', $txn_type)) { 101 | $action = 'signup'; 102 | } 103 | } 104 | 105 | return new TransactionResult($item_number, $txn_id, $amount, $status, $action, $input); 106 | } 107 | 108 | throw new TransactionException('Item Number Not Found for Paypal IPN'); 109 | } 110 | 111 | /** 112 | * Handle PDT data. 113 | * 114 | * @param array $input 115 | * @return \Userdesk\Subscription\Class\TransactionResult|null 116 | */ 117 | public function pdt(array $input){ 118 | $req = 'cmd=_notify-synch'; 119 | $tx_token = array_get($input, 'tx'); 120 | $auth_token = array_get($this->config, 'auth'); 121 | 122 | if(empty($auth_token)){ 123 | throw new TransactionException('Invalid Paypal Auth Token'); 124 | } 125 | 126 | $req .= "&tx=$tx_token&at=$auth_token"; 127 | 128 | $header = "POST /cgi-bin/webscr HTTP/1.0\r\n"; 129 | $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; 130 | $header .= "Content-Length: ".strlen ($req)."\r\n\r\n"; 131 | $fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30); 132 | 133 | if (!$fp) { 134 | throw new TransactionException('Cannot Connect to Paypal'); 135 | } else { 136 | fputs ($fp, $header.$req); 137 | $res = ''; 138 | $headerdone = false; 139 | 140 | while (!feof ($fp)) { 141 | $line = fgets ($fp, 1024); 142 | 143 | if (strcmp ($line, "\r\n") == 0) { 144 | $headerdone = true; 145 | } elseif ($headerdone) { 146 | $res .= $line; 147 | } 148 | } 149 | 150 | $lines = explode ("\n", $res); 151 | 152 | fclose ($fp); 153 | 154 | $keys = collect([]); 155 | 156 | if (strcmp ($lines[0], "SUCCESS") == 0) { 157 | for ($i = 1; $i < count ($lines); $i++) { 158 | @list ($key, $val) = explode ("=", $lines[$i]); 159 | $keys->put(urldecode($key), urldecode($val)); 160 | } 161 | 162 | $item_number = $keys->get('item_number'); 163 | $payment_status = $keys->get('payment_status'); 164 | 165 | $subscr_id = $keys->get('subscr_id', $keys->get('txn_id')); 166 | $item_name = $keys->get('item_name'); 167 | $payment_amount = $keys->get('mc_gross'); 168 | $txn_id = $keys->get('txn_id'); 169 | $payer_email = $keys->get('payer_email'); 170 | 171 | $action = ''; 172 | 173 | if(preg_match ('/^Completed|Pending$/', $payment_status)){ 174 | $action = 'signup'; 175 | } 176 | 177 | return new TransactionResult($item_number, $subscr_id, 0, $payment_status, $action, $keys->all()); 178 | } elseif (strcmp ($lines[0], "FAIL") == 0) { 179 | throw new TransactionException("Paypal check failed"); 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * Return Processor Info. 186 | * 187 | * @return \Userdesk\Subscription\Class\ProcessorInfo|null 188 | */ 189 | public function info(){ 190 | return new ProcessorInfo('Paypal', '/vendor/laravel-subscription/logo/paypal.png', 'https://www.paypal.com'); 191 | } 192 | 193 | private function getRecurrenceString($recur){ 194 | return ($recur=='day')?'D':(($recur=='year')?'Y':'M'); 195 | } 196 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Subscription Billing for Laravel 5 2 | 3 | [![Build Status](https://travis-ci.org/navneetrai/laravel-subscription.svg)](https://travis-ci.org/navneetrai/laravel-subscription) 4 | [![Coverage Status](https://coveralls.io/repos/navneetrai/laravel-subscription/badge.svg)](https://coveralls.io/r/navneetrai/laravel-subscription) 5 | [![Total Downloads](https://poser.pugx.org/navneetrai/laravel-subscription/downloads.svg)](https://packagist.org/packages/navneetrai/laravel-subscription) 6 | [![Latest Stable Version](https://poser.pugx.org/navneetrai/laravel-subscription/v/stable.svg)](https://packagist.org/packages/navneetrai/laravel-subscription) 7 | [![Latest Unstable Version](https://poser.pugx.org/navneetrai/laravel-subscription/v/unstable.svg)](https://packagist.org/packages/navneetrai/laravel-subscription) 8 | [![License](https://poser.pugx.org/navneetrai/laravel-subscription/license.svg)](https://packagist.org/packages/navneetrai/laravel-subscription) 9 | 10 | laravel-subscription is a simple laravel 5 library for creating subscription billing and handling server notifications. It is primarily meant for people outside countries like US, UK and Canada where [Stripe](https://stripe.com/), [Paypal Payments Pro](https://www.paypal.com/webapps/mpp/paypal-payments-pro) are not available. 11 | 12 | If you want to handle non-recurring payments, you can use [Omnipay](http://omnipay.thephpleague.com/) for one-time payments and token billing. 13 | 14 | --- 15 | 16 | - [Supported services](#supported-services) 17 | - [Installation](#installation) 18 | - [Registering the Package](#registering-the-package) 19 | - [Configuration](#configuration) 20 | - [Usage](#usage) 21 | 22 | ## Supported services 23 | 24 | The library supports [Paypal](https://www.paypal.com) and credit card processors [2Checkout](https://www.2checkout.com/), [PayFast](https://www.payfast.co.za) and [CCNow](http://www.ccnow.com/). More services will be implemented soon. 25 | 26 | Included service implementations: 27 | 28 | - Paypal 29 | - 2Checkout 30 | - PayFast 31 | - CCNow 32 | - more to come! 33 | 34 | 35 | ## Installation 36 | 37 | Add laravel-subscription to your composer.json file: 38 | 39 | ``` 40 | "require": { 41 | "navneetrai/laravel-subscription": "^1.0" 42 | } 43 | ``` 44 | 45 | Use composer to install this package. 46 | 47 | ``` 48 | $ composer update 49 | ``` 50 | 51 | ### Registering the Package 52 | 53 | Register the service provider within the ```providers``` array found in ```config/app.php```: 54 | 55 | ```php 56 | 'providers' => [ 57 | // ... 58 | 59 | Userdesk\Subscription\SubscriptionServiceProvider::class, 60 | ] 61 | ``` 62 | 63 | Add an alias within the ```aliases``` array found in ```config/app.php```: 64 | 65 | 66 | ```php 67 | 'aliases' => [ 68 | // ... 69 | 70 | 'Subscription' => Userdesk\Subscription\Facades\Subscription::class, 71 | ] 72 | ``` 73 | 74 | ## Configuration 75 | 76 | There are two ways to configure laravel-subscription. 77 | 78 | #### Option 1 79 | 80 | Create configuration file for package using artisan command 81 | 82 | ``` 83 | $ php artisan vendor:publish --provider="Userdesk\Subscription\SubscriptionServiceProvider" 84 | ``` 85 | 86 | #### Option 2 87 | 88 | Create configuration file manually in config directory ``config/subscription.php`` and put there code from below. 89 | 90 | ```php 91 | [ 112 | 'paypal'=>[ 113 | 'email'=>'', 114 | 'logo'=>'', 115 | 'auth'=>'' 116 | ], 117 | ] 118 | 119 | ]; 120 | ``` 121 | 122 | ### Credentials 123 | 124 | Add your credentials to ``config/subscription.php`` (depending on which option of configuration you choose) 125 | 126 | 127 | ## Usage 128 | 129 | ### Basic usage 130 | 131 | Just follow the steps below and you will be able to get a processor: 132 | 133 | ```php 134 | $paypal = Subscription::processor('Paypal'); 135 | ``` 136 | 137 | #### Getting Processor Informationation 138 | 139 | You can get basic Information for any processor by: 140 | 141 | ```php 142 | $processor = Subscription::processor($proc); 143 | 144 | $info = $processor->info(); 145 | ``` 146 | 147 | The value returned is a ``ProcessorInfo`` object. You can call ``getName``, ``getLogo`` and ``getUrl`` methods on this processor to display Processor Name, Logo and Website Url for display purposes. 148 | 149 | For ``getLogo`` method to work correctly you'll need to copy package assets to your project using 150 | 151 | ``` 152 | $ php artisan vendor:publish --provider="Userdesk\Subscription\SubscriptionServiceProvider" 153 | ``` 154 | 155 | #### Completing Subscription 156 | 157 | Once you have the processor object you can call: 158 | 159 | ```php 160 | $processor = Subscription::processor($proc); 161 | 162 | $processor->complete($id, $product, $consumer); 163 | ``` 164 | 165 | Complete method redirects to source processor so that your user can complete his payment. 166 | 167 | ``$id`` is your unique Order ID. ``$product`` and ``$consumer`` are objects implementing ``SubscriptionProductContract`` and ``SubscriptionConsumerContract`` respectively. 168 | 169 | A basic implementation of ``SubscriptionProductContract`` and ``SubscriptionConsumerContract`` are included with source in form of ``Classes\SubscriptionProduct`` and ``Classes\SubscriptionConsumer`` respectively. 170 | 171 | #### Handling Processor Notifications 172 | 173 | You can handle Processor Notifications and Processor Cart Return Data by forwarding them to ``ipn`` and ``pdt`` functions respectively. 174 | 175 | Both these function excpects only one input with request data as array and returns ``TransactionResult`` object. 176 | 177 | ```php 178 | public function cartComplete(Request $request, $proc){ 179 | $processor = Subscription::processor($proc); 180 | try{ 181 | $result = $processor->pdt($request->all()); 182 | }catch(TransactionException $exception){ 183 | Log::error($exception->getMessage()); 184 | } 185 | 186 | if(!empty($result)){ 187 | $cartId = $result->getId(); 188 | if(!empty($cartId)){ 189 | $action = $result->getAction(); 190 | if($action=='signup'){ 191 | //Handle successful Signup 192 | } 193 | }else{ 194 | Log::error('Cart Not Found For PDT', ['proc'=>$proc, 'data'=>$request->all()]); 195 | } 196 | } 197 | } 198 | ``` 199 | 200 | ```php 201 | public function handleIpn(Request $request, $proc){ 202 | $processor = Subscription::processor($proc); 203 | try{ 204 | $result = $processor->ipn($request->all()); 205 | }catch(Userdesk\Subscription\Exceptions\TransactionException $exception){ 206 | //Handle Exceptions 207 | Log::error($exception->getMessage()); 208 | } 209 | 210 | if(!empty($result)){ 211 | $cartId = $result->getId(); 212 | if(!empty($cartId)){ 213 | $action = $result->getAction(); 214 | 215 | if($action=='signup'){ 216 | //Handle Signup Code 217 | }else if($action=='payment'){ 218 | $transactionId = $result->getIdent(); 219 | $amount = $result->getAmount(); 220 | //Handle successful payments 221 | }else if($action=='refund'){ 222 | $transactionId = $result->getIdent(); 223 | $amount = $result->getAmount(); 224 | //Handle refunds 225 | }else if($action=='cancel'){ 226 | //Handle cancellations; 227 | } 228 | }else{ 229 | Log::error('Cart Not Found For IPN', ['proc'=>$proc, 'data'=>$request->all()]); 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | **It is important to remember that IPN notifications are generally delivered via POST. So, you should add post method and remove csrf check for any route handling ipn notifications.** -------------------------------------------------------------------------------- /src/Services/PayFast.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | /** 19 | * Create Redirect response to complete cart. 20 | * 21 | * @param int $id 22 | * @param \Userdesk\Subscription\Contracts\Product $product 23 | * @param \Userdesk\Subscription\Contracts\Consumer $consumer 24 | * @return \Illuminate\Http\Response|null 25 | */ 26 | public function complete(int $id, SubscriptionProductContract $product, SubscriptionConsumerContract $consumer = null){ 27 | $sandbox = array_get($this->config, 'sandbox', 0); 28 | $params = [ 29 | // Merchant details 30 | 'merchant_id' => array_get($this->config, 'merchant_id'), 31 | 'merchant_key' => array_get($this->config, 'merchant_key'), 32 | 'return_url' => $product->getReturnUrl(), 33 | 'cancel_url' => $product->getCancelUrl(), 34 | 'notify_url' => $product->getIpnUrl(), 35 | // Buyer details 36 | 'name_first' => $consumer->getFirstName(), 37 | 'name_last' => $consumer->getLastName(), 38 | 'email_address'=> $sandbox?'sbtu01@payfast.co.za':$consumer->getEmail(), 39 | //'cell_number'=>$consumer->getPhone(), 40 | // Transaction details 41 | 'm_payment_id' => $id, //Unique payment ID to pass through to notify_url 42 | 'amount' => $product->getFirstPrice(), 43 | 'item_name' => $product->getTitle(), 44 | 'item_description' => $product->getDescription() 45 | ]; 46 | 47 | $frequency = $product->getFrequency(); 48 | $recurrence = $product->getRecurrence(); 49 | 50 | if(!$sandbox && ($recurrence != 'none')){ 51 | $params = array_merge($params, [ 52 | 'subscription_type'=>1, 53 | 'billing_date' => gmdate('Y-m-d', strtotime("+".$frequency." ".$recurrence)), 54 | 'recurring_amount'=>$product->getPrice(), 55 | 'frequency'=>($recurrence=='month') ? (($frequency==1)?3:(($frequency==3)?4:(($frequency==6)?5:0))) : (($recurrence=='year') ? 6:0), 56 | 'cycles'=>0 57 | ]); 58 | } 59 | 60 | $pfOutput = ''; 61 | 62 | // Create GET string 63 | foreach( $params as $key => $val ){ 64 | if(isset($val)){ 65 | $pfOutput .= $key .'='. urlencode( trim( $val ) ) .'&'; 66 | } 67 | } 68 | 69 | 70 | $passPhrase = array_get($this->config, 'passphrase', ''); 71 | 72 | // Remove last ampersand 73 | $getString = substr( $pfOutput, 0, -1 ); 74 | if( !empty( $passPhrase ) ){ 75 | $getString .= '&passphrase='. urlencode( trim( $passPhrase ) ); 76 | } 77 | 78 | $params['signature'] = md5( $getString ); 79 | 80 | $pfHost = $sandbox ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; 81 | 82 | $redir_url = sprintf('https://%s/eng/process?%s', $pfHost, http_build_query($params)); 83 | return redirect()->away($redir_url); 84 | } 85 | 86 | /** 87 | * Handle IPN data. 88 | * 89 | * @param array $input 90 | * @return \Userdesk\Subscription\Class\TransactionResult|null 91 | */ 92 | public function ipn(array $input){ 93 | $sandbox = array_get($this->config, 'sandbox', 0); 94 | 95 | // Variable initialization 96 | $pfError = false; 97 | $passPhrase = array_get($this->config, 'passphrase', ''); 98 | 99 | $pfParamString = ''; 100 | $pfHost = $sandbox ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; 101 | 102 | //// Dump the submitted variables and calculate security signature 103 | if( !$pfError ) { 104 | $keys = collect([]); 105 | // Strip any slashes in data 106 | foreach( $input as $key => $val ){ 107 | $keys->put($key, stripslashes($val)); 108 | if( $key != 'signature' ){ 109 | $pfParamString .= $key .'='. urlencode( $val ) .'&'; 110 | } 111 | } 112 | 113 | // Remove the last '&' from the parameter string 114 | $pfParamString = substr( $pfParamString, 0, -1 ); 115 | $pfTempParamString = $pfParamString; 116 | 117 | if( !empty( $passPhrase ) ) { 118 | $pfTempParamString .= '&passphrase='.urlencode( $passPhrase ); 119 | } 120 | 121 | 122 | $signature = md5( $pfTempParamString ); 123 | 124 | if(empty($input['signature'])||( $input['signature'] != $signature )){ 125 | $pfError = true; 126 | throw new TransactionException("Payfast Security signature mismatch"); 127 | } 128 | } 129 | 130 | //// Verify source IP 131 | /*if( !$pfError ) { 132 | $validHosts = array( 133 | 'www.payfast.co.za', 134 | 'sandbox.payfast.co.za', 135 | 'w1w.payfast.co.za', 136 | 'w2w.payfast.co.za', 137 | ); 138 | 139 | $validIps = array(); 140 | 141 | foreach( $validHosts as $pfHostname ) { 142 | $ips = gethostbynamel( $pfHostname ); 143 | 144 | if( $ips !== false ) 145 | $validIps = array_merge( $validIps, $ips ); 146 | } 147 | 148 | // Remove duplicates 149 | $validIps = array_unique( $validIps ); 150 | 151 | if( !in_array( $_SERVER['REMOTE_ADDR'], $validIps ) ) { 152 | $pfError = true; 153 | throw new TransactionException("Payfast Bad source IP address"); 154 | } 155 | }*/ 156 | 157 | //// Connect to server to validate data received 158 | if( !$pfError ) { 159 | $output = ''; 160 | // Use cURL (If it's available) 161 | if( function_exists( 'curl_init' ) ) { 162 | $output .= "\n\nUsing cURL\n\n"; // DEBUG 163 | 164 | // Create default cURL object 165 | $ch = curl_init(); 166 | 167 | // Base settings 168 | $curlOpts = array( 169 | // Base options 170 | CURLOPT_USERAGENT => 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)', // Set user agent 171 | CURLOPT_RETURNTRANSFER => true, // Return output as string rather than outputting it 172 | CURLOPT_HEADER => false, // Don't include header in output 173 | CURLOPT_SSL_VERIFYHOST => 2, 174 | CURLOPT_SSL_VERIFYPEER => false, 175 | 176 | // Standard settings 177 | CURLOPT_URL => 'https://'. $pfHost . '/eng/query/validate', 178 | CURLOPT_POST => true, 179 | CURLOPT_POSTFIELDS => $pfParamString, 180 | ); 181 | curl_setopt_array( $ch, $curlOpts ); 182 | 183 | // Execute CURL 184 | $res = curl_exec( $ch ); 185 | curl_close( $ch ); 186 | 187 | if( $res === false ) { 188 | $pfError = true; 189 | throw new TransactionException("An error occurred executing cURL"); 190 | } 191 | } 192 | // Use fsockopen 193 | else 194 | { 195 | $output .= "\n\nUsing fsockopen\n\n"; // DEBUG 196 | 197 | // Construct Header 198 | $header = "POST /eng/query/validate HTTP/1.0\r\n"; 199 | $header .= "Host: ". $pfHost ."\r\n"; 200 | $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; 201 | $header .= "Content-Length: " . strlen( $pfParamString ) . "\r\n\r\n"; 202 | 203 | // Connect to server 204 | $socket = fsockopen( 'ssl://'. $pfHost, 443, $errno, $errstr, 10 ); 205 | 206 | // Send command to server 207 | fputs( $socket, $header . $pfParamString ); 208 | 209 | // Read the response from the server 210 | $res = ''; 211 | $headerDone = false; 212 | 213 | while( !feof( $socket ) ) 214 | { 215 | $line = fgets( $socket, 1024 ); 216 | 217 | // Check if we are finished reading the header yet 218 | if( strcmp( $line, "\r\n" ) == 0 ){ 219 | // read the header 220 | $headerDone = true; 221 | } 222 | // If header has been processed 223 | else if( $headerDone ) 224 | { 225 | // Read the main response 226 | $res .= $line; 227 | } 228 | } 229 | } 230 | } 231 | 232 | //// Interpret the response from server 233 | if( !$pfError ) { 234 | $lines = explode( "\n", $res ); 235 | $result = trim( $lines[0] ); 236 | 237 | if( strcmp( $result, 'VALID' ) == 0 ){ 238 | $item_number = $keys->get('m_payment_id'); 239 | if(!empty($item_number)){ 240 | $payment_status = $keys->get('payment_status'); 241 | 242 | $item_name = $keys->get('item_name'); 243 | $payment_amount = $keys->get('amount_gross'); 244 | $txn_id = $keys->get('pf_payment_id'); 245 | $payer_email = $keys->get('email_address'); 246 | 247 | $action = ''; 248 | 249 | if($payment_status == 'COMPLETE'){ 250 | $action = 'payment'; 251 | }else if($payment_status == 'CANCELLED'){ 252 | $action = 'cancel'; 253 | } 254 | 255 | return new TransactionResult($item_number, $txn_id, $payment_amount, $payment_status, $action, $keys->all()); 256 | }else{ 257 | throw new TransactionException('Item Number Not Found for Payfast IPN'); 258 | } 259 | }else{ 260 | // Log for investigation 261 | $pfError = true; 262 | throw new TransactionException("The data received is invalid"); 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Handle PDT data. 269 | * 270 | * @param array $input 271 | * @return \Userdesk\Subscription\Class\TransactionResult|null 272 | */ 273 | public function pdt(array $input){ 274 | $sandbox = array_get($this->config, 'sandbox', 0); 275 | 276 | // Variable Initialization 277 | $pmtToken = isset( $input['pt'] ) ? $input['pt'] : null; 278 | 279 | if( !empty( $pmtToken ) ) { 280 | // Variable Initialization 281 | $error = false; 282 | $authToken = array_get($this->config, 'auth_token', ''); 283 | $req = 'pt='. $pmtToken .'&at='. $authToken; 284 | $host = $sandbox ? 'sandbox.payfast.co.za' : 'www.payfast.co.za'; 285 | 286 | //// Connect to server 287 | if( !$error ) 288 | { 289 | // Construct Header 290 | $header = "POST /eng/query/fetch HTTP/1.0\r\n"; 291 | $header .= 'Host: '. $host ."\r\n"; 292 | $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; 293 | $header .= 'Content-Length: '. strlen( $req ) ."\r\n\r\n"; 294 | 295 | // Connect to server 296 | $socket = fsockopen( 'ssl://'. $host, 443, $errno, $errstr, 10 ); 297 | 298 | if( !$socket ) 299 | { 300 | $error = true; 301 | print( 'errno = '. $errno .', errstr = '. $errstr ); 302 | } 303 | } 304 | 305 | //// Get data from server 306 | if( !$error ) 307 | { 308 | // Send command to server 309 | fputs( $socket, $header . $req ); 310 | 311 | // Read the response from the server 312 | $res = ''; 313 | $headerDone = false; 314 | 315 | while( !feof( $socket ) ) 316 | { 317 | $line = fgets( $socket, 1024 ); 318 | 319 | // Check if we are finished reading the header yet 320 | if( strcmp( $line, "\r\n" ) == 0 ) 321 | { 322 | // read the header 323 | $headerDone = true; 324 | } 325 | // If header has been processed 326 | else if( $headerDone ) 327 | { 328 | // Read the main response 329 | $res .= $line; 330 | } 331 | } 332 | 333 | 334 | // Parse the returned data 335 | $lines = explode( "\n", $res ); 336 | } 337 | 338 | //// Interpret the response from server 339 | if( !$error ) 340 | { 341 | $result = trim( $lines[0] ); 342 | 343 | // If the transaction was successful 344 | if( strcmp( $result, 'SUCCESS' ) == 0 ){ 345 | $keys = collect([]); 346 | // Process the reponse into an associative array of data 347 | for( $i = 1; $i < count( $lines ); $i++ ){ 348 | if(strpos($lines[$i], "=")>0){ 349 | list( $key, $val ) = explode( "=", $lines[$i] ); 350 | $keys->put(urldecode($key), urldecode($val)); 351 | } 352 | } 353 | } 354 | // If the transaction was NOT successful 355 | else if( strcmp( $result, 'FAIL' ) == 0 ) 356 | { 357 | // Log for investigation 358 | $error = true; 359 | // 360 | } 361 | } 362 | 363 | // Close socket if successfully opened 364 | if( $socket ) 365 | fclose( $socket ); 366 | 367 | 368 | if( !$error ){ 369 | $item_number = $keys->get('m_payment_id'); 370 | $payment_status = $keys->get('payment_status'); 371 | 372 | $subscr_id = $keys->get('subscr_id', $keys->get('pf_payment_id')); 373 | $item_name = $keys->get('item_name'); 374 | $payment_amount = $keys->get('amount_gross'); 375 | $txn_id = $keys->get('pf_payment_id'); 376 | $payer_email = $keys->get('email_address'); 377 | 378 | $action = ''; 379 | 380 | if($payment_status == 'COMPLETE'){ 381 | $action = 'signup'; 382 | } 383 | 384 | return new TransactionResult($item_number, $subscr_id, 0, $payment_status, $action, $keys->all()); 385 | } else { 386 | throw new TransactionException("Payfast PDT check failed"); 387 | } 388 | 389 | } 390 | } 391 | 392 | /** 393 | * Return Processor Info. 394 | * 395 | * @return \Userdesk\Subscription\Class\ProcessorInfo|null 396 | */ 397 | public function info(){ 398 | return new ProcessorInfo('PayFast', '/vendor/laravel-subscription/logo/payfast.png', 'https://www.payfast.co.za'); 399 | } 400 | 401 | private function getRecurrenceString($recur){ 402 | return ($recur=='day')?'D':(($recur=='year')?'Y':'M'); 403 | } 404 | } --------------------------------------------------------------------------------