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.
- data/.gitignore +2 -0
- data/.travis.yml +9 -0
- data/CONTRIBUTORS +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +42 -0
- data/History.txt +4 -0
- data/LICENSE +21 -0
- data/README.rdoc +12 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/bin/paysio +7 -0
- data/gemfiles/default-with-activesupport.gemfile +3 -0
- data/gemfiles/json.gemfile +4 -0
- data/gemfiles/yajl.gemfile +4 -0
- data/lib/data/ca-certificates.crt +3918 -0
- data/lib/paysio/account.rb +4 -0
- data/lib/paysio/api_operations/create.rb +16 -0
- data/lib/paysio/api_operations/delete.rb +11 -0
- data/lib/paysio/api_operations/list.rb +16 -0
- data/lib/paysio/api_operations/update.rb +15 -0
- data/lib/paysio/api_resource.rb +33 -0
- data/lib/paysio/charge.rb +39 -0
- data/lib/paysio/coupon.rb +7 -0
- data/lib/paysio/customer.rb +51 -0
- data/lib/paysio/errors/api_connection_error.rb +4 -0
- data/lib/paysio/errors/api_error.rb +4 -0
- data/lib/paysio/errors/authentication_error.rb +4 -0
- data/lib/paysio/errors/card_error.rb +11 -0
- data/lib/paysio/errors/invalid_request_error.rb +10 -0
- data/lib/paysio/errors/paysio_error.rb +20 -0
- data/lib/paysio/event.rb +5 -0
- data/lib/paysio/json.rb +21 -0
- data/lib/paysio/list_object.rb +14 -0
- data/lib/paysio/paysio_object.rb +159 -0
- data/lib/paysio/singleton_api_resource.rb +20 -0
- data/lib/paysio/util.rb +100 -0
- data/lib/paysio/version.rb +3 -0
- data/lib/paysio.rb +252 -0
- data/paysio.gemspec +28 -0
- data/test/test_helper.rb +162 -0
- data/test/test_paysio.rb +479 -0
- data/test/test_paysio_with_active_support.rb +2 -0
- 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
|
data/test/test_helper.rb
ADDED
@@ -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
|