payjp 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +82 -0
  4. data/.rubocop_todo.yml +35 -0
  5. data/.travis.yml +22 -0
  6. data/CONTRIBUTORS +0 -0
  7. data/Gemfile +8 -0
  8. data/History.txt +0 -0
  9. data/LICENSE +21 -0
  10. data/README.rdoc +43 -0
  11. data/Rakefile +8 -0
  12. data/VERSION +1 -0
  13. data/gemfiles/default-with-activesupport.gemfile +10 -0
  14. data/gemfiles/json.gemfile +12 -0
  15. data/gemfiles/yajl.gemfile +12 -0
  16. data/lib/payjp.rb +279 -0
  17. data/lib/payjp/account.rb +11 -0
  18. data/lib/payjp/api_operations/create.rb +16 -0
  19. data/lib/payjp/api_operations/delete.rb +11 -0
  20. data/lib/payjp/api_operations/list.rb +17 -0
  21. data/lib/payjp/api_operations/request.rb +39 -0
  22. data/lib/payjp/api_operations/update.rb +17 -0
  23. data/lib/payjp/api_resource.rb +35 -0
  24. data/lib/payjp/card.rb +18 -0
  25. data/lib/payjp/charge.rb +27 -0
  26. data/lib/payjp/customer.rb +8 -0
  27. data/lib/payjp/errors/api_connection_error.rb +4 -0
  28. data/lib/payjp/errors/api_error.rb +4 -0
  29. data/lib/payjp/errors/authentication_error.rb +4 -0
  30. data/lib/payjp/errors/card_error.rb +11 -0
  31. data/lib/payjp/errors/invalid_request_error.rb +10 -0
  32. data/lib/payjp/errors/payjp_error.rb +20 -0
  33. data/lib/payjp/event.rb +5 -0
  34. data/lib/payjp/list_object.rb +33 -0
  35. data/lib/payjp/payjp_object.rb +259 -0
  36. data/lib/payjp/plan.rb +8 -0
  37. data/lib/payjp/subscription.rb +37 -0
  38. data/lib/payjp/token.rb +5 -0
  39. data/lib/payjp/transfer.rb +5 -0
  40. data/lib/payjp/util.rb +121 -0
  41. data/lib/payjp/version.rb +3 -0
  42. data/payjp.gemspec +27 -0
  43. data/test/payjp/account_test.rb +17 -0
  44. data/test/payjp/api_resource_test.rb +445 -0
  45. data/test/payjp/charge_test.rb +83 -0
  46. data/test/payjp/customer_card_test.rb +61 -0
  47. data/test/payjp/customer_test.rb +35 -0
  48. data/test/payjp/event_test.rb +26 -0
  49. data/test/payjp/list_object_test.rb +16 -0
  50. data/test/payjp/metadata_test.rb +103 -0
  51. data/test/payjp/payjp_object_test.rb +28 -0
  52. data/test/payjp/plan_test.rb +48 -0
  53. data/test/payjp/subscription_test.rb +58 -0
  54. data/test/payjp/token_test.rb +34 -0
  55. data/test/payjp/transfer_test.rb +34 -0
  56. data/test/payjp/util_test.rb +34 -0
  57. data/test/test_data.rb +291 -0
  58. data/test/test_helper.rb +45 -0
  59. metadata +201 -0
