payjp 0.0.2

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