paysio 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +9 -0
  3. data/CONTRIBUTORS +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +42 -0
  6. data/History.txt +4 -0
  7. data/LICENSE +21 -0
  8. data/README.rdoc +12 -0
  9. data/Rakefile +12 -0
  10. data/VERSION +1 -0
  11. data/bin/paysio +7 -0
  12. data/gemfiles/default-with-activesupport.gemfile +3 -0
  13. data/gemfiles/json.gemfile +4 -0
  14. data/gemfiles/yajl.gemfile +4 -0
  15. data/lib/data/ca-certificates.crt +3918 -0
  16. data/lib/paysio/account.rb +4 -0
  17. data/lib/paysio/api_operations/create.rb +16 -0
  18. data/lib/paysio/api_operations/delete.rb +11 -0
  19. data/lib/paysio/api_operations/list.rb +16 -0
  20. data/lib/paysio/api_operations/update.rb +15 -0
  21. data/lib/paysio/api_resource.rb +33 -0
  22. data/lib/paysio/charge.rb +39 -0
  23. data/lib/paysio/coupon.rb +7 -0
  24. data/lib/paysio/customer.rb +51 -0
  25. data/lib/paysio/errors/api_connection_error.rb +4 -0
  26. data/lib/paysio/errors/api_error.rb +4 -0
  27. data/lib/paysio/errors/authentication_error.rb +4 -0
  28. data/lib/paysio/errors/card_error.rb +11 -0
  29. data/lib/paysio/errors/invalid_request_error.rb +10 -0
  30. data/lib/paysio/errors/paysio_error.rb +20 -0
  31. data/lib/paysio/event.rb +5 -0
  32. data/lib/paysio/json.rb +21 -0
  33. data/lib/paysio/list_object.rb +14 -0
  34. data/lib/paysio/paysio_object.rb +159 -0
  35. data/lib/paysio/singleton_api_resource.rb +20 -0
  36. data/lib/paysio/util.rb +100 -0
  37. data/lib/paysio/version.rb +3 -0
  38. data/lib/paysio.rb +252 -0
  39. data/paysio.gemspec +28 -0
  40. data/test/test_helper.rb +162 -0
  41. data/test/test_paysio.rb +479 -0
  42. data/test/test_paysio_with_active_support.rb +2 -0
  43. metadata +193 -0
