├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── LogicalGrape │ └── PayPalIpnLaravel │ │ ├── Exception │ │ └── InvalidIpnException.php │ │ ├── Facades │ │ └── IPN.php │ │ ├── Models │ │ ├── IpnOrder.php │ │ ├── IpnOrderItem.php │ │ └── IpnOrderItemOption.php │ │ ├── PayPalIpn.php │ │ └── PayPalIpnServiceProvider.php ├── config │ ├── .gitkeep │ └── config.php └── migrations │ ├── .gitkeep │ ├── 2013_11_12_210243_create_ipn_orders_table.php │ ├── 2013_11_12_211256_create_ipn_order_items.php │ └── 2013_11_12_212006_create_ipn_order_item_options.php └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - curl -s http://getcomposer.org/installer | php 10 | - php composer.phar install --dev 11 | 12 | script: phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Logical Grape 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PayPal IPN for Laravel 4 2 | ======================== 3 | 4 | This package allows for the painless creation of a PayPal IPN listener in the Laravel 4 framework. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | PayPal IPN for Laravel can be found on [Packagist](https://packagist.org/packages/logicalgrape/paypal-ipn-laravel). 11 | The recommended way is through [composer](http://getcomposer.org). 12 | 13 | Edit `composer.json` and add: 14 | 15 | ```json 16 | { 17 | "require": { 18 | "logicalgrape/paypal-ipn-laravel": "dev-master" 19 | } 20 | } 21 | ``` 22 | 23 | And install dependencies: 24 | 25 | ```bash 26 | $ curl -sS https://getcomposer.org/installer | php 27 | $ php composer.phar install 28 | ``` 29 | 30 | 31 | Usage 32 | ----- 33 | 34 | Find the `providers` key in `app/config/app.php` and register the **PayPal IPN Service Provider**. 35 | 36 | ```php 37 | 'providers' => array( 38 | // ... 39 | 40 | 'LogicalGrape\PayPalIpnLaravel\PayPalIpnServiceProvider', 41 | ) 42 | ``` 43 | 44 | Find the `aliases` key in `app/config/app.php` and register the **PayPal IPN Facade**. 45 | 46 | ```php 47 | 'aliases' => array( 48 | // ... 49 | 50 | 'IPN' => 'LogicalGrape\PayPalIpnLaravel\Facades\IPN', 51 | ) 52 | ``` 53 | 54 | 55 | Migrations 56 | ---------- 57 | 58 | Run the migrations to create the tables to hold IPN data 59 | 60 | ```bash 61 | $ php artisan migrate --package logicalgrape/paypal-ipn-laravel 62 | ``` 63 | 64 | 65 | Configuration 66 | ------------- 67 | 68 | Publish and edit the configuration file 69 | 70 | ```bash 71 | $ php artisan config:publish logicalgrape/paypal-ipn-laravel 72 | ``` 73 | 74 | 75 | Example 76 | ------- 77 | 78 | Create the controller PayPal will POST to 79 | 80 | ```bash 81 | $ php artisan controller:make IpnController --only=store 82 | ``` 83 | 84 | Open the newly created controller and add the following to the store action 85 | 86 | ```php 87 | $order = IPN::getOrder(); 88 | ``` 89 | 90 | Edit `app/routes.php` and add: 91 | 92 | ```php 93 | Route::post('ipn', array('uses' => 'IpnController@store', 'as' => 'ipn')); 94 | ``` 95 | 96 | 97 | Resources 98 | --------- 99 | To help with IPN testing, PayPal provides the 100 | [PayPal IPN Simulator](https://developer.paypal.com/webapps/developer/applications/ipn_simulator). 101 | 102 | 103 | Support 104 | ------- 105 | 106 | [Please open an issue on GitHub](https://github.com/logicalgrape/paypal-ipn-laravel/issues) 107 | 108 | 109 | License 110 | ------- 111 | 112 | GeocoderLaravel is released under the MIT License. See the bundled 113 | [LICENSE](https://github.com/logicalgrape/paypal-ipn-laravel/blob/master/LICENSE) 114 | file for details. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logicalgrape/paypal-ipn-laravel", 3 | "description": "A PayPal IPN client for Laravel.", 4 | "keywords": ["paypal", "ipn", "laravel"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Chris Roemmich", 9 | "email": "chris@logicalgrape.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0", 14 | "illuminate/support": ">=4.0.0", 15 | "mike182uk/paypal-ipn-listener": "v1.1.0" 16 | }, 17 | "autoload": { 18 | "classmap": [ 19 | "src/migrations" 20 | ], 21 | "psr-0": { 22 | "LogicalGrape\\PayPalIpnLaravel\\": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/LogicalGrape/PayPalIpnLaravel/Exception/InvalidIpnException.php: -------------------------------------------------------------------------------- 1 | hasMany('LogicalGrape\PayPalIpnLaravel\Models\IpnOrderItem'); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/LogicalGrape/PayPalIpnLaravel/Models/IpnOrderItem.php: -------------------------------------------------------------------------------- 1 | belongsTo('LogicalGrape\PayPalIpnLaravel\Models\IpnOrder'); 36 | } 37 | 38 | public function options() { 39 | return $this->hasMany('LogicalGrape\PayPalIpnLaravel\Models\IpnOrderItemOption'); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/LogicalGrape/PayPalIpnLaravel/Models/IpnOrderItemOption.php: -------------------------------------------------------------------------------- 1 | belongsTo('LogicalGrape\PayPalIpnLaravel\Models\IpnOrderItem'); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/LogicalGrape/PayPalIpnLaravel/PayPalIpn.php: -------------------------------------------------------------------------------- 1 | getRequestHandler(); 39 | 40 | $listener = new PayPalListener($request); 41 | $listener->setMode($this->getEnvironment()); 42 | 43 | if ($listener->verifyIpn()) { 44 | return $this->store($request->getData()); 45 | } else { 46 | throw new InvalidIpnException("PayPal as responded with INVALID"); 47 | } 48 | } 49 | 50 | /** 51 | * Get the request handler. 52 | * 53 | * @return Request 54 | */ 55 | public function getRequestHandler() 56 | { 57 | $config = Config::get('paypal-ipn-laravel::request_handler', 'auto'); 58 | if ($config == 'curl' || ($config == 'auto' && is_callable('curl_init'))) { 59 | return new CurlRequest(Input::all()); 60 | } else { 61 | return new SocketRequest(Input::all()); 62 | } 63 | } 64 | 65 | /** 66 | * Get the PayPal environment configuration value. 67 | * 68 | * @return string 69 | */ 70 | public function getEnvironment() 71 | { 72 | return Config::get('paypal-ipn-laravel::environment', 'production'); 73 | } 74 | 75 | /** 76 | * Set the PayPal environment runtime configuration value. 77 | * 78 | * @param string $environment 79 | */ 80 | public function setEnvironment($environment) 81 | { 82 | Config::set('paypal-ipn-laravel::environment', $environment); 83 | } 84 | 85 | /** 86 | * Stores the IPN contents and returns the IpnOrder object. 87 | * 88 | * @param array $data 89 | * @return IpnOrder 90 | */ 91 | private function store($data) 92 | { 93 | if (array_key_exists('txn_id', $data)) { 94 | $order = IpnOrder::firstOrNew(array('txn_id' => $data['txn_id'])); 95 | $order->fill($data); 96 | } else { 97 | $order = new IpnOrder($data); 98 | } 99 | $order->full_ipn = json_encode(Input::all()); 100 | $order->save(); 101 | 102 | $this->storeOrderItems($order, $data); 103 | 104 | return $order; 105 | } 106 | 107 | /** 108 | * Stores the order items from the IPN contents. 109 | * 110 | * @param IpnOrder $order 111 | * @param array $data 112 | */ 113 | private function storeOrderItems($order, $data) 114 | { 115 | $cart = isset($data['num_cart_items']); 116 | $numItems = (isset($data['num_cart_items'])) ? $data['num_cart_items'] : 1; 117 | 118 | for ($i = 0; $i < $numItems; $i++) { 119 | 120 | $suffix = ($numItems > 1 || $cart) ? ($i + 1) : ''; 121 | $suffixUnderscore = ($numItems > 1 || $cart) ? '_' . $suffix : $suffix; 122 | 123 | $item = new IpnOrderItem(); 124 | if (isset($data['item_name' . $suffix])) 125 | $item->item_name = $data['item_name' . $suffix]; 126 | if (isset($data['item_number' . $suffix])) 127 | $item->item_number = $data['item_number' . $suffix]; 128 | if (isset($data['quantity' . $suffix])) 129 | $item->quantity = $data['quantity' . $suffix]; 130 | if (isset($data['mc_gross' . $suffixUnderscore])) 131 | $item->mc_gross = $data['mc_gross' . $suffixUnderscore]; 132 | if (isset($data['mc_handling' . $suffix])) 133 | $item->mc_handling = $data['mc_handling' . $suffix]; 134 | if (isset($data['mc_shipping' . $suffix])) 135 | $item->mc_shipping = $data['mc_shipping' . $suffix]; 136 | if (isset($data['tax' . $suffix])) 137 | $item->tax = $data['tax' . $suffix]; 138 | 139 | $order->items()->save($item); 140 | 141 | // Set the order item options if any 142 | // $count = 7 because PayPal allows you to set a maximum of 7 options per item 143 | // Reference: https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_Appx_websitestandard_htmlvariables 144 | for ($ii = 1, $count = 7; $ii < $count; $ii++) { 145 | if (isset($data['option_name' . $ii . '_' . $suffix])) { 146 | $option = new IpnOrderItemOption(); 147 | $option->option_name = $data['option_name' . $ii . '_' . $suffix]; 148 | if (isset($data['option_selection' . $ii . '_' . $suffix])) { 149 | $option->option_selection = $data['option_selection' . $ii . '_' . $suffix]; 150 | } 151 | $item->options()->save($option); 152 | } 153 | } 154 | } 155 | } 156 | 157 | } -------------------------------------------------------------------------------- /src/LogicalGrape/PayPalIpnLaravel/PayPalIpnServiceProvider.php: -------------------------------------------------------------------------------- 1 | package('logicalgrape/paypal-ipn-laravel'); 23 | } 24 | 25 | /** 26 | * Register the service provider. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->app['paypalipn'] = $this->app->share(function ($app) { 33 | return new PayPalIpn(); 34 | }); 35 | } 36 | 37 | /** 38 | * Get the services provided by the provider. 39 | * 40 | * @return array 41 | */ 42 | public function provides() 43 | { 44 | return array('paypalipn'); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicalgrape/paypal-ipn-laravel/cd94efb600c0a072e68c65ff95203ead9c7df017/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | 'production', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Request Handler 20 | |-------------------------------------------------------------------------- 21 | | 22 | | To verify an IPN message, a POST request must be made to the PalPal 23 | | server. The default, 'auto' will usually work best, as it uses cURL when 24 | | available and falls back to fsock. 25 | | 26 | | Supported: 'auto', 'curl', 'fsock' 27 | | 28 | */ 29 | 'request_handler' => 'auto', 30 | 31 | ); -------------------------------------------------------------------------------- /src/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicalgrape/paypal-ipn-laravel/cd94efb600c0a072e68c65ff95203ead9c7df017/src/migrations/.gitkeep -------------------------------------------------------------------------------- /src/migrations/2013_11_12_210243_create_ipn_orders_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('notify_version', 64)->nullable(); 19 | $table->string('verify_sign', 127)->nullable(); 20 | $table->integer('test_ipn')->nullable(); 21 | $table->string('protection_eligibility', 24)->nullable(); 22 | $table->string('charset', 127)->nullable(); 23 | $table->string('btn_id', 40)->nullable(); 24 | $table->string('address_city', 40)->nullable(); 25 | $table->string('address_country', 64)->nullable(); 26 | $table->string('address_country_code', 2)->nullable(); 27 | $table->string('address_name', 128)->nullable(); 28 | $table->string('address_state', 40)->nullable(); 29 | $table->string('address_status', 20)->nullable(); 30 | $table->string('address_street', 200)->nullable(); 31 | $table->string('address_zip', 20)->nullable(); 32 | $table->string('first_name', 64)->nullable(); 33 | $table->string('last_name', 64)->nullable(); 34 | $table->string('payer_business_name', 127)->nullable(); 35 | $table->string('payer_email', 127)->nullable(); 36 | $table->string('payer_id', 13)->nullable(); 37 | $table->string('payer_status', 20)->nullable(); 38 | $table->string('contact_phone', 20)->nullable(); 39 | $table->string('residence_country', 2)->nullable(); 40 | $table->string('business', 127)->nullable(); 41 | $table->string('receiver_email', 127)->nullable(); 42 | $table->string('receiver_id', 64)->nullable(); 43 | $table->string('custom', 255)->nullable(); 44 | $table->string('invoice', 127)->nullable(); 45 | $table->string('memo', 255)->nullable(); 46 | $table->decimal('tax', 9, 2)->nullable(); 47 | $table->string('auth_id', 19)->nullable(); 48 | $table->string('auth_exp', 28)->nullable(); 49 | $table->decimal('auth_amount', 9, 2)->nullable(); 50 | $table->string('auth_status', 20)->nullable(); 51 | $table->integer('num_cart_items')->nullable(); 52 | $table->string('parent_txn_id', 19)->nullable(); 53 | $table->string('payment_date', 28)->nullable(); 54 | $table->string('payment_status', 20)->nullable(); 55 | $table->string('payment_type', 10)->nullable(); 56 | $table->string('pending_reason', 20)->nullable(); 57 | $table->string('reason_code', 20)->nullable(); 58 | $table->string('remaining_settle', 9, 2)->nullable(); 59 | $table->string('shipping_method', 64)->nullable(); 60 | $table->string('shipping', 9, 2)->nullable(); 61 | $table->string('transaction_entity', 20)->nullable(); 62 | $table->string('txn_id', 19)->nullable(); 63 | $table->string('txn_type', 20)->nullable(); 64 | $table->string('exchange_rate', 9, 2)->nullable(); 65 | $table->string('mc_currency', 3)->nullable(); 66 | $table->string('mc_fee', 9, 2)->nullable(); 67 | $table->string('mc_gross', 9, 2)->nullable(); 68 | $table->string('mc_handling', 9, 2)->nullable(); 69 | $table->string('mc_shipping', 9, 2)->nullable(); 70 | $table->string('payment_fee', 9, 2)->nullable(); 71 | $table->string('payment_gross', 9, 2)->nullable(); 72 | $table->string('settle_amount', 9, 2)->nullable(); 73 | $table->string('settle_currency', 3)->nullable(); 74 | $table->string('auction_buyer_id', 64)->nullable(); 75 | $table->string('auction_closing_date', 28)->nullable(); 76 | $table->integer('auction_multi_item')->nullable(); 77 | $table->string('for_auction', 10)->nullable(); 78 | $table->string('subscr_date', 28)->nullable(); 79 | $table->string('subscr_effective', 28)->nullable(); 80 | $table->string('period1', 10)->nullable(); 81 | $table->string('period2', 10)->nullable(); 82 | $table->string('period3', 10)->nullable(); 83 | $table->decimal('amount1', 9, 2)->nullable(); 84 | $table->decimal('amount2', 9, 2)->nullable(); 85 | $table->decimal('amount3', 9, 2)->nullable(); 86 | $table->decimal('mc_amount1', 9, 2)->nullable(); 87 | $table->decimal('mc_amount2', 9, 2)->nullable(); 88 | $table->decimal('mc_amount3', 9, 2)->nullable(); 89 | $table->string('reattempt', 1)->nullable(); 90 | $table->string('retry_at', 28)->nullable(); 91 | $table->integer('recur_times')->nullable(); 92 | $table->string('username', 64)->nullable(); 93 | $table->string('password', 24)->nullable(); 94 | $table->string('subscr_id', 19)->nullable(); 95 | $table->string('case_id', 28)->nullable(); 96 | $table->string('case_type', 28)->nullable(); 97 | $table->string('case_creation_date', 28)->nullable(); 98 | $table->string('order_status')->nullable(); 99 | $table->decimal('discount', 9, 2)->nullable(); 100 | $table->decimal('shipping_discount', 9, 2)->nullable(); 101 | $table->string('ipn_track_id', 127)->nullable(); 102 | $table->string('transaction_subject', 255)->nullable(); 103 | $table->text('full_ipn'); 104 | $table->timestamps(); 105 | $table->softDeletes(); 106 | }); 107 | } 108 | 109 | /** 110 | * Reverse the migrations. 111 | * 112 | * @return void 113 | */ 114 | public function down() 115 | { 116 | Schema::drop('ipn_orders'); 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/migrations/2013_11_12_211256_create_ipn_order_items.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->integer('ipn_order_id'); 18 | $table->string('item_name', 127)->nullable(); 19 | $table->string('item_number', 127)->nullable(); 20 | $table->string('quantity', 127)->nullable(); 21 | $table->decimal('mc_gross', 9, 2)->nullable(); 22 | $table->decimal('mc_handling', 9, 2)->nullable(); 23 | $table->decimal('mc_shipping', 9, 2)->nullable(); 24 | $table->decimal('tax', 9, 2)->nullable(); 25 | $table->timestamps(); 26 | $table->softDeletes(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::drop('ipn_order_items'); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/migrations/2013_11_12_212006_create_ipn_order_item_options.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->integer('ipn_order_item_id'); 18 | $table->string('option_name', 64)->nullable(); 19 | $table->string('option_selection', 200)->nullable(); 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::drop('ipn_order_item_options'); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicalgrape/paypal-ipn-laravel/cd94efb600c0a072e68c65ff95203ead9c7df017/tests/.gitkeep --------------------------------------------------------------------------------