@@ -0,0 +1,11 @@
1
+ module Payjp
2
+ class Account < APIResource
3
+ def url
4
+ '/v1/accounts'
5
+ end
6
+
7
+ def self.retrieve
8
+ super(Object.new)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Payjp
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params = {}, opts = {})
6
+ response, opts = request(:post, url, params, opts)
7
+ Util.convert_to_payjp_object(response, opts)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Payjp
2
+ module APIOperations
3
+ module Delete
4
+ def delete(params = {}, opts = {})
5
+ opts = Util.normalize_opts(opts)
6
+ response, opts = request(:delete, url, params, opts)
7
+ refresh_from(response, opts)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Payjp
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters = {}, opts = {})
6
+ opts = Util.normalize_opts(opts)
7
+ response, opts = request(:get, url, filters, opts)
8
+ Util.convert_to_payjp_object(response, opts)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ module Payjp
2
+ module APIOperations
3
+ module Request
4
+ module ClassMethods
5
+ OPTS_KEYS_TO_PERSIST = Set[:api_key, :api_base, :payjp_account, :payjp_version]
6
+
7
+ def request(method, url, params = {}, opts = {})
8
+ opts = Util.normalize_opts(opts)
9
+
10
+ headers = opts.clone
11
+ api_key = headers.delete(:api_key)
12
+ api_base = headers.delete(:api_base)
13
+ # Assume all remaining opts must be headers
14
+
15
+ response, opts[:api_key] = Payjp.request(method, url, api_key, params, headers, api_base)
16
+
17
+ # Hash#select returns an array before 1.9
18
+ opts_to_persist = {}
19
+ opts.each do |k, v|
20
+ opts_to_persist[k] = v if OPTS_KEYS_TO_PERSIST.include?(k)
21
+ end
22
+
23
+ [response, opts_to_persist]
24
+ end
25
+ end
26
+
27
+ def self.included(base)
28
+ base.extend(ClassMethods)
29
+ end
30
+
31
+ protected
32
+
33
+ def request(method, url, params = {}, opts = {})
34
+ opts = @opts.merge(Util.normalize_opts(opts))
35
+ self.class.request(method, url, params, opts)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module Payjp
2
+ module APIOperations
3
+ module Update
4
+ def save(params = {})
5
+ values = self.class.serialize_params(self).merge(params)
6
+
7
+ if values.length > 0
8
+ values.delete(:id)
9
+
10
+ response, opts = request(:post, url, values)
11
+ refresh_from(response, opts)
12
+ end
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module Payjp
2
+ class APIResource < PayjpObject
3
+ include Payjp::APIOperations::Request
4
+
5
+ def self.class_name
6
+ name.split('::')[-1]
7
+ end
8
+
9
+ def self.url
10
+ if self == APIResource
11
+ raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)')
12
+ end
13
+ "/v1/#{CGI.escape(class_name.downcase)}s"
14
+ end
15
+
16
+ def url
17
+ unless id = self['id']
18
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
19
+ end
20
+ "#{self.class.url}/#{CGI.escape(id)}"
21
+ end
22
+
23
+ def refresh
24
+ response, opts = request(:get, url, @retrieve_params)
25
+ refresh_from(response, opts)
26
+ end
27
+
28
+ def self.retrieve(id, opts = {})
29
+ opts = Util.normalize_opts(opts)
30
+ instance = new(id, opts)
31
+ instance.refresh
32
+ instance
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ module Payjp
2
+ class Card < APIResource
3
+ include Payjp::APIOperations::Create
4
+ include Payjp::APIOperations::Update
5
+ include Payjp::APIOperations::Delete
6
+ include Payjp::APIOperations::List
7
+
8
+ def url
9
+ if respond_to?(:customer)
10
+ "#{Customer.url}/#{CGI.escape(customer)}/cards/#{CGI.escape(id)}"
11
+ end
12
+ end
13
+
14
+ def self.retrieve(_id, _opts = nil)
15
+ raise NotImplementedError.new("Cards cannot be retrieved without a customer ID. Retrieve a card using customer.cards.retrieve('card_id')")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module Payjp
2
+ class Charge < APIResource
3
+ include Payjp::APIOperations::List
4
+ include Payjp::APIOperations::Create
5
+ include Payjp::APIOperations::Update
6
+
7
+ def refund(params = {}, opts = {})
8
+ response, opts = request(:post, refund_url, params, opts)
9
+ refresh_from(response, opts)
10
+ end
11
+
12
+ def capture(params = {}, opts = {})
13
+ response, opts = request(:post, capture_url, params, opts)
14
+ refresh_from(response, opts)
15
+ end
16
+
17
+ private
18
+
19
+ def refund_url
20
+ url + '/refund'
21
+ end
22
+
23
+ def capture_url
24
+ url + '/capture'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ module Payjp
2
+ class Customer < APIResource
3
+ include Payjp::APIOperations::Create
4
+ include Payjp::APIOperations::Delete
5
+ include Payjp::APIOperations::Update
6
+ include Payjp::APIOperations::List
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module Payjp
2
+ class APIConnectionError < PayjpError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Payjp
2
+ class APIError < PayjpError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Payjp
2
+ class AuthenticationError < PayjpError
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ module Payjp
2
+ class CardError < PayjpError
3
+ attr_reader :param, :code
4
+
5
+ def initialize(message, param, code, http_status = nil, http_body = nil, json_body = nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ @code = code
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Payjp
2
+ class InvalidRequestError < PayjpError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status = nil, http_body = nil, json_body = nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Payjp
2
+ class PayjpError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message = nil, http_status = nil, http_body = nil, json_body = nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Payjp
2
+ class Event < APIResource
3
+ include Payjp::APIOperations::List
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ module Payjp
2
+ class ListObject < PayjpObject
3
+ include Payjp::APIOperations::Request
4
+
5
+ def [](k)
6
+ case k
7
+ when String, Symbol
8
+ super
9
+ else
10
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support String keys. (HINT: List calls return an object with a 'data' (which is the data array). You likely want to call #data[#{k.inspect}])")
11
+ end
12
+ end
13
+
14
+ def each(&blk)
15
+ data.each(&blk)
16
+ end
17
+
18
+ def retrieve(id, opts = {})
19
+ response, opts = request(:get, "#{url}/#{CGI.escape(id)}", {}, opts)
20
+ Util.convert_to_payjp_object(response, opts)
21
+ end
22
+
23
+ def create(params = {}, opts = {})
24
+ response, opts = request(:post, url, params, opts)
25
+ Util.convert_to_payjp_object(response, opts)
26
+ end
27
+
28
+ def all(params = {}, opts = {})
29
+ response, opts = request(:get, url, params, opts)
30
+ Util.convert_to_payjp_object(response, opts)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,259 @@
1
+ module Payjp
2
+ class PayjpObject
3
+ include Enumerable
4
+
5
+ @@permanent_attributes = Set.new([:id])
6
+
7
+ # The default :id method is deprecated and isn't useful to us
8
+ undef :id if method_defined?(:id)
9
+
10
+ def initialize(id = nil, opts = {})
11
+ # parameter overloading!
12
+ if id.is_a?(Hash)
13
+ @retrieve_params = id.dup
14
+ @retrieve_params.delete(:id)
15
+ id = id[:id]
16
+ else
17
+ @retrieve_params = {}
18
+ end
19
+
20
+ @opts = opts
21
+ @values = {}
22
+ # This really belongs in APIResource, but not putting it there allows us
23
+ # to have a unified inspect method
24
+ @unsaved_values = Set.new
25
+ @transient_values = Set.new
26
+ @values[:id] = id if id
27
+ end
28
+
29
+ def self.construct_from(values, opts = {})
30
+ new(values[:id]).refresh_from(values, opts)
31
+ end
32
+
33
+ def to_s(*_args)
34
+ JSON.pretty_generate(@values)
35
+ end
36
+
37
+ def inspect
38
+ id_string = (self.respond_to?(:id) && !id.nil?) ? " id=#{id}" : ""
39
+ "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
40
+ end
41
+
42
+ def refresh_from(values, opts, partial = false)
43
+ @opts = opts
44
+ @original_values = Marshal.load(Marshal.dump(values)) # deep copy
45
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
46
+ added = Set.new(values.keys - @values.keys)
47
+ # Wipe old state before setting new. This is useful for e.g. updating a
48
+ # customer, where there is no persistent card parameter. Mark those values
49
+ # which don't persist as transient
50
+
51
+ instance_eval do
52
+ remove_accessors(removed)
53
+ add_accessors(added)
54
+ end
55
+ removed.each do |k|
56
+ @values.delete(k)
57
+ @transient_values.add(k)
58
+ @unsaved_values.delete(k)
59
+ end
60
+ values.each do |k, v|
61
+ @values[k] = Util.convert_to_payjp_object(v, @opts)
62
+ @transient_values.delete(k)
63
+ @unsaved_values.delete(k)
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ def [](k)
70
+ @values[k.to_sym]
71
+ end
72
+
73
+ def []=(k, v)
74
+ send(:"#{k}=", v)
75
+ end
76
+
77
+ def keys
78
+ @values.keys
79
+ end
80
+
81
+ def values
82
+ @values.values
83
+ end
84
+
85
+ def to_json(*_a)
86
+ JSON.generate(@values)
87
+ end
88
+
89
+ def as_json(*a)
90
+ @values.as_json(*a)
91
+ end
92
+
93
+ def to_hash
94
+ @values.inject({}) do |acc, (key, value)|
95
+ acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
96
+ acc
97
+ end
98
+ end
99
+
100
+ def each(&blk)
101
+ @values.each(&blk)
102
+ end
103
+
104
+ def _dump(_level)
105
+ Marshal.dump([@values, @opts])
106
+ end
107
+
108
+ def self._load(args)
109
+ values, opts = Marshal.load(args)
110
+ construct_from(values, opts)
111
+ end
112
+
113
+ if RUBY_VERSION < '1.9.2'
114
+ def respond_to?(symbol)
115
+ @values.has_key?(symbol) || super
116
+ end
117
+ end
118
+
119
+ def serialize_nested_object(key)
120
+ new_value = @values[key]
121
+ return {} if new_value.is_a?(APIResource)
122
+
123
+ if @unsaved_values.include?(key)
124
+ # the object has been reassigned
125
+ # e.g. as object.key = {foo => bar}
126
+ update = new_value
127
+ new_keys = update.keys.map(&:to_sym)
128
+
129
+ # remove keys at the server, but not known locally
130
+ if @original_values.include?(key)
131
+ keys_to_unset = @original_values[key].keys - new_keys
132
+ keys_to_unset.each { |key| update[key] = '' }
133
+ end
134
+
135
+ update
136
+ else
137
+ # can be serialized normally
138
+ self.class.serialize_params(new_value)
139
+ end
140
+ end
141
+
142
+ def self.serialize_params(obj, original_value = nil)
143
+ case obj
144
+ when nil
145
+ ''
146
+ when PayjpObject
147
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
148
+ obj_values = obj.instance_variable_get(:@values)
149
+ update_hash = {}
150
+
151
+ unsaved_keys.each do |k|
152
+ update_hash[k] = serialize_params(obj_values[k])
153
+ end
154
+
155
+ obj_values.each do |k, v|
156
+ if v.is_a?(PayjpObject) || v.is_a?(Hash)
157
+ update_hash[k] = obj.serialize_nested_object(k)
158
+ elsif v.is_a?(Array)
159
+ original_value = obj.instance_variable_get(:@original_values)[k]
160
+ if original_value && original_value.length > v.length
161
+ # url params provide no mechanism for deleting an item in an array,
162
+ # just overwriting the whole array or adding new items. So let's not
163
+ # allow deleting without a full overwrite until we have a solution.
164
+ raise ArgumentError.new(
165
+ "You cannot delete an item from an array, you must instead set a new array"
166
+ )
167
+ end
168
+ update_hash[k] = serialize_params(v, original_value)
169
+ end
170
+ end
171
+
172
+ update_hash
173
+ when Array
174
+ update_hash = {}
175
+ obj.each_with_index do |value, index|
176
+ update = serialize_params(value)
177
+ if update != {} && (!original_value || update != original_value[index])
178
+ update_hash[index] = update
179
+ end
180
+ end
181
+
182
+ if update_hash == {}
183
+ nil
184
+ else
185
+ update_hash
186
+ end
187
+ else
188
+ obj
189
+ end
190
+ end
191
+
192
+ protected
193
+
194
+ def metaclass
195
+ class << self; self; end
196
+ end
197
+
198
+ def remove_accessors(keys)
199
+ metaclass.instance_eval do
200
+ keys.each do |k|
201
+ next if @@permanent_attributes.include?(k)
202
+ k_eq = :"#{k}="
203
+ remove_method(k) if method_defined?(k)
204
+ remove_method(k_eq) if method_defined?(k_eq)
205
+ end
206
+ end
207
+ end
208
+
209
+ def add_accessors(keys)
210
+ metaclass.instance_eval do
211
+ keys.each do |k|
212
+ next if @@permanent_attributes.include?(k)
213
+ k_eq = :"#{k}="
214
+ define_method(k) { @values[k] }
215
+ define_method(k_eq) do |v|
216
+ if v == ""
217
+ raise ArgumentError.new(
218
+ "You cannot set #{k} to an empty string." \
219
+ "We interpret empty strings as nil in requests." \
220
+ "You may set #{self}.#{k} = nil to delete the property.")
221
+ end
222
+ @values[k] = v
223
+ @unsaved_values.add(k)
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ def method_missing(name, *args)
230
+ # TODO: only allow setting in updateable classes.
231
+ if name.to_s.end_with?('=')
232
+ attr = name.to_s[0...-1].to_sym
233
+ add_accessors([attr])
234
+ begin
235
+ mth = method(name)
236
+ rescue NameError
237
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
238
+ end
239
+ return mth.call(args[0])
240
+ else
241
+ return @values[name] if @values.has_key?(name)
242
+ end
243
+
244
+ begin
245
+ super
246
+ rescue NoMethodError => e
247
+ if @transient_values.include?(name)
248
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Payjp's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
249
+ else
250
+ raise
251
+ end
252
+ end
253
+ end
254
+
255
+ def respond_to_missing?(symbol, include_private = false)
256
+ @values && @values.has_key?(symbol) || super
257
+ end
258
+ end
259
+ end