bill_forward 1.2014.296

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. data/.gitignore +27 -0
  2. data/.idea/.name +1 -0
  3. data/.idea/compiler.xml +23 -0
  4. data/.idea/copyright/profiles_settings.xml +3 -0
  5. data/.idea/encodings.xml +5 -0
  6. data/.idea/inspectionProfiles/Project_Default.xml +11 -0
  7. data/.idea/inspectionProfiles/profiles_settings.xml +7 -0
  8. data/.idea/misc.xml +23 -0
  9. data/.idea/modules.xml +9 -0
  10. data/.idea/scopes/scope_settings.xml +5 -0
  11. data/.idea/vcs.xml +7 -0
  12. data/.rspec +2 -0
  13. data/Gemfile +9 -0
  14. data/LICENSE.md +22 -0
  15. data/README.md +227 -0
  16. data/Rakefile +73 -0
  17. data/bill_forward.gemspec +29 -0
  18. data/bill_forward.iml +28 -0
  19. data/lib/bill_forward.rb +18 -0
  20. data/lib/bill_forward/billing_entity.rb +263 -0
  21. data/lib/bill_forward/client.rb +355 -0
  22. data/lib/bill_forward/custom_hash.rb +14 -0
  23. data/lib/bill_forward/deny_method.rb +4 -0
  24. data/lib/bill_forward/entities/account.rb +19 -0
  25. data/lib/bill_forward/entities/address.rb +25 -0
  26. data/lib/bill_forward/entities/amendments/amendment.rb +11 -0
  27. data/lib/bill_forward/entities/amendments/invoice_recalculation_amendment.rb +10 -0
  28. data/lib/bill_forward/entities/api_configuration.rb +11 -0
  29. data/lib/bill_forward/entities/authorize_net_token.rb +9 -0
  30. data/lib/bill_forward/entities/credit_note.rb +13 -0
  31. data/lib/bill_forward/entities/invoice.rb +25 -0
  32. data/lib/bill_forward/entities/invoice_parts/invoice_line.rb +37 -0
  33. data/lib/bill_forward/entities/invoice_parts/invoice_payment.rb +29 -0
  34. data/lib/bill_forward/entities/invoice_parts/tax_line.rb +23 -0
  35. data/lib/bill_forward/entities/invoice_parts/taxation_link.rb +5 -0
  36. data/lib/bill_forward/entities/organisation.rb +37 -0
  37. data/lib/bill_forward/entities/payment_method.rb +5 -0
  38. data/lib/bill_forward/entities/payment_method_subscription_link.rb +5 -0
  39. data/lib/bill_forward/entities/pricing_component.rb +21 -0
  40. data/lib/bill_forward/entities/pricing_component_tier.rb +5 -0
  41. data/lib/bill_forward/entities/pricing_component_value.rb +5 -0
  42. data/lib/bill_forward/entities/pricing_component_value_change.rb +5 -0
  43. data/lib/bill_forward/entities/product.rb +5 -0
  44. data/lib/bill_forward/entities/product_rate_plan.rb +19 -0
  45. data/lib/bill_forward/entities/profile.rb +15 -0
  46. data/lib/bill_forward/entities/role.rb +4 -0
  47. data/lib/bill_forward/entities/subscription.rb +53 -0
  48. data/lib/bill_forward/entities/unit_of_measure.rb +5 -0
  49. data/lib/bill_forward/insertable_entity.rb +32 -0
  50. data/lib/bill_forward/mutable_entity.rb +47 -0
  51. data/lib/bill_forward/resource_path.rb +11 -0
  52. data/lib/bill_forward/type_check.rb +21 -0
  53. data/lib/bill_forward/version.rb +4 -0
  54. data/spec/component/account_spec.rb +200 -0
  55. data/spec/component/billing_entity_spec.rb +153 -0
  56. data/spec/component/invoice_spec.rb +155 -0
  57. data/spec/component/subscription_spec.rb +357 -0
  58. data/spec/functional/account_spec.rb +25 -0
  59. data/spec/functional/bad_citizen/account_spec.rb +103 -0
  60. data/spec/functional/bad_citizen/credit_note_spec.rb +41 -0
  61. data/spec/functional/bad_citizen/payment_method_spec.rb +34 -0
  62. data/spec/functional/bad_citizen/product_rate_plan_spec.rb +105 -0
  63. data/spec/functional/bad_citizen/product_spec.rb +22 -0
  64. data/spec/functional/bad_citizen/situational/authorize_net_token_spec.rb +27 -0
  65. data/spec/functional/bad_citizen/situational/invoice_recalculation_amendment_spec.rb +27 -0
  66. data/spec/functional/bad_citizen/situational/invoice_spec.rb +22 -0
  67. data/spec/functional/bad_citizen/situational/malordered_entity_spec.rb +43 -0
  68. data/spec/functional/bad_citizen/situational/organisation_spec.rb +39 -0
  69. data/spec/functional/bad_citizen/situational/payment_method_spec.rb +47 -0
  70. data/spec/functional/bad_citizen/situational/subscription_chargeable_spec.rb +255 -0
  71. data/spec/functional/bad_citizen/subscription_spec.rb +179 -0
  72. data/spec/functional/bad_citizen/subscription_with_credit_spec.rb +240 -0
  73. data/spec/functional/bad_citizen/unit_of_measure_spec.rb +20 -0
  74. data/spec/functional/billing_entity_spec.rb +22 -0
  75. data/spec/functional/client_spec.rb +24 -0
  76. data/spec/functional/organisation_spec.rb +28 -0
  77. data/spec/setup_test_constants.rb +73 -0
  78. data/spec/spec_helper.rb +11 -0
  79. data/spec/syntax/account_spec.rb +24 -0
  80. data/spec/syntax/address_spec.rb +19 -0
  81. data/spec/syntax/api_configuration_spec.rb +13 -0
  82. data/spec/syntax/billing_entity_spec.rb +93 -0
  83. data/spec/syntax/client_spec.rb +8 -0
  84. metadata +287 -0
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bill_forward/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bill_forward"
8
+ spec.version = BillForward::VERSION
9
+ spec.authors = ["BillForward"]
10
+ spec.email = ["support@billforward.net"]
11
+ spec.summary = "BillForward Ruby Client Library"
12
+ spec.description = "Enables you to call the BillForward API easily using Ruby"
13
+ spec.homepage = "http://www.billforward.net"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'rest-client', '~> 1.6.8'
22
+ spec.add_dependency 'json', '~> 1.8.1'
23
+ spec.add_dependency 'require_all'
24
+ spec.add_dependency 'activesupport', '>= 3.1.0', '< 4'
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "rake"
29
+ end
data/bill_forward.iml ADDED
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="CompassSettings">
4
+ <option name="compassSupportEnabled" value="true" />
5
+ </component>
6
+ <component name="FacetManager">
7
+ <facet type="gem" name="Ruby Gem">
8
+ <configuration>
9
+ <option name="GEM_APP_ROOT_PATH" value="$MODULE_DIR$" />
10
+ <option name="GEM_APP_TEST_PATH" value="$MODULE_DIR$/test" />
11
+ <option name="GEM_APP_LIB_PATH" value="$MODULE_DIR$/lib" />
12
+ </configuration>
13
+ </facet>
14
+ </component>
15
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
16
+ <exclude-output />
17
+ <content url="file://$MODULE_DIR$">
18
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
19
+ </content>
20
+ <orderEntry type="jdk" jdkName="ruby-2.0.0-p451" jdkType="RUBY_SDK" />
21
+ <orderEntry type="sourceFolder" forTests="false" />
22
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v1.6.3, ruby-2.0.0-p451) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="json (v1.8.1, ruby-2.0.0-p451) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="mime-types (v2.3, ruby-2.0.0-p451) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="rest-client (v1.6.7, ruby-2.0.0-p451) [gem]" level="application" />
26
+ </component>
27
+ </module>
28
+
@@ -0,0 +1,18 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ # used for escaping query parameters
4
+ require 'erb'
5
+
6
+ # Rails extensions
7
+ # 'indifferent hashes' are used to enable string access to entities unserialized with symbol keys
8
+ require 'active_support/core_ext/hash/indifferent_access'
9
+ # we need ordered hashes because API requires '@type' to be first key in object
10
+ require 'active_support/ordered_hash'
11
+ # provides 'blank?' function
12
+ require 'active_support/core_ext/string'
13
+
14
+ # requirer that negotiates dependency order, relative pathing
15
+ require 'require_all'
16
+
17
+ # require all ruby files in relative directory 'bill_forward' and all its subdirectories
18
+ require_rel 'bill_forward'
@@ -0,0 +1,263 @@
1
+ module BillForward
2
+ class BillingEntity
3
+ # legacy Ruby gives us this 'id' chuff. we kinda need it back.
4
+ undef id if defined? id
5
+ attr_accessor :_client
6
+
7
+ def initialize(state_params = nil, client = nil)
8
+ raise AbstractInstantiateError.new('This abstract class cannot be instantiated!') if self.class == MutableEntity
9
+
10
+ client = self.class.singleton_client if client.nil?
11
+ state_params = {} if state_params.nil?
12
+
13
+ TypeCheck.verifyObj(Client, client, 'client')
14
+ TypeCheck.verifyObj(Hash, state_params, 'state_params')
15
+
16
+ @_registered_entities = Hash.new
17
+ @_registered_entity_arrays = Hash.new
18
+
19
+ @_client = client
20
+ # initiate with empty state params
21
+ # use indifferent hash so 'id' and :id are the same
22
+ @_state_params = HashWithIndifferentAccess.new
23
+ # legacy Ruby gives us this 'id' chuff. we kinda need it back.
24
+ @_state_params.instance_eval { undef id if defined? id }
25
+ # populate state params now
26
+ unserialize_all state_params
27
+ end
28
+
29
+ class << self
30
+ attr_accessor :resource_path
31
+
32
+ def get_by_id(id, query_params = {}, customClient = nil)
33
+ client = customClient
34
+ client = singleton_client if client.nil?
35
+
36
+ raise ArgumentError.new("id cannot be nil") if id.nil?
37
+ TypeCheck.verifyObj(Hash, query_params, 'query_params')
38
+
39
+ route = resource_path.path
40
+ endpoint = ''
41
+ url_full = "#{route}/#{endpoint}#{id}"
42
+
43
+ response = client.get_first(url_full, query_params)
44
+
45
+ # maybe use build_entity here for consistency
46
+ self.new(response, client)
47
+ end
48
+
49
+ def get_all(query_params = {}, customClient = nil)
50
+ client = customClient
51
+ client = singleton_client if client.nil?
52
+
53
+ TypeCheck.verifyObj(Hash, query_params, 'query_params')
54
+
55
+ route = resource_path.path
56
+ endpoint = ''
57
+ url_full = "#{route}/#{endpoint}"
58
+
59
+ response = client.get(url_full, query_params)
60
+ results = response["results"]
61
+
62
+ # maybe use build_entity_array here for consistency
63
+ entity_array = Array.new
64
+ # maybe it's an empty array, but that's okay too.
65
+ results.each do |value|
66
+ entity = self.new(value, client)
67
+ entity_array.push(entity)
68
+ end
69
+ entity_array
70
+ end
71
+
72
+ def singleton_client
73
+ Client.default_client
74
+ end
75
+ end
76
+
77
+ def method_missing(method_id, *arguments, &block)
78
+ # no call to super; our criteria is all keys.
79
+ #setter
80
+ if /^(\w+)=$/ =~ method_id.to_s
81
+ return set_state_param($1, arguments.first)
82
+ end
83
+ #getter
84
+ get_state_param(method_id.to_s)
85
+ end
86
+
87
+ def [](key)
88
+ method_missing(key)
89
+ end
90
+
91
+ def []=(key, value)
92
+ set_key = key.to_s+'='
93
+ method_missing(set_key, value)
94
+ end
95
+
96
+ def to_ordered_hash
97
+ ordered_hash = hash_with_type_at_top(@_state_params)
98
+ ordered_hash
99
+ end
100
+
101
+ def to_json(*a)
102
+ ordered_hash = to_ordered_hash
103
+ ordered_hash.to_json
104
+ # @_state_params.to_json
105
+ end
106
+
107
+ def to_unordered_hash
108
+ json_string = to_json
109
+ JSON.parse(json_string)
110
+ end
111
+
112
+ def to_s
113
+ parsed = to_unordered_hash
114
+ JSON.pretty_generate(parsed)
115
+ end
116
+
117
+ def serialize
118
+ to_json
119
+ end
120
+
121
+ protected
122
+ def hash_with_type_at_top(hash)
123
+ new_hash = OrderedHashWithDotAccess.new
124
+
125
+ # API presently requires '@type' (if present) to be first key in JSON
126
+ if hash.has_key? '@type'
127
+ # insert existing @type as first element in ordered hash
128
+ new_hash['@type'] = hash.with_indifferent_access['@type']
129
+ end
130
+
131
+ # add key-value pairs excepting '@type' back in
132
+ # no, we don't care about the order of these.
133
+ hash.with_indifferent_access.reject {|key, value| key == '@type'}.each do |key, value|
134
+ new_hash[key] = value
135
+ end
136
+
137
+ return new_hash
138
+ end
139
+
140
+ def set_state_param(key, value)
141
+ @_state_params[key] = value
142
+ get_state_param(key)
143
+ end
144
+
145
+ def get_state_param(key)
146
+ @_state_params[key]
147
+ end
148
+
149
+ def unserialize_all(hash)
150
+ TypeCheck.verifyObj(Hash, hash, 'hash')
151
+
152
+ hash.each do |key, value|
153
+ unserialized = unserialize_one value
154
+ set_state_param(key, unserialized)
155
+ end
156
+ end
157
+
158
+ def unserialize_hash(hash)
159
+ TypeCheck.verifyObj(Hash, hash, 'hash')
160
+
161
+ # API presently requires '@type' (if present) to be first key in JSON
162
+ hash = hash_with_type_at_top(hash)
163
+
164
+ hash.each do |key, value|
165
+ # recurse down, so that all nested hashes get same treatment
166
+ unserialized = unserialize_one value
167
+
168
+ # replace with unserialized version
169
+ hash[key] = unserialized
170
+ end
171
+
172
+ hash
173
+ end
174
+
175
+ def unserialize_array(array)
176
+ TypeCheck.verifyObj(Array, array, 'array')
177
+
178
+ array.each_with_index do |value, index|
179
+ # recurse down, so that all nested hashes get same treatment
180
+ unserialized = unserialize_one value
181
+
182
+ # replace with unserialized version
183
+ array[index] = unserialized
184
+ end
185
+
186
+ array
187
+ end
188
+
189
+ def unserialize_one(value)
190
+ if value.is_a? Hash
191
+ value = unserialize_hash(value)
192
+ elsif value.is_a? Array
193
+ value = unserialize_array(value)
194
+ end
195
+ value
196
+ end
197
+
198
+ def unserialize_entity(key, entity_class, hash)
199
+ # ensure that the provided entity class derives from BillingEntity
200
+ TypeCheck.verifyClass(BillingEntity, entity_class, 'entity_class')
201
+ TypeCheck.verifyObj(Hash, hash, 'hash')
202
+
203
+ # register the entity as one that requires bespoke serialization
204
+ @_registered_entities[key] = entity_class
205
+ # if key exists in the provided hash, add it to current entity's model
206
+ if hash.has_key? key
207
+ entity = build_entity(entity_class, hash[key])
208
+ set_state_param(key, entity)
209
+ end
210
+ end
211
+
212
+ def unserialize_array_of_entities(key, entity_class, hash)
213
+ # ensure that the provided entity class derives from BillingEntity
214
+ TypeCheck.verifyClass(BillingEntity, entity_class, 'entity_class')
215
+ TypeCheck.verifyObj(Hash, hash, 'hash')
216
+
217
+ # register the array of entities as one that requires bespoke serialization
218
+ @_registered_entity_arrays[key] = entity_class
219
+ # if key exists in the provided hash, add it to current entity's model
220
+ if hash.has_key? key
221
+ entities = build_entity_array(entity_class, hash[key])
222
+ set_state_param(key, entities)
223
+ end
224
+ end
225
+
226
+ def build_entity_array(entity_class, entity_hashes)
227
+ TypeCheck.verifyObj(Array, entity_hashes, 'entity_hashes')
228
+
229
+ entity_array = Array.new
230
+ # maybe it's an empty array, but that's okay too.
231
+ entity_hashes.each do |value|
232
+ new_entity = build_entity(entity_class, value)
233
+ entity_array.push(new_entity)
234
+ end
235
+ entity_array
236
+ end
237
+
238
+ def build_entity(entity_class, entity)
239
+ if entity.is_a? Hash
240
+ # either we are given a serialized entity
241
+ # we must unserialize it
242
+
243
+ # this entity should the same client as we do
244
+ client = @_client
245
+
246
+ new_entity = entity_class.new(entity, client)
247
+ elsif entity.is_a? entity_class
248
+ # or we are given an already-constructed entity
249
+ # just return it as-is
250
+
251
+ # for consistency we might want to set this entity to use the same client as us. Let's not for now.
252
+ new_entity = entity
253
+ else
254
+ expectedClassName = entity_class.name
255
+ actualClassName = entity.class.name
256
+ raise TypeError.new("Expected instance of either: 'Hash' or '#{expectedClassName}' at argument 'entity'. "+
257
+ "Instead received: '#{actualClassName}'")
258
+ end
259
+
260
+ new_entity
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,355 @@
1
+ module BillForward
2
+ class ClientException < Exception
3
+ attr_accessor :response
4
+
5
+ def initialize(message, response=nil)
6
+ super(message)
7
+
8
+ begin
9
+ if response.nil?
10
+ self.response = nil
11
+ else
12
+ self.response = JSON.parse response
13
+ end
14
+ rescue => e
15
+ self.response = nil
16
+ end
17
+ end
18
+ end
19
+
20
+ class ClientInstantiationException < Exception
21
+ end
22
+
23
+ class ApiError < Exception
24
+ attr_reader :json
25
+ attr_reader :raw
26
+
27
+ def initialize(json, raw)
28
+ @json = json
29
+ @raw = raw
30
+ end
31
+ end
32
+
33
+ class ApiAuthorizationError < ApiError
34
+ end
35
+
36
+ class ApiTokenException < ClientException
37
+
38
+ end
39
+
40
+ class Client
41
+ attr_accessor :host
42
+ attr_accessor :use_logging
43
+ attr_accessor :api_token
44
+
45
+ # provide access to self statics
46
+ class << self
47
+ # default client is a singleton client
48
+ attr_reader :default_client
49
+ def default_client=(default_client)
50
+ if (default_client == nil)
51
+ # meaningless, but required for resetting this class after a test run
52
+ @default_client = nil
53
+ return
54
+ end
55
+
56
+ TypeCheck.verifyObj(Client, default_client, 'default_client')
57
+ @default_client = default_client
58
+ end
59
+ def default_client()
60
+ raise ClientInstantiationException.new("Failed to get default BillForward API Client; " +
61
+ "'default_client' is nil. Please set a 'default_client' first.") if
62
+ @default_client.nil?
63
+ @default_client
64
+ end
65
+ end
66
+
67
+
68
+ # Constructs a client, and sets it to be used as the default client.
69
+ # @param options={} [Hash] Options with which to construct client
70
+ #
71
+ # @return [Client] The constructed client
72
+ def self.make_default_client(options)
73
+ constructedClient = self.new(options)
74
+ self.default_client = constructedClient
75
+ end
76
+
77
+ def initialize(options={})
78
+ TypeCheck.verifyObj(Hash, options, 'options')
79
+ @use_logging = options[:use_logging]
80
+
81
+ if options[:host]
82
+ @host = options[:host]
83
+ else
84
+ raise ClientInstantiationException.new "Failed to initialize BillForward API Client\n" +
85
+ "Required parameters: :host, and either [:api_token] or all of [:client_id, :client_secret, :username, :password].\n" +
86
+ "Supplied Parameters: #{options}"
87
+ end
88
+
89
+ if options[:use_proxy]
90
+ @use_proxy = options[:use_proxy]
91
+ @proxy_url = options[:proxy_url]
92
+ end
93
+
94
+ if options[:api_token]
95
+ @api_token = options[:api_token]
96
+ else
97
+ @api_token = nil
98
+ if options[:client_id] and options[:client_secret] and options[:username] and options[:password]
99
+ @client_id = options[:client_id]
100
+ @client_secret = options[:client_secret]
101
+ @username = options[:username]
102
+ @password = options[:password]
103
+ else
104
+ raise ClientException.new "Failed to initialize BillForward API Client\n"+
105
+ "Required parameters: :host and :use_logging, and either [:api_token] or all of [:client_id, :client_secret, :username, :password].\n" +
106
+ "Supplied Parameters: #{options}"
107
+ end
108
+
109
+ end
110
+
111
+ @authorization = nil
112
+ end
113
+
114
+
115
+
116
+ # def get_results(url)
117
+ # response = get(url)
118
+
119
+ # return [] if response.nil? or response["results"].length == 0
120
+
121
+ # response["results"]
122
+ # end
123
+
124
+ def get_first(url, params={})
125
+ response = get(url, params)
126
+
127
+ raise IndexError.new("Cannot get first; request returned empty list of results.") if response.nil? or response["results"].length == 0
128
+
129
+ response["results"][0]
130
+ end
131
+
132
+ def retire_first(url, params={})
133
+ response = retire(url, params)
134
+
135
+ raise IndexError.new("Cannot get first; request returned empty list of results.") if response.nil? or response["results"].length == 0
136
+
137
+ response["results"][0]
138
+ end
139
+
140
+ def put_first(url, data, params={})
141
+ response = put(url, data, params)
142
+
143
+ raise IndexError.new("Cannot get first; request returned empty list of results.") if response.nil? or response["results"].length == 0
144
+
145
+ response["results"][0]
146
+ end
147
+
148
+ def post_first(url, data, params={})
149
+ response = post(url, data, params)
150
+
151
+ raise IndexError.new("Cannot get first; request returned empty list of results.") if response.nil? or response["results"].length == 0
152
+
153
+ response["results"][0]
154
+ end
155
+
156
+ def execute_request(method, url, token, payload=nil)
157
+ # Enable Fiddler:
158
+ if @use_proxy
159
+ RestClient.proxy = @proxy_url
160
+ end
161
+
162
+ # content_type seems to be broken on generic execute.
163
+ # darn.
164
+ # RestClient::Request.execute(options)
165
+ options = {
166
+ :Authorization => "Bearer #{token}",
167
+ :accept => 'application/json'
168
+ }
169
+ if (method == 'post' || method == 'put')
170
+ options.update(:content_type => 'application/json'
171
+ )
172
+ end
173
+
174
+ if (method == 'post')
175
+ RestClient.post(url, payload, options)
176
+ elsif (method == 'put')
177
+ RestClient.put(url, payload, options)
178
+ elsif (method == 'get')
179
+ RestClient.get(url, options)
180
+ elsif (method == 'delete')
181
+ RestClient.delete(url, options)
182
+ end
183
+ end
184
+
185
+ def get(url, params={})
186
+ TypeCheck.verifyObj(Hash, params, 'params')
187
+ request('get', url, params, nil)
188
+ end
189
+
190
+ def retire(url, params={})
191
+ TypeCheck.verifyObj(Hash, params, 'params')
192
+ request('delete', url, params, nil)
193
+ end
194
+
195
+ def post(url, data, params={})
196
+ TypeCheck.verifyObj(String, data, 'data')
197
+ TypeCheck.verifyObj(Hash, params, 'params')
198
+ request('post', url, params, data)
199
+ end
200
+
201
+ def put(url, data, params={})
202
+ TypeCheck.verifyObj(String, data, 'data')
203
+ TypeCheck.verifyObj(Hash, params, 'params')
204
+ request('put', url, params, data)
205
+ end
206
+
207
+ private
208
+ def uri_encode(params = {})
209
+ TypeCheck.verifyObj(Hash, params, 'params')
210
+
211
+ encoded_params = Array.new
212
+
213
+ params.each do |key, value|
214
+ encoded_key = ERB::Util.url_encode key
215
+ encoded_value = ERB::Util.url_encode value
216
+ encoded_params.push("#{encoded_key}=#{encoded_value}")
217
+ end
218
+ query = encoded_params.join '&'
219
+
220
+ end
221
+
222
+ def request(method, url, params={}, payload=nil)
223
+ full_url = "#{@host}#{url}"
224
+
225
+ # Make params into query parameters
226
+ full_url += "?#{uri_encode(params)}" if params && params.any?
227
+ token = get_token
228
+
229
+ log "#{method} #{url}"
230
+ log "token: #{token}"
231
+
232
+ begin
233
+ response = execute_request(method, full_url, token, payload)
234
+
235
+ parsed = JSON.parse(response.to_str)
236
+ pretty = JSON.pretty_generate(parsed)
237
+ log "response: \n#{pretty}"
238
+
239
+ return parsed
240
+ rescue SocketError => e
241
+ handle_restclient_error(e)
242
+ rescue NoMethodError => e
243
+ # Work around RestClient bug
244
+ if e.message =~ /\WRequestFailed\W/
245
+ e = APIConnectionError.new('Unexpected HTTP response code')
246
+ handle_restclient_error(e)
247
+ else
248
+ raise
249
+ end
250
+ rescue RestClient::ExceptionWithResponse => e
251
+ if rcode = e.http_code and rbody = e.http_body
252
+ handle_api_error(rcode, rbody)
253
+ else
254
+ handle_restclient_error(e)
255
+ end
256
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
257
+ handle_restclient_error(e)
258
+ end
259
+ end
260
+
261
+ def handle_restclient_error(e)
262
+ connection_message = "Please check your internet connection and try again. "
263
+
264
+ case e
265
+ when RestClient::RequestTimeout
266
+ message = "Could not connect to BillForward (#{@host}). #{connection_message}"
267
+ when RestClient::ServerBrokeConnection
268
+ message = "The connection to the server (#{@host}) broke before the " \
269
+ "request completed. #{connection_message}"
270
+ when SocketError
271
+ message = "Unexpected error communicating when trying to connect to BillForward. " \
272
+ "Please confirm that (#{@host}) is a BillForward API URL. "
273
+ else
274
+ message = "Unexpected error communicating with BillForward. "
275
+ end
276
+
277
+ raise ClientException.new(message + "\n\n(Network error: #{e.message})")
278
+ end
279
+
280
+ def handle_api_error(rcode, rbody)
281
+ begin
282
+ # Example error JSON:
283
+ # {
284
+ # "errorType" : "ValidationError",
285
+ # "errorMessage" : "Validation Error - Entity: Subscription Field: type Value: null Message: may not be null\nValidation Error - Entity: Subscription Field: productID Value: null Message: may not be null\nValidation Error - Entity: Subscription Field: name Value: null Message: may not be null\n",
286
+ # "errorParameters" : [ "type", "productID", "name" ]
287
+ # }
288
+
289
+ error = JSON.parse(rbody)
290
+
291
+ errorType = error['errorType']
292
+ errorMessage = error['errorMessage']
293
+ if (error.key? 'errorParameters')
294
+ errorParameters = error['errorParameters']
295
+ raise_message = "\n====\n#{rcode} API Error.\nType: #{errorType}\nMessage: #{errorMessage}\nParameters: #{errorParameters}\n====\n"
296
+ else
297
+ if (errorType == 'Oauth')
298
+ split = errorMessage.split(', ')
299
+
300
+ error = split.first.split('=').last
301
+ description = split.last.split('=').last
302
+
303
+ raise_message = "\n====\n#{rcode} Authorization failed.\nType: #{type}\nError: #{error}\nDescription: #{description}\n====\n"
304
+
305
+ raise ApiAuthorizationError.new(error, rbody), raise_message
306
+ else
307
+ raise_message = "\n====\n#{rcode} API Error.\nType: #{errorType}\nMessage: #{errorMessage}\n====\n"
308
+ end
309
+ end
310
+
311
+ raise ApiError.new(error, rbody), raise_message
312
+ end
313
+
314
+ raise_message = "\n====\n#{rcode} API Error.\n Response body: #{rbody}\n====\n"
315
+ raise ApiError.new(nil, rbody), raise_message
316
+ end
317
+
318
+ def log(*args)
319
+ if @use_logging
320
+ puts *args
321
+ end
322
+ end
323
+
324
+ def get_token
325
+ if @api_token
326
+ @api_token
327
+ else
328
+ if @authorization and Time.now < @authorization["expires_at"]
329
+ return @authorization["access_token"]
330
+ end
331
+ begin
332
+ response = RestClient.get("#{@host}oauth/token", :params => {
333
+ :username => @username,
334
+ :password => @password,
335
+ :client_id => @client_id,
336
+ :client_secret => @client_secret,
337
+ :grant_type => "password"
338
+ }, :accept => :json)
339
+
340
+ @authorization = JSON.parse(response.to_str)
341
+ @authorization["expires_at"] = Time.now + @authorization["expires_in"]
342
+
343
+ @authorization["access_token"]
344
+ rescue => e
345
+ if e.respond_to? "response"
346
+ log "BILL FORWARD CLIENT ERROR", e.response
347
+ else
348
+ log "BILL FORWARD CLIENT ERROR", e, e.to_json
349
+ end
350
+ nil
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end