stateless-systems-paypal 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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