stateless-systems-paypal 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,271 @@
1
+ require 'net/http'
2
+
3
+ module Paypal
4
+ # Parser and handler for incoming Instant payment notifications from paypal.
5
+ # The Example shows a typical handler in a rails application. Note that this
6
+ # is an example, please read the Paypal API documentation for all the details
7
+ # on creating a safe payment controller.
8
+ #
9
+ # Example
10
+ #
11
+ # class BackendController < ApplicationController
12
+ #
13
+ # def paypal_ipn
14
+ # notify = Paypal::Notification.new(request.raw_post)
15
+ #
16
+ # order = Order.find(notify.item_id)
17
+ #
18
+ # if notify.acknowledge
19
+ # begin
20
+ #
21
+ # if notify.complete? and order.total == notify.amount and notify.business == 'sales@myshop.com'
22
+ # order.status = 'success'
23
+ #
24
+ # shop.ship(order)
25
+ # else
26
+ # logger.error("Failed to verify Paypal's notification, please investigate")
27
+ # end
28
+ #
29
+ # rescue => e
30
+ # order.status = 'failed'
31
+ # raise
32
+ # ensure
33
+ # order.save
34
+ # end
35
+ # end
36
+ #
37
+ # render :nothing
38
+ # end
39
+ # end
40
+ class Notification
41
+ attr_accessor :params
42
+ attr_accessor :raw
43
+
44
+ # Overwrite this url. It points to the Paypal sandbox by default.
45
+ # Please note that the Paypal technical overview (doc directory)
46
+ # speaks of a https:// address for production use. In my tests
47
+ # this https address does not in fact work.
48
+ #
49
+ # Example:
50
+ # Paypal::Notification.ipn_url = http://www.paypal.com/cgi-bin/webscr
51
+ #
52
+ cattr_accessor :ipn_url
53
+ @@ipn_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr'
54
+
55
+
56
+ # Overwrite this certificate. It contains the Paypal sandbox certificate by default.
57
+ #
58
+ # Example:
59
+ # Paypal::Notification.paypal_cert = File::read("paypal_cert.pem")
60
+ cattr_accessor :paypal_cert
61
+ @@paypal_cert = """
62
+ -----BEGIN CERTIFICATE-----
63
+ MIIDoTCCAwqgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBmDELMAkGA1UEBhMCVVMx
64
+ EzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCFNhbiBKb3NlMRUwEwYDVQQK
65
+ EwxQYXlQYWwsIEluYy4xFjAUBgNVBAsUDXNhbmRib3hfY2VydHMxFDASBgNVBAMU
66
+ C3NhbmRib3hfYXBpMRwwGgYJKoZIhvcNAQkBFg1yZUBwYXlwYWwuY29tMB4XDTA0
67
+ MDQxOTA3MDI1NFoXDTM1MDQxOTA3MDI1NFowgZgxCzAJBgNVBAYTAlVTMRMwEQYD
68
+ VQQIEwpDYWxpZm9ybmlhMREwDwYDVQQHEwhTYW4gSm9zZTEVMBMGA1UEChMMUGF5
69
+ UGFsLCBJbmMuMRYwFAYDVQQLFA1zYW5kYm94X2NlcnRzMRQwEgYDVQQDFAtzYW5k
70
+ Ym94X2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNvbTCBnzANBgkqhkiG
71
+ 9w0BAQEFAAOBjQAwgYkCgYEAt5bjv/0N0qN3TiBL+1+L/EjpO1jeqPaJC1fDi+cC
72
+ 6t6tTbQ55Od4poT8xjSzNH5S48iHdZh0C7EqfE1MPCc2coJqCSpDqxmOrO+9QXsj
73
+ HWAnx6sb6foHHpsPm7WgQyUmDsNwTWT3OGR398ERmBzzcoL5owf3zBSpRP0NlTWo
74
+ nPMCAwEAAaOB+DCB9TAdBgNVHQ4EFgQUgy4i2asqiC1rp5Ms81Dx8nfVqdIwgcUG
75
+ A1UdIwSBvTCBuoAUgy4i2asqiC1rp5Ms81Dx8nfVqdKhgZ6kgZswgZgxCzAJBgNV
76
+ BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQHEwhTYW4gSm9zZTEV
77
+ MBMGA1UEChMMUGF5UGFsLCBJbmMuMRYwFAYDVQQLFA1zYW5kYm94X2NlcnRzMRQw
78
+ EgYDVQQDFAtzYW5kYm94X2FwaTEcMBoGCSqGSIb3DQEJARYNcmVAcGF5cGFsLmNv
79
+ bYIBADAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAFc288DYGX+GX2+W
80
+ P/dwdXwficf+rlG+0V9GBPJZYKZJQ069W/ZRkUuWFQ+Opd2yhPpneGezmw3aU222
81
+ CGrdKhOrBJRRcpoO3FjHHmXWkqgbQqDWdG7S+/l8n1QfDPp+jpULOrcnGEUY41Im
82
+ jZJTylbJQ1b5PBBjGiP0PpK48cdF
83
+ -----END CERTIFICATE-----
84
+ """
85
+
86
+ # Creates a new paypal object. Pass the raw html you got from paypal in.
87
+ # In a rails application this looks something like this
88
+ #
89
+ # def paypal_ipn
90
+ # paypal = Paypal::Notification.new(request.raw_post)
91
+ # ...
92
+ # end
93
+ def initialize(post)
94
+ empty!
95
+ parse(post)
96
+ end
97
+
98
+ # Was the transaction complete?
99
+ def complete?
100
+ status == "Completed"
101
+ end
102
+
103
+ def failed?
104
+ status == "Failed"
105
+ end
106
+
107
+ def denied?
108
+ status == "Denied"
109
+ end
110
+
111
+ # When was this payment received by the client.
112
+ # sometimes it can happen that we get the notification much later.
113
+ # One possible scenario is that our web application was down. In this case paypal tries several
114
+ # times an hour to inform us about the notification
115
+ def received_at
116
+ Time.parse params['payment_date']
117
+ end
118
+
119
+ # Whats the status of this transaction?
120
+ def status
121
+ params['payment_status']
122
+ end
123
+
124
+ # Id of this transaction (paypal number)
125
+ def transaction_id
126
+ params['txn_id']
127
+ end
128
+
129
+ # What type of transaction are we dealing with?
130
+ # "cart" "send_money" "web_accept" are possible here.
131
+ def type
132
+ params['txn_type']
133
+ end
134
+
135
+ # the money amount we received in X.2 decimal.
136
+ def gross
137
+ params['mc_gross']
138
+ end
139
+
140
+ # the markup paypal charges for the transaction
141
+ def fee
142
+ params['mc_fee']
143
+ end
144
+
145
+ # What currency have we been dealing with
146
+ def currency
147
+ params['mc_currency']
148
+ end
149
+
150
+ # This is the item number which we submitted to paypal
151
+ def item_id
152
+ params['item_number']
153
+ end
154
+
155
+ # This is the email address associated to the paypal account that recieved
156
+ # the payment.
157
+ def business
158
+ params['business']
159
+ end
160
+
161
+ # This is the item_name which you passed to paypal
162
+ def item_name
163
+ params['item_name']
164
+ end
165
+
166
+ # This is the invocie which you passed to paypal
167
+ def invoice
168
+ params['invoice']
169
+ end
170
+
171
+ # This is the invocie which you passed to paypal
172
+ def test?
173
+ params['test_ipn'] == '1'
174
+ end
175
+
176
+ # This is the custom field which you passed to paypal
177
+ def custom
178
+ params['custom']
179
+ end
180
+
181
+ # Reason for pending status, nil if status is not pending.
182
+ def pending_reason
183
+ params['pending_reason']
184
+ end
185
+
186
+ # Reason for reversed status, nil if status is not reversed.
187
+ def reason_code
188
+ params['reason_code']
189
+ end
190
+
191
+ # Memo entered by customer if any
192
+ def memo
193
+ params['memo']
194
+ end
195
+
196
+ # Well, the payment type.
197
+ def payment_type
198
+ params['payment_type']
199
+ end
200
+
201
+ # The exchange rate used if there was a conversion.
202
+ def exchange_rate
203
+ params['exchange_rate']
204
+ end
205
+
206
+ def gross_cents
207
+ (gross.to_f * 100.0).round
208
+ end
209
+
210
+ # This combines the gross and currency and returns a proper Money object.
211
+ # this requires the money library located at http://dist.leetsoft.com/api/money
212
+ def amount
213
+ return Money.new(gross_cents, currency) rescue ArgumentError
214
+ return Money.new(gross_cents) # maybe you have an own money object which doesn't take a currency?
215
+ end
216
+
217
+ # reset the notification.
218
+ def empty!
219
+ @params = Hash.new
220
+ @raw = ""
221
+ end
222
+
223
+ # Acknowledge the transaction to paypal. This method has to be called after a new
224
+ # ipn arrives. Paypal will verify that all the information we received are correct and will return a
225
+ # ok or a fail.
226
+ #
227
+ # Example:
228
+ #
229
+ # def paypal_ipn
230
+ # notify = PaypalNotification.new(request.raw_post)
231
+ #
232
+ # if notify.acknowledge
233
+ # ... process order ... if notify.complete?
234
+ # else
235
+ # ... log possible hacking attempt ...
236
+ # end
237
+ def acknowledge
238
+ payload = raw
239
+
240
+ uri = URI.parse(self.class.ipn_url)
241
+ request_path = "#{uri.path}?cmd=_notify-validate"
242
+
243
+ request = Net::HTTP::Post.new(request_path)
244
+ request['Content-Length'] = "#{payload.size}"
245
+ request['User-Agent'] = "paypal-ruby -- http://rubyforge.org/projects/paypal/"
246
+
247
+ http = Net::HTTP.new(uri.host, uri.port)
248
+
249
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless @ssl_strict
250
+ http.use_ssl = true
251
+
252
+ request = http.request(request, payload)
253
+
254
+ raise StandardError.new("Faulty paypal result: #{request.body}") unless ["VERIFIED", "INVALID"].include?(request.body)
255
+
256
+ request.body == "VERIFIED"
257
+ end
258
+
259
+ private
260
+
261
+ # Take the posted data and move the relevant data into a hash
262
+ def parse(post)
263
+ @raw = post
264
+ for line in post.split('&')
265
+ key, value = *line.scan( %r{^(\w+)\=(.*)$} ).flatten
266
+ params[key] = CGI.unescape(value)
267
+ end
268
+ end
269
+
270
+ end
271
+ end
data/lib/paypal.rb ADDED
@@ -0,0 +1,31 @@
1
+ #--
2
+ # Copyright (c) 2005 Tobias Luetke
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'cgi'
25
+ require 'net/http'
26
+ require 'net/https'
27
+ require 'active_support'
28
+
29
+
30
+ require File.dirname(__FILE__) + '/notification'
31
+ require File.dirname(__FILE__) + '/helper'
data/misc/paypal.psd ADDED
Binary file
@@ -0,0 +1,197 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'paypal'
5
+
6
+ require 'rubygems'
7
+ require 'actionpack' rescue LoadError raise(StandardErrror.new("This test needs ActionPack installed as gem to run"))
8
+ require 'action_controller'
9
+ require 'action_view'
10
+ require 'money'
11
+
12
+
13
+ # Little hack class which pretends to be a active controller
14
+ class TestController < ActionController::Base
15
+ allow_forgery_protection
16
+ include Paypal::Helpers
17
+ include ActionView::Helpers::TagHelper
18
+ include ActionView::Helpers::FormHelper
19
+ include ActionView::Helpers::UrlHelper
20
+ include ActionView::Helpers::FormTagHelper
21
+
22
+ def url_for(options, *parameters_for_method_reference)
23
+ "http://www.sandbox.paypal.com/cgi-bin/webscr"
24
+ end
25
+
26
+ # Used for testing paypal_form_start using a block (test_paypal_form_start_with_block)
27
+ def capture(&block)
28
+ return yield
29
+ end
30
+
31
+ # Used for testing paypal_form_start using a block (test_paypal_form_start_with_block)
32
+ def concat(one, two)
33
+ return eval("self", two)+one
34
+ end
35
+ end
36
+
37
+ class HelperTest < Test::Unit::TestCase
38
+
39
+
40
+ def assert_inputs(options, text)
41
+ all = text.scan(/name\=\"(\w+)\"/).flatten
42
+
43
+ xor = (options.keys | all) - (options.keys & all)
44
+
45
+ # xor
46
+ assert_equal [], xor, "options do not match expectations does not have keys #{xor.inspect} only appear in one of both sides in \n\n#{text}"
47
+
48
+ text.scan(/name\=\"([^"]+).*?value\=\"([^"]+)/) do |key, value|
49
+ if options.has_key?(key)
50
+ assert_equal options[key], value, "key #{key} was '#{options[key]}' and not '#{value}' in \n\n#{text}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def setup
56
+ @helpers = TestController.new
57
+ end
58
+
59
+ def test_paypal_form_start
60
+ assert_equal %{<form action="http://www.sandbox.paypal.com/cgi-bin/webscr" method="post">}, @helpers.paypal_form_tag
61
+ end
62
+
63
+ def test_paypal_form_start_with_block
64
+ strTest = "OriginalString"
65
+ assert_equal %Q(OriginalString<form action="http://www.sandbox.paypal.com/cgi-bin/webscr" method="post">SomeTextHere</form>),
66
+ strTest.instance_eval(%Q(TestController.new.paypal_form_tag do
67
+ "SomeTextHere"
68
+ end))
69
+ end
70
+
71
+ def test_paypal_setup_with_money
72
+ actual = @helpers.paypal_setup("100", Money.us_dollar(50000), "bob@bigbusiness.com")
73
+ assert_inputs({ "amount" => "500.00",
74
+ "business" => "bob@bigbusiness.com",
75
+ "charset" => "utf-8",
76
+ "cmd" => "_xclick",
77
+ "currency_code" => "USD",
78
+ "item_name" => "Store purchase",
79
+ "item_number" => "100",
80
+ "no_note" => "1",
81
+ "no_shipping" => "1",
82
+ "quantity" => "1"}, actual)
83
+ end
84
+
85
+ def test_paypal_setup_with_money_and_tax
86
+ actual = @helpers.paypal_setup("100", Money.us_dollar(50000), "bob@bigbusiness.com", :tax => Money.us_dollar(500))
87
+ assert_inputs({ "amount" => "500.00",
88
+ "business" => "bob@bigbusiness.com",
89
+ "charset" => "utf-8",
90
+ "cmd" => "_xclick",
91
+ "currency_code" => "USD",
92
+ "item_name" => "Store purchase",
93
+ "item_number" => "100",
94
+ "no_note" => "1",
95
+ "no_shipping" => "1",
96
+ "quantity" => "1",
97
+ "tax" => "5.00"}, actual)
98
+ end
99
+
100
+ def test_paypal_setup_with_money_and_invoice
101
+ actual = @helpers.paypal_setup("100", Money.us_dollar(50000), "bob@bigbusiness.com", :invoice => "Cool invoice!")
102
+ assert_inputs({ "amount" => "500.00",
103
+ "business" => "bob@bigbusiness.com",
104
+ "charset" => "utf-8",
105
+ "cmd" => "_xclick",
106
+ "currency_code" => "USD",
107
+ "invoice" => "Cool invoice!",
108
+ "item_name" => "Store purchase",
109
+ "item_number" => "100",
110
+ "no_note" => "1",
111
+ "no_shipping" => "1",
112
+ "quantity" => "1"}, actual)
113
+ end
114
+
115
+ def test_paypal_setup_with_money_and_custom
116
+ actual = @helpers.paypal_setup("100", Money.us_dollar(50000), "bob@bigbusiness.com", :custom => "Custom")
117
+ assert_inputs({ "amount" => "500.00",
118
+ "business" => "bob@bigbusiness.com",
119
+ "charset" => "utf-8",
120
+ "cmd" => "_xclick",
121
+ "currency_code" => "USD",
122
+ "custom" => "Custom",
123
+ "item_name" => "Store purchase",
124
+ "item_number" => "100",
125
+ "no_note" => "1",
126
+ "no_shipping" => "1",
127
+ "quantity" => "1",
128
+ }, actual)
129
+ end
130
+
131
+ def test_paypal_setup_with_float
132
+ actual = @helpers.paypal_setup("100", 50.00, "bob@bigbusiness.com", :currency => 'CAD')
133
+ assert_inputs({ "amount" => "50.00",
134
+ "business" => "bob@bigbusiness.com",
135
+ "charset" => "utf-8",
136
+ "cmd" => "_xclick",
137
+ "currency_code" => "CAD",
138
+ "item_name" => "Store purchase",
139
+ "item_number" => "100",
140
+ "no_note" => "1",
141
+ "no_shipping" => "1",
142
+ "quantity" => "1"}, actual)
143
+ end
144
+
145
+ def test_paypal_setup_with_string
146
+ actual = @helpers.paypal_setup("100", "50.00", "bob@bigbusiness.com", :currency => 'CAD')
147
+ assert_inputs({ "amount" => "50.00",
148
+ "business" => "bob@bigbusiness.com",
149
+ "charset" => "utf-8",
150
+ "cmd" => "_xclick",
151
+ "currency_code" => "CAD",
152
+ "item_name" => "Store purchase",
153
+ "item_number" => "100",
154
+ "no_note" => "1",
155
+ "no_shipping" => "1",
156
+ "quantity" => "1"}, actual)
157
+ end
158
+
159
+ def test_paypal_setup_options
160
+ actual = @helpers.paypal_setup("100", Money.us_dollar(100), "bob@bigbusiness.com", :item_name => "MegaBob's shop purchase", :return => 'http://www.bigbusiness.com', :cancel_return => 'http://www.bigbusiness.com', :notify_url => 'http://www.bigbusiness.com', :no_shipping => 0, :no_note => 0 )
161
+ assert_inputs({ "amount" => "1.00",
162
+ "business" => "bob@bigbusiness.com",
163
+ "cancel_return" => "http://www.bigbusiness.com",
164
+ "charset" => "utf-8",
165
+ "cmd" => "_xclick",
166
+ "currency_code" => "USD",
167
+ "item_name" => "MegaBob's shop purchase",
168
+ "item_number" => "100",
169
+ "no_note" => "0",
170
+ "no_shipping" => "0",
171
+ "notify_url" => "http://www.bigbusiness.com",
172
+ "quantity" => "1",
173
+ "return" => "http://www.bigbusiness.com"}, actual )
174
+ end
175
+
176
+ def test_paypal_setup_subscription
177
+ actual = @helpers.paypal_setup("100", Money.us_dollar(2800), "bob@bigbusiness.com", :item_name => "MegaBob's shop purchase", :return => 'http://www.bigbusiness.com', :cancel_return => 'http://www.bigbusiness.com', :notify_url => 'http://www.bigbusiness.com', :no_shipping => 0, :no_note => 0, :subscription => { :recurring => true, :period => :monthly, :retry => true })
178
+ assert_inputs({ "a3" => "28.00",
179
+ "business" => "bob@bigbusiness.com",
180
+ "cancel_return" => "http://www.bigbusiness.com",
181
+ "charset" => "utf-8",
182
+ "cmd" => "_ext-enter",
183
+ "redirect_cmd" => "_xclick-subscriptions",
184
+ "currency_code" => "USD",
185
+ "item_name" => "MegaBob's shop purchase",
186
+ "item_number" => "100",
187
+ "no_note" => "0",
188
+ "no_shipping" => "0",
189
+ "notify_url" => "http://www.bigbusiness.com",
190
+ "quantity" => "1",
191
+ "src" => "1",
192
+ "sra" => "1",
193
+ "t3" => "M",
194
+ "return" => "http://www.bigbusiness.com"}, actual )
195
+ end
196
+
197
+ end