data/lib/paysio.rb ADDED
@@ -0,0 +1,252 @@
1
+ # Paysio Ruby bindings
2
+ # API spec at https://paysio.com/docs/api
3
+ require 'cgi'
4
+ require 'set'
5
+ require 'rubygems'
6
+ require 'openssl'
7
+
8
+ gem 'rest-client', '~> 1.4'
9
+ require 'rest_client'
10
+ require 'multi_json'
11
+
12
+ # Version
13
+ require 'paysio/version'
14
+
15
+ # API operations
16
+ require 'paysio/api_operations/create'
17
+ require 'paysio/api_operations/update'
18
+ require 'paysio/api_operations/delete'
19
+ require 'paysio/api_operations/list'
20
+
21
+ # Resources
22
+ require 'paysio/util'
23
+ require 'paysio/json'
24
+ require 'paysio/paysio_object'
25
+ require 'paysio/api_resource'
26
+ require 'paysio/singleton_api_resource'
27
+ require 'paysio/list_object'
28
+ require 'paysio/account'
29
+ require 'paysio/customer'
30
+ require 'paysio/charge'
31
+ require 'paysio/coupon'
32
+ require 'paysio/event'
33
+
34
+ # Errors
35
+ require 'paysio/errors/paysio_error'
36
+ require 'paysio/errors/api_error'
37
+ require 'paysio/errors/api_connection_error'
38
+ require 'paysio/errors/card_error'
39
+ require 'paysio/errors/invalid_request_error'
40
+ require 'paysio/errors/authentication_error'
41
+
42
+ module Paysio
43
+ @@ssl_bundle_path = File.join(File.dirname(__FILE__), 'data/ca-certificates.crt')
44
+ @@api_key = nil
45
+ @@api_base = 'api.paysio.com'
46
+ @@verify_ssl_certs = true
47
+ @@api_version = nil
48
+
49
+ def self.api_url(url='')
50
+ "https://#{api_key}@#{api_base}/#{url}"
51
+ end
52
+
53
+ def self.api_key=(api_key)
54
+ @@api_key = api_key
55
+ end
56
+
57
+ def self.api_key
58
+ @@api_key
59
+ end
60
+
61
+ def self.api_base=(api_base)
62
+ @@api_base = api_base
63
+ end
64
+
65
+ def self.api_base
66
+ @@api_base
67
+ end
68
+
69
+ def self.verify_ssl_certs=(verify)
70
+ @@verify_ssl_certs = verify
71
+ end
72
+
73
+ def self.verify_ssl_certs
74
+ @@verify_ssl_certs
75
+ end
76
+
77
+ def self.api_version=(version)
78
+ @@api_version = version
79
+ end
80
+
81
+ def self.api_version
82
+ @@api_version
83
+ end
84
+
85
+ def self.request(method, url, api_key, params={}, headers={})
86
+ api_key ||= @@api_key
87
+ raise AuthenticationError.new('No API key provided. (HINT: set your API key using "Paysio.api_key = <API-KEY>". You can generate API keys from the Paysio web interface. See https://paysio.com/api for details, or email support@paysio.com if you have any questions.)') unless api_key
88
+
89
+ if !verify_ssl_certs
90
+ unless @no_verify
91
+ $stderr.puts "WARNING: Running without SSL cert verification. Execute 'Paysio.verify_ssl_certs = true' to enable verification."
92
+ @no_verify = true
93
+ end
94
+ ssl_opts = { :verify_ssl => false }
95
+ elsif !Util.file_readable(@@ssl_bundle_path)
96
+ unless @no_bundle
97
+ $stderr.puts "WARNING: Running without SSL cert verification because #{@@ssl_bundle_path} isn't readable"
98
+ @no_bundle = true
99
+ end
100
+ ssl_opts = { :verify_ssl => false }
101
+ else
102
+ ssl_opts = {
103
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
104
+ :ssl_ca_file => @@ssl_bundle_path
105
+ }
106
+ end
107
+ uname = (@@uname ||= RUBY_PLATFORM =~ /linux|darwin/i ? `uname -a 2>/dev/null`.strip : nil)
108
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
109
+ ua = {
110
+ :bindings_version => Paysio::VERSION,
111
+ :lang => 'ruby',
112
+ :lang_version => lang_version,
113
+ :platform => RUBY_PLATFORM,
114
+ :publisher => 'paysio',
115
+ :uname => uname
116
+ }
117
+
118
+ params = Util.objects_to_ids(params)
119
+ url = self.api_url(url)
120
+ case method.to_s.downcase.to_sym
121
+ when :get, :head, :delete
122
+ # Make params into GET parameters
123
+ if params && params.count > 0
124
+ query_string = Util.flatten_params(params).collect{|key, value| "#{key}=#{Util.url_encode(value)}"}.join('&')
125
+ url += "?#{query_string}"
126
+ end
127
+ payload = nil
128
+ else
129
+ payload = Util.flatten_params(params).collect{|(key, value)| "#{key}=#{Util.url_encode(value)}"}.join('&')
130
+ end
131
+
132
+ begin
133
+ headers = { :x_paysio_client_user_agent => Paysio::JSON.dump(ua) }.merge(headers)
134
+ rescue => e
135
+ headers = {
136
+ :x_paysio_client_raw_user_agent => ua.inspect,
137
+ :error => "#{e} (#{e.class})"
138
+ }.merge(headers)
139
+ end
140
+
141
+ headers = {
142
+ :user_agent => "Paysio/v1 RubyBindings/#{Paysio::VERSION}",
143
+ :content_type => 'application/x-www-form-urlencoded'
144
+ }.merge(headers)
145
+
146
+ if self.api_version
147
+ headers[:paysio_version] = self.api_version
148
+ end
149
+
150
+ opts = {
151
+ :method => method,
152
+ :url => url,
153
+ :headers => headers,
154
+ :open_timeout => 30,
155
+ :payload => payload,
156
+ :timeout => 80
157
+ }.merge(ssl_opts)
158
+
159
+ begin
160
+ response = execute_request(opts)
161
+ rescue SocketError => e
162
+ self.handle_restclient_error(e)
163
+ rescue NoMethodError => e
164
+ # Work around RestClient bug
165
+ if e.message =~ /\WRequestFailed\W/
166
+ e = APIConnectionError.new('Unexpected HTTP response code')
167
+ self.handle_restclient_error(e)
168
+ else
169
+ raise
170
+ end
171
+ rescue RestClient::ExceptionWithResponse => e
172
+ if rcode = e.http_code and rbody = e.http_body
173
+ self.handle_api_error(rcode, rbody)
174
+ else
175
+ self.handle_restclient_error(e)
176
+ end
177
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
178
+ self.handle_restclient_error(e)
179
+ end
180
+
181
+ rbody = response.body
182
+ rcode = response.code
183
+ begin
184
+ # Would use :symbolize_names => true, but apparently there is
185
+ # some library out there that makes symbolize_names not work.
186
+ resp = Paysio::JSON.load(rbody)
187
+ rescue MultiJson::DecodeError
188
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
189
+ end
190
+
191
+ resp = Util.symbolize_names(resp)
192
+ [resp, api_key]
193
+ end
194
+
195
+ private
196
+
197
+ def self.execute_request(opts)
198
+ RestClient::Request.execute(opts)
199
+ end
200
+
201
+ def self.handle_api_error(rcode, rbody)
202
+ begin
203
+ error_obj = Paysio::JSON.load(rbody)
204
+ error_obj = Util.symbolize_names(error_obj)
205
+ error = error_obj[:error] or raise PaysioError.new # escape from parsing
206
+ rescue MultiJson::DecodeError, PaysioError
207
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
208
+ end
209
+
210
+ case rcode
211
+ when 400, 404 then
212
+ raise invalid_request_error(error, rcode, rbody, error_obj)
213
+ when 401
214
+ raise authentication_error(error, rcode, rbody, error_obj)
215
+ when 402
216
+ raise card_error(error, rcode, rbody, error_obj)
217
+ else
218
+ raise api_error(error, rcode, rbody, error_obj)
219
+ end
220
+ end
221
+
222
+ def self.invalid_request_error(error, rcode, rbody, error_obj)
223
+ InvalidRequestError.new(error[:message], error[:param], rcode, rbody, error_obj)
224
+ end
225
+
226
+ def self.authentication_error(error, rcode, rbody, error_obj)
227
+ AuthenticationError.new(error[:message], rcode, rbody, error_obj)
228
+ end
229
+
230
+ def self.card_error(error, rcode, rbody, error_obj)
231
+ CardError.new(error[:message], error[:param], error[:code], rcode, rbody, error_obj)
232
+ end
233
+
234
+ def self.api_error(error, rcode, rbody, error_obj)
235
+ APIError.new(error[:message], rcode, rbody, error_obj)
236
+ end
237
+
238
+ def self.handle_restclient_error(e)
239
+ case e
240
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
241
+ message = "Could not connect to Paysio (#{@@api_base}). Please check your internet connection and try again. If this problem persists, you should check Paysio's service status at https://twitter.com/paysiostatus, or let us know at support@paysio.com."
242
+ when RestClient::SSLCertificateNotVerified
243
+ message = "Could not verify Paysio's SSL certificate. Please make sure that your network is not intercepting certificates. (Try going to https://api.paysio.com/v1 in your browser.) If this problem persists, let us know at support@paysio.com."
244
+ when SocketError
245
+ message = "Unexpected error communicating when trying to connect to Paysio. HINT: You may be seeing this message because your DNS is not working. To check, try running 'host paysio.com' from the command line."
246
+ else
247
+ message = "Unexpected error communicating with Paysio. If this problem persists, let us know at support@paysio.com."
248
+ end
249
+ message += "\n\n(Network error: #{e.message})"
250
+ raise APIConnectionError.new(message)
251
+ end
252
+ end
data/paysio.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'paysio/version'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = 'paysio'
7
+ s.version = Paysio::VERSION
8
+ s.summary = 'Ruby bindings for the Pays.io API'
9
+ s.description = 'See https://paysio.com for details.'
10
+ s.authors = ['Iskander Haziev']
11
+ s.email = ['gvalmon@gmail.com']
12
+ s.homepage = 'https://paysio.com'
13
+ s.executables = 'paysio'
14
+ s.require_paths = %w{lib}
15
+
16
+ s.add_dependency('rest-client', '~> 1.4')
17
+ s.add_dependency('multi_json', '>= 1.0.4', '< 2')
18
+
19
+ s.add_development_dependency('mocha')
20
+ s.add_development_dependency('shoulda')
21
+ s.add_development_dependency('test-unit')
22
+ s.add_development_dependency('rake')
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- test/*`.split("\n")
26
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27
+ s.require_paths = ['lib']
28
+ end
@@ -0,0 +1,162 @@
1
+ require 'stringio'
2
+ require 'test/unit'
3
+ require 'paysio'
4
+ require 'mocha'
5
+ include Mocha
6
+
7
+ #monkeypatch request methods
8
+ module Paysio
9
+ @mock_rest_client = nil
10
+
11
+ def self.mock_rest_client=(mock_client)
12
+ @mock_rest_client = mock_client
13
+ end
14
+
15
+ def self.execute_request(opts)
16
+ get_params = (opts[:headers] || {})[:params]
17
+ post_params = opts[:payload]
18
+ case opts[:method]
19
+ when :get then @mock_rest_client.get opts[:url], get_params, post_params
20
+ when :post then @mock_rest_client.post opts[:url], get_params, post_params
21
+ when :delete then @mock_rest_client.delete opts[:url], get_params, post_params
22
+ end
23
+ end
24
+ end
25
+
26
+ def test_response(body, code=200)
27
+ # When an exception is raised, restclient clobbers method_missing. Hence we
28
+ # can't just use the stubs interface.
29
+ body = MultiJson.dump(body) if !(body.kind_of? String)
30
+ m = mock
31
+ m.instance_variable_set('@paysio_values', { :body => body, :code => code })
32
+ def m.body; @paysio_values[:body]; end
33
+ def m.code; @paysio_values[:code]; end
34
+ m
35
+ end
36
+
37
+ def test_customer(params={})
38
+ {
39
+ :subscription_history => [],
40
+ :bills => [],
41
+ :charges => [],
42
+ :livemode => false,
43
+ :object => "customer",
44
+ :id => "c_test_customer",
45
+ :active_card => {
46
+ :type => "Visa",
47
+ :last4 => "4242",
48
+ :exp_month => 11,
49
+ :country => "US",
50
+ :exp_year => 2012,
51
+ :id => "cc_test_card",
52
+ :object => "card"
53
+ },
54
+ :created => 1304114758
55
+ }.merge(params)
56
+ end
57
+
58
+ def test_customer_array
59
+ {
60
+ :data => [test_customer, test_customer, test_customer],
61
+ :object => 'list',
62
+ :url => '/v1/customers'
63
+ }
64
+ end
65
+
66
+ def test_charge(params={})
67
+ {
68
+ :refunded => false,
69
+ :paid => true,
70
+ :amount => 100,
71
+ :card => {
72
+ :type => "Visa",
73
+ :last4 => "4242",
74
+ :exp_month => 11,
75
+ :country => "US",
76
+ :exp_year => 2012,
77
+ :id => "cc_test_card",
78
+ :object => "card"
79
+ },
80
+ :id => "ch_test_charge",
81
+ :reason => "execute_charge",
82
+ :livemode => false,
83
+ :currency => "usd",
84
+ :object => "charge",
85
+ :created => 1304114826
86
+ }.merge(params)
87
+ end
88
+
89
+ def test_charge_array
90
+ {
91
+ :data => [test_charge, test_charge, test_charge],
92
+ :object => 'list',
93
+ :url => '/v1/charges'
94
+ }
95
+ end
96
+
97
+ def test_card(params={})
98
+ {
99
+ :type => "Visa",
100
+ :last4 => "4242",
101
+ :exp_month => 11,
102
+ :country => "US",
103
+ :exp_year => 2012,
104
+ :id => "cc_test_card",
105
+ :object => "card"
106
+ }.merge(params)
107
+ end
108
+
109
+ def test_coupon(params={})
110
+ {
111
+ :duration => 'repeating',
112
+ :duration_in_months => 3,
113
+ :percent_off => 25,
114
+ :id => "co_test_coupon",
115
+ :object => "coupon"
116
+ }.merge(params)
117
+ end
118
+
119
+ def test_invalid_api_key_error
120
+ {
121
+ "error" => {
122
+ "type" => "invalid_request_error",
123
+ "message" => "Invalid API Key provided: invalid"
124
+ }
125
+ }
126
+ end
127
+
128
+ def test_invalid_exp_year_error
129
+ {
130
+ "error" => {
131
+ "code" => "invalid_expiry_year",
132
+ "param" => "exp_year",
133
+ "type" => "card_error",
134
+ "message" => "Your card's expiration year is invalid"
135
+ }
136
+ }
137
+ end
138
+
139
+ def test_missing_id_error
140
+ {
141
+ :error => {
142
+ :param => "id",
143
+ :type => "invalid_request_error",
144
+ :message => "Missing id"
145
+ }
146
+ }
147
+ end
148
+
149
+ def test_api_error
150
+ {
151
+ :error => {
152
+ :type => "api_error"
153
+ }
154
+ }
155
+ end
156
+
157
+ def test_delete_discount_response
158
+ {
159
+ :deleted => true,
160
+ :id => "di_test_coupon"
161
+ }
162
+ end