paysio 1.0.0

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.
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