bill_forward 1.2014.296

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